feat(exasol): transpile JSON_OBJECT via CONCAT with NULL handling#7568
feat(exasol): transpile JSON_OBJECT via CONCAT with NULL handling#7568mkcorneli wants to merge 1 commit intotobymao:mainfrom
Conversation
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") |
There was a problem hiding this comment.
| value = pair.args.get("expression") | |
| value = pair.expression |
| prefix = ", " if i > 0 else "" | ||
| key_label = f'{prefix}"{key.name.replace(chr(34), chr(92) + chr(34))}": ' |
There was a problem hiding this comment.
Why are we using chr here? This is quite unconventional.
| 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")], | ||
| ) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
| if value.is_string: | ||
| wrapped: exp.Expression = exp.Concat( | ||
| expressions=[ | ||
| exp.Literal.string('"'), | ||
| value, | ||
| exp.Literal.string('"'), | ||
| ] | ||
| ) |
There was a problem hiding this comment.
I don't think we should handle this as a special case, see other comment re: casting.
|
@nnamdi16 I saw your other comment in the |
|
Hey @mkcorneli, any plans to finish this? |
Summary
Transpile
JSON_OBJECT(k, v, ...)to a CONCAT expression with per-value-type NULL handling. The base generator emits the colon syntaxJSON_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
Implementation
jsonobject_sqlmethod inExasolGeneratorplus a one-line TRANSFORMS entry routingexp.JSONObjectthrough it (the base class already mapsexp.JSONObject→_jsonobject_sql, which would otherwise shadow the auto-discovered method).CONCAT('"', value, '"')is_type(*exp.DataType.TEXT_TYPES)with annotate-on-demand) →CASE WHEN v IS NULL THEN 'null' ELSE CONCAT('"', v, '"') ENDCOALESCE(CAST(v AS VARCHAR(100)), 'null')'{}'exp.Concatrenders as||because Exasol hasCONCAT_COALESCE = True.Test plan
tests/dialects/test_exasol.py::test_json_object— empty args, string-typed column (CASE WHEN), numeric-typed column (COALESCE/CAST), multi-pair commasmake check— 0 failuresRelated
Replaces part of #7539. See companion PRs for the CTE-auto-alias and GROUP-BY-alias-test changes.