diff --git a/sqlglot/generators/exasol.py b/sqlglot/generators/exasol.py index a729471af5..d560603dfb 100644 --- a/sqlglot/generators/exasol.py +++ b/sqlglot/generators/exasol.py @@ -14,6 +14,7 @@ ) from sqlglot.errors import UnsupportedError from sqlglot.generator import unsupported_args +from sqlglot.optimizer.annotate_types import annotate_types from sqlglot.optimizer.scope import build_scope from sqlglot.parsers.exasol import DATE_UNITS @@ -398,6 +399,7 @@ def datatype_sql(self, expression: exp.DataType) -> str: ] ), exp.SubstringIndex: _substring_index_sql, + exp.JSONObject: lambda self, e: self.jsonobject_sql(e), exp.WeekOfYear: rename_func("WEEK"), # https://docs.exasol.com/db/latest/sql_references/functions/alphabeticallistfunctions/to_date.htm exp.Date: rename_func("TO_DATE"), @@ -929,6 +931,61 @@ def jsonextract_sql(self, expression: exp.JSONExtract) -> str: return sql + def jsonobject_sql(self, expression: exp.JSONObject) -> str: + pairs = expression.expressions or [] + if not pairs: + return self.sql(exp.Literal.string("{}")) + + concat_args: list[exp.Expression] = [exp.Literal.string("{")] + for i, pair in enumerate(pairs): + if not isinstance(pair, exp.JSONKeyValue): + return self.function_fallback_sql(expression) + key = pair.this + value = pair.args.get("expression") + if key is None or value is None: + return self.function_fallback_sql(expression) + + prefix = ", " if i > 0 else "" + key_label = f'{prefix}"{key.name.replace(chr(34), chr(92) + chr(34))}": ' + concat_args.append(exp.Literal.string(key_label)) + + 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")], + ) + + concat_args.append(wrapped) + + concat_args.append(exp.Literal.string("}")) + return self.sql(exp.Concat(expressions=concat_args)) + @unsupported_args("flag") def regexplike_sql(self, expression: exp.RegexpLike) -> str: if not expression.args.get("full_match"): diff --git a/tests/dialects/test_exasol.py b/tests/dialects/test_exasol.py index 3dabcf07c8..5daeeb80e4 100644 --- a/tests/dialects/test_exasol.py +++ b/tests/dialects/test_exasol.py @@ -991,3 +991,52 @@ def test_show_tables(self): "exasol": "SELECT TABLE_NAME FROM SYS.EXA_ALL_TABLES WHERE TABLE_SCHEMA = CURRENT_SCHEMA" }, ) + + def test_json_object(self): + from sqlglot import parse_one + from sqlglot.optimizer.annotate_types import annotate_types + from sqlglot.optimizer.qualify import qualify + + # Empty JSON_OBJECT -> '{}' literal + self.validate_all( + "SELECT '{}'", + read={"mysql": "SELECT JSON_OBJECT()"}, + write={"exasol": "SELECT '{}'"}, + ) + + # String-typed column: NULL-safe quoted value via CASE + ast = parse_one("SELECT JSON_OBJECT('name', str_name) AS j FROM t", read="mysql") + schema = {"t": {"str_name": "VARCHAR"}} + annotated = annotate_types( + qualify(ast, schema=schema, dialect="mysql"), + schema=schema, + dialect="mysql", + ) + result = annotated.sql("exasol") + self.assertIn("CASE WHEN", result) + self.assertIn("IS NULL THEN 'null'", result) + self.assertIn("'\"name\": '", result) + + # Numeric-typed column: COALESCE(CAST(..) AS VARCHAR(100)), 'null') + ast = parse_one("SELECT JSON_OBJECT('id', int_id) AS j FROM t", read="mysql") + schema = {"t": {"int_id": "INT"}} + annotated = annotate_types( + qualify(ast, schema=schema, dialect="mysql"), + schema=schema, + dialect="mysql", + ) + result = annotated.sql("exasol") + self.assertIn("COALESCE(CAST(", result) + self.assertIn("AS VARCHAR(100)), 'null')", result) + + # Multi-pair with comma separators + ast = parse_one("SELECT JSON_OBJECT('id', int_id, 'name', str_name) FROM t", read="mysql") + schema = {"t": {"int_id": "INT", "str_name": "VARCHAR"}} + annotated = annotate_types( + qualify(ast, schema=schema, dialect="mysql"), + schema=schema, + dialect="mysql", + ) + result = annotated.sql("exasol") + self.assertIn("'\"id\": '", result) + self.assertIn("', \"name\": '", result)