Skip to content

feat(exasol): transpile JSON_OBJECT via CONCAT with NULL handling#7568

Open
mkcorneli wants to merge 1 commit intotobymao:mainfrom
mkcorneli:feat/exasol-json-object
Open

feat(exasol): transpile JSON_OBJECT via CONCAT with NULL handling#7568
mkcorneli wants to merge 1 commit intotobymao:mainfrom
mkcorneli:feat/exasol-json-object

Conversation

@mkcorneli
Copy link
Copy Markdown
Contributor

Summary

Transpile JSON_OBJECT(k, v, ...) to a CONCAT expression with per-value-type NULL handling. The base generator emits the colon syntax JSON_OBJECT('k': v), which Exasol rejects with "syntax error, unexpected ':'".

Split from #7539 per review request to land orthogonal changes as separate PRs.

Before / After

>>> sqlglot.transpile("SELECT JSON_OBJECT('k', v)", read="mysql", write="exasol")[0]
# Before:
"SELECT JSON_OBJECT('k': v)"                                       # Exasol rejects
# After:
"SELECT '{' || '\"k\": ' || COALESCE(CAST(v AS VARCHAR(100)), 'null') || '}'"

Implementation

  • New jsonobject_sql method in ExasolGenerator plus a one-line TRANSFORMS entry routing exp.JSONObject through it (the base class already maps exp.JSONObject_jsonobject_sql, which would otherwise shadow the auto-discovered method).
  • Per-type NULL handling for the value branch:
    • Literal strings → CONCAT('"', value, '"')
    • String-typed columns (via is_type(*exp.DataType.TEXT_TYPES) with annotate-on-demand) → CASE WHEN v IS NULL THEN 'null' ELSE CONCAT('"', v, '"') END
    • Numeric / date / other → COALESCE(CAST(v AS VARCHAR(100)), 'null')
    • Empty arg list → literal '{}'
  • exp.Concat renders as || because Exasol has CONCAT_COALESCE = True.
  • No f-string SQL construction — keys are escaped before being emitted as string literals.

Test plan

  • tests/dialects/test_exasol.py::test_json_object — empty args, string-typed column (CASE WHEN), numeric-typed column (COALESCE/CAST), multi-pair commas
  • make check — 0 failures

Related

Replaces part of #7539. See companion PRs for the CTE-auto-alias and GROUP-BY-alias-test changes.

Exasol rejects the colon-syntax `JSON_OBJECT('k': v)` emitted by the
base generator. Add `jsonobject_sql` to ExasolGenerator that builds
the JSON object as a CONCAT expression with per-value-type NULL
handling. String-typed columns use
`CASE WHEN v IS NULL THEN 'null' ELSE CONCAT('"', v, '"') END`;
numeric/date/other types use
`COALESCE(CAST(v AS VARCHAR(100)), 'null')`. Empty arg list emits the
literal `'{}'`.
if not isinstance(pair, exp.JSONKeyValue):
return self.function_fallback_sql(expression)
key = pair.this
value = pair.args.get("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.

Suggested change
value = pair.args.get("expression")
value = pair.expression

Comment on lines +948 to +949
prefix = ", " if i > 0 else ""
key_label = f'{prefix}"{key.name.replace(chr(34), chr(92) + chr(34))}": '
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.

Why are we using chr here? This is quite unconventional.

Comment on lines +952 to +982
if value.is_string:
wrapped: exp.Expression = exp.Concat(
expressions=[
exp.Literal.string('"'),
value,
exp.Literal.string('"'),
]
)
else:
typed_value = value if value.type else annotate_types(value, dialect=self.dialect)
if typed_value.is_type(*exp.DataType.TEXT_TYPES):
wrapped = exp.Case(
ifs=[
exp.If(
this=typed_value.is_(exp.Null()),
true=exp.Literal.string("null"),
)
],
default=exp.Concat(
expressions=[
exp.Literal.string('"'),
typed_value.copy(),
exp.Literal.string('"'),
]
),
)
else:
wrapped = exp.Coalesce(
this=exp.cast(typed_value, exp.DataType.build("VARCHAR(100)")),
expressions=[exp.Literal.string("null")],
)
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.

Why do we need this logic at all? Can't we just produce the following in place of value?

COALESCE(CAST(value AS VARCHAR), 'null')

]
)
else:
typed_value = value if value.type else annotate_types(value, dialect=self.dialect)
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.

This isn't right, annotate_types shouldn't run with "exasol" as its source dialect. If you're relying on type inference, you must assume that the AST parsed with the source dialect is already annotated, i.e., you're responsible for qualifying + annotating the source AST before transpiling it to Exasol.

Comment on lines +952 to +959
if value.is_string:
wrapped: exp.Expression = exp.Concat(
expressions=[
exp.Literal.string('"'),
value,
exp.Literal.string('"'),
]
)
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 we should handle this as a special case, see other comment re: casting.

@georgesittas
Copy link
Copy Markdown
Collaborator

@nnamdi16 I saw your other comment in the JSON_OBJECT PR. Tagging you since this one's related.

@georgesittas
Copy link
Copy Markdown
Collaborator

Hey @mkcorneli, any plans to finish this?

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