On 2026-03-12 Th 11:55 PM, jian he wrote:
Hi.

The regression test was very verbose; I removed some of it.
Also polished function ExecEvalJsonIsPredicate a little bit.



Here's a v4. I changed resultBaseType to exprBaseType - I think it's clearer. I also trimmed the tests a bit more, and dropped the new objects after testing them. TheĀ  error message now shows the domain name rather than the underlying base type. I think that's more useful.


cheers


andrew


--
Andrew Dunstan
EDB: https://www.enterprisedb.com
From 885a440b54e67c0a5cfcb77a035a094068f735a3 Mon Sep 17 00:00:00 2001
From: Andrew Dunstan <[email protected]>
Date: Fri, 13 Mar 2026 11:51:26 +0800
Subject: [PATCH v4] Allow IS JSON predicate to work with domain types

The IS JSON predicate only accepted the base types text, json, jsonb, and
bytea.  Extend it to also accept domain types over those base types by
resolving through getBaseType() during parse analysis.

The base type OID is stored in the JsonIsPredicate node (as exprBaseType)
so the executor can dispatch to the correct validation path without
repeating the domain lookup at runtime.

When a non-supported type (or domain over a non-supported type) is used,
the error message displays the original type name as written by the user,
rather than the resolved base type.

Author: jian he <[email protected]>
Reviewed-by: Andrew Dunstan <[email protected]>
Discussion: https://postgr.es/m/CACJufxEk34DnJFG72CRsPPT4tsJL9arobX0tNPsn7yH28J=z...@mail.gmail.com
---
 src/backend/executor/execExprInterp.c |  9 ++---
 src/backend/nodes/makefuncs.c         |  3 +-
 src/backend/parser/gram.y             |  8 ++--
 src/backend/parser/parse_expr.c       | 11 +++---
 src/include/nodes/makefuncs.h         |  2 +-
 src/include/nodes/primnodes.h         |  1 +
 src/test/regress/expected/sqljson.out | 57 +++++++++++++++++++++++++++
 src/test/regress/sql/sqljson.sql      | 31 +++++++++++++++
 8 files changed, 105 insertions(+), 17 deletions(-)

diff --git a/src/backend/executor/execExprInterp.c b/src/backend/executor/execExprInterp.c
index 61ff5ddc74c..bdb2598503d 100644
--- a/src/backend/executor/execExprInterp.c
+++ b/src/backend/executor/execExprInterp.c
@@ -4740,7 +4740,6 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op)
 {
 	JsonIsPredicate *pred = op->d.is_json.pred;
 	Datum		js = *op->resvalue;
-	Oid			exprtype;
 	bool		res;
 
 	if (*op->resnull)
@@ -4749,9 +4748,7 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op)
 		return;
 	}
 
-	exprtype = exprType(pred->expr);
-
-	if (exprtype == TEXTOID || exprtype == JSONOID)
+	if (pred->exprBaseType == TEXTOID || pred->exprBaseType == JSONOID)
 	{
 		text	   *json = DatumGetTextP(js);
 
@@ -4784,10 +4781,10 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op)
 		 * Do full parsing pass only for uniqueness check or for JSON text
 		 * validation.
 		 */
-		if (res && (pred->unique_keys || exprtype == TEXTOID))
+		if (res && (pred->unique_keys || pred->exprBaseType == TEXTOID))
 			res = json_validate(json, pred->unique_keys, false);
 	}
-	else if (exprtype == JSONBOID)
+	else if (pred->exprBaseType == JSONBOID)
 	{
 		if (pred->item_type == JS_TYPE_ANY)
 			res = true;
diff --git a/src/backend/nodes/makefuncs.c b/src/backend/nodes/makefuncs.c
index 2caec621d73..3cd35c5c457 100644
--- a/src/backend/nodes/makefuncs.c
+++ b/src/backend/nodes/makefuncs.c
@@ -984,7 +984,7 @@ makeJsonKeyValue(Node *key, Node *value)
  */
 Node *
 makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type,
-					bool unique_keys, int location)
+					bool unique_keys, Oid exprBaseType, int location)
 {
 	JsonIsPredicate *n = makeNode(JsonIsPredicate);
 
@@ -992,6 +992,7 @@ makeJsonIsPredicate(Node *expr, JsonFormat *format, JsonValueType item_type,
 	n->format = format;
 	n->item_type = item_type;
 	n->unique_keys = unique_keys;
+	n->exprBaseType = exprBaseType;
 	n->location = location;
 
 	return (Node *) n;
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f01f5734fe9..0fe4ca081a7 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -15700,7 +15700,7 @@ a_expr:		c_expr									{ $$ = $1; }
 				{
 					JsonFormat *format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1);
 
-					$$ = makeJsonIsPredicate($1, format, $3, $4, @1);
+					$$ = makeJsonIsPredicate($1, format, $3, $4, InvalidOid, @1);
 				}
 			/*
 			 * Required by SQL/JSON, but there are conflicts
@@ -15709,7 +15709,7 @@ a_expr:		c_expr									{ $$ = $1; }
 				IS  json_predicate_type_constraint
 					json_key_uniqueness_constraint_opt		%prec IS
 				{
-					$$ = makeJsonIsPredicate($1, $2, $4, $5, @1);
+					$$ = makeJsonIsPredicate($1, $2, $4, $5, InvalidOid, @1);
 				}
 			*/
 			| a_expr IS NOT
@@ -15718,7 +15718,7 @@ a_expr:		c_expr									{ $$ = $1; }
 				{
 					JsonFormat *format = makeJsonFormat(JS_FORMAT_DEFAULT, JS_ENC_DEFAULT, -1);
 
-					$$ = makeNotExpr(makeJsonIsPredicate($1, format, $4, $5, @1), @1);
+					$$ = makeNotExpr(makeJsonIsPredicate($1, format, $4, $5, InvalidOid, @1), @1);
 				}
 			/*
 			 * Required by SQL/JSON, but there are conflicts
@@ -15728,7 +15728,7 @@ a_expr:		c_expr									{ $$ = $1; }
 					json_predicate_type_constraint
 					json_key_uniqueness_constraint_opt		%prec IS
 				{
-					$$ = makeNotExpr(makeJsonIsPredicate($1, $2, $5, $6, @1), @1);
+					$$ = makeNotExpr(makeJsonIsPredicate($1, $2, $5, $6, InvalidOid, @1), @1);
 				}
 			*/
 			| DEFAULT
diff --git a/src/backend/parser/parse_expr.c b/src/backend/parser/parse_expr.c
index 96991cae764..3ef2fda0e7a 100644
--- a/src/backend/parser/parse_expr.c
+++ b/src/backend/parser/parse_expr.c
@@ -4066,7 +4066,7 @@ transformJsonParseArg(ParseState *pstate, Node *jsexpr, JsonFormat *format,
 	Node	   *raw_expr = transformExprRecurse(pstate, jsexpr);
 	Node	   *expr = raw_expr;
 
-	*exprtype = exprType(expr);
+	*exprtype = getBaseType(exprType(expr));
 
 	/* prepare input document */
 	if (*exprtype == BYTEAOID)
@@ -4119,13 +4119,14 @@ transformJsonIsPredicate(ParseState *pstate, JsonIsPredicate *pred)
 	/* make resulting expression */
 	if (exprtype != TEXTOID && exprtype != JSONOID && exprtype != JSONBOID)
 		ereport(ERROR,
-				(errcode(ERRCODE_DATATYPE_MISMATCH),
-				 errmsg("cannot use type %s in IS JSON predicate",
-						format_type_be(exprtype))));
+				errcode(ERRCODE_DATATYPE_MISMATCH),
+				errmsg("cannot use type %s in IS JSON predicate",
+					   format_type_be(exprType(expr))),
+				parser_errposition(pstate, exprLocation(expr)));
 
 	/* This intentionally(?) drops the format clause. */
 	return makeJsonIsPredicate(expr, NULL, pred->item_type,
-							   pred->unique_keys, pred->location);
+							   pred->unique_keys, exprtype, pred->location);
 }
 
 /*
diff --git a/src/include/nodes/makefuncs.h b/src/include/nodes/makefuncs.h
index 982ec25ae14..bf54d39feb0 100644
--- a/src/include/nodes/makefuncs.h
+++ b/src/include/nodes/makefuncs.h
@@ -117,7 +117,7 @@ extern JsonValueExpr *makeJsonValueExpr(Expr *raw_expr, Expr *formatted_expr,
 extern Node *makeJsonKeyValue(Node *key, Node *value);
 extern Node *makeJsonIsPredicate(Node *expr, JsonFormat *format,
 								 JsonValueType item_type, bool unique_keys,
-								 int location);
+								 Oid exprBaseType, int location);
 extern JsonBehavior *makeJsonBehavior(JsonBehaviorType btype, Node *expr,
 									  int location);
 extern JsonTablePath *makeJsonTablePath(Const *pathvalue, char *pathname);
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 384df50c80a..89fe766dda9 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -1762,6 +1762,7 @@ typedef struct JsonIsPredicate
 	JsonFormat *format;			/* FORMAT clause, if specified */
 	JsonValueType item_type;	/* JSON item type */
 	bool		unique_keys;	/* check key uniqueness? */
+	Oid			exprBaseType;	/* base type of the subject expression */
 	ParseLoc	location;		/* token location, or -1 if unknown */
 } JsonIsPredicate;
 
diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out
index c7b9e575445..f448331c690 100644
--- a/src/test/regress/expected/sqljson.out
+++ b/src/test/regress/expected/sqljson.out
@@ -1148,6 +1148,63 @@ SELECT NULL::bytea IS JSON;
 
 SELECT NULL::int IS JSON;
 ERROR:  cannot use type integer in IS JSON predicate
+LINE 1: SELECT NULL::int IS JSON;
+               ^
+-- IS JSON with domain types
+CREATE DOMAIN jd1 AS json CHECK ((VALUE ->'a')::text <> '3');
+CREATE DOMAIN jd2 AS jsonb CHECK ((VALUE ->'a') = '1'::jsonb);
+CREATE DOMAIN jd3 AS text CHECK (VALUE <> 'a');
+CREATE DOMAIN jd4 AS bytea CHECK (VALUE <> '\x61');
+CREATE DOMAIN jd5 AS date CHECK (VALUE <> NULL);
+-- NULLs through domains should return NULL (not error)
+SELECT NULL::jd1 IS JSON, NULL::jd2 IS JSON, NULL::jd3 IS JSON, NULL::jd4 IS JSON;
+ ?column? | ?column? | ?column? | ?column? 
+----------+----------+----------+----------
+          |          |          | 
+(1 row)
+
+SELECT NULL::jd1 IS NOT JSON;
+ ?column? 
+----------
+ 
+(1 row)
+
+-- domain over unsupported base type should error
+SELECT NULL::jd5 IS JSON; -- error
+ERROR:  cannot use type jd5 in IS JSON predicate
+LINE 1: SELECT NULL::jd5 IS JSON;
+               ^
+SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS; -- error
+ERROR:  cannot use type jd5 in IS JSON predicate
+LINE 1: SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS;
+               ^
+-- domain constraint violation during cast
+SELECT a::jd2 IS JSON WITH UNIQUE KEYS as col1 FROM (VALUES('{"a": 1, "a": 2}')) s(a); -- error
+ERROR:  value for domain jd2 violates check constraint "jd2_check"
+-- view creation and deparsing with domain IS JSON
+CREATE VIEW domain_isjson AS
+WITH cte(a) AS (VALUES('{"a": 1, "a": 2}'))
+SELECT	a::jd1 IS JSON WITH UNIQUE KEYS as jd1,
+		a::jd3 IS JSON WITH UNIQUE KEYS as jd3,
+		a::jd4 IS JSON WITH UNIQUE KEYS as jd4
+FROM cte;
+\sv domain_isjson
+CREATE OR REPLACE VIEW public.domain_isjson AS
+ WITH cte(a) AS (
+         VALUES ('{"a": 1, "a": 2}'::text)
+        )
+ SELECT a::jd1 IS JSON WITH UNIQUE KEYS AS jd1,
+    a::jd3 IS JSON WITH UNIQUE KEYS AS jd3,
+    a::jd4 IS JSON WITH UNIQUE KEYS AS jd4
+   FROM cte
+SELECT * FROM domain_isjson;
+ jd1 | jd3 | jd4 
+-----+-----+-----
+ f   | f   | f
+(1 row)
+
+DROP VIEW domain_isjson;
+DROP DOMAIN jd5, jd4, jd3, jd2, jd1;
 SELECT '' IS JSON;
  ?column? 
 ----------
diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql
index 343d344d270..d75158afd20 100644
--- a/src/test/regress/sql/sqljson.sql
+++ b/src/test/regress/sql/sqljson.sql
@@ -395,6 +395,37 @@ SELECT NULL::text IS JSON;
 SELECT NULL::bytea IS JSON;
 SELECT NULL::int IS JSON;
 
+-- IS JSON with domain types
+CREATE DOMAIN jd1 AS json CHECK ((VALUE ->'a')::text <> '3');
+CREATE DOMAIN jd2 AS jsonb CHECK ((VALUE ->'a') = '1'::jsonb);
+CREATE DOMAIN jd3 AS text CHECK (VALUE <> 'a');
+CREATE DOMAIN jd4 AS bytea CHECK (VALUE <> '\x61');
+CREATE DOMAIN jd5 AS date CHECK (VALUE <> NULL);
+
+-- NULLs through domains should return NULL (not error)
+SELECT NULL::jd1 IS JSON, NULL::jd2 IS JSON, NULL::jd3 IS JSON, NULL::jd4 IS JSON;
+SELECT NULL::jd1 IS NOT JSON;
+
+-- domain over unsupported base type should error
+SELECT NULL::jd5 IS JSON; -- error
+SELECT NULL::jd5 IS JSON WITH UNIQUE KEYS; -- error
+
+-- domain constraint violation during cast
+SELECT a::jd2 IS JSON WITH UNIQUE KEYS as col1 FROM (VALUES('{"a": 1, "a": 2}')) s(a); -- error
+
+-- view creation and deparsing with domain IS JSON
+CREATE VIEW domain_isjson AS
+WITH cte(a) AS (VALUES('{"a": 1, "a": 2}'))
+SELECT	a::jd1 IS JSON WITH UNIQUE KEYS as jd1,
+		a::jd3 IS JSON WITH UNIQUE KEYS as jd3,
+		a::jd4 IS JSON WITH UNIQUE KEYS as jd4
+FROM cte;
+\sv domain_isjson
+SELECT * FROM domain_isjson;
+
+DROP VIEW domain_isjson;
+DROP DOMAIN jd5, jd4, jd3, jd2, jd1;
+
 SELECT '' IS JSON;
 
 SELECT bytea '\x00' IS JSON;
-- 
2.43.0

Reply via email to