feat(tsql): support FOR JSON clause [CLAUDE]#7649
Open
lvanoverberghe wants to merge 5 commits into
Open
Conversation
Adds parsing and generation for T-SQL's FOR JSON query modifier, mirroring
the existing FOR XML handling.
FOR JSON { AUTO | PATH } [, ROOT [ ('name') ]]
[, INCLUDE_NULL_VALUES]
[, WITHOUT_ARRAY_WRAPPER]
Other dialects emit "Unsupported query option." and drop the clause, matching
the existing FOR XML behavior.
Reference: https://learn.microsoft.com/en-us/sql/relational-databases/json/format-query-results-as-json-with-for-json-sql-server
Per PR review: collapse the separate for_json_ arg back into for_ by
introducing a ForClause container node with a `kind` discriminant
("XML" or "JSON"). The QUERY_MODIFIER_PARSERS entry for FOR returns the
fixed `for_` key again, matching the pre-existing convention.
Also fixes a long-standing bug where `for_` was missing from
QUERY_MODIFIERS in core.py, so Select/SetOperation/Subquery did not
declare the arg they were already storing.
georgesittas
approved these changes
May 15, 2026
Addresses PR review:
- BROWSE is the third alternative in `[ FOR { BROWSE | <XML> | <JSON> } ]`.
Parsed as `ForClause(kind="BROWSE")` with no options; emitted only by the
TSQL generator (other dialects silently drop, matching FOR XML/JSON).
- Rename JSON_OPTIONS -> FOR_JSON_OPTIONS (per reviewer request) and
XML_OPTIONS -> FOR_XML_OPTIONS for symmetry.
| for_modifiers = self.expressions(expression, key="for_") | ||
| return f"{self.sep()}FOR XML{self.seg(for_modifiers)}" if for_modifiers else "" | ||
| return self.sql(expression, "for_") | ||
|
|
Collaborator
There was a problem hiding this comment.
I don't think this is useful anymore. There is only one call-site, in query_modifiers, so we can just use self.sql(expression, "for_") directly there, similar to other modifiers.
Comment on lines
+296
to
+301
| def forclause_sql(self, expression: exp.ForClause) -> str: | ||
| # FOR BROWSE has no options; the base implementation would drop it. | ||
| if expression.args["kind"] == "BROWSE": | ||
| return f"{self.sep()}FOR BROWSE" | ||
| return super().forclause_sql(expression) | ||
|
|
Collaborator
There was a problem hiding this comment.
I think it is fine for this to live in the base class as well. The ForClause is only used for T-SQL, I think, anyway.
…UDE] Address review feedback: - Drop the for_modifiers helper; call self.sql(expression, "for_") inline in select_sql, matching the other modifier emissions. - Move the FOR BROWSE branch into the base forclause_sql so the T-SQL generator no longer needs an override. Drop the cross-dialect validate_all assertion that documented our (self-imposed) "silently dropped in postgres/duckdb" behavior, which no longer holds.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds parsing and generation for T-SQL's
FOR JSONquery modifier, mirroring the existingFOR XMLhandling.Grammar covered:
FOR JSON { AUTO | PATH } [, ROOT [ ('name') ]]
[, INCLUDE_NULL_VALUES]
[, WITHOUT_ARRAY_WRAPPER]
Reference: https://learn.microsoft.com/en-us/sql/relational-databases/json/format-query-results-as-json-with-for-json-sql-server
Behavior in other dialects
Other dialects emit
Unsupported query option.and drop theFOR JSONclause — same behavior asFOR XMLtoday. Locked in viavalidate_allagainst postgres/duckdb.Test plan
FOR JSONinside a derived tablevalidate_all→ tsql/postgres/duckdb)ruff check,ruff format,mypycleanNoted follow-up (not in this PR)
The dynamic args
for_(XML) andfor_json_(JSON) could be consolidated into a singlefor_slot with a kind discriminator (e.g.exp.ForClause(this="XML"|"JSON", expressions=[...])). Left as-is for parity with the current XML implementation; happy to do this in a separate PR if preferred.