From 36c8e8534ed2e4868b8b511a8f300ccb3015c74e Mon Sep 17 00:00:00 2001 From: Narayanan Venkateswaran Date: Wed, 14 Oct 2026 12:12:18 +0000 Subject: [PATCH v1] Allow NULL and UNIQUE clauses in empty JSON_OBJECT() The SQL standard permits calling JSON_OBJECT() with no key-value pairs to construct an empty JSON object while still specifying modifier clauses such as NULL-handling (NULL ON NULL / ABSENT ON NULL), key uniqueness constraints (WITH / WITHOUT UNIQUE KEYS), and the output type (RETURNING ...). Previously, gram.y only accepted empty arguments via: JSON_OBJECT '(' json_returning_clause_opt ')' Any attempt to specify null or uniqueness clauses without key-value pairs, such as JSON_OBJECT(ABSENT ON NULL) or JSON_OBJECT(WITH UNIQUE KEYS), resulted in a syntax error because the rule with modifier clauses unconditionally required a non-empty json_name_and_value_list. Simply making json_name_and_value_list optional causes shift/reduce conflicts in Bison because tokens like NULL and ABSENT can serve as both expressions/identifiers and clause keywords, requiring more than one token of lookahead to disambiguate. Fix by factoring json_object_constructor_null_clause and json_key_uniqueness_constraint into non-optional base rules, and adding dedicated productions for empty JSON_OBJECT() calls when a null clause or uniqueness clause is present. Add regression tests in sqljson covering all valid combinations of modifier clauses for empty JSON_OBJECT() calls. --- src/backend/parser/gram.y | 41 +++++++++++++++++++++++++-- src/test/regress/expected/sqljson.out | 36 +++++++++++++++++++++++ src/test/regress/sql/sqljson.sql | 6 ++++ 3 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y index 0563453fe24..95a739a4361 100644 --- a/src/backend/parser/gram.y +++ b/src/backend/parser/gram.y @@ -689,7 +689,9 @@ 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 /* @@ -16484,6 +16486,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); @@ -17531,11 +17560,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,9 +17593,13 @@ 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; } ; diff --git a/src/test/regress/expected/sqljson.out b/src/test/regress/expected/sqljson.out index 30f9e10ef0a..029c0cee879 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 ------------- diff --git a/src/test/regress/sql/sqljson.sql b/src/test/regress/sql/sqljson.sql index 9fa203ccb53..55dd1558160 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); -- 2.56.0.385.gd3acb90ef8-goog