agora inbox for pgsql-hackers@postgresql.org
help / color / mirror / Atom feedLogical Implication
18+ messages / 7 participants
[nested] [flat]
* Logical Implication
@ 2026-09-11 13:35 Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 14:41 ` Re: Logical Implication Thom Brown <thom@linux.com>
0 siblings, 2 replies; 18+ messages in thread
From: Vik Fearing @ 2026-09-11 13:35 UTC (permalink / raw)
To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
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
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-09-11 14:10 ` Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
1 sibling, 1 reply; 18+ messages in thread
From: Nathan Bossart @ 2026-09-11 14:10 UTC (permalink / raw)
To: Vik Fearing <vik@postgresfriends.org>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Fri, Sep 11, 2026 at 03:35:57PM +0200, Vik Fearing wrote:
> 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 am probably not the target audience for a feature like this, but I'm not
sure I find the proposed new syntax to be substantially more readable. For
example, something like
NOT shipped OR shipped_date IS NOT NULL
would translate to
shipped IMPLIES shipped_date IS NOT NULL
In my head, I read the first as something like "either the package is not
shipped or the shipping date is set," and the latter as "if the package is
shipped then the shipping date is set." So I guess that does save me one
round of negation; ISTM that's the main benefit.
> 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.
Even though there may be precedence, I think the proposal would be
strengthened by teaching it to survive the round trip. Since the benefit
is readability, presumably it would be useful in situations where you're
reading a CHECK constraint that someone else wrote.
--
nathan
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
@ 2026-09-11 16:12 ` Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 18+ messages in thread
From: Vik Fearing @ 2026-09-11 16:12 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi Nathan,
Thanks for taking a look at it.
On 11/09/2026 16:10, Nathan Bossart wrote:
> On Fri, Sep 11, 2026 at 03:35:57PM +0200, Vik Fearing wrote:
>> 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 am probably not the target audience for a feature like this, but I'm not
> sure I find the proposed new syntax to be substantially more readable. For
> example, something like
>
> NOT shipped OR shipped_date IS NOT NULL
>
> would translate to
>
> shipped IMPLIES shipped_date IS NOT NULL
>
> In my head, I read the first as something like "either the package is not
> shipped or the shipping date is set," and the latter as "if the package is
> shipped then the shipping date is set." So I guess that does save me one
> round of negation; ISTM that's the main benefit.
Yes. To me, the latter is more intuitive.
>> 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.
> Even though there may be precedence, I think the proposal would be
> strengthened by teaching it to survive the round trip. Since the benefit
> is readability, presumably it would be useful in situations where you're
> reading a CHECK constraint that someone else wrote.
Perhaps. There is also the precedent of LIKE coming back as ~~ which I
think is a lot worse than getting NOT a OR b instead of a IMPLIES b. I
am happy to do the work to round trip IMPLIES but I will wait for more
opinions before I do so. My own opinion is that it's not worth it.
--
Vik Fearing
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-09-24 14:33 ` Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
0 siblings, 1 reply; 18+ messages in thread
From: Nathan Bossart @ 2026-09-24 14:33 UTC (permalink / raw)
To: Vik Fearing <vik@postgresfriends.org>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
I spent some time looking at this one from a few different angles.
* NULL behavior: while not a conscious decision of your patch, a straight
translation of "a IMPLIES b" to "NOT a OR b" would settle the standard on
Kleene logic for material implication [0]. I think that's okay; IIUC that
would be the case for any popular SQL database that implements it in a
similar fashion. According to Wikipedia, SQL uses "a comment fragment of
the Kleene and Łukasiewicz three-valued logic (which differ in their
definition of implication; however, SQL defines no such operation)" [1].
This proposal changes that, so it seems worth calling out.
* The name: it's hard to imagine another name for this operation.
"implies" is pretty commonly used for material implication in other
languages, and it reads nicely, so I have no concerns here.
* Short circuiting: IIUC the new IMPLIES operation wouldn't be guaranteed
to short-circuit, which matches the behavior of AND and OR and thus is
probably fine.
* Past discussion: I only found a very brief past discussion about this
that quickly veered into a discussion about exclusive-OR [2].
* Precedence: Placing it below OR seems to be pretty common, so no concerns
there. I don't know where it belongs in relation to INTERSECT, UNION, or
EXCEPT, though. Any thoughts about that?
* Associativity: The proposal chooses right associativity. I thought about
this part the most, and I'm not sure any associativity is desirable. I
think we should forbid chained implications for two reasons: 1) if we do it
one way and the SQL committee chooses the other, we're in a tight spot, and
2) I cannot come up with any natural examples to illustrate the desired
behavior. Take the following example:
shipped IMPLIES zip code set IMPLIES ship date in the past
Under right associativity, this would translate to
NOT shipped OR NOT zip code set OR ship date in the past
The former reads as "if shipped, then the zip code is set and the ship date
is in the past", but the latter is pretty obviously not that. If "shipped"
is true and "zip code" is not set, it would return true, for example.
Granted, the user probably should have written
shipped IMPLIES (zip code set AND ship date in the past)
but that feels like an easy mistake to make, at least to me. I'm not sure
left associativity is any better in this regard.
On Fri, Sep 11, 2026 at 06:12:51PM +0200, Vik Fearing wrote:
> On 11/09/2026 16:10, Nathan Bossart wrote:
>> On Fri, Sep 11, 2026 at 03:35:57PM +0200, Vik Fearing wrote:
>>> 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.
>>
>> Even though there may be precedence, I think the proposal would be
>> strengthened by teaching it to survive the round trip. Since the benefit
>> is readability, presumably it would be useful in situations where you're
>> reading a CHECK constraint that someone else wrote.
>
> Perhaps. There is also the precedent of LIKE coming back as ~~ which I think
> is a lot worse than getting NOT a OR b instead of a IMPLIES b. I am happy
> to do the work to round trip IMPLIES but I will wait for more opinions
> before I do so. My own opinion is that it's not worth it.
I'm fine with leaving that out for now.
[0] https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics
[1] https://en.wikipedia.org/wiki/Null_(SQL)#Comparisons_with_NULL_and_the_three-valued_logic_(3VL)
[2] https://postgr.es/m/flat/61B8D172-D35A-4715-A793-6EFDD2795E5D%40epcylon.com
--
nathan
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
@ 2026-09-29 15:20 ` Vik Fearing <vik@postgresfriends.org>
2026-09-29 16:30 ` Re: Logical Implication Jacob Champion <jacob.champion@enterprisedb.com>
2026-09-29 20:23 ` Re: Logical Implication Zsolt Parragi <zsolt.parragi@percona.com>
0 siblings, 2 replies; 18+ messages in thread
From: Vik Fearing @ 2026-09-29 15:20 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On 24/09/2026 16:33, Nathan Bossart wrote:
> I spent some time looking at this one from a few different angles.
Thanks!
> * NULL behavior: while not a conscious decision of your patch, a straight
> translation of "a IMPLIES b" to "NOT a OR b" would settle the standard on
> Kleene logic for material implication [0]. I think that's okay; IIUC that
> would be the case for any popular SQL database that implements it in a
> similar fashion. According to Wikipedia, SQL uses "a comment fragment of
> the Kleene and Łukasiewicz three-valued logic (which differ in their
> definition of implication; however, SQL defines no such operation)" [1].
> This proposal changes that, so it seems worth calling out.
I've added some documentation for this.
> * Short circuiting: IIUC the new IMPLIES operation wouldn't be guaranteed
> to short-circuit, which matches the behavior of AND and OR and thus is
> probably fine.
Yeah, SQL is not a short-circuiting language, and since this gets
transformed during parse analysis, it inherits whatever short-circuiting
postgres does do.
> * Precedence: Placing it below OR seems to be pretty common, so no concerns
> there. I don't know where it belongs in relation to INTERSECT, UNION, or
> EXCEPT, though. Any thoughts about that?
It doesn't belong anywhere in relation to those. Those are set
operators and this is a boolean operator.
> * Associativity: The proposal chooses right associativity. I thought about
> this part the most, and I'm not sure any associativity is desirable. I
> think we should forbid chained implications for two reasons: 1) if we do it
> one way and the SQL committee chooses the other, we're in a tight spot, and
I will make sure this is settled with the committee long before v20 hits
feature freeze. I don't expect them to contradict me on this because
all languages that have this operator (e.g. Haskell, Rocq, Agda, TLA+,
SMT-LIB, Alloy, Isabelle, etc) all use right associativity.
The precedent for right associativity is enormous.
> 2) I cannot come up with any natural examples to illustrate the desired
> behavior. Take the following example:
>
> shipped IMPLIES zip code set IMPLIES ship date in the past
>
> Under right associativity, this would translate to
>
> NOT shipped OR NOT zip code set OR ship date in the past
>
> The former reads as "if shipped, then the zip code is set and the ship date
> is in the past", but the latter is pretty obviously not that. If "shipped"
> is true and "zip code" is not set, it would return true, for example.
> Granted, the user probably should have written
>
> shipped IMPLIES (zip code set AND ship date in the past)
>
> but that feels like an easy mistake to make, at least to me. I'm not sure
> left associativity is any better in this regard.
I disagree with you here.
a IMPLIES b IMPLIES c
NOT a OR (NOT b OR c)
(NOT a OR NOT b) OR c
NOT (a AND b) OR c
(a AND b) IMPLIES c
which reads as "if shipped and the zip code is set then the ship date is
in the past".
In your example, if not shipped or the zip code is not set, the
antecedent is not true so the implication asserts nothing and is
satisfied, so your conclusion that it would return true is correct.
Attached is a v2 with doc and test improvements. No code changes.
--
Vik Fearing
From 3af33be59146fb650a2c3026638799ddd8824652 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@chouppes.com>
Date: Thu, 10 Sep 2026 14:30:51 +0200
Subject: [PATCH v2 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
@@ -46,89 +46,100 @@
</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>
--
2.55.0
From 796d8ced7eb30bcd4357093742a141171aa8bd26 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@chouppes.com>
Date: Thu, 10 Sep 2026 14:31:03 +0200
Subject: [PATCH v2 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.
Because NOT and OR are already Kleene operators, that expansion settles
the three-valued truth table on Kleene implication, where Unknown
IMPLIES Unknown is Unknown. SQL has until now used only the fragment
of three-valued logic on which Kleene and Lukasiewicz agree, and
implication is exactly where the two differ: Lukasiewicz would make
Unknown IMPLIES Unknown be True, which could not then be written as
NOT a OR b.
Right associativity makes a chain equivalent to a single implication
whose antecedent is the conjunction of the leading operands, so
a IMPLIES b IMPLIES c is (a AND b) IMPLIES c.
---
doc/src/sgml/func/func-logical.sgml | 80 ++++++++++++++
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 | 149 ++++++++++++++++++++++++++
src/test/regress/sql/boolean.sql | 60 +++++++++++
10 files changed, 351 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/func/func-logical.sgml b/doc/src/sgml/func/func-logical.sgml
index 5ba6389db56..4769202e805 100644
--- a/doc/src/sgml/func/func-logical.sgml
+++ b/doc/src/sgml/func/func-logical.sgml
@@ -39,10 +39,19 @@
<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,
@@ -146,6 +155,77 @@
</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>
+ Chaining implications is therefore the same as a single implication whose
+ antecedent is the conjunction of the leading operands:
+ <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ <replaceable>b</replaceable> <literal>IMPLIES</literal>
+ <replaceable>c</replaceable> is equivalent to
+ (<replaceable>a</replaceable> <literal>AND</literal>
+ <replaceable>b</replaceable>) <literal>IMPLIES</literal>
+ <replaceable>c</replaceable>. It is not equivalent to
+ <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ (<replaceable>b</replaceable> <literal>AND</literal>
+ <replaceable>c</replaceable>); that meaning has to be written with
+ explicit parentheses.
+ </para>
+
<para>
The operators <literal>AND</literal> and <literal>OR</literal> are
commutative, that is, you can switch the left and right operands
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
@@ -1103,6 +1103,12 @@ CAST ( '<replaceable>string</replaceable>' AS <replaceable>type</replaceable> )
<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>
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
@@ -643,6 +643,10 @@ _outA_Expr(StringInfo str, const A_Expr *node)
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;
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
@@ -514,6 +514,11 @@ _readA_Expr(ReadNodeContext *ctx)
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;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0563453fe24..cd70db96c61 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -749,7 +749,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
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
@@ -844,6 +845,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
/* Precedence: lowest to highest */
%left UNION EXCEPT
%left INTERSECT
+%right IMPLIES
%left OR
%left AND
%right NOT
@@ -15374,6 +15376,11 @@ a_expr: c_expr { $$ = $1; }
{ $$ = 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
@@ -18190,6 +18197,7 @@ unreserved_keyword:
| IMMEDIATE
| IMMUTABLE
| IMPLICIT_P
+ | IMPLIES
| IMPORT_P
| INCLUDE
| INCLUDING
@@ -18782,6 +18790,7 @@ bare_label_keyword:
| IMMEDIATE
| IMMUTABLE
| IMPLICIT_P
+ | IMPLIES
| IMPORT_P
| IN_P
| INCLUDE
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 05a6b72d4c9..3f9dc86fa00 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ 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);
@@ -213,6 +214,9 @@ transformExprRecurse(ParseState *pstate, Node *expr)
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 */
@@ -1446,6 +1450,37 @@ transformBoolExpr(ParseState *pstate, BoolExpr *a)
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)
{
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0debcd193ab..45fa8c70263 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -342,6 +342,7 @@ typedef enum A_Expr_Kind
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
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 53ae96c0399..94d2aef1bb1 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -207,6 +207,7 @@ 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)
diff --git a/src/test/regress/expected/boolean.out b/src/test/regress/expected/boolean.out
index 0e99eb7ffc0..caa63f51f40 100644
--- a/src/test/regress/expected/boolean.out
+++ b/src/test/regress/expected/boolean.out
@@ -566,6 +566,155 @@ SELECT isnul OR istrue OR isfalse FROM booltbl4;
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)
+
+-- so a chain is implication from the conjunction of the leading operands
+-- (exportation), not implication of their conjunction. The second row is the
+-- interesting one: it tells the two apart.
+SELECT a, b, c,
+ a IMPLIES b IMPLIES c AS chained,
+ (a AND b) IMPLIES c AS conj_antecedent,
+ a IMPLIES (b AND c) AS conj_consequent
+ FROM (VALUES (true, true, true),
+ (true, false, false),
+ (false, true, false)) AS t(a, b, c);
+ a | b | c | chained | conj_antecedent | conj_consequent
+---+---+---+---------+-----------------+-----------------
+ t | t | t | t | t | t
+ t | f | f | t | t | f
+ f | t | f | t | t | t
+(3 rows)
+
+-- exportation holds for all three truth values, so this returns no rows
+SELECT a, b, c
+ FROM (VALUES (true), (false), (null)) AS x(a),
+ (VALUES (true), (false), (null)) AS y(b),
+ (VALUES (true), (false), (null)) AS z(c)
+ WHERE (a IMPLIES b IMPLIES c) IS DISTINCT FROM ((a AND b) IMPLIES c);
+ a | b | c
+---+---+---
+(0 rows)
+
+-- 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
diff --git a/src/test/regress/sql/boolean.sql b/src/test/regress/sql/boolean.sql
index 85c6b019882..f288cf46726 100644
--- a/src/test/regress/sql/boolean.sql
+++ b/src/test/regress/sql/boolean.sql
@@ -250,6 +250,66 @@ 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;
+
+-- so a chain is implication from the conjunction of the leading operands
+-- (exportation), not implication of their conjunction. The second row is the
+-- interesting one: it tells the two apart.
+SELECT a, b, c,
+ a IMPLIES b IMPLIES c AS chained,
+ (a AND b) IMPLIES c AS conj_antecedent,
+ a IMPLIES (b AND c) AS conj_consequent
+ FROM (VALUES (true, true, true),
+ (true, false, false),
+ (false, true, false)) AS t(a, b, c);
+
+-- exportation holds for all three truth values, so this returns no rows
+SELECT a, b, c
+ FROM (VALUES (true), (false), (null)) AS x(a),
+ (VALUES (true), (false), (null)) AS y(b),
+ (VALUES (true), (false), (null)) AS z(c)
+ WHERE (a IMPLIES b IMPLIES c) IS DISTINCT FROM ((a AND b) IMPLIES c);
+
+-- 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;
--
2.55.0
Attachments:
[text/plain] v2-0001-doc-Rearrange-the-logical-operator-truth-tables.patch (4.5K, ../../9ab35dbb-d9a4-4c1e-bae2-7763ddeb720c@postgresfriends.org/2-v2-0001-doc-Rearrange-the-logical-operator-truth-tables.patch)
download | inline diff:
From 3af33be59146fb650a2c3026638799ddd8824652 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@chouppes.com>
Date: Thu, 10 Sep 2026 14:30:51 +0200
Subject: [PATCH v2 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
@@ -46,89 +46,100 @@
</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>
--
2.55.0
[text/plain] v2-0002-Add-the-IMPLIES-boolean-operator.patch (19.2K, ../../9ab35dbb-d9a4-4c1e-bae2-7763ddeb720c@postgresfriends.org/3-v2-0002-Add-the-IMPLIES-boolean-operator.patch)
download | inline diff:
From 796d8ced7eb30bcd4357093742a141171aa8bd26 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@chouppes.com>
Date: Thu, 10 Sep 2026 14:31:03 +0200
Subject: [PATCH v2 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.
Because NOT and OR are already Kleene operators, that expansion settles
the three-valued truth table on Kleene implication, where Unknown
IMPLIES Unknown is Unknown. SQL has until now used only the fragment
of three-valued logic on which Kleene and Lukasiewicz agree, and
implication is exactly where the two differ: Lukasiewicz would make
Unknown IMPLIES Unknown be True, which could not then be written as
NOT a OR b.
Right associativity makes a chain equivalent to a single implication
whose antecedent is the conjunction of the leading operands, so
a IMPLIES b IMPLIES c is (a AND b) IMPLIES c.
---
doc/src/sgml/func/func-logical.sgml | 80 ++++++++++++++
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 | 149 ++++++++++++++++++++++++++
src/test/regress/sql/boolean.sql | 60 +++++++++++
10 files changed, 351 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/func/func-logical.sgml b/doc/src/sgml/func/func-logical.sgml
index 5ba6389db56..4769202e805 100644
--- a/doc/src/sgml/func/func-logical.sgml
+++ b/doc/src/sgml/func/func-logical.sgml
@@ -39,10 +39,19 @@
<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,
@@ -146,6 +155,77 @@
</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>
+ Chaining implications is therefore the same as a single implication whose
+ antecedent is the conjunction of the leading operands:
+ <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ <replaceable>b</replaceable> <literal>IMPLIES</literal>
+ <replaceable>c</replaceable> is equivalent to
+ (<replaceable>a</replaceable> <literal>AND</literal>
+ <replaceable>b</replaceable>) <literal>IMPLIES</literal>
+ <replaceable>c</replaceable>. It is not equivalent to
+ <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ (<replaceable>b</replaceable> <literal>AND</literal>
+ <replaceable>c</replaceable>); that meaning has to be written with
+ explicit parentheses.
+ </para>
+
<para>
The operators <literal>AND</literal> and <literal>OR</literal> are
commutative, that is, you can switch the left and right operands
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
@@ -1103,6 +1103,12 @@ CAST ( '<replaceable>string</replaceable>' AS <replaceable>type</replaceable> )
<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>
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
@@ -643,6 +643,10 @@ _outA_Expr(StringInfo str, const A_Expr *node)
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;
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
@@ -514,6 +514,11 @@ _readA_Expr(ReadNodeContext *ctx)
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;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index 0563453fe24..cd70db96c61 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -749,7 +749,8 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
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
@@ -844,6 +845,7 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query);
/* Precedence: lowest to highest */
%left UNION EXCEPT
%left INTERSECT
+%right IMPLIES
%left OR
%left AND
%right NOT
@@ -15374,6 +15376,11 @@ a_expr: c_expr { $$ = $1; }
{ $$ = 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
@@ -18190,6 +18197,7 @@ unreserved_keyword:
| IMMEDIATE
| IMMUTABLE
| IMPLICIT_P
+ | IMPLIES
| IMPORT_P
| INCLUDE
| INCLUDING
@@ -18782,6 +18790,7 @@ bare_label_keyword:
| IMMEDIATE
| IMMUTABLE
| IMPLICIT_P
+ | IMPLIES
| IMPORT_P
| IN_P
| INCLUDE
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 05a6b72d4c9..3f9dc86fa00 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -54,6 +54,7 @@ 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);
@@ -213,6 +214,9 @@ transformExprRecurse(ParseState *pstate, Node *expr)
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 */
@@ -1446,6 +1450,37 @@ transformBoolExpr(ParseState *pstate, BoolExpr *a)
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)
{
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index 0debcd193ab..45fa8c70263 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -342,6 +342,7 @@ typedef enum A_Expr_Kind
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
diff --git a/src/include/parser/kwlist.h b/src/include/parser/kwlist.h
index 53ae96c0399..94d2aef1bb1 100644
--- a/src/include/parser/kwlist.h
+++ b/src/include/parser/kwlist.h
@@ -207,6 +207,7 @@ 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)
diff --git a/src/test/regress/expected/boolean.out b/src/test/regress/expected/boolean.out
index 0e99eb7ffc0..caa63f51f40 100644
--- a/src/test/regress/expected/boolean.out
+++ b/src/test/regress/expected/boolean.out
@@ -566,6 +566,155 @@ SELECT isnul OR istrue OR isfalse FROM booltbl4;
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)
+
+-- so a chain is implication from the conjunction of the leading operands
+-- (exportation), not implication of their conjunction. The second row is the
+-- interesting one: it tells the two apart.
+SELECT a, b, c,
+ a IMPLIES b IMPLIES c AS chained,
+ (a AND b) IMPLIES c AS conj_antecedent,
+ a IMPLIES (b AND c) AS conj_consequent
+ FROM (VALUES (true, true, true),
+ (true, false, false),
+ (false, true, false)) AS t(a, b, c);
+ a | b | c | chained | conj_antecedent | conj_consequent
+---+---+---+---------+-----------------+-----------------
+ t | t | t | t | t | t
+ t | f | f | t | t | f
+ f | t | f | t | t | t
+(3 rows)
+
+-- exportation holds for all three truth values, so this returns no rows
+SELECT a, b, c
+ FROM (VALUES (true), (false), (null)) AS x(a),
+ (VALUES (true), (false), (null)) AS y(b),
+ (VALUES (true), (false), (null)) AS z(c)
+ WHERE (a IMPLIES b IMPLIES c) IS DISTINCT FROM ((a AND b) IMPLIES c);
+ a | b | c
+---+---+---
+(0 rows)
+
+-- 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
diff --git a/src/test/regress/sql/boolean.sql b/src/test/regress/sql/boolean.sql
index 85c6b019882..f288cf46726 100644
--- a/src/test/regress/sql/boolean.sql
+++ b/src/test/regress/sql/boolean.sql
@@ -250,6 +250,66 @@ 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;
+
+-- so a chain is implication from the conjunction of the leading operands
+-- (exportation), not implication of their conjunction. The second row is the
+-- interesting one: it tells the two apart.
+SELECT a, b, c,
+ a IMPLIES b IMPLIES c AS chained,
+ (a AND b) IMPLIES c AS conj_antecedent,
+ a IMPLIES (b AND c) AS conj_consequent
+ FROM (VALUES (true, true, true),
+ (true, false, false),
+ (false, true, false)) AS t(a, b, c);
+
+-- exportation holds for all three truth values, so this returns no rows
+SELECT a, b, c
+ FROM (VALUES (true), (false), (null)) AS x(a),
+ (VALUES (true), (false), (null)) AS y(b),
+ (VALUES (true), (false), (null)) AS z(c)
+ WHERE (a IMPLIES b IMPLIES c) IS DISTINCT FROM ((a AND b) IMPLIES c);
+
+-- 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;
--
2.55.0
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-09-29 16:30 ` Jacob Champion <jacob.champion@enterprisedb.com>
2026-09-29 16:40 ` Re: Logical Implication Isaac Morland <isaac.morland@gmail.com>
1 sibling, 1 reply; 18+ messages in thread
From: Jacob Champion @ 2026-09-29 16:30 UTC (permalink / raw)
To: Vik Fearing <vik@postgresfriends.org>; Nathan Bossart <nathandbossart@gmail.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Tue, Sep 29, 2026 at 8:20 AM Vik Fearing <vik@postgresfriends.org> wrote:
> On 24/09/2026 16:33, Nathan Bossart wrote:
> > 2) I cannot come up with any natural examples to illustrate the desired
> > behavior. Take the following example:
> >
> > shipped IMPLIES zip code set IMPLIES ship date in the past
> >
> > Under right associativity, this would translate to
> >
> > NOT shipped OR NOT zip code set OR ship date in the past
> >
> > The former reads as "if shipped, then the zip code is set and the ship date
> > is in the past", but the latter is pretty obviously not that.
For the very little it's worth, I agree with Nathan. I tend to intuit
A => B => C
as meaning
(A => B) and (B => C),
probably because that's kind of how you would write it in an informal
proof? Forcing parentheses would make you write what is meant, which
is a property I enjoy when I'm trying to understand SQL queries
written by someone who is not a logician or a Haskell programmer. :D
> I will make sure this is settled with the committee long before v20 hits
> feature freeze. I don't expect them to contradict me on this because
> all languages that have this operator (e.g. Haskell, Rocq, Agda, TLA+,
> SMT-LIB, Alloy, Isabelle, etc) all use right associativity.
>
> The precedent for right associativity is enormous.
(Well, but that's presumably because those languages are tightly tied
to function currying, proofs, and related mathematics, right? Doesn't
feel like that applies to your typical CHECK constraint.)
Either way, you don't really need to convince me; I just wanted to +1
Nathan's "no associativity" idea and run away. And associative or not,
IMPLIES is a lot nicer than the NOT-OR construction.
Thanks!
--Jacob
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-29 16:30 ` Re: Logical Implication Jacob Champion <jacob.champion@enterprisedb.com>
@ 2026-09-29 16:40 ` Isaac Morland <isaac.morland@gmail.com>
0 siblings, 0 replies; 18+ messages in thread
From: Isaac Morland @ 2026-09-29 16:40 UTC (permalink / raw)
To: Jacob Champion <jacob.champion@enterprisedb.com>; +Cc: Vik Fearing <vik@postgresfriends.org>; Nathan Bossart <nathandbossart@gmail.com>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Tue, 29 Sept 2026 at 12:30, Jacob Champion <
jacob.champion@enterprisedb.com> wrote:
Either way, you don't really need to convince me; I just wanted to +1
> Nathan's "no associativity" idea and run away. And associative or not,
> IMPLIES is a lot nicer than the NOT-OR construction.
>
Probably good. I can easily imagine myself seeing a IMPLIES b IMPLIES c and
reading (a IMPLIES b) AND (b IMPLIES c) even though I know that's not how
Postgres operators work.
Personally, I currently spell IMPLIES as "<=" which would be about as good
as you could expect, except that the usual mathematical
implication operator visually appears much closer to "=>". Also any NULL
input at all results in NULL output even when interpreting the NULL as
"unknown" would result in a TRUE result.
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-09-29 20:23 ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-29 21:51 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
1 sibling, 1 reply; 18+ messages in thread
From: Zsolt Parragi @ 2026-09-29 20:23 UTC (permalink / raw)
To: Vik Fearing <vik@postgresfriends.org>; +Cc: pgsql-hackers@lists.postgresql.org, Nathan Bossart <nathandbossart@gmail.com>
Hello!
One test nitpick:
+SELECT istrue IMPLIES isnul IS NULL FROM booltbl4;
istrue IMPLIES (isnul IS NULL) and (istrue IMPLIES isnul) IS NULL are
both true. isfalse wouldn't have the same issue.
%left INTERSECT
+%right IMPLIES
%left OR
And +1 for using %nonassoc instead
That way "a IMPLIES b IMPLIES c" would be a syntax error, leaving open
the later possibility of making it right associative. The other way of
going with right, and then restricting it later doesn't seem that
clean, even with documenting it, as right associative use can appear
as text in plpgsql bodies and similar.
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-29 20:23 ` Re: Logical Implication Zsolt Parragi <zsolt.parragi@percona.com>
@ 2026-09-29 21:51 ` Vik Fearing <vik@postgresfriends.org>
2026-09-29 22:27 ` Re: Logical Implication Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-30 15:09 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 2 replies; 18+ messages in thread
From: Vik Fearing @ 2026-09-29 21:51 UTC (permalink / raw)
To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: pgsql-hackers@lists.postgresql.org, Nathan Bossart <nathandbossart@gmail.com>
On 29/09/2026 22:23, Zsolt Parragi wrote:
> Hello!
>
> One test nitpick:
>
> +SELECT istrue IMPLIES isnul IS NULL FROM booltbl4;
>
> istrue IMPLIES (isnul IS NULL) and (istrue IMPLIES isnul) IS NULL are
> both true. isfalse wouldn't have the same issue.
Sorry, I don't understand what you mean by this.
> %left INTERSECT
> +%right IMPLIES
> %left OR
>
>
> And +1 for using %nonassoc instead
That's three now. I'll prepare a new patch.
--
Vik Fearing
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-29 20:23 ` Re: Logical Implication Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-29 21:51 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-09-29 22:27 ` Zsolt Parragi <zsolt.parragi@percona.com>
1 sibling, 0 replies; 18+ messages in thread
From: Zsolt Parragi @ 2026-09-29 22:27 UTC (permalink / raw)
To: Vik Fearing <vik@postgresfriends.org>; +Cc: pgsql-hackers@lists.postgresql.org
> Sorry, I don't understand what you mean by this.
I meant that the current test case would result in true in both
ordering - so in this sense, it's not the best testcase:
SELECT (istrue IMPLIES isnul) IS NULL FROM booltbl4;
SELECT istrue IMPLIES (isnul IS NULL) FROM booltbl4;
are both true
If you change it to
SELECT isfalse IMPLIES isnul IS NULL FROM booltbl4;
Then those would have different results, making it a better testcase.
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-29 20:23 ` Re: Logical Implication Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-29 21:51 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-09-30 15:09 ` Nathan Bossart <nathandbossart@gmail.com>
2026-10-03 16:18 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
1 sibling, 1 reply; 18+ messages in thread
From: Nathan Bossart @ 2026-09-30 15:09 UTC (permalink / raw)
To: Vik Fearing <vik@postgresfriends.org>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org
On Tue, Sep 29, 2026 at 11:51:39PM +0200, Vik Fearing wrote:
> On 29/09/2026 22:23, Zsolt Parragi wrote:
>> And +1 for using %nonassoc instead
>
> That's three now. I'll prepare a new patch.
Thanks. One other thing I'd like to explore a bit more is whether to make
IMPLIES "round-trippable," at least to see how much more complicated it
would be. (I'm happy to do this part BTW).
--
nathan
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-29 20:23 ` Re: Logical Implication Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-29 21:51 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-30 15:09 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
@ 2026-10-03 16:18 ` Vik Fearing <vik@postgresfriends.org>
2026-10-06 18:19 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 1 reply; 18+ messages in thread
From: Vik Fearing @ 2026-10-03 16:18 UTC (permalink / raw)
To: Nathan Bossart <nathandbossart@gmail.com>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Jacob Champion <jacob.champion@enterprisedb.com>; Isaac Morland <isaac.morland@gmail.com>
On 30/09/2026 17:09, Nathan Bossart wrote:
> On Tue, Sep 29, 2026 at 11:51:39PM +0200, Vik Fearing wrote:
>> On 29/09/2026 22:23, Zsolt Parragi wrote:
>>> And +1 for using %nonassoc instead
>> That's three now. I'll prepare a new patch.
> Thanks. One other thing I'd like to explore a bit more is whether to make
> IMPLIES "round-trippable," at least to see how much more complicated it
> would be. (I'm happy to do this part BTW).
>
Attached is a new patch set that makes IMPLIES non-associative, changes
the tests, and also survives a round trip. I put the latter in a
separate patch in case it isn't wanted after all.
I think I have incorporated everyone's feedback, but let me know if I
missed something.
--
Vik Fearing
From 080d0997228b04a275fdf09308f5b78fce3c201d Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@postgresfriends.org>
Date: Thu, 10 Sep 2026 14:30:51 +0200
Subject: [PATCH v3 1/3] 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
base-commit: e5d25959cf8761c7dbdd7cfbb91338e94a40e8e2
--
2.56.0
From 4aaba1edcb64a70eb3ca1eeac63f6c5288df1d8b Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@postgresfriends.org>
Date: Thu, 10 Sep 2026 14:31:03 +0200
Subject: [PATCH v3 2/3] Add the IMPLIES boolean operator
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
a IMPLIES b is material implication, parsed as a non-associative
operator binding below OR and expanded to (NOT a OR b) in the parser, so
type errors are reported against IMPLIES itself.
Because NOT and OR are already Kleene operators, that expansion settles
the three-valued truth table on Kleene implication, where Unknown
IMPLIES Unknown is Unknown. SQL has until now used only the fragment
of three-valued logic on which Kleene and Łukasiewicz agree, and
implication is exactly where the two differ: Łukasiewicz would make
Unknown IMPLIES Unknown be True, which could not then be written as
NOT a OR b.
IMPLIES is non-associative, so a IMPLIES b IMPLIES c is a syntax error
and the intended grouping has to be parenthesized. The two groupings
are different formulas, there is no consensus on which one a chain ought
to mean, and the grammar declines to choose, just as it already does for
a = b = c. Nothing is foreclosed by that: if the standard later gives
IMPLIES an associativity, accepting chains is a one-line change, and no
query that is accepted today changes meaning.
---
doc/src/sgml/func/func-logical.sgml | 98 +++++++++++++++++
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 | 147 ++++++++++++++++++++++++++
src/test/regress/sql/boolean.sql | 60 +++++++++++
10 files changed, 367 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/func/func-logical.sgml b/doc/src/sgml/func/func-logical.sgml
index 5ba6389db56..0f3c6feb295 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,108 @@
<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, so
+ <literal>a = 1 IMPLIES b = 2 OR c = 3</literal> means
+ <literal>(a = 1) IMPLIES ((b = 2) OR (c = 3))</literal>. See <xref
+ linkend="sql-precedence"/>.
+ </para>
+
+ <para>
+ Implication is not associative, and <literal>IMPLIES</literal> therefore
+ does not chain: <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ <replaceable>b</replaceable> <literal>IMPLIES</literal>
+ <replaceable>c</replaceable> is a syntax error, just as
+ <literal>a = b = c</literal> is. The grouping has to be written
+ explicitly, and the two groupings mean different things:
+ (<replaceable>a</replaceable> <literal>IMPLIES</literal>
+ <replaceable>b</replaceable>) <literal>IMPLIES</literal>
+ <replaceable>c</replaceable> is not equivalent to
+ <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ (<replaceable>b</replaceable> <literal>IMPLIES</literal>
+ <replaceable>c</replaceable>).
+ </para>
+
+ <para>
+ Of the two, grouping to the right is usually the one wanted: it is the
+ same as a single implication whose antecedent is the conjunction of the
+ two leading operands, so <replaceable>a</replaceable>
+ <literal>IMPLIES</literal> (<replaceable>b</replaceable>
+ <literal>IMPLIES</literal> <replaceable>c</replaceable>) is equivalent to
+ (<replaceable>a</replaceable> <literal>AND</literal>
+ <replaceable>b</replaceable>) <literal>IMPLIES</literal>
+ <replaceable>c</replaceable>, that is, <quote>if both
+ <replaceable>a</replaceable> and <replaceable>b</replaceable>, then
+ <replaceable>c</replaceable></quote>. To say <quote>if
+ <replaceable>a</replaceable>, then both <replaceable>b</replaceable> and
+ <replaceable>c</replaceable></quote> instead, write
+ <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ (<replaceable>b</replaceable> <literal>AND</literal>
+ <replaceable>c</replaceable>).
+ </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..d0887e8677f 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></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 0563453fe24..73c3b1f4943 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -742,21 +742,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
@@ -837,20 +838,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
+%nonassoc 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
@@ -15367,20 +15369,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);
}
@@ -18183,20 +18190,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
@@ -18775,20 +18783,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 05a6b72d4c9..3f9dc86fa00 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);
@@ -1439,20 +1443,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 0debcd193ab..45fa8c70263 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -335,20 +335,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 53ae96c0399..94d2aef1bb1 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..5c092b09a5e 100644
--- a/src/test/regress/expected/boolean.out
+++ b/src/test/regress/expected/boolean.out
@@ -559,20 +559,167 @@ 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 non-associative, so a chain is refused rather than grouped
+SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4; -- error
+ERROR: syntax error at or near "IMPLIES"
+LINE 1: SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4;
+ ^
+-- and it has to be, because the two groupings are different formulas. The
+-- first row below is the interesting one: it tells them apart.
+SELECT a, b, c,
+ (a IMPLIES b) IMPLIES c AS grouped_left,
+ a IMPLIES (b IMPLIES c) AS grouped_right,
+ (a AND b) IMPLIES c AS conj_antecedent,
+ a IMPLIES (b AND c) AS conj_consequent
+ FROM (VALUES (false, false, false),
+ (true, false, false),
+ (true, true, true)) AS t(a, b, c);
+ a | b | c | grouped_left | grouped_right | conj_antecedent | conj_consequent
+---+---+---+--------------+---------------+-----------------+-----------------
+ f | f | f | f | t | t | t
+ t | f | f | t | t | t | f
+ t | t | t | t | t | t | t
+(3 rows)
+
+-- grouping to the right is implication from the conjunction of the operands
+-- (exportation), which holds for all three truth values, so no rows here
+SELECT a, b, c
+ FROM (VALUES (true), (false), (null)) AS x(a),
+ (VALUES (true), (false), (null)) AS y(b),
+ (VALUES (true), (false), (null)) AS z(c)
+ WHERE (a IMPLIES (b IMPLIES c)) IS DISTINCT FROM ((a AND b) IMPLIES c);
+ a | b | c
+---+---+---
+(0 rows)
+
+-- 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 isfalse 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..dfe96cb8ae5 100644
--- a/src/test/regress/sql/boolean.sql
+++ b/src/test/regress/sql/boolean.sql
@@ -243,20 +243,80 @@ 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 non-associative, so a chain is refused rather than grouped
+SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4; -- error
+
+-- and it has to be, because the two groupings are different formulas. The
+-- first row below is the interesting one: it tells them apart.
+SELECT a, b, c,
+ (a IMPLIES b) IMPLIES c AS grouped_left,
+ a IMPLIES (b IMPLIES c) AS grouped_right,
+ (a AND b) IMPLIES c AS conj_antecedent,
+ a IMPLIES (b AND c) AS conj_consequent
+ FROM (VALUES (false, false, false),
+ (true, false, false),
+ (true, true, true)) AS t(a, b, c);
+
+-- grouping to the right is implication from the conjunction of the operands
+-- (exportation), which holds for all three truth values, so no rows here
+SELECT a, b, c
+ FROM (VALUES (true), (false), (null)) AS x(a),
+ (VALUES (true), (false), (null)) AS y(b),
+ (VALUES (true), (false), (null)) AS z(c)
+ WHERE (a IMPLIES (b IMPLIES c)) IS DISTINCT FROM ((a AND b) IMPLIES c);
+
+-- 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 isfalse 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.56.0
From 00165798ea90aad84c0f059fdd6f8f112346b271 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@postgresfriends.org>
Date: Sat, 3 Oct 2026 17:18:22 +0200
Subject: [PATCH v3 3/3] Keep IMPLIES in stored expressions
Until now a IMPLIES b was expanded to (NOT a OR b) during parse analysis,
so views, check constraints, index expressions and predicates, and
policies were stored, deparsed, and dumped in terms of the expansion
rather than as written.
Make IMPLIES a fourth BoolExprType so that it survives into the stored
tree and ruleutils.c can print it back. In pretty mode, NOT, AND, and
OR beneath IMPLIES need no parentheses, while an IMPLIES beneath any
boolean operator, IMPLIES included, always gets them, since IMPLIES has
the lowest precedence and is not associative.
The expansion moves to eval_const_expressions, so the rest of the
planner, including qual canonicalization, predicate proving for partial
indexes, and outer join reduction, never sees IMPLIES and needs no
changes. EXPLAIN therefore shows the expanded and simplified form, much
as it does for BETWEEN.
The executor and postgres_fdw's deparser should not see an unexpanded
IMPLIES either, but each handles one anyway, as (NOT a OR b), rather than
fail. The executor needs no new opcodes for that, so JIT is unaffected,
and the deparser sends the expansion because the remote server might
not know IMPLIES.
---
contrib/postgres_fdw/deparse.c | 12 ++++
src/backend/executor/execExpr.c | 20 +++++-
src/backend/nodes/outfuncs.c | 3 +
src/backend/nodes/readfuncs.c | 2 +
src/backend/optimizer/util/clauses.c | 23 ++++++-
src/backend/parser/parse_expr.c | 18 ++---
src/backend/utils/adt/ruleutils.c | 22 +++++-
src/include/nodes/primnodes.h | 11 ++-
src/test/regress/expected/boolean.out | 97 +++++++++++++++++++++++++--
src/test/regress/sql/boolean.sql | 39 ++++++++++-
10 files changed, 220 insertions(+), 27 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ff9fe0f87e4..69a8529f583 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -3742,20 +3742,32 @@ deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context)
op = "AND";
break;
case OR_EXPR:
op = "OR";
break;
case NOT_EXPR:
appendStringInfoString(buf, "(NOT ");
deparseExpr(linitial(node->args), context);
appendStringInfoChar(buf, ')');
return;
+ case IMPLIES_EXPR:
+
+ /*
+ * eval_const_expressions should already have expanded this, but
+ * if not, send the expansion, which any remote server accepts.
+ */
+ appendStringInfoString(buf, "((NOT ");
+ deparseExpr(linitial(node->args), context);
+ appendStringInfoString(buf, ") OR ");
+ deparseExpr(lsecond(node->args), context);
+ appendStringInfoChar(buf, ')');
+ return;
}
appendStringInfoChar(buf, '(');
first = true;
foreach(lc, node->args)
{
if (!first)
appendStringInfo(buf, " %s ", op);
deparseExpr((Expr *) lfirst(lc), context);
first = false;
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 82e846a1f4f..6c069e56b50 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1379,21 +1379,21 @@ ExecInitExprRec(Expr *node, ExprState *state,
}
case T_BoolExpr:
{
BoolExpr *boolexpr = (BoolExpr *) node;
int nargs = list_length(boolexpr->args);
List *adjust_jumps = NIL;
int off;
ListCell *lc;
- /* allocate scratch memory used by all steps of AND/OR */
+ /* allocate scratch memory used by all steps of AND/OR/IMPLIES */
if (boolexpr->boolop != NOT_EXPR)
scratch.d.boolexpr.anynull = palloc_object(bool);
/*
* For each argument evaluate the argument itself, then
* perform the bool operation's appropriate handling.
*
* We can evaluate each argument into our result area, since
* the short-circuiting logic means we only need to remember
* previous NULL values.
@@ -1432,20 +1432,38 @@ ExecInitExprRec(Expr *node, ExprState *state,
else if (off + 1 == nargs)
scratch.opcode = EEOP_BOOL_OR_STEP_LAST;
else
scratch.opcode = EEOP_BOOL_OR_STEP;
break;
case NOT_EXPR:
Assert(nargs == 1);
scratch.opcode = EEOP_BOOL_NOT_STEP;
break;
+ case IMPLIES_EXPR:
+
+ /*
+ * The planner normally expands this, but in case
+ * it didn't, evaluate it as NOT a OR b. The NOT
+ * step doesn't jump, so it needs no adjusting.
+ */
+ Assert(nargs == 2);
+
+ if (off == 0)
+ {
+ scratch.opcode = EEOP_BOOL_NOT_STEP;
+ ExprEvalPushStep(state, &scratch);
+ scratch.opcode = EEOP_BOOL_OR_STEP_FIRST;
+ }
+ else
+ scratch.opcode = EEOP_BOOL_OR_STEP_LAST;
+ break;
default:
elog(ERROR, "unrecognized boolop: %d",
(int) boolexpr->boolop);
break;
}
scratch.d.boolexpr.jumpdone = -1;
ExprEvalPushStep(state, &scratch);
adjust_jumps = lappend_int(adjust_jumps,
state->steps_len - 1);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 9a35ff53de0..75b7856725b 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -415,20 +415,23 @@ _outBoolExpr(StringInfo str, const BoolExpr *node)
{
case AND_EXPR:
opstr = "and";
break;
case OR_EXPR:
opstr = "or";
break;
case NOT_EXPR:
opstr = "not";
break;
+ case IMPLIES_EXPR:
+ opstr = "implies";
+ break;
}
appendStringInfoString(str, " :boolop ");
outToken(str, opstr);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
}
static void
_outForeignKeyOptInfo(StringInfo str, const ForeignKeyOptInfo *node)
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 4cc019a012b..d06051e9851 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -288,20 +288,22 @@ _readBoolExpr(ReadNodeContext *ctx)
/* do-it-yourself enum representation */
token = pg_strtok(ctx, &length); /* skip :boolop */
token = pg_strtok(ctx, &length); /* get field value */
if (length == 3 && strncmp(token, "and", 3) == 0)
local_node->boolop = AND_EXPR;
else if (length == 2 && strncmp(token, "or", 2) == 0)
local_node->boolop = OR_EXPR;
else if (length == 3 && strncmp(token, "not", 3) == 0)
local_node->boolop = NOT_EXPR;
+ else if (length == 7 && strncmp(token, "implies", 7) == 0)
+ local_node->boolop = IMPLIES_EXPR;
else
elog(ERROR, "unrecognized boolop \"%.*s\"", length, token);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
READ_DONE();
}
static A_Const *
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 3e1f210652d..f012d5b9db0 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -1135,21 +1135,22 @@ contain_nonstrict_functions_walker(Node *node, void *context)
/* else fall through to check args */
}
else if (IsA(node, BoolExpr))
{
BoolExpr *expr = (BoolExpr *) node;
switch (expr->boolop)
{
case AND_EXPR:
case OR_EXPR:
- /* AND, OR are inherently non-strict */
+ case IMPLIES_EXPR:
+ /* AND, OR, IMPLIES are inherently non-strict */
return true;
default:
break;
}
}
else if (IsA(node, SubLink))
{
/* In some cases a sublink might be strict, but in general not */
return true;
}
@@ -3371,20 +3372,40 @@ eval_const_expressions_mutator(Node *node,
Assert(list_length(expr->args) == 1);
arg = eval_const_expressions_mutator(linitial(expr->args),
context);
/*
* Use negate_clause() to see if we can simplify
* away the NOT.
*/
return negate_clause(arg);
}
+ case IMPLIES_EXPR:
+ {
+ Node *newexpr;
+
+ /*
+ * Expand a IMPLIES b into NOT a OR b and simplify
+ * that, so that nothing downstream need know
+ * about IMPLIES.
+ */
+ Assert(list_length(expr->args) == 2);
+ newexpr = (Node *)
+ makeBoolExpr(OR_EXPR,
+ list_make2(makeBoolExpr(NOT_EXPR,
+ list_make1(linitial(expr->args)),
+ expr->location),
+ lsecond(expr->args)),
+ expr->location);
+ return eval_const_expressions_mutator(newexpr,
+ context);
+ }
default:
elog(ERROR, "unrecognized boolop: %d",
(int) expr->boolop);
break;
}
break;
}
case T_JsonValueExpr:
{
JsonValueExpr *jve = (JsonValueExpr *) node;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 3f9dc86fa00..3a83bce2691 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1444,47 +1444,39 @@ transformBoolExpr(ParseState *pstate, BoolExpr *a)
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".
+ * Transform "a IMPLIES 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.
+ * This means the same as "NOT a OR b", but we keep it as its own BoolExpr so
+ * that stored expressions deparse as written. eval_const_expressions does the
+ * expansion, so the planner proper never sees IMPLIES.
*/
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),
+ return (Node *) makeBoolExpr(IMPLIES_EXPR, list_make2(lexpr, rexpr),
a->location);
}
static Node *
transformFuncCall(ParseState *pstate, FuncCall *fn)
{
Node *last_srf = pstate->p_last_srf;
List *targs;
ListCell *args;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 5e8e1683db0..b33e4cff332 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9048,27 +9048,33 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
{
BoolExprType type;
BoolExprType parentType;
type = ((BoolExpr *) node)->boolop;
parentType = ((BoolExpr *) parentNode)->boolop;
switch (type)
{
case NOT_EXPR:
case AND_EXPR:
- if (parentType == AND_EXPR || parentType == OR_EXPR)
+ if (parentType == AND_EXPR ||
+ parentType == OR_EXPR ||
+ parentType == IMPLIES_EXPR)
return true;
break;
case OR_EXPR:
- if (parentType == OR_EXPR)
+ if (parentType == OR_EXPR ||
+ parentType == IMPLIES_EXPR)
return true;
break;
+ case IMPLIES_EXPR:
+ /* lowest precedence, and not associative */
+ break;
}
}
return false;
case T_FuncExpr:
{
/* special handling for casts and COERCE_SQL_SYNTAX */
CoercionForm type = ((FuncExpr *) parentNode)->funcformat;
if (type == COERCE_EXPLICIT_CAST ||
type == COERCE_IMPLICIT_CAST ||
@@ -9529,20 +9535,32 @@ get_rule_expr(Node *node, deparse_context *context,
case NOT_EXPR:
if (!PRETTY_PAREN(context))
appendStringInfoChar(buf, '(');
appendStringInfoString(buf, "NOT ");
get_rule_expr_paren(first_arg, context,
false, node);
if (!PRETTY_PAREN(context))
appendStringInfoChar(buf, ')');
break;
+ case IMPLIES_EXPR:
+ if (!PRETTY_PAREN(context))
+ appendStringInfoChar(buf, '(');
+ get_rule_expr_paren(first_arg, context,
+ false, node);
+ appendStringInfoString(buf, " IMPLIES ");
+ get_rule_expr_paren(lsecond(expr->args), context,
+ false, node);
+ if (!PRETTY_PAREN(context))
+ appendStringInfoChar(buf, ')');
+ break;
+
default:
elog(ERROR, "unrecognized boolop: %d",
(int) expr->boolop);
}
}
break;
case T_SubLink:
get_sublink_expr((SubLink *) node, context);
break;
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 2a832a27f49..f206e7ddddc 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -931,29 +931,34 @@ typedef struct ScalarArrayOpExpr
Oid inputcollid pg_node_attr(query_jumble_ignore);
/* the scalar and array operands */
List *args;
/* token location, or -1 if unknown */
ParseLoc location;
} ScalarArrayOpExpr;
/*
- * BoolExpr - expression node for the basic Boolean operators AND, OR, NOT
+ * BoolExpr - expression node for the basic Boolean operators AND, OR, NOT,
+ * and IMPLIES
*
* Notice the arguments are given as a List. For NOT, of course the list
* must always have exactly one element. For AND and OR, there can be two
- * or more arguments.
+ * or more arguments. IMPLIES is not associative and always has exactly two.
+ *
+ * IMPLIES is kept only so that stored expressions can be deparsed as the user
+ * wrote them; eval_const_expressions expands it to NOT a OR b, so the rest of
+ * the planner never sees it.
*/
typedef enum BoolExprType
{
- AND_EXPR, OR_EXPR, NOT_EXPR
+ AND_EXPR, OR_EXPR, NOT_EXPR, IMPLIES_EXPR
} BoolExprType;
typedef struct BoolExpr
{
pg_node_attr(custom_read_write)
Expr xpr;
BoolExprType boolop;
List *args; /* arguments to this expression */
ParseLoc location; /* token location, or -1 if unknown */
diff --git a/src/test/regress/expected/boolean.out b/src/test/regress/expected/boolean.out
index 5c092b09a5e..70c31e38244 100644
--- a/src/test/regress/expected/boolean.out
+++ b/src/test/regress/expected/boolean.out
@@ -674,30 +674,117 @@ SELECT isfalse IMPLIES isnul IS NULL FROM booltbl4;
-- 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;
+-- stored expressions keep IMPLIES, and deparse with only the parentheses
+-- that are needed
+CREATE VIEW boolview AS
+ SELECT istrue IMPLIES isnul AS i,
+ NOT istrue IMPLIES isnul AS not_antecedent,
+ istrue OR isfalse IMPLIES isnul AND istrue AS or_and,
+ (istrue IMPLIES isfalse) IMPLIES isnul AS grouped_left,
+ istrue IMPLIES (isfalse IMPLIES isnul) AS grouped_right,
+ (istrue IMPLIES isfalse) OR isnul AS under_or,
+ NOT (istrue IMPLIES isfalse) AS under_not,
+ (istrue IMPLIES isfalse) IS TRUE AS under_is
+ FROM booltbl4;
SELECT pg_get_viewdef('boolview', true);
- pg_get_viewdef
-----------------------------------
- SELECT NOT istrue OR isnul AS i+
+ pg_get_viewdef
+--------------------------------------------------------------
+ SELECT istrue IMPLIES isnul AS i, +
+ NOT istrue IMPLIES isnul AS not_antecedent, +
+ istrue OR isfalse IMPLIES isnul AND istrue AS or_and, +
+ (istrue IMPLIES isfalse) IMPLIES isnul AS grouped_left, +
+ istrue IMPLIES (isfalse IMPLIES isnul) AS grouped_right,+
+ (istrue IMPLIES isfalse) OR isnul AS under_or, +
+ NOT (istrue IMPLIES isfalse) AS under_not, +
+ (istrue IMPLIES isfalse) IS TRUE AS under_is +
+ FROM booltbl4;
+(1 row)
+
+SELECT pg_get_viewdef('boolview', false);
+ pg_get_viewdef
+-----------------------------------------------------------------
+ SELECT (istrue IMPLIES isnul) AS i, +
+ ((NOT istrue) IMPLIES isnul) AS not_antecedent, +
+ ((istrue OR isfalse) IMPLIES (isnul AND istrue)) AS or_and,+
+ ((istrue IMPLIES isfalse) IMPLIES isnul) AS grouped_left, +
+ (istrue IMPLIES (isfalse IMPLIES isnul)) AS grouped_right, +
+ ((istrue IMPLIES isfalse) OR isnul) AS under_or, +
+ (NOT (istrue IMPLIES isfalse)) AS under_not, +
+ ((istrue IMPLIES isfalse) IS TRUE) AS under_is +
FROM booltbl4;
(1 row)
+SELECT * FROM boolview;
+ i | not_antecedent | or_and | grouped_left | grouped_right | under_or | under_not | under_is
+--------+----------------+--------+--------------+---------------+----------+-----------+----------
+ (null) | t | (null) | t | t | (null) | t | f
+(1 row)
+
DROP VIEW boolview;
+CREATE TABLE implies_check (a int, b int,
+ CHECK (a > 0 IMPLIES b > 0));
+SELECT pg_get_constraintdef(oid) FROM pg_constraint
+ WHERE conrelid = 'implies_check'::regclass;
+ pg_get_constraintdef
+-----------------------------------
+ CHECK (((a > 0) IMPLIES (b > 0)))
+(1 row)
+
+INSERT INTO implies_check VALUES (1, 1), (0, 0), (NULL, 0), (1, NULL);
+INSERT INTO implies_check VALUES (1, 0); -- error
+ERROR: new row for relation "implies_check" violates check constraint "implies_check_check"
+DETAIL: Failing row contains (1, 0).
+-- the planner expands it, so EXPLAIN shows NOT a OR b, simplified
+EXPLAIN (COSTS OFF, VERBOSE)
+SELECT * FROM implies_check WHERE a > 0 IMPLIES b > 0;
+ QUERY PLAN
+-------------------------------------------------------------
+ Seq Scan on public.implies_check
+ Output: a, b
+ Filter: ((implies_check.a <= 0) OR (implies_check.b > 0))
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM implies_check WHERE a IS NULL IMPLIES false;
+ QUERY PLAN
+---------------------------
+ Seq Scan on implies_check
+ Filter: (a IS NOT NULL)
+(2 rows)
+
+-- and a partial index on IMPLIES is usable for the expanded form
+CREATE INDEX implies_check_idx ON implies_check (b)
+ WHERE a IS NULL IMPLIES b > 0;
+SELECT pg_get_indexdef('implies_check_idx'::regclass);
+ pg_get_indexdef
+------------------------------------------------------------------------------------------------------------
+ CREATE INDEX implies_check_idx ON public.implies_check USING btree (b) WHERE ((a IS NULL) IMPLIES (b > 0))
+(1 row)
+
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT b FROM implies_check WHERE a IS NOT NULL OR b > 0;
+ QUERY PLAN
+----------------------------------------------------------
+ Index Only Scan using implies_check_idx on implies_check
+(1 row)
+
+RESET enable_seqscan;
+DROP TABLE implies_check;
-- 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;
diff --git a/src/test/regress/sql/boolean.sql b/src/test/regress/sql/boolean.sql
index dfe96cb8ae5..f977cabc987 100644
--- a/src/test/regress/sql/boolean.sql
+++ b/src/test/regress/sql/boolean.sql
@@ -290,25 +290,60 @@ SELECT a, b, c
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 isfalse 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;
+-- stored expressions keep IMPLIES, and deparse with only the parentheses
+-- that are needed
+CREATE VIEW boolview AS
+ SELECT istrue IMPLIES isnul AS i,
+ NOT istrue IMPLIES isnul AS not_antecedent,
+ istrue OR isfalse IMPLIES isnul AND istrue AS or_and,
+ (istrue IMPLIES isfalse) IMPLIES isnul AS grouped_left,
+ istrue IMPLIES (isfalse IMPLIES isnul) AS grouped_right,
+ (istrue IMPLIES isfalse) OR isnul AS under_or,
+ NOT (istrue IMPLIES isfalse) AS under_not,
+ (istrue IMPLIES isfalse) IS TRUE AS under_is
+ FROM booltbl4;
SELECT pg_get_viewdef('boolview', true);
+SELECT pg_get_viewdef('boolview', false);
+SELECT * FROM boolview;
DROP VIEW boolview;
+CREATE TABLE implies_check (a int, b int,
+ CHECK (a > 0 IMPLIES b > 0));
+SELECT pg_get_constraintdef(oid) FROM pg_constraint
+ WHERE conrelid = 'implies_check'::regclass;
+INSERT INTO implies_check VALUES (1, 1), (0, 0), (NULL, 0), (1, NULL);
+INSERT INTO implies_check VALUES (1, 0); -- error
+
+-- the planner expands it, so EXPLAIN shows NOT a OR b, simplified
+EXPLAIN (COSTS OFF, VERBOSE)
+SELECT * FROM implies_check WHERE a > 0 IMPLIES b > 0;
+EXPLAIN (COSTS OFF)
+SELECT * FROM implies_check WHERE a IS NULL IMPLIES false;
+
+-- and a partial index on IMPLIES is usable for the expanded form
+CREATE INDEX implies_check_idx ON implies_check (b)
+ WHERE a IS NULL IMPLIES b > 0;
+SELECT pg_get_indexdef('implies_check_idx'::regclass);
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT b FROM implies_check WHERE a IS NOT NULL OR b > 0;
+RESET enable_seqscan;
+DROP TABLE implies_check;
+
-- 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;
--
2.56.0
Attachments:
[text/plain] v3-0001-doc-Rearrange-the-logical-operator-truth-tables.patch (5.2K, ../../b70aba34-e799-4c09-aa3e-281a335ebf2f@postgresfriends.org/2-v3-0001-doc-Rearrange-the-logical-operator-truth-tables.patch)
download | inline diff:
From 080d0997228b04a275fdf09308f5b78fce3c201d Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@postgresfriends.org>
Date: Thu, 10 Sep 2026 14:30:51 +0200
Subject: [PATCH v3 1/3] 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
base-commit: e5d25959cf8761c7dbdd7cfbb91338e94a40e8e2
--
2.56.0
[text/plain] v3-0002-Add-the-IMPLIES-boolean-operator.patch (28.3K, ../../b70aba34-e799-4c09-aa3e-281a335ebf2f@postgresfriends.org/3-v3-0002-Add-the-IMPLIES-boolean-operator.patch)
download | inline diff:
From 4aaba1edcb64a70eb3ca1eeac63f6c5288df1d8b Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@postgresfriends.org>
Date: Thu, 10 Sep 2026 14:31:03 +0200
Subject: [PATCH v3 2/3] Add the IMPLIES boolean operator
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
a IMPLIES b is material implication, parsed as a non-associative
operator binding below OR and expanded to (NOT a OR b) in the parser, so
type errors are reported against IMPLIES itself.
Because NOT and OR are already Kleene operators, that expansion settles
the three-valued truth table on Kleene implication, where Unknown
IMPLIES Unknown is Unknown. SQL has until now used only the fragment
of three-valued logic on which Kleene and Łukasiewicz agree, and
implication is exactly where the two differ: Łukasiewicz would make
Unknown IMPLIES Unknown be True, which could not then be written as
NOT a OR b.
IMPLIES is non-associative, so a IMPLIES b IMPLIES c is a syntax error
and the intended grouping has to be parenthesized. The two groupings
are different formulas, there is no consensus on which one a chain ought
to mean, and the grammar declines to choose, just as it already does for
a = b = c. Nothing is foreclosed by that: if the standard later gives
IMPLIES an associativity, accepting chains is a one-line change, and no
query that is accepted today changes meaning.
---
doc/src/sgml/func/func-logical.sgml | 98 +++++++++++++++++
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 | 147 ++++++++++++++++++++++++++
src/test/regress/sql/boolean.sql | 60 +++++++++++
10 files changed, 367 insertions(+), 1 deletion(-)
diff --git a/doc/src/sgml/func/func-logical.sgml b/doc/src/sgml/func/func-logical.sgml
index 5ba6389db56..0f3c6feb295 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,108 @@
<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, so
+ <literal>a = 1 IMPLIES b = 2 OR c = 3</literal> means
+ <literal>(a = 1) IMPLIES ((b = 2) OR (c = 3))</literal>. See <xref
+ linkend="sql-precedence"/>.
+ </para>
+
+ <para>
+ Implication is not associative, and <literal>IMPLIES</literal> therefore
+ does not chain: <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ <replaceable>b</replaceable> <literal>IMPLIES</literal>
+ <replaceable>c</replaceable> is a syntax error, just as
+ <literal>a = b = c</literal> is. The grouping has to be written
+ explicitly, and the two groupings mean different things:
+ (<replaceable>a</replaceable> <literal>IMPLIES</literal>
+ <replaceable>b</replaceable>) <literal>IMPLIES</literal>
+ <replaceable>c</replaceable> is not equivalent to
+ <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ (<replaceable>b</replaceable> <literal>IMPLIES</literal>
+ <replaceable>c</replaceable>).
+ </para>
+
+ <para>
+ Of the two, grouping to the right is usually the one wanted: it is the
+ same as a single implication whose antecedent is the conjunction of the
+ two leading operands, so <replaceable>a</replaceable>
+ <literal>IMPLIES</literal> (<replaceable>b</replaceable>
+ <literal>IMPLIES</literal> <replaceable>c</replaceable>) is equivalent to
+ (<replaceable>a</replaceable> <literal>AND</literal>
+ <replaceable>b</replaceable>) <literal>IMPLIES</literal>
+ <replaceable>c</replaceable>, that is, <quote>if both
+ <replaceable>a</replaceable> and <replaceable>b</replaceable>, then
+ <replaceable>c</replaceable></quote>. To say <quote>if
+ <replaceable>a</replaceable>, then both <replaceable>b</replaceable> and
+ <replaceable>c</replaceable></quote> instead, write
+ <replaceable>a</replaceable> <literal>IMPLIES</literal>
+ (<replaceable>b</replaceable> <literal>AND</literal>
+ <replaceable>c</replaceable>).
+ </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..d0887e8677f 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></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 0563453fe24..73c3b1f4943 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -742,21 +742,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
@@ -837,20 +838,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
+%nonassoc 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
@@ -15367,20 +15369,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);
}
@@ -18183,20 +18190,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
@@ -18775,20 +18783,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 05a6b72d4c9..3f9dc86fa00 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);
@@ -1439,20 +1443,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 0debcd193ab..45fa8c70263 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -335,20 +335,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 53ae96c0399..94d2aef1bb1 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..5c092b09a5e 100644
--- a/src/test/regress/expected/boolean.out
+++ b/src/test/regress/expected/boolean.out
@@ -559,20 +559,167 @@ 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 non-associative, so a chain is refused rather than grouped
+SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4; -- error
+ERROR: syntax error at or near "IMPLIES"
+LINE 1: SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4;
+ ^
+-- and it has to be, because the two groupings are different formulas. The
+-- first row below is the interesting one: it tells them apart.
+SELECT a, b, c,
+ (a IMPLIES b) IMPLIES c AS grouped_left,
+ a IMPLIES (b IMPLIES c) AS grouped_right,
+ (a AND b) IMPLIES c AS conj_antecedent,
+ a IMPLIES (b AND c) AS conj_consequent
+ FROM (VALUES (false, false, false),
+ (true, false, false),
+ (true, true, true)) AS t(a, b, c);
+ a | b | c | grouped_left | grouped_right | conj_antecedent | conj_consequent
+---+---+---+--------------+---------------+-----------------+-----------------
+ f | f | f | f | t | t | t
+ t | f | f | t | t | t | f
+ t | t | t | t | t | t | t
+(3 rows)
+
+-- grouping to the right is implication from the conjunction of the operands
+-- (exportation), which holds for all three truth values, so no rows here
+SELECT a, b, c
+ FROM (VALUES (true), (false), (null)) AS x(a),
+ (VALUES (true), (false), (null)) AS y(b),
+ (VALUES (true), (false), (null)) AS z(c)
+ WHERE (a IMPLIES (b IMPLIES c)) IS DISTINCT FROM ((a AND b) IMPLIES c);
+ a | b | c
+---+---+---
+(0 rows)
+
+-- 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 isfalse 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..dfe96cb8ae5 100644
--- a/src/test/regress/sql/boolean.sql
+++ b/src/test/regress/sql/boolean.sql
@@ -243,20 +243,80 @@ 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 non-associative, so a chain is refused rather than grouped
+SELECT isfalse IMPLIES istrue IMPLIES isfalse FROM booltbl4; -- error
+
+-- and it has to be, because the two groupings are different formulas. The
+-- first row below is the interesting one: it tells them apart.
+SELECT a, b, c,
+ (a IMPLIES b) IMPLIES c AS grouped_left,
+ a IMPLIES (b IMPLIES c) AS grouped_right,
+ (a AND b) IMPLIES c AS conj_antecedent,
+ a IMPLIES (b AND c) AS conj_consequent
+ FROM (VALUES (false, false, false),
+ (true, false, false),
+ (true, true, true)) AS t(a, b, c);
+
+-- grouping to the right is implication from the conjunction of the operands
+-- (exportation), which holds for all three truth values, so no rows here
+SELECT a, b, c
+ FROM (VALUES (true), (false), (null)) AS x(a),
+ (VALUES (true), (false), (null)) AS y(b),
+ (VALUES (true), (false), (null)) AS z(c)
+ WHERE (a IMPLIES (b IMPLIES c)) IS DISTINCT FROM ((a AND b) IMPLIES c);
+
+-- 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 isfalse 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.56.0
[text/plain] v3-0003-Keep-IMPLIES-in-stored-expressions.patch (22.3K, ../../b70aba34-e799-4c09-aa3e-281a335ebf2f@postgresfriends.org/4-v3-0003-Keep-IMPLIES-in-stored-expressions.patch)
download | inline diff:
From 00165798ea90aad84c0f059fdd6f8f112346b271 Mon Sep 17 00:00:00 2001
From: Vik Fearing <vik@postgresfriends.org>
Date: Sat, 3 Oct 2026 17:18:22 +0200
Subject: [PATCH v3 3/3] Keep IMPLIES in stored expressions
Until now a IMPLIES b was expanded to (NOT a OR b) during parse analysis,
so views, check constraints, index expressions and predicates, and
policies were stored, deparsed, and dumped in terms of the expansion
rather than as written.
Make IMPLIES a fourth BoolExprType so that it survives into the stored
tree and ruleutils.c can print it back. In pretty mode, NOT, AND, and
OR beneath IMPLIES need no parentheses, while an IMPLIES beneath any
boolean operator, IMPLIES included, always gets them, since IMPLIES has
the lowest precedence and is not associative.
The expansion moves to eval_const_expressions, so the rest of the
planner, including qual canonicalization, predicate proving for partial
indexes, and outer join reduction, never sees IMPLIES and needs no
changes. EXPLAIN therefore shows the expanded and simplified form, much
as it does for BETWEEN.
The executor and postgres_fdw's deparser should not see an unexpanded
IMPLIES either, but each handles one anyway, as (NOT a OR b), rather than
fail. The executor needs no new opcodes for that, so JIT is unaffected,
and the deparser sends the expansion because the remote server might
not know IMPLIES.
---
contrib/postgres_fdw/deparse.c | 12 ++++
src/backend/executor/execExpr.c | 20 +++++-
src/backend/nodes/outfuncs.c | 3 +
src/backend/nodes/readfuncs.c | 2 +
src/backend/optimizer/util/clauses.c | 23 ++++++-
src/backend/parser/parse_expr.c | 18 ++---
src/backend/utils/adt/ruleutils.c | 22 +++++-
src/include/nodes/primnodes.h | 11 ++-
src/test/regress/expected/boolean.out | 97 +++++++++++++++++++++++++--
src/test/regress/sql/boolean.sql | 39 ++++++++++-
10 files changed, 220 insertions(+), 27 deletions(-)
diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index ff9fe0f87e4..69a8529f583 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -3742,20 +3742,32 @@ deparseBoolExpr(BoolExpr *node, deparse_expr_cxt *context)
op = "AND";
break;
case OR_EXPR:
op = "OR";
break;
case NOT_EXPR:
appendStringInfoString(buf, "(NOT ");
deparseExpr(linitial(node->args), context);
appendStringInfoChar(buf, ')');
return;
+ case IMPLIES_EXPR:
+
+ /*
+ * eval_const_expressions should already have expanded this, but
+ * if not, send the expansion, which any remote server accepts.
+ */
+ appendStringInfoString(buf, "((NOT ");
+ deparseExpr(linitial(node->args), context);
+ appendStringInfoString(buf, ") OR ");
+ deparseExpr(lsecond(node->args), context);
+ appendStringInfoChar(buf, ')');
+ return;
}
appendStringInfoChar(buf, '(');
first = true;
foreach(lc, node->args)
{
if (!first)
appendStringInfo(buf, " %s ", op);
deparseExpr((Expr *) lfirst(lc), context);
first = false;
diff --git a/src/backend/executor/execExpr.c b/src/backend/executor/execExpr.c
index 82e846a1f4f..6c069e56b50 100644
--- a/src/backend/executor/execExpr.c
+++ b/src/backend/executor/execExpr.c
@@ -1379,21 +1379,21 @@ ExecInitExprRec(Expr *node, ExprState *state,
}
case T_BoolExpr:
{
BoolExpr *boolexpr = (BoolExpr *) node;
int nargs = list_length(boolexpr->args);
List *adjust_jumps = NIL;
int off;
ListCell *lc;
- /* allocate scratch memory used by all steps of AND/OR */
+ /* allocate scratch memory used by all steps of AND/OR/IMPLIES */
if (boolexpr->boolop != NOT_EXPR)
scratch.d.boolexpr.anynull = palloc_object(bool);
/*
* For each argument evaluate the argument itself, then
* perform the bool operation's appropriate handling.
*
* We can evaluate each argument into our result area, since
* the short-circuiting logic means we only need to remember
* previous NULL values.
@@ -1432,20 +1432,38 @@ ExecInitExprRec(Expr *node, ExprState *state,
else if (off + 1 == nargs)
scratch.opcode = EEOP_BOOL_OR_STEP_LAST;
else
scratch.opcode = EEOP_BOOL_OR_STEP;
break;
case NOT_EXPR:
Assert(nargs == 1);
scratch.opcode = EEOP_BOOL_NOT_STEP;
break;
+ case IMPLIES_EXPR:
+
+ /*
+ * The planner normally expands this, but in case
+ * it didn't, evaluate it as NOT a OR b. The NOT
+ * step doesn't jump, so it needs no adjusting.
+ */
+ Assert(nargs == 2);
+
+ if (off == 0)
+ {
+ scratch.opcode = EEOP_BOOL_NOT_STEP;
+ ExprEvalPushStep(state, &scratch);
+ scratch.opcode = EEOP_BOOL_OR_STEP_FIRST;
+ }
+ else
+ scratch.opcode = EEOP_BOOL_OR_STEP_LAST;
+ break;
default:
elog(ERROR, "unrecognized boolop: %d",
(int) boolexpr->boolop);
break;
}
scratch.d.boolexpr.jumpdone = -1;
ExprEvalPushStep(state, &scratch);
adjust_jumps = lappend_int(adjust_jumps,
state->steps_len - 1);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 9a35ff53de0..75b7856725b 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -415,20 +415,23 @@ _outBoolExpr(StringInfo str, const BoolExpr *node)
{
case AND_EXPR:
opstr = "and";
break;
case OR_EXPR:
opstr = "or";
break;
case NOT_EXPR:
opstr = "not";
break;
+ case IMPLIES_EXPR:
+ opstr = "implies";
+ break;
}
appendStringInfoString(str, " :boolop ");
outToken(str, opstr);
WRITE_NODE_FIELD(args);
WRITE_LOCATION_FIELD(location);
}
static void
_outForeignKeyOptInfo(StringInfo str, const ForeignKeyOptInfo *node)
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 4cc019a012b..d06051e9851 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -288,20 +288,22 @@ _readBoolExpr(ReadNodeContext *ctx)
/* do-it-yourself enum representation */
token = pg_strtok(ctx, &length); /* skip :boolop */
token = pg_strtok(ctx, &length); /* get field value */
if (length == 3 && strncmp(token, "and", 3) == 0)
local_node->boolop = AND_EXPR;
else if (length == 2 && strncmp(token, "or", 2) == 0)
local_node->boolop = OR_EXPR;
else if (length == 3 && strncmp(token, "not", 3) == 0)
local_node->boolop = NOT_EXPR;
+ else if (length == 7 && strncmp(token, "implies", 7) == 0)
+ local_node->boolop = IMPLIES_EXPR;
else
elog(ERROR, "unrecognized boolop \"%.*s\"", length, token);
READ_NODE_FIELD(args);
READ_LOCATION_FIELD(location);
READ_DONE();
}
static A_Const *
diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c
index 3e1f210652d..f012d5b9db0 100644
--- a/src/backend/optimizer/util/clauses.c
+++ b/src/backend/optimizer/util/clauses.c
@@ -1135,21 +1135,22 @@ contain_nonstrict_functions_walker(Node *node, void *context)
/* else fall through to check args */
}
else if (IsA(node, BoolExpr))
{
BoolExpr *expr = (BoolExpr *) node;
switch (expr->boolop)
{
case AND_EXPR:
case OR_EXPR:
- /* AND, OR are inherently non-strict */
+ case IMPLIES_EXPR:
+ /* AND, OR, IMPLIES are inherently non-strict */
return true;
default:
break;
}
}
else if (IsA(node, SubLink))
{
/* In some cases a sublink might be strict, but in general not */
return true;
}
@@ -3371,20 +3372,40 @@ eval_const_expressions_mutator(Node *node,
Assert(list_length(expr->args) == 1);
arg = eval_const_expressions_mutator(linitial(expr->args),
context);
/*
* Use negate_clause() to see if we can simplify
* away the NOT.
*/
return negate_clause(arg);
}
+ case IMPLIES_EXPR:
+ {
+ Node *newexpr;
+
+ /*
+ * Expand a IMPLIES b into NOT a OR b and simplify
+ * that, so that nothing downstream need know
+ * about IMPLIES.
+ */
+ Assert(list_length(expr->args) == 2);
+ newexpr = (Node *)
+ makeBoolExpr(OR_EXPR,
+ list_make2(makeBoolExpr(NOT_EXPR,
+ list_make1(linitial(expr->args)),
+ expr->location),
+ lsecond(expr->args)),
+ expr->location);
+ return eval_const_expressions_mutator(newexpr,
+ context);
+ }
default:
elog(ERROR, "unrecognized boolop: %d",
(int) expr->boolop);
break;
}
break;
}
case T_JsonValueExpr:
{
JsonValueExpr *jve = (JsonValueExpr *) node;
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 3f9dc86fa00..3a83bce2691 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -1444,47 +1444,39 @@ transformBoolExpr(ParseState *pstate, BoolExpr *a)
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".
+ * Transform "a IMPLIES 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.
+ * This means the same as "NOT a OR b", but we keep it as its own BoolExpr so
+ * that stored expressions deparse as written. eval_const_expressions does the
+ * expansion, so the planner proper never sees IMPLIES.
*/
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),
+ return (Node *) makeBoolExpr(IMPLIES_EXPR, list_make2(lexpr, rexpr),
a->location);
}
static Node *
transformFuncCall(ParseState *pstate, FuncCall *fn)
{
Node *last_srf = pstate->p_last_srf;
List *targs;
ListCell *args;
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 5e8e1683db0..b33e4cff332 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9048,27 +9048,33 @@ isSimpleNode(Node *node, Node *parentNode, int prettyFlags)
{
BoolExprType type;
BoolExprType parentType;
type = ((BoolExpr *) node)->boolop;
parentType = ((BoolExpr *) parentNode)->boolop;
switch (type)
{
case NOT_EXPR:
case AND_EXPR:
- if (parentType == AND_EXPR || parentType == OR_EXPR)
+ if (parentType == AND_EXPR ||
+ parentType == OR_EXPR ||
+ parentType == IMPLIES_EXPR)
return true;
break;
case OR_EXPR:
- if (parentType == OR_EXPR)
+ if (parentType == OR_EXPR ||
+ parentType == IMPLIES_EXPR)
return true;
break;
+ case IMPLIES_EXPR:
+ /* lowest precedence, and not associative */
+ break;
}
}
return false;
case T_FuncExpr:
{
/* special handling for casts and COERCE_SQL_SYNTAX */
CoercionForm type = ((FuncExpr *) parentNode)->funcformat;
if (type == COERCE_EXPLICIT_CAST ||
type == COERCE_IMPLICIT_CAST ||
@@ -9529,20 +9535,32 @@ get_rule_expr(Node *node, deparse_context *context,
case NOT_EXPR:
if (!PRETTY_PAREN(context))
appendStringInfoChar(buf, '(');
appendStringInfoString(buf, "NOT ");
get_rule_expr_paren(first_arg, context,
false, node);
if (!PRETTY_PAREN(context))
appendStringInfoChar(buf, ')');
break;
+ case IMPLIES_EXPR:
+ if (!PRETTY_PAREN(context))
+ appendStringInfoChar(buf, '(');
+ get_rule_expr_paren(first_arg, context,
+ false, node);
+ appendStringInfoString(buf, " IMPLIES ");
+ get_rule_expr_paren(lsecond(expr->args), context,
+ false, node);
+ if (!PRETTY_PAREN(context))
+ appendStringInfoChar(buf, ')');
+ break;
+
default:
elog(ERROR, "unrecognized boolop: %d",
(int) expr->boolop);
}
}
break;
case T_SubLink:
get_sublink_expr((SubLink *) node, context);
break;
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 2a832a27f49..f206e7ddddc 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -931,29 +931,34 @@ typedef struct ScalarArrayOpExpr
Oid inputcollid pg_node_attr(query_jumble_ignore);
/* the scalar and array operands */
List *args;
/* token location, or -1 if unknown */
ParseLoc location;
} ScalarArrayOpExpr;
/*
- * BoolExpr - expression node for the basic Boolean operators AND, OR, NOT
+ * BoolExpr - expression node for the basic Boolean operators AND, OR, NOT,
+ * and IMPLIES
*
* Notice the arguments are given as a List. For NOT, of course the list
* must always have exactly one element. For AND and OR, there can be two
- * or more arguments.
+ * or more arguments. IMPLIES is not associative and always has exactly two.
+ *
+ * IMPLIES is kept only so that stored expressions can be deparsed as the user
+ * wrote them; eval_const_expressions expands it to NOT a OR b, so the rest of
+ * the planner never sees it.
*/
typedef enum BoolExprType
{
- AND_EXPR, OR_EXPR, NOT_EXPR
+ AND_EXPR, OR_EXPR, NOT_EXPR, IMPLIES_EXPR
} BoolExprType;
typedef struct BoolExpr
{
pg_node_attr(custom_read_write)
Expr xpr;
BoolExprType boolop;
List *args; /* arguments to this expression */
ParseLoc location; /* token location, or -1 if unknown */
diff --git a/src/test/regress/expected/boolean.out b/src/test/regress/expected/boolean.out
index 5c092b09a5e..70c31e38244 100644
--- a/src/test/regress/expected/boolean.out
+++ b/src/test/regress/expected/boolean.out
@@ -674,30 +674,117 @@ SELECT isfalse IMPLIES isnul IS NULL FROM booltbl4;
-- 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;
+-- stored expressions keep IMPLIES, and deparse with only the parentheses
+-- that are needed
+CREATE VIEW boolview AS
+ SELECT istrue IMPLIES isnul AS i,
+ NOT istrue IMPLIES isnul AS not_antecedent,
+ istrue OR isfalse IMPLIES isnul AND istrue AS or_and,
+ (istrue IMPLIES isfalse) IMPLIES isnul AS grouped_left,
+ istrue IMPLIES (isfalse IMPLIES isnul) AS grouped_right,
+ (istrue IMPLIES isfalse) OR isnul AS under_or,
+ NOT (istrue IMPLIES isfalse) AS under_not,
+ (istrue IMPLIES isfalse) IS TRUE AS under_is
+ FROM booltbl4;
SELECT pg_get_viewdef('boolview', true);
- pg_get_viewdef
-----------------------------------
- SELECT NOT istrue OR isnul AS i+
+ pg_get_viewdef
+--------------------------------------------------------------
+ SELECT istrue IMPLIES isnul AS i, +
+ NOT istrue IMPLIES isnul AS not_antecedent, +
+ istrue OR isfalse IMPLIES isnul AND istrue AS or_and, +
+ (istrue IMPLIES isfalse) IMPLIES isnul AS grouped_left, +
+ istrue IMPLIES (isfalse IMPLIES isnul) AS grouped_right,+
+ (istrue IMPLIES isfalse) OR isnul AS under_or, +
+ NOT (istrue IMPLIES isfalse) AS under_not, +
+ (istrue IMPLIES isfalse) IS TRUE AS under_is +
+ FROM booltbl4;
+(1 row)
+
+SELECT pg_get_viewdef('boolview', false);
+ pg_get_viewdef
+-----------------------------------------------------------------
+ SELECT (istrue IMPLIES isnul) AS i, +
+ ((NOT istrue) IMPLIES isnul) AS not_antecedent, +
+ ((istrue OR isfalse) IMPLIES (isnul AND istrue)) AS or_and,+
+ ((istrue IMPLIES isfalse) IMPLIES isnul) AS grouped_left, +
+ (istrue IMPLIES (isfalse IMPLIES isnul)) AS grouped_right, +
+ ((istrue IMPLIES isfalse) OR isnul) AS under_or, +
+ (NOT (istrue IMPLIES isfalse)) AS under_not, +
+ ((istrue IMPLIES isfalse) IS TRUE) AS under_is +
FROM booltbl4;
(1 row)
+SELECT * FROM boolview;
+ i | not_antecedent | or_and | grouped_left | grouped_right | under_or | under_not | under_is
+--------+----------------+--------+--------------+---------------+----------+-----------+----------
+ (null) | t | (null) | t | t | (null) | t | f
+(1 row)
+
DROP VIEW boolview;
+CREATE TABLE implies_check (a int, b int,
+ CHECK (a > 0 IMPLIES b > 0));
+SELECT pg_get_constraintdef(oid) FROM pg_constraint
+ WHERE conrelid = 'implies_check'::regclass;
+ pg_get_constraintdef
+-----------------------------------
+ CHECK (((a > 0) IMPLIES (b > 0)))
+(1 row)
+
+INSERT INTO implies_check VALUES (1, 1), (0, 0), (NULL, 0), (1, NULL);
+INSERT INTO implies_check VALUES (1, 0); -- error
+ERROR: new row for relation "implies_check" violates check constraint "implies_check_check"
+DETAIL: Failing row contains (1, 0).
+-- the planner expands it, so EXPLAIN shows NOT a OR b, simplified
+EXPLAIN (COSTS OFF, VERBOSE)
+SELECT * FROM implies_check WHERE a > 0 IMPLIES b > 0;
+ QUERY PLAN
+-------------------------------------------------------------
+ Seq Scan on public.implies_check
+ Output: a, b
+ Filter: ((implies_check.a <= 0) OR (implies_check.b > 0))
+(3 rows)
+
+EXPLAIN (COSTS OFF)
+SELECT * FROM implies_check WHERE a IS NULL IMPLIES false;
+ QUERY PLAN
+---------------------------
+ Seq Scan on implies_check
+ Filter: (a IS NOT NULL)
+(2 rows)
+
+-- and a partial index on IMPLIES is usable for the expanded form
+CREATE INDEX implies_check_idx ON implies_check (b)
+ WHERE a IS NULL IMPLIES b > 0;
+SELECT pg_get_indexdef('implies_check_idx'::regclass);
+ pg_get_indexdef
+------------------------------------------------------------------------------------------------------------
+ CREATE INDEX implies_check_idx ON public.implies_check USING btree (b) WHERE ((a IS NULL) IMPLIES (b > 0))
+(1 row)
+
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT b FROM implies_check WHERE a IS NOT NULL OR b > 0;
+ QUERY PLAN
+----------------------------------------------------------
+ Index Only Scan using implies_check_idx on implies_check
+(1 row)
+
+RESET enable_seqscan;
+DROP TABLE implies_check;
-- 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;
diff --git a/src/test/regress/sql/boolean.sql b/src/test/regress/sql/boolean.sql
index dfe96cb8ae5..f977cabc987 100644
--- a/src/test/regress/sql/boolean.sql
+++ b/src/test/regress/sql/boolean.sql
@@ -290,25 +290,60 @@ SELECT a, b, c
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 isfalse 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;
+-- stored expressions keep IMPLIES, and deparse with only the parentheses
+-- that are needed
+CREATE VIEW boolview AS
+ SELECT istrue IMPLIES isnul AS i,
+ NOT istrue IMPLIES isnul AS not_antecedent,
+ istrue OR isfalse IMPLIES isnul AND istrue AS or_and,
+ (istrue IMPLIES isfalse) IMPLIES isnul AS grouped_left,
+ istrue IMPLIES (isfalse IMPLIES isnul) AS grouped_right,
+ (istrue IMPLIES isfalse) OR isnul AS under_or,
+ NOT (istrue IMPLIES isfalse) AS under_not,
+ (istrue IMPLIES isfalse) IS TRUE AS under_is
+ FROM booltbl4;
SELECT pg_get_viewdef('boolview', true);
+SELECT pg_get_viewdef('boolview', false);
+SELECT * FROM boolview;
DROP VIEW boolview;
+CREATE TABLE implies_check (a int, b int,
+ CHECK (a > 0 IMPLIES b > 0));
+SELECT pg_get_constraintdef(oid) FROM pg_constraint
+ WHERE conrelid = 'implies_check'::regclass;
+INSERT INTO implies_check VALUES (1, 1), (0, 0), (NULL, 0), (1, NULL);
+INSERT INTO implies_check VALUES (1, 0); -- error
+
+-- the planner expands it, so EXPLAIN shows NOT a OR b, simplified
+EXPLAIN (COSTS OFF, VERBOSE)
+SELECT * FROM implies_check WHERE a > 0 IMPLIES b > 0;
+EXPLAIN (COSTS OFF)
+SELECT * FROM implies_check WHERE a IS NULL IMPLIES false;
+
+-- and a partial index on IMPLIES is usable for the expanded form
+CREATE INDEX implies_check_idx ON implies_check (b)
+ WHERE a IS NULL IMPLIES b > 0;
+SELECT pg_get_indexdef('implies_check_idx'::regclass);
+SET enable_seqscan = off;
+EXPLAIN (COSTS OFF)
+SELECT b FROM implies_check WHERE a IS NOT NULL OR b > 0;
+RESET enable_seqscan;
+DROP TABLE implies_check;
+
-- 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;
--
2.56.0
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-29 20:23 ` Re: Logical Implication Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-29 21:51 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-30 15:09 ` Re: Logical Implication Nathan Bossart <nathandbossart@gmail.com>
2026-10-03 16:18 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-10-06 18:19 ` Nathan Bossart <nathandbossart@gmail.com>
0 siblings, 0 replies; 18+ messages in thread
From: Nathan Bossart @ 2026-10-06 18:19 UTC (permalink / raw)
To: Vik Fearing <vik@postgresfriends.org>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; pgsql-hackers@lists.postgresql.org, Jacob Champion <jacob.champion@enterprisedb.com>; Isaac Morland <isaac.morland@gmail.com>
On Sat, Oct 03, 2026 at 06:18:43PM +0200, Vik Fearing wrote:
> Attached is a new patch set that makes IMPLIES non-associative, changes the
> tests, and also survives a round trip. I put the latter in a separate patch
> in case it isn't wanted after all.
Thanks!
> <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:
I find the reorganization to be an improvement, and I intend to commit this
one sooner than later. The only thing I'd change is the switch from NULL
to "unknown" in the tables. Users deal with NULL at the command level, and
the rest of the docs use NULL, so I think the existing text has the right
idea by noting once that NULL represents "unknown" and then using NULL
throughout.
> <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
This part goes on to mention that there is no guaranteed evaluation order
for AND and OR. Maybe it should mention IMPLIES, too.
> + case IMPLIES_EXPR:
> +
> + /*
> + * The planner normally expands this, but in case
> + * it didn't, evaluate it as NOT a OR b. The NOT
> + * step doesn't jump, so it needs no adjusting.
> + */
> + Assert(nargs == 2);
IMHO this and the postgres_fdw equivalent should be replaced with
elog(ERROR, ...) instead. Otherwise, they'll be untested dead code that
mask problems elsewhere.
For the tests, I'd probably trim those down a bit to what feels essential.
For example, we probably don't need to test that IMPLIES is unreserved.
I'll handle this part.
Note to self: this will need a catversion bump.
--
nathan
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-09-11 14:41 ` Thom Brown <thom@linux.com>
2026-09-11 16:06 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
1 sibling, 1 reply; 18+ messages in thread
From: Thom Brown @ 2026-09-11 14:41 UTC (permalink / raw)
To: Vik Fearing <vik@postgresfriends.org>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Fri, 11 Sept 2026 at 14:36, Vik Fearing <vik@postgresfriends.org> wrote:
>
> 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.
What behaviour should be expected for the following cases:
a IMPLIES b IMPLIES c
CHECK (a IMPLIES b) -- where b is NULL
-- And what does that deparse to?
a IMPLIES (100 / b > 10)
Thom
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:41 ` Re: Logical Implication Thom Brown <thom@linux.com>
@ 2026-09-11 16:06 ` Vik Fearing <vik@postgresfriends.org>
2026-09-11 16:18 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
0 siblings, 1 reply; 18+ messages in thread
From: Vik Fearing @ 2026-09-11 16:06 UTC (permalink / raw)
To: Thom Brown <thom@linux.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi Thom,
Thanks for taking a look at it.
On 11/09/2026 16:41, Thom Brown wrote:
> What behaviour should be expected for the following cases:
>
> a IMPLIES b IMPLIES c
a IMPLIES (b IMPLIES c), it is right associative. I put this exact case
in the documentation.
It deparses to (NOT a) OR (NOT b) OR c.
> CHECK (a IMPLIES b) -- where b is NULL
> -- And what does that deparse to?
This is in the documentation as well, it deparses to NOT a OR NULL
which is either TRUE or UNKNOWN depending on what a is. In both cases
the CHECK passes.
> a IMPLIES (100 / b > 10)
This is an error because the operator only applies to booleans.
--
Vik Fearing
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:41 ` Re: Logical Implication Thom Brown <thom@linux.com>
2026-09-11 16:06 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-09-11 16:18 ` Vik Fearing <vik@postgresfriends.org>
2026-09-17 04:53 ` Re: Logical Implication solai v <solai.cdac@gmail.com>
0 siblings, 1 reply; 18+ messages in thread
From: Vik Fearing @ 2026-09-11 16:18 UTC (permalink / raw)
To: Thom Brown <thom@linux.com>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On 11/09/2026 18:06, Vik Fearing wrote:
>
>> a IMPLIES (100 / b > 10)
>
>
> This is an error because the operator only applies to booleans.
Sorry, my bad. I parsed it incorrectly in my head, assuming b was a boolean.
Its result is (NOT a) OR ((100 / b) > 10)
--
Vik Fearing
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:41 ` Re: Logical Implication Thom Brown <thom@linux.com>
2026-09-11 16:06 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 16:18 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
@ 2026-09-17 04:53 ` solai v <solai.cdac@gmail.com>
2026-09-17 08:39 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
0 siblings, 1 reply; 18+ messages in thread
From: solai v @ 2026-09-17 04:53 UTC (permalink / raw)
To: Vik Fearing <vik@postgresfriends.org>; +Cc: Thom Brown <thom@linux.com>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi all
I tested the IMPLIES boolean operator patch.
First, I reproduced the issue on the unpatched PostgreSQL, where using
the IMPLIES operator resulted in a syntax error. I then applied the
two patches, rebuilt PostgreSQL, and repeated the same tests.
After applying the patch, I verified the IMPLIES operator with all
TRUE/FALSE combinations and confirmed that its results match the
equivalent NOT a OR b expressions. I also tested its use in a CHECK
constraint, including both valid and invalid cases. In addition, I
tested NULL handling, right associativity, and rejection of
non-boolean operands.
All the tested cases behaved as expected. Finally, I ran the
PostgreSQL regression test suite, and all tests passed.
Overall, the patch is working as expected based on the tests performed.
Regards
solai
^ permalink raw reply [nested|flat] 18+ messages in thread
* Re: Logical Implication
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:41 ` Re: Logical Implication Thom Brown <thom@linux.com>
2026-09-11 16:06 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 16:18 ` Re: Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-17 04:53 ` Re: Logical Implication solai v <solai.cdac@gmail.com>
@ 2026-09-17 08:39 ` Vik Fearing <vik@postgresfriends.org>
0 siblings, 0 replies; 18+ messages in thread
From: Vik Fearing @ 2026-09-17 08:39 UTC (permalink / raw)
To: solai v <solai.cdac@gmail.com>; +Cc: Thom Brown <thom@linux.com>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On 17/09/2026 06:53, solai v wrote:
> Overall, the patch is working as expected based on the tests performed.
Thanks for testing!
--
Vik Fearing
^ permalink raw reply [nested|flat] 18+ messages in thread
end of thread, other threads:[~2026-10-06 18:19 UTC | newest]
Thread overview: 18+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2026-09-11 13:35 Logical Implication Vik Fearing <vik@postgresfriends.org>
2026-09-11 14:10 ` Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 16:12 ` Vik Fearing <vik@postgresfriends.org>
2026-09-24 14:33 ` Nathan Bossart <nathandbossart@gmail.com>
2026-09-29 15:20 ` Vik Fearing <vik@postgresfriends.org>
2026-09-29 16:30 ` Jacob Champion <jacob.champion@enterprisedb.com>
2026-09-29 16:40 ` Isaac Morland <isaac.morland@gmail.com>
2026-09-29 20:23 ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-29 21:51 ` Vik Fearing <vik@postgresfriends.org>
2026-09-29 22:27 ` Zsolt Parragi <zsolt.parragi@percona.com>
2026-09-30 15:09 ` Nathan Bossart <nathandbossart@gmail.com>
2026-10-03 16:18 ` Vik Fearing <vik@postgresfriends.org>
2026-10-06 18:19 ` Nathan Bossart <nathandbossart@gmail.com>
2026-09-11 14:41 ` Thom Brown <thom@linux.com>
2026-09-11 16:06 ` Vik Fearing <vik@postgresfriends.org>
2026-09-11 16:18 ` Vik Fearing <vik@postgresfriends.org>
2026-09-17 04:53 ` solai v <solai.cdac@gmail.com>
2026-09-17 08:39 ` Vik Fearing <vik@postgresfriends.org>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox