From 454dbae1977707dd8ed101670e39a8516cc5e3d3 Mon Sep 17 00:00:00 2001 From: Narayanan Venkateswaran Date: Mon, 19 Oct 2026 20:10:07 +0000 Subject: [PATCH v2] Allow NULL and UNIQUE clauses in empty JSON_OBJECT() and JSON_ARRAY() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Calling JSON_OBJECT() or JSON_ARRAY() with no value expressions while specifying modifier clauses—such as NULL-handling (NULL ON NULL or ABSENT ON NULL), key uniqueness constraints (WITH or WITHOUT UNIQUE KEYS for objects), or the output type (RETURNING ...)—previously failed with a syntax error. In gram.y, the rules handling these modifier clauses unconditionally required non-empty argument expression lists (json_name_and_value_list for objects and json_value_expr_list for arrays). The rules accepting empty argument lists only accepted the RETURNING clause. Fix this 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 productions in gram.y for empty constructor calls when modifier clauses are present. Add regression tests in sqljson covering modifier clause combinations for empty JSON_OBJECT() and JSON_ARRAY() calls. --- src/backend/parser/gram.y | 61 +++++++++++++++++++++++++-- src/test/regress/expected/sqljson.out | 60 ++++++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 10 +++++ 3 files changed, 128 insertions(+), 3 deletions(-) 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.212.g129ede20d9-goog