Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions common/utils/src/main/resources/error/error-conditions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1963,6 +1963,16 @@
"<functionName> has an invalid or unsupported JSON path <path>. Only simple, wildcard-free paths are supported."
]
},
"INVALID_JSON_QUERY_RETURNING_TYPE" : {
"message" : [
"<functionName> cannot return a value of type <returningType>. The RETURNING type must be a string type."
]
},
"INVALID_JSON_QUERY_WRAPPER_AND_QUOTES" : {
"message" : [
"<functionName> cannot combine an OMIT QUOTES clause with a WITH ARRAY WRAPPER clause. OMIT QUOTES applies only to an unwrapped result."
]
},
"INVALID_JSON_SCALAR_RETURNING_TYPE" : {
"message" : [
"<functionName> cannot return a value of type <returningType>. The RETURNING type must be a scalar (string, numeric, boolean, or datetime) type."
Expand Down Expand Up @@ -5589,6 +5599,24 @@
],
"sqlState" : "42K0E"
},
"JSON_QUERY_ON_ERROR" : {
"message" : [
"<functionName> could not extract a value at path <path>."
],
"subClass" : {
"EMPTY" : {
"message" : [
"The path matched no value. This error was requested by the ERROR ON EMPTY clause."
]
},
"ERROR" : {
"message" : [
"The input is not valid JSON. This error was requested by the ERROR ON ERROR clause."
]
}
},
"sqlState" : "2203G"
},
"JSON_VALUE_ON_ERROR" : {
"message" : [
"<functionName> could not extract a scalar value at path <path>."
Expand Down
8 changes: 8 additions & 0 deletions docs/sql-ref-ansi-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ Below is a list of all the keywords in Spark SQL.
|COMPUTE|non-reserved|non-reserved|non-reserved|
|CONCATENATE|non-reserved|non-reserved|non-reserved|
|CONDITION|non-reserved|non-reserved|non-reserved|
|CONDITIONAL|non-reserved|non-reserved|non-reserved|
|CONSTRAINT|reserved|non-reserved|reserved|
|CONTAINS|non-reserved|non-reserved|non-reserved|
|CONTINUE|non-reserved|non-reserved|non-reserved|
Expand Down Expand Up @@ -618,8 +619,10 @@ Below is a list of all the keywords in Spark SQL.
|ITERATE|non-reserved|non-reserved|non-reserved|
|JOIN|reserved|strict-non-reserved|reserved|
|JSON|non-reserved|non-reserved|non-reserved|
|JSON_QUERY|non-reserved|non-reserved|reserved|
|JSON_TABLE|non-reserved|non-reserved|reserved|
|JSON_VALUE|non-reserved|non-reserved|reserved|
|KEEP|non-reserved|non-reserved|non-reserved|
|KEY|non-reserved|non-reserved|non-reserved|
|KEYS|non-reserved|non-reserved|non-reserved|
|LANGUAGE|non-reserved|non-reserved|reserved|
Expand Down Expand Up @@ -679,8 +682,10 @@ Below is a list of all the keywords in Spark SQL.
|NULL|reserved|non-reserved|reserved|
|NULLS|non-reserved|non-reserved|non-reserved|
|NUMERIC|non-reserved|non-reserved|non-reserved|
|OBJECT|non-reserved|non-reserved|non-reserved|
|OF|non-reserved|non-reserved|reserved|
|OFFSET|reserved|non-reserved|reserved|
|OMIT|non-reserved|non-reserved|reserved|
|ON|reserved|strict-non-reserved|reserved|
|ONLY|reserved|non-reserved|reserved|
|OPEN|non-reserved|non-reserved|reserved|
Expand Down Expand Up @@ -714,6 +719,7 @@ Below is a list of all the keywords in Spark SQL.
|QUALIFY|non-reserved|non-reserved|non-reserved|
|QUARTER|non-reserved|non-reserved|non-reserved|
|QUERY|non-reserved|non-reserved|non-reserved|
|QUOTES|non-reserved|non-reserved|non-reserved|
|RANGE|non-reserved|non-reserved|reserved|
|READ|non-reserved|non-reserved|non-reserved|
|READS|non-reserved|non-reserved|non-reserved|
Expand Down Expand Up @@ -824,6 +830,7 @@ Below is a list of all the keywords in Spark SQL.
|UNARCHIVE|non-reserved|non-reserved|non-reserved|
|UNBOUNDED|non-reserved|non-reserved|non-reserved|
|UNCACHE|non-reserved|non-reserved|non-reserved|
|UNCONDITIONAL|non-reserved|non-reserved|non-reserved|
|UNIFORM|non-reserved|non-reserved|non-reserved|
|UNION|reserved|strict-non-reserved|reserved|
|UNIQUE|reserved|non-reserved|reserved|
Expand Down Expand Up @@ -858,6 +865,7 @@ Below is a list of all the keywords in Spark SQL.
|WITH|reserved|non-reserved|reserved|
|WITHIN|reserved|non-reserved|reserved|
|WITHOUT|non-reserved|non-reserved|non-reserved|
|WRAPPER|non-reserved|non-reserved|non-reserved|
|X|non-reserved|non-reserved|non-reserved|
|YEAR|non-reserved|non-reserved|non-reserved|
|YEARS|non-reserved|non-reserved|non-reserved|
Expand Down
174 changes: 174 additions & 0 deletions docs/sql-ref-syntax-qry-select-json-query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
layout: global
title: JSON_QUERY
displayTitle: JSON_QUERY
license: |
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---

### Description

The `JSON_QUERY` function extracts the JSON value located by a SQL/JSON path from a JSON document
and returns it as JSON text (a `STRING`). This is the SQL-standard way (SQL:2016) to pull an object,
array, or scalar fragment out of JSON, and is commonly used to migrate queries from other systems
such as Oracle, SQL Server, and Trino. Unlike
[JSON_TABLE](sql-ref-syntax-qry-select-json-table.html), which produces rows in a `FROM` clause,
`JSON_QUERY` is an expression that can appear anywhere a value is allowed.

Where [JSON_VALUE](sql-ref-syntax-qry-select-json-value.html) returns a single scalar (and treats an
object or array match as an error), `JSON_QUERY` returns the matched value serialized as JSON text,
whether it is an object, an array, or a scalar.

This implementation supports simple, wildcard-free SQL/JSON paths only. The `PASSING` clause, path
predicates and filters, and explicit `lax` / `strict` path modes defined by SQL:2016 are not
supported.

### Syntax

```sql
JSON_QUERY ( json_expr, path
[ RETURNING data_type ]
[ wrapper_behavior ]
[ quotes_behavior ]
[ empty_behavior ON EMPTY ]
[ error_behavior ON ERROR ] )

wrapper_behavior
{ WITHOUT [ ARRAY ] WRAPPER
| WITH [ CONDITIONAL | UNCONDITIONAL ] [ ARRAY ] WRAPPER }

quotes_behavior
{ KEEP QUOTES | OMIT QUOTES }

empty_behavior
{ NULL | ERROR | EMPTY ARRAY | EMPTY OBJECT }

error_behavior
{ NULL | ERROR | EMPTY ARRAY | EMPTY OBJECT }
```

### Parameters

* **json_expr**

An expression that evaluates to a `STRING` containing the JSON document. A `NULL` input yields
`NULL` directly (it triggers neither the `ON EMPTY` nor the `ON ERROR` behavior).

* **path**

A SQL/JSON path literal that locates the value, for example `'$.a.b'` or `'$.items[0]'`. The
path must be wildcard-free; a path containing `[*]` is rejected at analysis time.

* **RETURNING data_type**

The type of the result. It must be a string type; the result is JSON text. If `RETURNING` is
omitted, the result type is `STRING`.

* **wrapper_behavior**

Whether to wrap the result in a JSON array:
* `WITHOUT ARRAY WRAPPER` (the default) returns the value unwrapped.
* `WITH UNCONDITIONAL ARRAY WRAPPER` (or simply `WITH ARRAY WRAPPER`) always wraps the value in a
one-element array.
* `WITH CONDITIONAL ARRAY WRAPPER` wraps the value only when it is a scalar; an object or array
is returned unwrapped.

* **quotes_behavior**

Whether to keep the surrounding quotes of a scalar string result:
* `KEEP QUOTES` (the default) leaves them, so a string is returned as a quoted JSON string.
* `OMIT QUOTES` strips them, returning the raw string content. It is a no-op for objects,
arrays, and non-string scalars, and cannot be combined with an array wrapper.

* **empty_behavior ON EMPTY**

What to produce when `path` matches nothing:
* `NULL` (the default) returns SQL `NULL`.
* `ERROR` raises an error.
* `EMPTY ARRAY` returns the JSON text `[]`.
* `EMPTY OBJECT` returns the JSON text `{}`.

* **error_behavior ON ERROR**

What to produce when the input is not well-formed JSON. The same four choices as `ON EMPTY`
apply, defaulting to `NULL`.

A path that matches an explicit JSON `null` is a present scalar value and returns the JSON text
`null` (it is neither the `ON EMPTY` nor the `ON ERROR` case).

Returning a scalar under the default `WITHOUT ARRAY WRAPPER` is an intentional convenience: the
matched scalar is emitted as JSON text (for example, `JSON_QUERY('{"id":7}', '$.id')` returns `7`),
whereas strict SQL:2016 treats a scalar without a wrapper as an error. The wrapper clauses behave the
standard way: `WITH CONDITIONAL ARRAY WRAPPER` wraps a scalar in a one-element array (`7` becomes
`[7]`) while leaving a single object or array unwrapped, and `WITH UNCONDITIONAL ARRAY WRAPPER`
always wraps.

### Examples

```sql
-- Extract an object as JSON text
SELECT json_query('{"id":7,"addr":{"city":"NYC"}}', '$.addr');
+---------------------------------------------------+
|json_query({"id":7,"addr":{"city":"NYC"}}, $.addr) |
+---------------------------------------------------+
|{"city":"NYC"} |
+---------------------------------------------------+

-- Extract an array
SELECT json_query('{"tags":["x","y"]}', '$.tags');
+-------------------------------------------+
|json_query({"tags":["x","y"]}, $.tags) |
+-------------------------------------------+
|["x","y"] |
+-------------------------------------------+

-- Wrap a scalar in an array with WITH ARRAY WRAPPER
-- (WITH ARRAY WRAPPER is a shorthand; the column name shows the canonical
-- WITH UNCONDITIONAL ARRAY WRAPPER form)
SELECT json_query('{"tags":["x","y"]}', '$.tags[0]' WITH ARRAY WRAPPER);
+----------------------------------------------------------------------------+
|json_query({"tags":["x","y"]}, $.tags[0] WITH UNCONDITIONAL ARRAY WRAPPER) |
+----------------------------------------------------------------------------+
|["x"] |
+----------------------------------------------------------------------------+

-- Strip the quotes from a scalar string with OMIT QUOTES
SELECT json_query('{"name":"Ada"}', '$.name' OMIT QUOTES);
+---------------------------------------------------+
|json_query({"name":"Ada"}, $.name OMIT QUOTES) |
+---------------------------------------------------+
|Ada |
+---------------------------------------------------+

-- A missing path defaults to NULL; supply a fallback with EMPTY ARRAY ON EMPTY
SELECT json_query('{"id":7}', '$.missing' EMPTY ARRAY ON EMPTY);
+---------------------------------------------------------+
|json_query({"id":7}, $.missing EMPTY ARRAY ON EMPTY) |
+---------------------------------------------------------+
|[] |
+---------------------------------------------------------+

-- ERROR ON ERROR raises instead of returning a value
SELECT json_query('not json', '$.a' ERROR ON ERROR);
[JSON_QUERY_ON_ERROR.ERROR] ...
```

### Related Statements

* [SELECT](sql-ref-syntax-qry-select.html)
* [JSON_VALUE](sql-ref-syntax-qry-select-json-value.html)
* [JSON_TABLE](sql-ref-syntax-qry-select-json-table.html)
* [Built-in Functions](sql-ref-functions-builtin.html)
1 change: 1 addition & 0 deletions docs/sql-ref-syntax-qry-select.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ SELECT [ hints , ... ] [ ALL | DISTINCT ] { [ [ named_expression | regex_column_
* [Set Operators](sql-ref-syntax-qry-select-setops.html)
* [TABLESAMPLE](sql-ref-syntax-qry-select-sampling.html)
* [Table-valued Function](sql-ref-syntax-qry-select-tvf.html)
* [JSON_QUERY](sql-ref-syntax-qry-select-json-query.html)
* [JSON_VALUE](sql-ref-syntax-qry-select-json-value.html)
* [Window Function](sql-ref-syntax-qry-select-window.html)
* [CASE Clause](sql-ref-syntax-qry-select-case.html)
Expand Down
1 change: 1 addition & 0 deletions docs/sql-ref-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ ability to generate logical and physical plan for a given query using
* [SORT BY Clause](sql-ref-syntax-qry-select-sortby.html)
* [TABLESAMPLE](sql-ref-syntax-qry-select-sampling.html)
* [Table-valued Function](sql-ref-syntax-qry-select-tvf.html)
* [JSON_QUERY](sql-ref-syntax-qry-select-json-query.html)
* [JSON_TABLE](sql-ref-syntax-qry-select-json-table.html)
* [JSON_VALUE](sql-ref-syntax-qry-select-json-value.html)
* [WHERE Clause](sql-ref-syntax-qry-select-where.html)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ COMPENSATION: 'COMPENSATION';
COMPUTE: 'COMPUTE';
CONCATENATE: 'CONCATENATE';
CONDITION: 'CONDITION';
CONDITIONAL: 'CONDITIONAL';
CONSTRAINT: 'CONSTRAINT';
CONTAINS: 'CONTAINS';
CONTINUE: 'CONTINUE';
Expand Down Expand Up @@ -335,8 +336,10 @@ ITEMS: 'ITEMS';
ITERATE: 'ITERATE';
JOIN: 'JOIN';
JSON: 'JSON';
JSON_QUERY: 'JSON_QUERY';
JSON_TABLE: 'JSON_TABLE';
JSON_VALUE: 'JSON_VALUE';
KEEP: 'KEEP';
KEY: 'KEY';
KEYS: 'KEYS';
LANGUAGE: 'LANGUAGE';
Expand Down Expand Up @@ -395,8 +398,10 @@ NULL: 'NULL';
NULLS: 'NULLS';
NUMERIC: 'NUMERIC';
NORELY: 'NORELY';
OBJECT: 'OBJECT';
OF: 'OF';
OFFSET: 'OFFSET';
OMIT: 'OMIT';
ON: 'ON';
ONLY: 'ONLY';
OPEN: 'OPEN';
Expand Down Expand Up @@ -430,6 +435,7 @@ PURGE: 'PURGE';
QUALIFY: 'QUALIFY';
QUARTER: 'QUARTER';
QUERY: 'QUERY';
QUOTES: 'QUOTES';
RANGE: 'RANGE';
READ: 'READ';
READS: 'READS';
Expand Down Expand Up @@ -540,6 +546,7 @@ TYPE: 'TYPE';
UNARCHIVE: 'UNARCHIVE';
UNBOUNDED: 'UNBOUNDED';
UNCACHE: 'UNCACHE';
UNCONDITIONAL: 'UNCONDITIONAL';
UNIFORM: 'UNIFORM';
UNION: 'UNION';
UNIQUE: 'UNIQUE';
Expand Down Expand Up @@ -574,6 +581,7 @@ WINDOW: 'WINDOW';
WITH: 'WITH';
WITHIN: 'WITHIN';
WITHOUT: 'WITHOUT';
WRAPPER: 'WRAPPER';
YEAR: 'YEAR';
YEARS: 'YEARS';
ZONE: 'ZONE';
Expand Down
Loading