Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions sqlglot/dialects/duckdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -1734,6 +1734,49 @@ def tobinary_sql(self: DuckDB.Generator, expression: exp.ToBinary) -> str:
# Fallback, which needs to be updated if want to support transpilation from other dialects than Snowflake
return self.func("TO_BINARY", value)

def todouble_sql(self, expression: exp.ToDouble) -> str:
"""
Transpile TO_DOUBLE to DuckDB with comprehensive format handling.

Handles Snowflake number format models by cleaning input strings based on format patterns.
"""
value = expression.this
format_arg = expression.args.get("format")

if format_arg and isinstance(format_arg, exp.Literal):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this branch handle every format supported by Snowflake? It's quite complicated and so I'd like to understand if the complexity stems from it being a complete solution or just hacking it together.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

https://docs.snowflake.com/en/sql-reference/sql-format-models

This can handle the formats specified under the "Fixed-position numeric formats" section, except for Hexadecimal digit (I couldn't find a working example of it and thus can't test it). These formats are included in the test query shown above.

There are also Text-minimal numeric formats, and as far as I know they are just the default format for DOUBLE and DECIMAL

# Use REGEXP_REPLACE to remove commas, dollar signs, and spaces
cleaned_value = exp.func(
"REGEXP_REPLACE", value, exp.Literal.string(r"[,$\s]"), exp.Literal.string("")
)

# Handle trailing signs (MI and S formats) - move '+' or '-' from end to beginning
format_upper = format_arg.this.upper()
if "MI" in format_upper or "S" in format_upper:
cleaned_value = (
exp.case()
.when(
exp.func("RIGHT", cleaned_value, exp.Literal.number(1)).eq(
exp.Literal.string("-")
),
exp.func(
"CONCAT",
exp.Literal.string("-"),
exp.func("RTRIM", cleaned_value, exp.Literal.string("-")),
),
)
.when(
exp.func("RIGHT", cleaned_value, exp.Literal.number(1)).eq(
exp.Literal.string("+")
),
exp.func("RTRIM", cleaned_value, exp.Literal.string("+")),
)
.else_(cleaned_value)
)

value = cleaned_value

return self.sql(exp.cast(value, exp.DataType.Type.DOUBLE))

def _greatest_least_sql(
self: DuckDB.Generator, expression: exp.Greatest | exp.Least
) -> str:
Expand Down
19 changes: 13 additions & 6 deletions tests/dialects/test_snowflake.py
Original file line number Diff line number Diff line change
Expand Up @@ -2339,17 +2339,24 @@ def test_sample(self):
},
)
self.validate_all(
"TO_DOUBLE(expr)",
"TO_DOUBLE('123.45')",
write={
"snowflake": "TO_DOUBLE(expr)",
"duckdb": "CAST(expr AS DOUBLE)",
"snowflake": "TO_DOUBLE('123.45')",
"duckdb": "CAST('123.45' AS DOUBLE)",
},
)
self.validate_all(
"TO_DOUBLE(expr, fmt)",
"TO_DOUBLE('$1,234.56', '$9,999.99')",
write={
"snowflake": "TO_DOUBLE(expr, fmt)",
"duckdb": UnsupportedError,
"snowflake": "TO_DOUBLE('$1,234.56', '$9,999.99')",
"duckdb": "CAST(REGEXP_REPLACE('$1,234.56', '[,$\s]', '', 'g') AS DOUBLE)",
},
)
self.validate_all(
"TO_DOUBLE('-4.56E-03', 'S9.99EEEE')",
write={
"snowflake": "TO_DOUBLE('-4.56E-03', 'S9.99EEEE')",
"duckdb": "CAST(CASE WHEN RIGHT(REGEXP_REPLACE('-4.56E-03', '[,$\s]', '', 'g'), 1) = '-' THEN '-' || RTRIM(REGEXP_REPLACE('-4.56E-03', '[,$\s]', '', 'g'), '-') WHEN RIGHT(REGEXP_REPLACE('-4.56E-03', '[,$\s]', '', 'g'), 1) = '+' THEN RTRIM(REGEXP_REPLACE('-4.56E-03', '[,$\s]', '', 'g'), '+') ELSE REGEXP_REPLACE('-4.56E-03', '[,$\s]', '', 'g') END AS DOUBLE)",
},
)

Expand Down