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 <jian.universality@gmail.com>
Reviewed-by: Andrew Dunstan <andrew@dunslane.net>
Reviewed-by: Kirill Reshke <reshkekirill@gmail.com>
Discussion: https://postgr.es/m/CACJufxEk34DnJFG72CRsPPT4tsJL9arobX0tNPsn7yH28J=zQg@mail.gmail.com
This commit is contained in:
Andrew Dunstan 2026-03-13 11:51:26 +08:00
parent f5eb854ab6
commit 3b4c2b9db2
8 changed files with 103 additions and 14 deletions

View file

@ -4742,7 +4742,7 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op)
{
JsonIsPredicate *pred = op->d.is_json.pred;
Datum js = *op->resvalue;
Oid exprtype;
Oid exprtype = pred->exprBaseType;
bool res;
if (*op->resnull)
@ -4751,8 +4751,6 @@ ExecEvalJsonIsPredicate(ExprState *state, ExprEvalStep *op)
return;
}
exprtype = exprType(pred->expr);
if (exprtype == TEXTOID || exprtype == JSONOID)
{
text *json = DatumGetTextP(js);

View file

@ -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;

View file

@ -16161,7 +16161,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
@ -16170,7 +16170,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
@ -16179,7 +16179,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
@ -16189,7 +16189,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

View file

@ -4077,7 +4077,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)
@ -4130,13 +4130,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);
}
/*

View file

@ -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);

View file

@ -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;

View file

@ -1313,6 +1313,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?
----------

View file

@ -484,6 +484,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;