pg.ddx.io  pgsql-hackers@postgresql.org mailing list archive  
help / color / mirror / Atom feed
From: Vik Fearing <vik@postgresfriends.org>
To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Subject: Logical Implication
Date: Fri, 11 Sep 2026 15:35:57 +0200
Message-ID: <37c76707-e6fa-4ee3-b57f-aa0bf1b0bba8@postgresfriends.org> (raw)

Hi.


I would like to intruduce an IMPLIES operator for booleans that reads 
better than its developed formula.  That is, I think

     a IMPLIES b

is better in CHECK constraints and elsewhere than

     NOT a OR b

.


I plan on submitting this to the SQL committee as well, but I've learned 
that they like an implementation to have it first, and I cannot imagine 
that they would quibble over the keyword used.


The first patch is a restructure of the documentation for NOT/AND/OR 
because IMPLIES is not symmetric about the diagonal and I didn't want it 
to stand out like a sore thumb. The second patch is the actual 
implementation.


It does not survive a round trip which has precedence with IN being 
changed to =ANY, BETWEEN changing to <= and >= (BETWEEN SYMMETRIC is 
even worse), etc; so I don't think that is a problem.


Thanks,

-- 

Vik Fearing


PS: based off of c1c5d28f4a2
From 90f7767a2b26ebf527b19cd44cb0a472246cf508 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@chouppes.com>
Date: Thu, 10 Sep 2026 14:30:51 +0200
Subject: [PATCH v1 1/2] doc: Rearrange the logical operator truth tables

Present AND, OR, and NOT as matrix tables indexed by operand, instead of
one combined row-per-case table, and name the third truth value unknown.
Mark the first column as row headers so that it is styled like the header
row.
---
 doc/src/sgml/func/func-logical.sgml | 105 +++++++++++++++-------------
 1 file changed, 58 insertions(+), 47 deletions(-)

diff --git a/doc/src/sgml/func/func-logical.sgml b/doc/src/sgml/func/func-logical.sgml
index 65e50e65a81..5ba6389db56 100644
--- a/doc/src/sgml/func/func-logical.sgml
+++ b/doc/src/sgml/func/func-logical.sgml
@@ -39,103 +39,114 @@
      <primary>negation</primary>
     </indexterm>
 
 <synopsis>
 <type>boolean</type> <literal>AND</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 <type>boolean</type> <literal>OR</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 <literal>NOT</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 </synopsis>
 
     <acronym>SQL</acronym> uses a three-valued logic system with true,
-    false, and <literal>null</literal>, which represents <quote>unknown</quote>.
-    Observe the following truth tables:
+    false, and unknown; the null value of a <type>boolean</type> and the truth
+    value unknown are one and the same.  In the truth tables below the left
+    operand of a binary operator selects the row, and the right operand the
+    column:
 
-    <informaltable>
+    <informaltable rowheader="firstcol">
      <tgroup cols="4">
       <thead>
        <row>
-        <entry><replaceable>a</replaceable></entry>
-        <entry><replaceable>b</replaceable></entry>
-        <entry><replaceable>a</replaceable> AND <replaceable>b</replaceable></entry>
-        <entry><replaceable>a</replaceable> OR <replaceable>b</replaceable></entry>
+        <entry>AND</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
       </thead>
 
       <tbody>
        <row>
-        <entry>TRUE</entry>
-        <entry>TRUE</entry>
-        <entry>TRUE</entry>
-        <entry>TRUE</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
 
        <row>
-        <entry>TRUE</entry>
-        <entry>FALSE</entry>
-        <entry>FALSE</entry>
-        <entry>TRUE</entry>
+        <entry>False</entry>
+        <entry>False</entry>
+        <entry>False</entry>
+        <entry>False</entry>
        </row>
 
        <row>
-        <entry>TRUE</entry>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
-        <entry>TRUE</entry>
+        <entry>Unknown</entry>
+        <entry>Unknown</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
+      </tbody>
+     </tgroup>
+    </informaltable>
 
+    <informaltable rowheader="firstcol">
+     <tgroup cols="4">
+      <thead>
        <row>
-        <entry>FALSE</entry>
-        <entry>FALSE</entry>
-        <entry>FALSE</entry>
-        <entry>FALSE</entry>
+        <entry>OR</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
+      </thead>
 
+      <tbody>
        <row>
-        <entry>FALSE</entry>
-        <entry>NULL</entry>
-        <entry>FALSE</entry>
-        <entry>NULL</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>True</entry>
        </row>
 
        <row>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
+        <entry>False</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
+       </row>
+
+       <row>
+        <entry>Unknown</entry>
+        <entry>True</entry>
+        <entry>Unknown</entry>
+        <entry>Unknown</entry>
        </row>
       </tbody>
      </tgroup>
     </informaltable>
 
-    <informaltable>
-     <tgroup cols="2">
+    <informaltable rowheader="firstcol">
+     <tgroup cols="4">
       <thead>
        <row>
-        <entry><replaceable>a</replaceable></entry>
-        <entry>NOT <replaceable>a</replaceable></entry>
+        <entry>NOT</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
       </thead>
 
       <tbody>
        <row>
-        <entry>TRUE</entry>
-        <entry>FALSE</entry>
-       </row>
-
-       <row>
-        <entry>FALSE</entry>
-        <entry>TRUE</entry>
-       </row>
-
-       <row>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
+        <entry></entry>
+        <entry>False</entry>
+        <entry>True</entry>
+        <entry>Unknown</entry>
        </row>
       </tbody>
      </tgroup>
     </informaltable>
    </para>
 
    <para>
     The operators <literal>AND</literal> and <literal>OR</literal> are
     commutative, that is, you can switch the left and right operands
     without affecting the result.  (However, it is not guaranteed that
-- 
2.55.0


From 4c65550e45dd3902077ed5d36d81c4db22c7bd94 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@chouppes.com>
Date: Thu, 10 Sep 2026 14:31:03 +0200
Subject: [PATCH v1 2/2] Add the IMPLIES boolean operator

a IMPLIES b is material implication, parsed as a right-associative
operator binding below OR and expanded to NOT a OR b in the parser, so
type errors are reported against IMPLIES itself.
---
 doc/src/sgml/func/func-logical.sgml   |  65 ++++++++++++++
 doc/src/sgml/syntax.sgml              |   6 ++
 src/backend/nodes/outfuncs.c          |   4 +
 src/backend/nodes/readfuncs.c         |   5 ++
 src/backend/parser/gram.y             |  11 ++-
 src/backend/parser/parse_expr.c       |  35 ++++++++
 src/include/nodes/parsenodes.h        |   1 +
 src/include/parser/kwlist.h           |   1 +
 src/test/regress/expected/boolean.out | 122 ++++++++++++++++++++++++++
 src/test/regress/sql/boolean.sql      |  42 +++++++++
 10 files changed, 291 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-logical.sgml b/doc/src/sgml/func/func-logical.sgml
index 5ba6389db56..52f97da042b 100644
--- a/doc/src/sgml/func/func-logical.sgml
+++ b/doc/src/sgml/func/func-logical.sgml
@@ -32,24 +32,33 @@
     </indexterm>
 
     <indexterm>
      <primary>disjunction</primary>
     </indexterm>
 
     <indexterm>
      <primary>negation</primary>
     </indexterm>
 
+    <indexterm>
+     <primary>IMPLIES</primary>
+    </indexterm>
+
+    <indexterm>
+     <primary>implication</primary>
+    </indexterm>
+
 <synopsis>
 <type>boolean</type> <literal>AND</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 <type>boolean</type> <literal>OR</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 <literal>NOT</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
+<type>boolean</type> <literal>IMPLIES</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 </synopsis>
 
     <acronym>SQL</acronym> uses a three-valued logic system with true,
     false, and unknown; the null value of a <type>boolean</type> and the truth
     value unknown are one and the same.  In the truth tables below the left
     operand of a binary operator selects the row, and the right operand the
     column:
 
     <informaltable rowheader="firstcol">
      <tgroup cols="4">
@@ -139,19 +148,75 @@
         <entry></entry>
         <entry>False</entry>
         <entry>True</entry>
         <entry>Unknown</entry>
        </row>
       </tbody>
      </tgroup>
     </informaltable>
    </para>
 
+   <para>
+    The <literal>IMPLIES</literal> operator is material implication:
+    <replaceable>a</replaceable> <literal>IMPLIES</literal>
+    <replaceable>b</replaceable> is equivalent to <literal>NOT</literal>
+    <replaceable>a</replaceable> <literal>OR</literal>
+    <replaceable>b</replaceable>, and reads as <quote>if
+    <replaceable>a</replaceable> then <replaceable>b</replaceable></quote>.
+    Unlike <literal>AND</literal> and <literal>OR</literal> it is not
+    commutative, which is why its table, unlike theirs, is not symmetric
+    about the diagonal:
+
+    <informaltable rowheader="firstcol">
+     <tgroup cols="4">
+      <thead>
+       <row>
+        <entry>IMPLIES</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
+       </row>
+      </thead>
+
+      <tbody>
+       <row>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
+       </row>
+
+       <row>
+        <entry>False</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+       </row>
+
+       <row>
+        <entry>Unknown</entry>
+        <entry>True</entry>
+        <entry>Unknown</entry>
+        <entry>Unknown</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </informaltable>
+   </para>
+
+   <para>
+    <literal>IMPLIES</literal> binds less tightly than every other operator,
+    <literal>OR</literal> included, and it is right-associative, so
+    <literal>a = 1 IMPLIES b = 2 IMPLIES c = 3</literal> means
+    <literal>(a = 1) IMPLIES ((b = 2) IMPLIES (c = 3))</literal>.  See <xref
+    linkend="sql-precedence"/>.
+   </para>
+
    <para>
     The operators <literal>AND</literal> and <literal>OR</literal> are
     commutative, that is, you can switch the left and right operands
     without affecting the result.  (However, it is not guaranteed that
     the left operand is evaluated before the right operand.  See <xref
     linkend="syntax-express-eval"/> for more information about the
     order of evaluation of subexpressions.)
    </para>
   </sect1>
diff --git a/doc/src/sgml/syntax.sgml b/doc/src/sgml/syntax.sgml
index 67482996861..bee928a8414 100644
--- a/doc/src/sgml/syntax.sgml
+++ b/doc/src/sgml/syntax.sgml
@@ -1096,20 +1096,26 @@ CAST ( '<replaceable>string</replaceable>' AS <replaceable>type</replaceable> )
        <entry><token>AND</token></entry>
        <entry>left</entry>
        <entry>logical conjunction</entry>
       </row>
 
       <row>
        <entry><token>OR</token></entry>
        <entry>left</entry>
        <entry>logical disjunction</entry>
       </row>
+
+      <row>
+       <entry><token>IMPLIES</token></entry>
+       <entry>right</entry>
+       <entry>logical implication</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
 
    <para>
     Note that the operator precedence rules also apply to user-defined
     operators that have the same names as the built-in operators
     mentioned above.  For example, if you define a
     <quote>+</quote> operator for some custom data type it will have
     the same precedence as the built-in <quote>+</quote> operator, no
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 40990143927..9a35ff53de0 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -636,20 +636,24 @@ _outA_Expr(StringInfo str, const A_Expr *node)
 			WRITE_NODE_FIELD(name);
 			break;
 		case AEXPR_BETWEEN_SYM:
 			appendStringInfoString(str, " BETWEEN_SYM");
 			WRITE_NODE_FIELD(name);
 			break;
 		case AEXPR_NOT_BETWEEN_SYM:
 			appendStringInfoString(str, " NOT_BETWEEN_SYM");
 			WRITE_NODE_FIELD(name);
 			break;
+		case AEXPR_IMPLIES:
+			appendStringInfoString(str, " IMPLIES");
+			WRITE_NODE_FIELD(name);
+			break;
 		default:
 			elog(ERROR, "unrecognized A_Expr_Kind: %d", (int) node->kind);
 			break;
 	}
 
 	WRITE_NODE_FIELD(lexpr);
 	WRITE_NODE_FIELD(rexpr);
 	WRITE_LOCATION_FIELD(rexpr_list_start);
 	WRITE_LOCATION_FIELD(rexpr_list_end);
 	WRITE_LOCATION_FIELD(location);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 2839a711f9e..4cc019a012b 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -507,20 +507,25 @@ _readA_Expr(ReadNodeContext *ctx)
 	else if (length == 11 && strncmp(token, "BETWEEN_SYM", 11) == 0)
 	{
 		local_node->kind = AEXPR_BETWEEN_SYM;
 		READ_NODE_FIELD(name);
 	}
 	else if (length == 15 && strncmp(token, "NOT_BETWEEN_SYM", 15) == 0)
 	{
 		local_node->kind = AEXPR_NOT_BETWEEN_SYM;
 		READ_NODE_FIELD(name);
 	}
+	else if (length == 7 && strncmp(token, "IMPLIES", 7) == 0)
+	{
+		local_node->kind = AEXPR_IMPLIES;
+		READ_NODE_FIELD(name);
+	}
 	else if (length == 5 && strncmp(token, ":name", 5) == 0)
 	{
 		local_node->kind = AEXPR_OP;
 		local_node->name = nodeRead(ctx, NULL, 0);
 	}
 	else
 		elog(ERROR, "unrecognized A_Expr kind: \"%.*s\"", length, token);
 
 	READ_NODE_FIELD(lexpr);
 	READ_NODE_FIELD(rexpr);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1015d303fbf..6c0c98d3a35 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -744,21 +744,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
 	EXPRESSION EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
 	FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
 
 	GENERATED GLOBAL GRANT GRANTED GREATEST GROUP_P GROUPING GROUPS
 
 	HANDLER HAVING HEADER_P HOLD HOUR_P
 
-	IDENTITY_P IF_P IGNORE_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE
+	IDENTITY_P IF_P IGNORE_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPLIES
+	IMPORT_P IN_P INCLUDE
 	INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
 	INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
 	INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
 
 	JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG
 	JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE
 
 	KEEP KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
@@ -839,20 +840,21 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token		MODE_TYPE_NAME
 %token		MODE_PLPGSQL_EXPR
 %token		MODE_PLPGSQL_ASSIGN1
 %token		MODE_PLPGSQL_ASSIGN2
 %token		MODE_PLPGSQL_ASSIGN3
 
 
 /* Precedence: lowest to highest */
 %left		UNION EXCEPT
 %left		INTERSECT
+%right		IMPLIES
 %left		OR
 %left		AND
 %right		NOT
 %nonassoc	IS ISNULL NOTNULL	/* IS sets precedence for IS NULL, etc */
 %nonassoc	'<' '>' '=' LESS_EQUALS GREATER_EQUALS NOT_EQUALS
 %nonassoc	BETWEEN IN_P LIKE ILIKE SIMILAR NOT_LA
 %nonassoc	ESCAPE			/* ESCAPE must be just above LIKE/ILIKE/SIMILAR */
 
 /*
  * Sometimes it is necessary to assign precedence to keywords that are not
@@ -15467,20 +15469,25 @@ a_expr:		c_expr									{ $$ = $1; }
 
 			| a_expr qual_Op a_expr				%prec Op
 				{ $$ = (Node *) makeA_Expr(AEXPR_OP, $2, $1, $3, @2); }
 			| qual_Op a_expr					%prec Op
 				{ $$ = (Node *) makeA_Expr(AEXPR_OP, $1, NULL, $2, @1); }
 
 			| a_expr AND a_expr
 				{ $$ = makeAndExpr($1, $3, @2); }
 			| a_expr OR a_expr
 				{ $$ = makeOrExpr($1, $3, @2); }
+			| a_expr IMPLIES a_expr
+				{
+					$$ = (Node *) makeSimpleA_Expr(AEXPR_IMPLIES, "IMPLIES",
+												   $1, $3, @2);
+				}
 			| NOT a_expr
 				{ $$ = makeNotExpr($2, @1); }
 			| NOT_LA a_expr						%prec NOT
 				{ $$ = makeNotExpr($2, @1); }
 
 			| a_expr LIKE a_expr
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_LIKE, "~~",
 												   $1, $3, @2);
 				}
@@ -18283,20 +18290,21 @@ unreserved_keyword:
 			| HANDLER
 			| HEADER_P
 			| HOLD
 			| HOUR_P
 			| IDENTITY_P
 			| IF_P
 			| IGNORE_P
 			| IMMEDIATE
 			| IMMUTABLE
 			| IMPLICIT_P
+			| IMPLIES
 			| IMPORT_P
 			| INCLUDE
 			| INCLUDING
 			| INCREMENT
 			| INDENT
 			| INDEX
 			| INDEXES
 			| INHERIT
 			| INHERITS
 			| INLINE_P
@@ -18876,20 +18884,21 @@ bare_label_keyword:
 			| GROUPS
 			| HANDLER
 			| HEADER_P
 			| HOLD
 			| IDENTITY_P
 			| IF_P
 			| ILIKE
 			| IMMEDIATE
 			| IMMUTABLE
 			| IMPLICIT_P
+			| IMPLIES
 			| IMPORT_P
 			| IN_P
 			| INCLUDE
 			| INCLUDING
 			| INCREMENT
 			| INDENT
 			| INDEX
 			| INDEXES
 			| INHERIT
 			| INHERITS
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e4acc67851b..540189e43ec 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -47,20 +47,21 @@ bool		Transform_null_equals = false;
 
 static Node *transformExprRecurse(ParseState *pstate, Node *expr);
 static Node *transformParamRef(ParseState *pstate, ParamRef *pref);
 static Node *transformAExprOp(ParseState *pstate, A_Expr *a);
 static Node *transformAExprOpAny(ParseState *pstate, A_Expr *a);
 static Node *transformAExprOpAll(ParseState *pstate, A_Expr *a);
 static Node *transformAExprDistinct(ParseState *pstate, A_Expr *a);
 static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
 static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
 static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformAExprImplies(ParseState *pstate, A_Expr *a);
 static Node *transformMergeSupportFunc(ParseState *pstate, MergeSupportFunc *f);
 static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
 static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
 static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
 static Node *transformCaseExpr(ParseState *pstate, CaseExpr *c);
 static Node *transformSubLink(ParseState *pstate, SubLink *sublink);
 static Node *transformArrayExpr(ParseState *pstate, A_ArrayExpr *a,
 								Oid array_type, Oid element_type, int32 typmod);
 static Node *transformRowExpr(ParseState *pstate, RowExpr *r, bool allowDefault);
 static Node *transformCoalesceExpr(ParseState *pstate, CoalesceExpr *c);
@@ -206,20 +207,23 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 					case AEXPR_SIMILAR:
 						/* we can transform these just like AEXPR_OP */
 						result = transformAExprOp(pstate, a);
 						break;
 					case AEXPR_BETWEEN:
 					case AEXPR_NOT_BETWEEN:
 					case AEXPR_BETWEEN_SYM:
 					case AEXPR_NOT_BETWEEN_SYM:
 						result = transformAExprBetween(pstate, a);
 						break;
+					case AEXPR_IMPLIES:
+						result = transformAExprImplies(pstate, a);
+						break;
 					default:
 						elog(ERROR, "unrecognized A_Expr kind: %d", a->kind);
 						result = NULL;	/* keep compiler quiet */
 						break;
 				}
 				break;
 			}
 
 		case T_BoolExpr:
 			result = transformBoolExpr(pstate, (BoolExpr *) expr);
@@ -1442,20 +1446,51 @@ transformBoolExpr(ParseState *pstate, BoolExpr *a)
 		Node	   *arg = (Node *) lfirst(lc);
 
 		arg = transformExprRecurse(pstate, arg);
 		arg = coerce_to_boolean(pstate, arg, opname);
 		args = lappend(args, arg);
 	}
 
 	return (Node *) makeBoolExpr(a->boolop, args, a->location);
 }
 
+/*
+ * Transform "a IMPLIES b" into the equivalent "NOT a OR b".
+ *
+ * We expand this here rather than in gram.y so that a non-boolean operand is
+ * complained of in terms of IMPLIES, rather than in terms of the NOT or OR
+ * that the construct happens to be built from.
+ */
+static Node *
+transformAExprImplies(ParseState *pstate, A_Expr *a)
+{
+	Node	   *lexpr;
+	Node	   *rexpr;
+
+	lexpr = transformExprRecurse(pstate, a->lexpr);
+	rexpr = transformExprRecurse(pstate, a->rexpr);
+
+	lexpr = coerce_to_boolean(pstate, lexpr, "IMPLIES");
+	rexpr = coerce_to_boolean(pstate, rexpr, "IMPLIES");
+
+	/*
+	 * Each operand appears exactly once in the expansion, so unlike BETWEEN
+	 * this does not risk evaluating anything twice.
+	 */
+	return (Node *) makeBoolExpr(OR_EXPR,
+								 list_make2(makeBoolExpr(NOT_EXPR,
+														 list_make1(lexpr),
+														 exprLocation(lexpr)),
+											rexpr),
+								 a->location);
+}
+
 static Node *
 transformFuncCall(ParseState *pstate, FuncCall *fn)
 {
 	Node	   *last_srf = pstate->p_last_srf;
 	List	   *targs;
 	ListCell   *args;
 
 	/* Transform the list of arguments ... */
 	targs = NIL;
 	foreach(args, fn->args)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 910eef936f1..98532b007a1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -338,20 +338,21 @@ typedef enum A_Expr_Kind
 	AEXPR_NOT_DISTINCT,			/* IS NOT DISTINCT FROM - name must be "=" */
 	AEXPR_NULLIF,				/* NULLIF - name must be "=" */
 	AEXPR_IN,					/* [NOT] IN - name must be "=" or "<>" */
 	AEXPR_LIKE,					/* [NOT] LIKE - name must be "~~" or "!~~" */
 	AEXPR_ILIKE,				/* [NOT] ILIKE - name must be "~~*" or "!~~*" */
 	AEXPR_SIMILAR,				/* [NOT] SIMILAR - name must be "~" or "!~" */
 	AEXPR_BETWEEN,				/* name must be "BETWEEN" */
 	AEXPR_NOT_BETWEEN,			/* name must be "NOT BETWEEN" */
 	AEXPR_BETWEEN_SYM,			/* name must be "BETWEEN SYMMETRIC" */
 	AEXPR_NOT_BETWEEN_SYM,		/* name must be "NOT BETWEEN SYMMETRIC" */
+	AEXPR_IMPLIES,				/* name must be "IMPLIES" */
 } A_Expr_Kind;
 
 typedef struct A_Expr
 {
 	pg_node_attr(custom_read_write)
 
 	NodeTag		type;
 	A_Expr_Kind kind;			/* see above */
 	List	   *name;			/* possibly-qualified name of operator */
 	Node	   *lexpr;			/* left argument, or NULL if none */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index beda7a22385..bdd65809bf6 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -200,20 +200,21 @@ PG_KEYWORD("having", HAVING, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("header", HEADER_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("hold", HOLD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("hour", HOUR_P, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("identity", IDENTITY_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("if", IF_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("ignore", IGNORE_P, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("ilike", ILIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("immediate", IMMEDIATE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("immutable", IMMUTABLE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("implicit", IMPLICIT_P, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("implies", IMPLIES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("import", IMPORT_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("in", IN_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("include", INCLUDE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("including", INCLUDING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("increment", INCREMENT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("indent", INDENT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("inherits", INHERITS, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/boolean.out b/src/test/regress/expected/boolean.out
index 0e99eb7ffc0..805641f38da 100644
--- a/src/test/regress/expected/boolean.out
+++ b/src/test/regress/expected/boolean.out
@@ -559,20 +559,142 @@ SELECT istrue OR isfalse OR isnul FROM booltbl4;
 ----------
  t
 (1 row)
 
 SELECT isnul OR istrue OR isfalse FROM booltbl4;
  ?column? 
 ----------
  t
 (1 row)
 
+-- Implication: "a IMPLIES b" is "NOT a OR b".  It is not commutative, so all
+-- nine combinations of three-valued logic have to be checked.
+SELECT istrue IMPLIES istrue, istrue IMPLIES isfalse, istrue IMPLIES isnul
+  FROM booltbl4;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | f        | (null)
+(1 row)
+
+SELECT isfalse IMPLIES istrue, isfalse IMPLIES isfalse, isfalse IMPLIES isnul
+  FROM booltbl4;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT isnul IMPLIES istrue, isnul IMPLIES isfalse, isnul IMPLIES isnul
+  FROM booltbl4;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | (null)   | (null)
+(1 row)
+
+-- the same, as constants, so that constant folding is exercised too
+SELECT true IMPLIES true, true IMPLIES false, true IMPLIES null;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | f        | (null)
+(1 row)
+
+SELECT false IMPLIES true, false IMPLIES false, false IMPLIES null;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT null IMPLIES true, null IMPLIES false, null::bool IMPLIES null;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | (null)   | (null)
+(1 row)
+
+-- IMPLIES is right-associative: false IMPLIES (true IMPLIES false), rather
+-- than (false IMPLIES true) IMPLIES false
+SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- and binds looser than OR, AND, NOT, the comparison operators and IS
+SELECT istrue OR isfalse IMPLIES isfalse FROM booltbl4;
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT isfalse IMPLIES isfalse AND isfalse FROM booltbl4;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT NOT istrue IMPLIES istrue FROM booltbl4;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT 1 = 1 IMPLIES 2 = 3;
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT istrue IMPLIES isnul IS NULL FROM booltbl4;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- non-boolean operands are reported in terms of IMPLIES, not of its expansion
+SELECT 1 IMPLIES true;                  -- error
+ERROR:  argument of IMPLIES must be type boolean, not type integer
+LINE 1: SELECT 1 IMPLIES true;
+               ^
+SELECT true IMPLIES 1;                  -- error
+ERROR:  argument of IMPLIES must be type boolean, not type integer
+LINE 1: SELECT true IMPLIES 1;
+                            ^
+-- the construct is expanded during parse analysis, so this is what is stored
+CREATE VIEW boolview AS SELECT istrue IMPLIES isnul AS i FROM booltbl4;
+SELECT pg_get_viewdef('boolview', true);
+          pg_get_viewdef          
+----------------------------------
+  SELECT NOT istrue OR isnul AS i+
+    FROM booltbl4;
+(1 row)
+
+DROP VIEW boolview;
+-- IMPLIES is unreserved, so it remains usable as an identifier
+CREATE TABLE implies (implies bool);
+INSERT INTO implies VALUES (false);
+SELECT implies IMPLIES implies FROM implies;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE implies;
+SELECT 1 AS implies;
+ implies 
+---------
+       1
+(1 row)
+
+SELECT 1 implies;
+ implies 
+---------
+       1
+(1 row)
+
 -- Casts
 SELECT 0::boolean;
  bool 
 ------
  f
 (1 row)
 
 SELECT 1::boolean;
  bool 
 ------
diff --git a/src/test/regress/sql/boolean.sql b/src/test/regress/sql/boolean.sql
index 85c6b019882..08265b531e2 100644
--- a/src/test/regress/sql/boolean.sql
+++ b/src/test/regress/sql/boolean.sql
@@ -243,20 +243,62 @@ SELECT isnul AND istrue AND isfalse FROM booltbl4;
 
 -- OR expression need to return null if there's any nulls and none
 -- of the value is true
 SELECT isfalse OR isnul OR isfalse FROM booltbl4;
 SELECT isfalse OR isfalse OR isnul FROM booltbl4;
 SELECT isnul OR isfalse OR isfalse FROM booltbl4;
 SELECT isfalse OR isnul OR istrue FROM booltbl4;
 SELECT istrue OR isfalse OR isnul FROM booltbl4;
 SELECT isnul OR istrue OR isfalse FROM booltbl4;
 
+-- Implication: "a IMPLIES b" is "NOT a OR b".  It is not commutative, so all
+-- nine combinations of three-valued logic have to be checked.
+SELECT istrue IMPLIES istrue, istrue IMPLIES isfalse, istrue IMPLIES isnul
+  FROM booltbl4;
+SELECT isfalse IMPLIES istrue, isfalse IMPLIES isfalse, isfalse IMPLIES isnul
+  FROM booltbl4;
+SELECT isnul IMPLIES istrue, isnul IMPLIES isfalse, isnul IMPLIES isnul
+  FROM booltbl4;
+
+-- the same, as constants, so that constant folding is exercised too
+SELECT true IMPLIES true, true IMPLIES false, true IMPLIES null;
+SELECT false IMPLIES true, false IMPLIES false, false IMPLIES null;
+SELECT null IMPLIES true, null IMPLIES false, null::bool IMPLIES null;
+
+-- IMPLIES is right-associative: false IMPLIES (true IMPLIES false), rather
+-- than (false IMPLIES true) IMPLIES false
+SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4;
+
+-- and binds looser than OR, AND, NOT, the comparison operators and IS
+SELECT istrue OR isfalse IMPLIES isfalse FROM booltbl4;
+SELECT isfalse IMPLIES isfalse AND isfalse FROM booltbl4;
+SELECT NOT istrue IMPLIES istrue FROM booltbl4;
+SELECT 1 = 1 IMPLIES 2 = 3;
+SELECT istrue IMPLIES isnul IS NULL FROM booltbl4;
+
+-- non-boolean operands are reported in terms of IMPLIES, not of its expansion
+SELECT 1 IMPLIES true;                  -- error
+SELECT true IMPLIES 1;                  -- error
+
+-- the construct is expanded during parse analysis, so this is what is stored
+CREATE VIEW boolview AS SELECT istrue IMPLIES isnul AS i FROM booltbl4;
+SELECT pg_get_viewdef('boolview', true);
+DROP VIEW boolview;
+
+-- IMPLIES is unreserved, so it remains usable as an identifier
+CREATE TABLE implies (implies bool);
+INSERT INTO implies VALUES (false);
+SELECT implies IMPLIES implies FROM implies;
+DROP TABLE implies;
+SELECT 1 AS implies;
+SELECT 1 implies;
+
 -- Casts
 SELECT 0::boolean;
 SELECT 1::boolean;
 SELECT 2::boolean;
 
 
 --
 -- Clean up
 -- Many tables are retained by the regression test, but these do not seem
 --  particularly useful so just get rid of them for now.
-- 
2.55.0



Attachments:

  [text/plain] v1-0001-doc-Rearrange-the-logical-operator-truth-tables.patch (5.1K, ../37c76707-e6fa-4ee3-b57f-aa0bf1b0bba8@postgresfriends.org/2-v1-0001-doc-Rearrange-the-logical-operator-truth-tables.patch)
  download | inline diff:
From 90f7767a2b26ebf527b19cd44cb0a472246cf508 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@chouppes.com>
Date: Thu, 10 Sep 2026 14:30:51 +0200
Subject: [PATCH v1 1/2] doc: Rearrange the logical operator truth tables

Present AND, OR, and NOT as matrix tables indexed by operand, instead of
one combined row-per-case table, and name the third truth value unknown.
Mark the first column as row headers so that it is styled like the header
row.
---
 doc/src/sgml/func/func-logical.sgml | 105 +++++++++++++++-------------
 1 file changed, 58 insertions(+), 47 deletions(-)

diff --git a/doc/src/sgml/func/func-logical.sgml b/doc/src/sgml/func/func-logical.sgml
index 65e50e65a81..5ba6389db56 100644
--- a/doc/src/sgml/func/func-logical.sgml
+++ b/doc/src/sgml/func/func-logical.sgml
@@ -39,103 +39,114 @@
      <primary>negation</primary>
     </indexterm>
 
 <synopsis>
 <type>boolean</type> <literal>AND</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 <type>boolean</type> <literal>OR</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 <literal>NOT</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 </synopsis>
 
     <acronym>SQL</acronym> uses a three-valued logic system with true,
-    false, and <literal>null</literal>, which represents <quote>unknown</quote>.
-    Observe the following truth tables:
+    false, and unknown; the null value of a <type>boolean</type> and the truth
+    value unknown are one and the same.  In the truth tables below the left
+    operand of a binary operator selects the row, and the right operand the
+    column:
 
-    <informaltable>
+    <informaltable rowheader="firstcol">
      <tgroup cols="4">
       <thead>
        <row>
-        <entry><replaceable>a</replaceable></entry>
-        <entry><replaceable>b</replaceable></entry>
-        <entry><replaceable>a</replaceable> AND <replaceable>b</replaceable></entry>
-        <entry><replaceable>a</replaceable> OR <replaceable>b</replaceable></entry>
+        <entry>AND</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
       </thead>
 
       <tbody>
        <row>
-        <entry>TRUE</entry>
-        <entry>TRUE</entry>
-        <entry>TRUE</entry>
-        <entry>TRUE</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
 
        <row>
-        <entry>TRUE</entry>
-        <entry>FALSE</entry>
-        <entry>FALSE</entry>
-        <entry>TRUE</entry>
+        <entry>False</entry>
+        <entry>False</entry>
+        <entry>False</entry>
+        <entry>False</entry>
        </row>
 
        <row>
-        <entry>TRUE</entry>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
-        <entry>TRUE</entry>
+        <entry>Unknown</entry>
+        <entry>Unknown</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
+      </tbody>
+     </tgroup>
+    </informaltable>
 
+    <informaltable rowheader="firstcol">
+     <tgroup cols="4">
+      <thead>
        <row>
-        <entry>FALSE</entry>
-        <entry>FALSE</entry>
-        <entry>FALSE</entry>
-        <entry>FALSE</entry>
+        <entry>OR</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
+      </thead>
 
+      <tbody>
        <row>
-        <entry>FALSE</entry>
-        <entry>NULL</entry>
-        <entry>FALSE</entry>
-        <entry>NULL</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>True</entry>
        </row>
 
        <row>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
+        <entry>False</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
+       </row>
+
+       <row>
+        <entry>Unknown</entry>
+        <entry>True</entry>
+        <entry>Unknown</entry>
+        <entry>Unknown</entry>
        </row>
       </tbody>
      </tgroup>
     </informaltable>
 
-    <informaltable>
-     <tgroup cols="2">
+    <informaltable rowheader="firstcol">
+     <tgroup cols="4">
       <thead>
        <row>
-        <entry><replaceable>a</replaceable></entry>
-        <entry>NOT <replaceable>a</replaceable></entry>
+        <entry>NOT</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
        </row>
       </thead>
 
       <tbody>
        <row>
-        <entry>TRUE</entry>
-        <entry>FALSE</entry>
-       </row>
-
-       <row>
-        <entry>FALSE</entry>
-        <entry>TRUE</entry>
-       </row>
-
-       <row>
-        <entry>NULL</entry>
-        <entry>NULL</entry>
+        <entry></entry>
+        <entry>False</entry>
+        <entry>True</entry>
+        <entry>Unknown</entry>
        </row>
       </tbody>
      </tgroup>
     </informaltable>
    </para>
 
    <para>
     The operators <literal>AND</literal> and <literal>OR</literal> are
     commutative, that is, you can switch the left and right operands
     without affecting the result.  (However, it is not guaranteed that
-- 
2.55.0



  [text/plain] v1-0002-Add-the-IMPLIES-boolean-operator.patch (23.5K, ../37c76707-e6fa-4ee3-b57f-aa0bf1b0bba8@postgresfriends.org/3-v1-0002-Add-the-IMPLIES-boolean-operator.patch)
  download | inline diff:
From 4c65550e45dd3902077ed5d36d81c4db22c7bd94 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@chouppes.com>
Date: Thu, 10 Sep 2026 14:31:03 +0200
Subject: [PATCH v1 2/2] Add the IMPLIES boolean operator

a IMPLIES b is material implication, parsed as a right-associative
operator binding below OR and expanded to NOT a OR b in the parser, so
type errors are reported against IMPLIES itself.
---
 doc/src/sgml/func/func-logical.sgml   |  65 ++++++++++++++
 doc/src/sgml/syntax.sgml              |   6 ++
 src/backend/nodes/outfuncs.c          |   4 +
 src/backend/nodes/readfuncs.c         |   5 ++
 src/backend/parser/gram.y             |  11 ++-
 src/backend/parser/parse_expr.c       |  35 ++++++++
 src/include/nodes/parsenodes.h        |   1 +
 src/include/parser/kwlist.h           |   1 +
 src/test/regress/expected/boolean.out | 122 ++++++++++++++++++++++++++
 src/test/regress/sql/boolean.sql      |  42 +++++++++
 10 files changed, 291 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/func/func-logical.sgml b/doc/src/sgml/func/func-logical.sgml
index 5ba6389db56..52f97da042b 100644
--- a/doc/src/sgml/func/func-logical.sgml
+++ b/doc/src/sgml/func/func-logical.sgml
@@ -32,24 +32,33 @@
     </indexterm>
 
     <indexterm>
      <primary>disjunction</primary>
     </indexterm>
 
     <indexterm>
      <primary>negation</primary>
     </indexterm>
 
+    <indexterm>
+     <primary>IMPLIES</primary>
+    </indexterm>
+
+    <indexterm>
+     <primary>implication</primary>
+    </indexterm>
+
 <synopsis>
 <type>boolean</type> <literal>AND</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 <type>boolean</type> <literal>OR</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 <literal>NOT</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
+<type>boolean</type> <literal>IMPLIES</literal> <type>boolean</type> <returnvalue>boolean</returnvalue>
 </synopsis>
 
     <acronym>SQL</acronym> uses a three-valued logic system with true,
     false, and unknown; the null value of a <type>boolean</type> and the truth
     value unknown are one and the same.  In the truth tables below the left
     operand of a binary operator selects the row, and the right operand the
     column:
 
     <informaltable rowheader="firstcol">
      <tgroup cols="4">
@@ -139,19 +148,75 @@
         <entry></entry>
         <entry>False</entry>
         <entry>True</entry>
         <entry>Unknown</entry>
        </row>
       </tbody>
      </tgroup>
     </informaltable>
    </para>
 
+   <para>
+    The <literal>IMPLIES</literal> operator is material implication:
+    <replaceable>a</replaceable> <literal>IMPLIES</literal>
+    <replaceable>b</replaceable> is equivalent to <literal>NOT</literal>
+    <replaceable>a</replaceable> <literal>OR</literal>
+    <replaceable>b</replaceable>, and reads as <quote>if
+    <replaceable>a</replaceable> then <replaceable>b</replaceable></quote>.
+    Unlike <literal>AND</literal> and <literal>OR</literal> it is not
+    commutative, which is why its table, unlike theirs, is not symmetric
+    about the diagonal:
+
+    <informaltable rowheader="firstcol">
+     <tgroup cols="4">
+      <thead>
+       <row>
+        <entry>IMPLIES</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
+       </row>
+      </thead>
+
+      <tbody>
+       <row>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>False</entry>
+        <entry>Unknown</entry>
+       </row>
+
+       <row>
+        <entry>False</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+        <entry>True</entry>
+       </row>
+
+       <row>
+        <entry>Unknown</entry>
+        <entry>True</entry>
+        <entry>Unknown</entry>
+        <entry>Unknown</entry>
+       </row>
+      </tbody>
+     </tgroup>
+    </informaltable>
+   </para>
+
+   <para>
+    <literal>IMPLIES</literal> binds less tightly than every other operator,
+    <literal>OR</literal> included, and it is right-associative, so
+    <literal>a = 1 IMPLIES b = 2 IMPLIES c = 3</literal> means
+    <literal>(a = 1) IMPLIES ((b = 2) IMPLIES (c = 3))</literal>.  See <xref
+    linkend="sql-precedence"/>.
+   </para>
+
    <para>
     The operators <literal>AND</literal> and <literal>OR</literal> are
     commutative, that is, you can switch the left and right operands
     without affecting the result.  (However, it is not guaranteed that
     the left operand is evaluated before the right operand.  See <xref
     linkend="syntax-express-eval"/> for more information about the
     order of evaluation of subexpressions.)
    </para>
   </sect1>
diff --git a/doc/src/sgml/syntax.sgml b/doc/src/sgml/syntax.sgml
index 67482996861..bee928a8414 100644
--- a/doc/src/sgml/syntax.sgml
+++ b/doc/src/sgml/syntax.sgml
@@ -1096,20 +1096,26 @@ CAST ( '<replaceable>string</replaceable>' AS <replaceable>type</replaceable> )
        <entry><token>AND</token></entry>
        <entry>left</entry>
        <entry>logical conjunction</entry>
       </row>
 
       <row>
        <entry><token>OR</token></entry>
        <entry>left</entry>
        <entry>logical disjunction</entry>
       </row>
+
+      <row>
+       <entry><token>IMPLIES</token></entry>
+       <entry>right</entry>
+       <entry>logical implication</entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
 
    <para>
     Note that the operator precedence rules also apply to user-defined
     operators that have the same names as the built-in operators
     mentioned above.  For example, if you define a
     <quote>+</quote> operator for some custom data type it will have
     the same precedence as the built-in <quote>+</quote> operator, no
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 40990143927..9a35ff53de0 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -636,20 +636,24 @@ _outA_Expr(StringInfo str, const A_Expr *node)
 			WRITE_NODE_FIELD(name);
 			break;
 		case AEXPR_BETWEEN_SYM:
 			appendStringInfoString(str, " BETWEEN_SYM");
 			WRITE_NODE_FIELD(name);
 			break;
 		case AEXPR_NOT_BETWEEN_SYM:
 			appendStringInfoString(str, " NOT_BETWEEN_SYM");
 			WRITE_NODE_FIELD(name);
 			break;
+		case AEXPR_IMPLIES:
+			appendStringInfoString(str, " IMPLIES");
+			WRITE_NODE_FIELD(name);
+			break;
 		default:
 			elog(ERROR, "unrecognized A_Expr_Kind: %d", (int) node->kind);
 			break;
 	}
 
 	WRITE_NODE_FIELD(lexpr);
 	WRITE_NODE_FIELD(rexpr);
 	WRITE_LOCATION_FIELD(rexpr_list_start);
 	WRITE_LOCATION_FIELD(rexpr_list_end);
 	WRITE_LOCATION_FIELD(location);
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 2839a711f9e..4cc019a012b 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -507,20 +507,25 @@ _readA_Expr(ReadNodeContext *ctx)
 	else if (length == 11 && strncmp(token, "BETWEEN_SYM", 11) == 0)
 	{
 		local_node->kind = AEXPR_BETWEEN_SYM;
 		READ_NODE_FIELD(name);
 	}
 	else if (length == 15 && strncmp(token, "NOT_BETWEEN_SYM", 15) == 0)
 	{
 		local_node->kind = AEXPR_NOT_BETWEEN_SYM;
 		READ_NODE_FIELD(name);
 	}
+	else if (length == 7 && strncmp(token, "IMPLIES", 7) == 0)
+	{
+		local_node->kind = AEXPR_IMPLIES;
+		READ_NODE_FIELD(name);
+	}
 	else if (length == 5 && strncmp(token, ":name", 5) == 0)
 	{
 		local_node->kind = AEXPR_OP;
 		local_node->name = nodeRead(ctx, NULL, 0);
 	}
 	else
 		elog(ERROR, "unrecognized A_Expr kind: \"%.*s\"", length, token);
 
 	READ_NODE_FIELD(lexpr);
 	READ_NODE_FIELD(rexpr);
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 1015d303fbf..6c0c98d3a35 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -744,21 +744,22 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 	ESCAPE EVENT EXCEPT EXCLUDE EXCLUDING EXCLUSIVE EXECUTE EXISTS EXPLAIN
 	EXPRESSION EXTENSION EXTERNAL EXTRACT
 
 	FALSE_P FAMILY FETCH FILTER FINALIZE FIRST_P FLOAT_P FOLLOWING FOR
 	FORCE FOREIGN FORMAT FORWARD FREEZE FROM FULL FUNCTION FUNCTIONS
 
 	GENERATED GLOBAL GRANT GRANTED GREATEST GROUP_P GROUPING GROUPS
 
 	HANDLER HAVING HEADER_P HOLD HOUR_P
 
-	IDENTITY_P IF_P IGNORE_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPORT_P IN_P INCLUDE
+	IDENTITY_P IF_P IGNORE_P ILIKE IMMEDIATE IMMUTABLE IMPLICIT_P IMPLIES
+	IMPORT_P IN_P INCLUDE
 	INCLUDING INCREMENT INDENT INDEX INDEXES INHERIT INHERITS INITIALLY INLINE_P
 	INNER_P INOUT INPUT_P INSENSITIVE INSERT INSTEAD INT_P INTEGER
 	INTERSECT INTERVAL INTO INVOKER IS ISNULL ISOLATION
 
 	JOIN JSON JSON_ARRAY JSON_ARRAYAGG JSON_EXISTS JSON_OBJECT JSON_OBJECTAGG
 	JSON_QUERY JSON_SCALAR JSON_SERIALIZE JSON_TABLE JSON_VALUE
 
 	KEEP KEY KEYS
 
 	LABEL LANGUAGE LARGE_P LAST_P LATERAL_P
@@ -839,20 +840,21 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
 %token		MODE_TYPE_NAME
 %token		MODE_PLPGSQL_EXPR
 %token		MODE_PLPGSQL_ASSIGN1
 %token		MODE_PLPGSQL_ASSIGN2
 %token		MODE_PLPGSQL_ASSIGN3
 
 
 /* Precedence: lowest to highest */
 %left		UNION EXCEPT
 %left		INTERSECT
+%right		IMPLIES
 %left		OR
 %left		AND
 %right		NOT
 %nonassoc	IS ISNULL NOTNULL	/* IS sets precedence for IS NULL, etc */
 %nonassoc	'<' '>' '=' LESS_EQUALS GREATER_EQUALS NOT_EQUALS
 %nonassoc	BETWEEN IN_P LIKE ILIKE SIMILAR NOT_LA
 %nonassoc	ESCAPE			/* ESCAPE must be just above LIKE/ILIKE/SIMILAR */
 
 /*
  * Sometimes it is necessary to assign precedence to keywords that are not
@@ -15467,20 +15469,25 @@ a_expr:		c_expr									{ $$ = $1; }
 
 			| a_expr qual_Op a_expr				%prec Op
 				{ $$ = (Node *) makeA_Expr(AEXPR_OP, $2, $1, $3, @2); }
 			| qual_Op a_expr					%prec Op
 				{ $$ = (Node *) makeA_Expr(AEXPR_OP, $1, NULL, $2, @1); }
 
 			| a_expr AND a_expr
 				{ $$ = makeAndExpr($1, $3, @2); }
 			| a_expr OR a_expr
 				{ $$ = makeOrExpr($1, $3, @2); }
+			| a_expr IMPLIES a_expr
+				{
+					$$ = (Node *) makeSimpleA_Expr(AEXPR_IMPLIES, "IMPLIES",
+												   $1, $3, @2);
+				}
 			| NOT a_expr
 				{ $$ = makeNotExpr($2, @1); }
 			| NOT_LA a_expr						%prec NOT
 				{ $$ = makeNotExpr($2, @1); }
 
 			| a_expr LIKE a_expr
 				{
 					$$ = (Node *) makeSimpleA_Expr(AEXPR_LIKE, "~~",
 												   $1, $3, @2);
 				}
@@ -18283,20 +18290,21 @@ unreserved_keyword:
 			| HANDLER
 			| HEADER_P
 			| HOLD
 			| HOUR_P
 			| IDENTITY_P
 			| IF_P
 			| IGNORE_P
 			| IMMEDIATE
 			| IMMUTABLE
 			| IMPLICIT_P
+			| IMPLIES
 			| IMPORT_P
 			| INCLUDE
 			| INCLUDING
 			| INCREMENT
 			| INDENT
 			| INDEX
 			| INDEXES
 			| INHERIT
 			| INHERITS
 			| INLINE_P
@@ -18876,20 +18884,21 @@ bare_label_keyword:
 			| GROUPS
 			| HANDLER
 			| HEADER_P
 			| HOLD
 			| IDENTITY_P
 			| IF_P
 			| ILIKE
 			| IMMEDIATE
 			| IMMUTABLE
 			| IMPLICIT_P
+			| IMPLIES
 			| IMPORT_P
 			| IN_P
 			| INCLUDE
 			| INCLUDING
 			| INCREMENT
 			| INDENT
 			| INDEX
 			| INDEXES
 			| INHERIT
 			| INHERITS
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index e4acc67851b..540189e43ec 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -47,20 +47,21 @@ bool		Transform_null_equals = false;
 
 static Node *transformExprRecurse(ParseState *pstate, Node *expr);
 static Node *transformParamRef(ParseState *pstate, ParamRef *pref);
 static Node *transformAExprOp(ParseState *pstate, A_Expr *a);
 static Node *transformAExprOpAny(ParseState *pstate, A_Expr *a);
 static Node *transformAExprOpAll(ParseState *pstate, A_Expr *a);
 static Node *transformAExprDistinct(ParseState *pstate, A_Expr *a);
 static Node *transformAExprNullIf(ParseState *pstate, A_Expr *a);
 static Node *transformAExprIn(ParseState *pstate, A_Expr *a);
 static Node *transformAExprBetween(ParseState *pstate, A_Expr *a);
+static Node *transformAExprImplies(ParseState *pstate, A_Expr *a);
 static Node *transformMergeSupportFunc(ParseState *pstate, MergeSupportFunc *f);
 static Node *transformBoolExpr(ParseState *pstate, BoolExpr *a);
 static Node *transformFuncCall(ParseState *pstate, FuncCall *fn);
 static Node *transformMultiAssignRef(ParseState *pstate, MultiAssignRef *maref);
 static Node *transformCaseExpr(ParseState *pstate, CaseExpr *c);
 static Node *transformSubLink(ParseState *pstate, SubLink *sublink);
 static Node *transformArrayExpr(ParseState *pstate, A_ArrayExpr *a,
 								Oid array_type, Oid element_type, int32 typmod);
 static Node *transformRowExpr(ParseState *pstate, RowExpr *r, bool allowDefault);
 static Node *transformCoalesceExpr(ParseState *pstate, CoalesceExpr *c);
@@ -206,20 +207,23 @@ transformExprRecurse(ParseState *pstate, Node *expr)
 					case AEXPR_SIMILAR:
 						/* we can transform these just like AEXPR_OP */
 						result = transformAExprOp(pstate, a);
 						break;
 					case AEXPR_BETWEEN:
 					case AEXPR_NOT_BETWEEN:
 					case AEXPR_BETWEEN_SYM:
 					case AEXPR_NOT_BETWEEN_SYM:
 						result = transformAExprBetween(pstate, a);
 						break;
+					case AEXPR_IMPLIES:
+						result = transformAExprImplies(pstate, a);
+						break;
 					default:
 						elog(ERROR, "unrecognized A_Expr kind: %d", a->kind);
 						result = NULL;	/* keep compiler quiet */
 						break;
 				}
 				break;
 			}
 
 		case T_BoolExpr:
 			result = transformBoolExpr(pstate, (BoolExpr *) expr);
@@ -1442,20 +1446,51 @@ transformBoolExpr(ParseState *pstate, BoolExpr *a)
 		Node	   *arg = (Node *) lfirst(lc);
 
 		arg = transformExprRecurse(pstate, arg);
 		arg = coerce_to_boolean(pstate, arg, opname);
 		args = lappend(args, arg);
 	}
 
 	return (Node *) makeBoolExpr(a->boolop, args, a->location);
 }
 
+/*
+ * Transform "a IMPLIES b" into the equivalent "NOT a OR b".
+ *
+ * We expand this here rather than in gram.y so that a non-boolean operand is
+ * complained of in terms of IMPLIES, rather than in terms of the NOT or OR
+ * that the construct happens to be built from.
+ */
+static Node *
+transformAExprImplies(ParseState *pstate, A_Expr *a)
+{
+	Node	   *lexpr;
+	Node	   *rexpr;
+
+	lexpr = transformExprRecurse(pstate, a->lexpr);
+	rexpr = transformExprRecurse(pstate, a->rexpr);
+
+	lexpr = coerce_to_boolean(pstate, lexpr, "IMPLIES");
+	rexpr = coerce_to_boolean(pstate, rexpr, "IMPLIES");
+
+	/*
+	 * Each operand appears exactly once in the expansion, so unlike BETWEEN
+	 * this does not risk evaluating anything twice.
+	 */
+	return (Node *) makeBoolExpr(OR_EXPR,
+								 list_make2(makeBoolExpr(NOT_EXPR,
+														 list_make1(lexpr),
+														 exprLocation(lexpr)),
+											rexpr),
+								 a->location);
+}
+
 static Node *
 transformFuncCall(ParseState *pstate, FuncCall *fn)
 {
 	Node	   *last_srf = pstate->p_last_srf;
 	List	   *targs;
 	ListCell   *args;
 
 	/* Transform the list of arguments ... */
 	targs = NIL;
 	foreach(args, fn->args)
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 910eef936f1..98532b007a1 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -338,20 +338,21 @@ typedef enum A_Expr_Kind
 	AEXPR_NOT_DISTINCT,			/* IS NOT DISTINCT FROM - name must be "=" */
 	AEXPR_NULLIF,				/* NULLIF - name must be "=" */
 	AEXPR_IN,					/* [NOT] IN - name must be "=" or "<>" */
 	AEXPR_LIKE,					/* [NOT] LIKE - name must be "~~" or "!~~" */
 	AEXPR_ILIKE,				/* [NOT] ILIKE - name must be "~~*" or "!~~*" */
 	AEXPR_SIMILAR,				/* [NOT] SIMILAR - name must be "~" or "!~" */
 	AEXPR_BETWEEN,				/* name must be "BETWEEN" */
 	AEXPR_NOT_BETWEEN,			/* name must be "NOT BETWEEN" */
 	AEXPR_BETWEEN_SYM,			/* name must be "BETWEEN SYMMETRIC" */
 	AEXPR_NOT_BETWEEN_SYM,		/* name must be "NOT BETWEEN SYMMETRIC" */
+	AEXPR_IMPLIES,				/* name must be "IMPLIES" */
 } A_Expr_Kind;
 
 typedef struct A_Expr
 {
 	pg_node_attr(custom_read_write)
 
 	NodeTag		type;
 	A_Expr_Kind kind;			/* see above */
 	List	   *name;			/* possibly-qualified name of operator */
 	Node	   *lexpr;			/* left argument, or NULL if none */
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index beda7a22385..bdd65809bf6 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -200,20 +200,21 @@ PG_KEYWORD("having", HAVING, RESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("header", HEADER_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("hold", HOLD, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("hour", HOUR_P, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("identity", IDENTITY_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("if", IF_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("ignore", IGNORE_P, UNRESERVED_KEYWORD, AS_LABEL)
 PG_KEYWORD("ilike", ILIKE, TYPE_FUNC_NAME_KEYWORD, BARE_LABEL)
 PG_KEYWORD("immediate", IMMEDIATE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("immutable", IMMUTABLE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("implicit", IMPLICIT_P, UNRESERVED_KEYWORD, BARE_LABEL)
+PG_KEYWORD("implies", IMPLIES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("import", IMPORT_P, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("in", IN_P, RESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("include", INCLUDE, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("including", INCLUDING, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("increment", INCREMENT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("indent", INDENT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("index", INDEX, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("indexes", INDEXES, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("inherit", INHERIT, UNRESERVED_KEYWORD, BARE_LABEL)
 PG_KEYWORD("inherits", INHERITS, UNRESERVED_KEYWORD, BARE_LABEL)
diff --git a/src/test/regress/expected/boolean.out b/src/test/regress/expected/boolean.out
index 0e99eb7ffc0..805641f38da 100644
--- a/src/test/regress/expected/boolean.out
+++ b/src/test/regress/expected/boolean.out
@@ -559,20 +559,142 @@ SELECT istrue OR isfalse OR isnul FROM booltbl4;
 ----------
  t
 (1 row)
 
 SELECT isnul OR istrue OR isfalse FROM booltbl4;
  ?column? 
 ----------
  t
 (1 row)
 
+-- Implication: "a IMPLIES b" is "NOT a OR b".  It is not commutative, so all
+-- nine combinations of three-valued logic have to be checked.
+SELECT istrue IMPLIES istrue, istrue IMPLIES isfalse, istrue IMPLIES isnul
+  FROM booltbl4;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | f        | (null)
+(1 row)
+
+SELECT isfalse IMPLIES istrue, isfalse IMPLIES isfalse, isfalse IMPLIES isnul
+  FROM booltbl4;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT isnul IMPLIES istrue, isnul IMPLIES isfalse, isnul IMPLIES isnul
+  FROM booltbl4;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | (null)   | (null)
+(1 row)
+
+-- the same, as constants, so that constant folding is exercised too
+SELECT true IMPLIES true, true IMPLIES false, true IMPLIES null;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | f        | (null)
+(1 row)
+
+SELECT false IMPLIES true, false IMPLIES false, false IMPLIES null;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | t        | t
+(1 row)
+
+SELECT null IMPLIES true, null IMPLIES false, null::bool IMPLIES null;
+ ?column? | ?column? | ?column? 
+----------+----------+----------
+ t        | (null)   | (null)
+(1 row)
+
+-- IMPLIES is right-associative: false IMPLIES (true IMPLIES false), rather
+-- than (false IMPLIES true) IMPLIES false
+SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- and binds looser than OR, AND, NOT, the comparison operators and IS
+SELECT istrue OR isfalse IMPLIES isfalse FROM booltbl4;
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT isfalse IMPLIES isfalse AND isfalse FROM booltbl4;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT NOT istrue IMPLIES istrue FROM booltbl4;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT 1 = 1 IMPLIES 2 = 3;
+ ?column? 
+----------
+ f
+(1 row)
+
+SELECT istrue IMPLIES isnul IS NULL FROM booltbl4;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- non-boolean operands are reported in terms of IMPLIES, not of its expansion
+SELECT 1 IMPLIES true;                  -- error
+ERROR:  argument of IMPLIES must be type boolean, not type integer
+LINE 1: SELECT 1 IMPLIES true;
+               ^
+SELECT true IMPLIES 1;                  -- error
+ERROR:  argument of IMPLIES must be type boolean, not type integer
+LINE 1: SELECT true IMPLIES 1;
+                            ^
+-- the construct is expanded during parse analysis, so this is what is stored
+CREATE VIEW boolview AS SELECT istrue IMPLIES isnul AS i FROM booltbl4;
+SELECT pg_get_viewdef('boolview', true);
+          pg_get_viewdef          
+----------------------------------
+  SELECT NOT istrue OR isnul AS i+
+    FROM booltbl4;
+(1 row)
+
+DROP VIEW boolview;
+-- IMPLIES is unreserved, so it remains usable as an identifier
+CREATE TABLE implies (implies bool);
+INSERT INTO implies VALUES (false);
+SELECT implies IMPLIES implies FROM implies;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE implies;
+SELECT 1 AS implies;
+ implies 
+---------
+       1
+(1 row)
+
+SELECT 1 implies;
+ implies 
+---------
+       1
+(1 row)
+
 -- Casts
 SELECT 0::boolean;
  bool 
 ------
  f
 (1 row)
 
 SELECT 1::boolean;
  bool 
 ------
diff --git a/src/test/regress/sql/boolean.sql b/src/test/regress/sql/boolean.sql
index 85c6b019882..08265b531e2 100644
--- a/src/test/regress/sql/boolean.sql
+++ b/src/test/regress/sql/boolean.sql
@@ -243,20 +243,62 @@ SELECT isnul AND istrue AND isfalse FROM booltbl4;
 
 -- OR expression need to return null if there's any nulls and none
 -- of the value is true
 SELECT isfalse OR isnul OR isfalse FROM booltbl4;
 SELECT isfalse OR isfalse OR isnul FROM booltbl4;
 SELECT isnul OR isfalse OR isfalse FROM booltbl4;
 SELECT isfalse OR isnul OR istrue FROM booltbl4;
 SELECT istrue OR isfalse OR isnul FROM booltbl4;
 SELECT isnul OR istrue OR isfalse FROM booltbl4;
 
+-- Implication: "a IMPLIES b" is "NOT a OR b".  It is not commutative, so all
+-- nine combinations of three-valued logic have to be checked.
+SELECT istrue IMPLIES istrue, istrue IMPLIES isfalse, istrue IMPLIES isnul
+  FROM booltbl4;
+SELECT isfalse IMPLIES istrue, isfalse IMPLIES isfalse, isfalse IMPLIES isnul
+  FROM booltbl4;
+SELECT isnul IMPLIES istrue, isnul IMPLIES isfalse, isnul IMPLIES isnul
+  FROM booltbl4;
+
+-- the same, as constants, so that constant folding is exercised too
+SELECT true IMPLIES true, true IMPLIES false, true IMPLIES null;
+SELECT false IMPLIES true, false IMPLIES false, false IMPLIES null;
+SELECT null IMPLIES true, null IMPLIES false, null::bool IMPLIES null;
+
+-- IMPLIES is right-associative: false IMPLIES (true IMPLIES false), rather
+-- than (false IMPLIES true) IMPLIES false
+SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4;
+
+-- and binds looser than OR, AND, NOT, the comparison operators and IS
+SELECT istrue OR isfalse IMPLIES isfalse FROM booltbl4;
+SELECT isfalse IMPLIES isfalse AND isfalse FROM booltbl4;
+SELECT NOT istrue IMPLIES istrue FROM booltbl4;
+SELECT 1 = 1 IMPLIES 2 = 3;
+SELECT istrue IMPLIES isnul IS NULL FROM booltbl4;
+
+-- non-boolean operands are reported in terms of IMPLIES, not of its expansion
+SELECT 1 IMPLIES true;                  -- error
+SELECT true IMPLIES 1;                  -- error
+
+-- the construct is expanded during parse analysis, so this is what is stored
+CREATE VIEW boolview AS SELECT istrue IMPLIES isnul AS i FROM booltbl4;
+SELECT pg_get_viewdef('boolview', true);
+DROP VIEW boolview;
+
+-- IMPLIES is unreserved, so it remains usable as an identifier
+CREATE TABLE implies (implies bool);
+INSERT INTO implies VALUES (false);
+SELECT implies IMPLIES implies FROM implies;
+DROP TABLE implies;
+SELECT 1 AS implies;
+SELECT 1 implies;
+
 -- Casts
 SELECT 0::boolean;
 SELECT 1::boolean;
 SELECT 2::boolean;
 
 
 --
 -- Clean up
 -- Many tables are retained by the regression test, but these do not seem
 --  particularly useful so just get rid of them for now.
-- 
2.55.0



view thread (18+ messages)  latest in thread

Message-ID: <37c76707-e6fa-4ee3-b57f-aa0bf1b0bba8@postgresfriends.org>
Permalink:  ../37c76707-e6fa-4ee3-b57f-aa0bf1b0bba8@postgresfriends.org/
Also on:    postgresql.org/message-id/37c76707-e6fa-4ee3-b57f-aa0bf1b0bba8@postgresfriends.org

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: pgsql-hackers@postgresql.org
  Cc: vik@postgresfriends.org, pgsql-hackers@lists.postgresql.org
  Subject: Re: Logical Implication
  In-Reply-To: <37c76707-e6fa-4ee3-b57f-aa0bf1b0bba8@postgresfriends.org>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox