From 39b95894e0822371283b99e776dc9164e9eb5ea6 Mon Sep 17 00:00:00 2001 From: Narayanan Venkateswaran Date: Tue, 27 Oct 2026 05:52:05 +0000 Subject: [PATCH v3] Allow NULL and UNIQUE clauses in empty JSON_OBJECT() and JSON_ARRAY() The SQL standard (ISO/IEC 9075-2) grammar specifies modifier clauses (NULL handling and key uniqueness constraints) within the optional group requiring expression arguments, grammatically disallowing them when no arguments are supplied. However, PostgreSQL's documentation specifies these clauses as independent optional clauses for both JSON_OBJECT() and JSON_ARRAY(), matching behavior in other implementations such as Oracle. Previously, gram.y only accepted empty calls via: JSON_OBJECT '(' json_returning_clause_opt ')' JSON_ARRAY '(' json_returning_clause_opt ')' Any attempt to specify null or uniqueness clauses without value expressions, such as JSON_OBJECT(ABSENT ON NULL) or JSON_ARRAY(NULL ON NULL), resulted in a syntax error because the rules handling modifier clauses unconditionally required non-empty argument expression lists. Fix by factoring json_object_constructor_null_clause, json_array_constructor_null_clause, and json_key_uniqueness_constraint into non-optional base rules, and adding dedicated conflict-free productions for empty constructor calls when modifier clauses are present. Also clarify the documentation and add regression tests in sqljson covering modifier clause combinations for empty JSON_OBJECT() and JSON_ARRAY() calls. --- doc/src/sgml/func/func-json.sgml | 7 ++- src/backend/parser/gram.y | 61 +++++++++++++++++++++++++-- src/test/regress/expected/sqljson.out | 60 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 10 +++++ 4 files changed, 133 insertions(+), 5 deletions(-) diff --git a/doc/src/sgml/func/func-json.sgml b/doc/src/sgml/func/func-json.sgml index 0763ec81ed8..134beae5bd2 100644 --- a/doc/src/sgml/func/func-json.sgml +++ b/doc/src/sgml/func/func-json.sgml @@ -617,8 +617,10 @@ Constructs a JSON array from either a series of value_expression parameters or from the results of query_expression, - which must be a SELECT query returning a single column. If - ABSENT ON NULL is specified, NULL values are ignored. + which must be a SELECT query returning a single column. + An empty JSON array is returned if no parameters are given; + modifier clauses may also be specified in that case. + If ABSENT ON NULL is specified, NULL values are ignored. This is always the case if a query_expression is used. If the query returns no rows, an empty JSON array is returned. @@ -719,6 +721,7 @@ Constructs a JSON object of all the key/value pairs given, or an empty object if none are given. + Modifier clauses may also be specified when constructing an empty object. key_expression is a scalar expression defining the JSON key, which is converted to the text type. diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0563453fe24..03586a5a94b 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -689,8 +689,11 @@ static Node *makeRecursiveViewSelect(char *relname, List *aliases, Node *query); json_table_default_plan_union_cross json_wrapper_behavior %type json_key_uniqueness_constraint_opt + json_key_uniqueness_constraint json_object_constructor_null_clause_opt + json_object_constructor_null_clause json_array_constructor_null_clause_opt + json_array_constructor_null_clause /* * Non-keyword token types. These are hard-wired into the "flex" lexer. @@ -16484,6 +16487,33 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_OBJECT '(' + json_object_constructor_null_clause + json_key_uniqueness_constraint_opt + json_returning_clause_opt ')' + { + JsonObjectConstructor *n = makeNode(JsonObjectConstructor); + + n->exprs = NULL; + n->absent_on_null = $3; + n->unique = $4; + n->output = (JsonOutput *) $5; + n->location = @1; + $$ = (Node *) n; + } + | JSON_OBJECT '(' + json_key_uniqueness_constraint + json_returning_clause_opt ')' + { + JsonObjectConstructor *n = makeNode(JsonObjectConstructor); + + n->exprs = NULL; + n->absent_on_null = false; + n->unique = $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } | JSON_OBJECT '(' json_returning_clause_opt ')' { JsonObjectConstructor *n = makeNode(JsonObjectConstructor); @@ -16525,6 +16555,19 @@ func_expr_common_subexpr: n->location = @1; $$ = (Node *) n; } + | JSON_ARRAY '(' + json_array_constructor_null_clause + json_returning_clause_opt + ')' + { + JsonArrayConstructor *n = makeNode(JsonArrayConstructor); + + n->exprs = NIL; + n->absent_on_null = $3; + n->output = (JsonOutput *) $4; + n->location = @1; + $$ = (Node *) n; + } | JSON_ARRAY '(' json_returning_clause_opt ')' @@ -17531,11 +17574,15 @@ json_predicate_type_constraint: * KEYS-less productions a precedence less than IDENT (i.e., less than KEYS). * This prevents reducing them when the next token is KEYS. */ -json_key_uniqueness_constraint_opt: +json_key_uniqueness_constraint: WITH UNIQUE KEYS { $$ = true; } | WITH UNIQUE %prec UNBOUNDED { $$ = true; } | WITHOUT UNIQUE KEYS { $$ = false; } | WITHOUT UNIQUE %prec UNBOUNDED { $$ = false; } + ; + +json_key_uniqueness_constraint_opt: + json_key_uniqueness_constraint { $$ = $1; } | /* EMPTY */ %prec UNBOUNDED { $$ = false; } ; @@ -17560,15 +17607,23 @@ json_name_and_value: ; /* empty means false for objects, true for arrays */ -json_object_constructor_null_clause_opt: +json_object_constructor_null_clause: NULL_P ON NULL_P { $$ = false; } | ABSENT ON NULL_P { $$ = true; } + ; + +json_object_constructor_null_clause_opt: + json_object_constructor_null_clause { $$ = $1; } | /* EMPTY */ { $$ = false; } ; -json_array_constructor_null_clause_opt: +json_array_constructor_null_clause: NULL_P ON NULL_P { $$ = false; } | ABSENT ON NULL_P { $$ = true; } + ; + +json_array_constructor_null_clause_opt: + json_array_constructor_null_clause { $$ = $1; } | /* EMPTY */ { $$ = true; } ; diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 30f9e10ef0a..6436aa56a70 100644 --- a/src/test/regress/expected/sqljson.out +++ b/src/test/regress/expected/sqljson.out @@ -295,6 +295,42 @@ SELECT JSON_OBJECT(); {} (1 row) +SELECT JSON_OBJECT(NULL ON NULL); + json_object +------------- + {} +(1 row) + +SELECT JSON_OBJECT(ABSENT ON NULL); + json_object +------------- + {} +(1 row) + +SELECT JSON_OBJECT(WITH UNIQUE KEYS); + json_object +------------- + {} +(1 row) + +SELECT JSON_OBJECT(WITHOUT UNIQUE); + json_object +------------- + {} +(1 row) + +SELECT JSON_OBJECT(NULL ON NULL WITH UNIQUE KEYS); + json_object +------------- + {} +(1 row) + +SELECT JSON_OBJECT(ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb); + json_object +------------- + {} +(1 row) + SELECT JSON_OBJECT(RETURNING json); json_object ------------- @@ -617,6 +653,30 @@ SELECT JSON_ARRAY(); [] (1 row) +SELECT JSON_ARRAY(NULL ON NULL); + json_array +------------ + [] +(1 row) + +SELECT JSON_ARRAY(ABSENT ON NULL); + json_array +------------ + [] +(1 row) + +SELECT JSON_ARRAY(NULL ON NULL RETURNING jsonb); + json_array +------------ + [] +(1 row) + +SELECT JSON_ARRAY(ABSENT ON NULL RETURNING jsonb); + json_array +------------ + [] +(1 row) + SELECT JSON_ARRAY(RETURNING json); json_array ------------ diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 9fa203ccb53..aa02adeee33 100644 --- a/src/test/regress/sql/sqljson.sql +++ b/src/test/regress/sql/sqljson.sql @@ -64,6 +64,12 @@ EXPLAIN (VERBOSE, COSTS OFF) SELECT JSON_SERIALIZE('{}' RETURNING bytea); -- JSON_OBJECT() SELECT JSON_OBJECT(); +SELECT JSON_OBJECT(NULL ON NULL); +SELECT JSON_OBJECT(ABSENT ON NULL); +SELECT JSON_OBJECT(WITH UNIQUE KEYS); +SELECT JSON_OBJECT(WITHOUT UNIQUE); +SELECT JSON_OBJECT(NULL ON NULL WITH UNIQUE KEYS); +SELECT JSON_OBJECT(ABSENT ON NULL WITH UNIQUE KEYS RETURNING jsonb); SELECT JSON_OBJECT(RETURNING json); SELECT JSON_OBJECT(RETURNING json FORMAT JSON); SELECT JSON_OBJECT(RETURNING jsonb); @@ -177,6 +183,10 @@ DROP TYPE mood; -- JSON_ARRAY() SELECT JSON_ARRAY(); +SELECT JSON_ARRAY(NULL ON NULL); +SELECT JSON_ARRAY(ABSENT ON NULL); +SELECT JSON_ARRAY(NULL ON NULL RETURNING jsonb); +SELECT JSON_ARRAY(ABSENT ON NULL RETURNING jsonb); SELECT JSON_ARRAY(RETURNING json); SELECT JSON_ARRAY(RETURNING json FORMAT JSON); SELECT JSON_ARRAY(RETURNING jsonb); -- 2.56.0.307.g5fe7bd680c-goog