Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(tsql): support FOR JSON clause [CLAUDE]#7649

Open
lvanoverberghe wants to merge 5 commits into
tobymao:mainfrom
lvanoverberghe:feat/tsql-for-json
Open

feat(tsql): support FOR JSON clause [CLAUDE]#7649
lvanoverberghe wants to merge 5 commits into
tobymao:mainfrom
lvanoverberghe:feat/tsql-for-json

Conversation

@lvanoverberghe
Copy link
Copy Markdown

Summary

Adds parsing and generation for T-SQL's FOR JSON query modifier, mirroring the existing FOR XML handling.

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 the FOR JSON clause — same behavior as FOR XML today. Locked in via validate_all against postgres/duckdb.

Test plan

  • Identity round-trip for every documented option combination (incl. arbitrary modifier ordering)
  • Canonical correlated-subquery JSON-shaping pattern
  • FOR JSON inside a derived table
  • Pretty-print round-trip
  • Cross-dialect drop (validate_all → tsql/postgres/duckdb)
  • Full test suite: 1108 tests / 18,028 subtests passing
  • ruff check, ruff format, mypy clean

Noted follow-up (not in this PR)

The dynamic args for_ (XML) and for_json_ (JSON) could be consolidated into a single for_ 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.

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
Comment thread sqlglot/parsers/tsql.py Outdated
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.
Copy link
Copy Markdown
Collaborator

@georgesittas georgesittas left a comment

Choose a reason for hiding this comment

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

Thanks!

Comment thread sqlglot/expressions/query.py
Comment thread sqlglot/parsers/tsql.py Outdated
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.
Comment thread sqlglot/generator.py Outdated
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_")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 thread sqlglot/generators/tsql.py Outdated
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.
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.

2 participants