Skip to content

[SPARK-58366][SQL] Support JSON_TABLE table-valued function - #57559

Open
ganeshashree wants to merge 3 commits into
apache:masterfrom
ganeshashree:SPARK-58366
Open

[SPARK-58366][SQL] Support JSON_TABLE table-valued function#57559
ganeshashree wants to merge 3 commits into
apache:masterfrom
ganeshashree:SPARK-58366

Conversation

@ganeshashree

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Add the ANSI SQL:2016 JSON_TABLE table-valued function, which shreds a JSON document into a relational table. A row path selects a sequence of JSON items and a COLUMNS clause projects a typed value out of each item into a column.

Syntax (flat, non-nested subset):

JSON_TABLE(json_expr, row_path
  COLUMNS (
    col1 FOR ORDINALITY,
    col2 <type> [PATH '<json_path>'],
    col3 <type> EXISTS [PATH '<json_path>']
  )
  [ { NULL | ERROR } ON ERROR ]
) [AS] alias
  • A row path ending in [*] expands a JSON array into one row per element; a non-wildcard path yields a single row for the matched value.
  • FOR ORDINALITY: a 1-based BIGINT row counter.
  • Value columns are extracted and cast to the declared type; the path may be explicit (PATH '...') or implicit ('$.<columnName>').
  • EXISTS columns are a presence test cast to the declared type; a present-but-null JSON value counts as existing, only an absent path is false.
  • { NULL | ERROR } ON ERROR: NULL ON ERROR (default) yields no rows on null/malformed input; ERROR ON ERROR raises.
  • Usable in a comma join and with LATERAL.

Implementation notes:

  • A JsonTable Generator expression is wrapped by the existing Generate operator, so no new execution operator is introduced.
  • A dedicated grammar production handles the COLUMNS(...) clause; AstBuilder.visitJsonTableRelation builds the plan. ERROR, ORDINALITY, and JSON_TABLE are added as non-reserved keywords.
  • A token-aware navigator (JsonTableEvaluator) extracts values so that a missing path, a JSON null, and the literal string "null" are all distinguished (unlike get_json_object). Non-trailing wildcard paths are rejected during analysis via DATATYPE_MISMATCH.INVALID_JSON_TABLE_PATH.
  • Only the flat subset is implemented; NESTED PATH, FORMAT JSON query columns, and DEFAULT ... ON EMPTY are left as follow-ups.

Why are the changes needed?

JSON_TABLE is the SQL-standard way to turn JSON into rows and columns, and is supported by Oracle, DB2, MySQL 8, PostgreSQL 17, Snowflake, and Trino. Spark previously required chaining from_json + explode/inline + get_json_object to achieve the same result. This folds that into one declarative, standard construct and eases migration from those systems.

Does this PR introduce any user-facing change?

Yes. It adds the JSON_TABLE SQL table-valued function and a SQL reference documentation page. ERROR, ORDINALITY, and JSON_TABLE are added as non-reserved keywords, so existing queries using them as identifiers continue to parse. There is no change to existing behavior.

How was this patch tested?

New end-to-end suite JsonTableSuite covering array expansion, ordinality, explicit/implicit paths, missing-vs-null semantics for value and EXISTS columns, NULL/ERROR ON ERROR, [*] over a non-array, mid-path wildcard rejection, non-explode string row items, arrays of scalars/strings, duplicate keys, structure-valued columns, LATERAL joins, nested-field extraction, and column aliases. Existing JsonExpressionsSuite, JsonFunctionsSuite, GeneratorFunctionSuite, PlanParserSuite, DDLParserSuite, SQLKeywordSuite, and SparkThrowableSuite continue to pass.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: Claude Code (Claude Opus 4.8)

@ganeshashree
ganeshashree force-pushed the SPARK-58366 branch 5 times, most recently from efcc563 to d8b3a24 Compare July 28, 2026 03:31

@cloud-fan cloud-fan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 blocking, 1 non-blocking, 1 nit.
The semantics are coherent and well covered, but wide projections should avoid reparsing every row once per output column.

Suggestions (1)

  • sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/jsonExpressions.scala:548: JSON_TABLE reparses each row item once per projected value or EXISTS column, so wide projections multiply parsing work by the column count. -- see inline

Nits: 1 minor item (see inline comments).

Verification

I traced the new grammar through AstBuilder into JsonTable and the existing Generate execution path. I also checked path validation, missing-versus-null handling, ANSI cast-mode capture, lazy parser cleanup, lateral activation, and the corresponding focused tests.

@cloud-fan

Copy link
Copy Markdown
Contributor

can we fix merge conflicts?

Add the ANSI SQL:2016 `JSON_TABLE` table-valued function, which shreds a JSON
document into a relational table. A row path selects a sequence of JSON items
and a `COLUMNS` clause projects a typed value out of each item into a column.

Syntax (flat, non-nested subset):

    JSON_TABLE(json_expr, row_path
      COLUMNS (
        col1 FOR ORDINALITY,
        col2 <type> [PATH '<json_path>'],
        col3 <type> EXISTS [PATH '<json_path>']
      )
      [ { NULL | ERROR } ON ERROR ]
    ) [AS] alias

- A row path ending in `[*]` expands a JSON array into one row per element; a
  non-wildcard path yields a single row for the matched value.
- `FOR ORDINALITY`: a 1-based BIGINT row counter.
- Value columns are extracted and cast to the declared type; the path may be
  explicit (`PATH '...'`) or implicit (`'$.<columnName>'`).
- `EXISTS` columns are a presence test cast to the declared type; a
  present-but-null JSON value counts as existing, only an absent path is false.
- `{ NULL | ERROR } ON ERROR`: `NULL ON ERROR` (default) yields no rows on
  null/malformed input; `ERROR ON ERROR` raises.
- Usable in a comma join and with `LATERAL`.

Implementation notes:
- A `JsonTable` `Generator` expression is wrapped by the existing `Generate`
  operator, so no new execution operator is introduced.
- A dedicated grammar production handles the `COLUMNS(...)` clause;
  `AstBuilder.visitJsonTableRelation` builds the plan. `ERROR`, `ORDINALITY`,
  and `JSON_TABLE` are added as non-reserved keywords.
- A token-aware navigator (`JsonTableEvaluator`) extracts values so that a
  missing path, a JSON `null`, and the literal string `"null"` are all
  distinguished (unlike `get_json_object`). Non-trailing wildcard paths are
  rejected during analysis via `DATATYPE_MISMATCH.INVALID_JSON_TABLE_PATH`.
- Only the flat subset is implemented; `NESTED PATH`, `FORMAT JSON` query
  columns, and `DEFAULT ... ON EMPTY` are left as follow-ups.

`JSON_TABLE` is the SQL-standard way to turn JSON into rows and columns, and is
supported by Oracle, DB2, MySQL 8, PostgreSQL 17, Snowflake, and Trino. Spark
previously required chaining `from_json` + `explode`/`inline` + `get_json_object`
to achieve the same result. This folds that into one declarative, standard
construct and eases migration from those systems.

Yes. It adds the `JSON_TABLE` SQL table-valued function and a SQL reference
documentation page. `ERROR`, `ORDINALITY`, and `JSON_TABLE` are added as
non-reserved keywords, so existing queries using them as identifiers continue to
parse. There is no change to existing behavior.

New end-to-end suite `JsonTableSuite` covering array expansion, ordinality,
explicit/implicit paths, missing-vs-null semantics for value and EXISTS columns,
`NULL`/`ERROR ON ERROR`, `[*]` over a non-array, mid-path wildcard rejection,
non-explode string row items, arrays of scalars/strings, duplicate keys,
structure-valued columns, `LATERAL` joins, nested-field extraction, and column
aliases. Existing `JsonExpressionsSuite`, `JsonFunctionsSuite`,
`GeneratorFunctionSuite`, `PlanParserSuite`, `DDLParserSuite`, `SQLKeywordSuite`,
and `SparkThrowableSuite` continue to pass.

Generated-by: Claude Code (Claude Opus 4.8)
Address review comments: JSON_TABLE previously created a fresh Jackson
parser and rescanned each row item once per projected value/EXISTS
column, making wide projections O(columns * row size) per row.

Build a prefix trie over the column paths once per invocation and resolve
every column in a single traversal of each row item via navigateColumns.
Ordinality-only tables skip parsing entirely, and object/array subtrees
with no matching column paths are skipped without descending.

Also fix the JsonPathResult.Found doc to match serializeCurrentValue.

Co-authored-by: Isaac
Address review comments on the JSON_TABLE feature:

- Add a `sql` override to `JsonTable` so analysis/type-check diagnostics
  render the full `JSON_TABLE(...)` syntax (row path, columns, ON ERROR)
  instead of only `json_table(<json_expr>)`.
- Fix the default-column-path docs: an omitted PATH reads the column name
  as a single object key (`$.name` for simple identifiers, `$['a.b']` for
  dotted/special names), not the nested path `$.<column_name>`.
- Link JSON_TABLE from the SELECT page's from_item list so it is
  discoverable from the main SELECT syntax page.

Co-authored-by: Isaac
@ganeshashree

Copy link
Copy Markdown
Contributor Author

can we fix merge conflicts?

Fixed merge conflicts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants