From f1f4398e46dcf3417ac7636b3bd5717a8923b068 Mon Sep 17 00:00:00 2001 From: Asha Shankar Date: Tue, 24 Mar 2026 18:25:36 -0700 Subject: [PATCH 01/11] feat(snowflake)!: Transpilation support for TO_NUMBER transpilation. --- sqlglot/generators/duckdb.py | 19 +++++++++++ sqlglot/generators/snowflake.py | 24 ++++++++++++-- sqlglot/parsers/snowflake.py | 58 ++++++++++++++++++++++++++++----- tests/dialects/test_duckdb.py | 32 ++++++++++++++++++ 4 files changed, 122 insertions(+), 11 deletions(-) diff --git a/sqlglot/generators/duckdb.py b/sqlglot/generators/duckdb.py index a150bf192f..62aa162eaa 100644 --- a/sqlglot/generators/duckdb.py +++ b/sqlglot/generators/duckdb.py @@ -2352,6 +2352,25 @@ def tobinary_sql(self, expression: exp.ToBinary) -> str: result = self.func("TO_BINARY", value) return f"TRY({result})" if is_safe else result + def tonumber_sql(self, expression: exp.ToNumber) -> str: + """ + Snowflake's TO_NUMBER without precision/scale defaults to NUMBER(38, 0), + which truncates decimals. The parser sets these defaults at parse time. + Always cast to DECIMAL(precision, scale) using the values from the AST. + """ + precision = expression.args.get("precision") + scale = expression.args.get("scale") + + # Build DECIMAL type with precision and scale from AST + # Parser ensures defaults (38, 0) are set when not specified + if precision and scale: + decimal_type = exp.DataType.build(f"DECIMAL({precision.name}, {scale.name})") + else: + # Fallback if somehow precision/scale are missing (shouldn't happen) + decimal_type = exp.DataType.build("DECIMAL(38, 0)") + + return self.sql(exp.cast(expression.this, decimal_type)) + def _greatest_least_sql(self, expression: exp.Greatest | exp.Least) -> str: """ Handle GREATEST/LEAST functions with dialect-aware NULL behavior. diff --git a/sqlglot/generators/snowflake.py b/sqlglot/generators/snowflake.py index 2eaa635706..948ee7ca02 100644 --- a/sqlglot/generators/snowflake.py +++ b/sqlglot/generators/snowflake.py @@ -712,12 +712,32 @@ def datatype_sql(self, expression: exp.DataType) -> str: return super().datatype_sql(expression) def tonumber_sql(self, expression: exp.ToNumber) -> str: + """ + Generate TO_NUMBER SQL, omitting default precision/scale for roundtrips. + + When precision=38 and scale=0 (Snowflake defaults set by parser), + omit them from output to preserve original SQL format. + """ + precision = expression.args.get("precision") + scale = expression.args.get("scale") + + # Check if these are the default values (38, 0) set by parser + is_default = ( + precision + and scale + and isinstance(precision, exp.Literal) + and isinstance(scale, exp.Literal) + and precision.name == "38" + and scale.name == "0" + ) + + # Omit defaults for roundtrip preservation return self.func( "TO_NUMBER", expression.this, expression.args.get("format"), - expression.args.get("precision"), - expression.args.get("scale"), + None if is_default else precision, + None if is_default else scale, ) def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str: diff --git a/sqlglot/parsers/snowflake.py b/sqlglot/parsers/snowflake.py index 10e614c046..a04b85123f 100644 --- a/sqlglot/parsers/snowflake.py +++ b/sqlglot/parsers/snowflake.py @@ -305,6 +305,54 @@ def _build_try_to_number(args: t.List[exp.Expr]) -> exp.Expr: ) +def _build_to_number(args: t.List[exp.Expr]) -> exp.ToNumber: + """ + Build TO_NUMBER with Snowflake default precision/scale of (38, 0). + + TO_NUMBER signature: (expr, [format], [precision], [scale]) + - If 1 arg: expr only → defaults precision=38, scale=0 + - If 2 args: expr, precision (no format) → scale defaults to 0 + - If 3 args: expr, precision, scale (no format) + - If 4 args: expr, format, precision, scale + + Format is a string pattern (e.g., '999.99'), precision/scale are integers. + """ + expr = seq_get(args, 0) + arg1 = seq_get(args, 1) + arg2 = seq_get(args, 2) + arg3 = seq_get(args, 3) + + # Determine if arg1 is format (string) or precision (number) + # Format is typically a string literal like '999.99' + has_format = arg1 and arg1.is_string + + if has_format: + # args = [expr, format, precision, scale] + format_arg = arg1 + precision = arg2 + scale = arg3 + else: + # args = [expr, precision, scale] (no format) + format_arg = None + precision = arg1 + scale = arg2 + + # Set Snowflake defaults when precision/scale are not specified + if precision is None and scale is None: + precision = exp.Literal.number(38) + scale = exp.Literal.number(0) + elif precision and scale is None: + # If only precision provided, scale defaults to 0 + scale = exp.Literal.number(0) + + return exp.ToNumber( + this=expr, + format=format_arg, + precision=precision, + scale=scale, + ) + + def _show_parser(*args: t.Any, **kwargs: t.Any) -> t.Callable[[SnowflakeParser], exp.Show]: def _parse(self: SnowflakeParser) -> exp.Show: return self._parse_show_snowflake(*args, **kwargs) @@ -659,15 +707,7 @@ class SnowflakeParser(parser.Parser): ), "TO_CHAR": build_timetostr_or_tochar, "TO_DATE": _build_datetime("TO_DATE", exp.DType.DATE), - **dict.fromkeys( - ("TO_DECIMAL", "TO_NUMBER", "TO_NUMERIC"), - lambda args: exp.ToNumber( - this=seq_get(args, 0), - format=seq_get(args, 1), - precision=seq_get(args, 2), - scale=seq_get(args, 3), - ), - ), + **dict.fromkeys(("TO_DECIMAL", "TO_NUMBER", "TO_NUMERIC"), _build_to_number), "TO_TIME": _build_datetime("TO_TIME", exp.DType.TIME), "TO_TIMESTAMP": _build_datetime("TO_TIMESTAMP", exp.DType.TIMESTAMP), "TO_TIMESTAMP_LTZ": _build_datetime("TO_TIMESTAMP_LTZ", exp.DType.TIMESTAMPLTZ), diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py index 35aaf2949e..ff1cc794e1 100644 --- a/tests/dialects/test_duckdb.py +++ b/tests/dialects/test_duckdb.py @@ -775,6 +775,38 @@ def test_duckdb(self): "snowflake": "SELECT IFF(_u.pos = _u_2.pos_2, _u_2.col, NULL) AS col FROM TABLE(FLATTEN(INPUT => ARRAY_GENERATE_RANGE(0, (GREATEST(ARRAY_SIZE([1, 2, 3])) - 1) + 1))) AS _u(seq, key, path, index, pos, this) CROSS JOIN TABLE(FLATTEN(INPUT => [1, 2, 3])) AS _u_2(seq, key, path, pos_2, col, this) WHERE _u.pos = _u_2.pos_2 OR (_u.pos > (ARRAY_SIZE([1, 2, 3]) - 1) AND _u_2.pos_2 = (ARRAY_SIZE([1, 2, 3]) - 1))", }, ) + + # TO_NUMBER transpilation from Snowflake to DuckDB + self.validate_all( + "SELECT CAST('12.3456' AS DECIMAL(38, 0))", + read={ + "snowflake": "SELECT TO_NUMBER('12.3456')", + }, + write={ + "duckdb": "SELECT CAST('12.3456' AS DECIMAL(38, 0))", + "snowflake": "SELECT TO_NUMBER('12.3456')", + }, + ) + self.validate_all( + "SELECT CAST('12.3456' AS DECIMAL(10, 1))", + read={ + "snowflake": "SELECT TO_NUMBER('12.3456', 10, 1)", + }, + write={ + "duckdb": "SELECT CAST('12.3456' AS DECIMAL(10, 1))", + "snowflake": "SELECT TO_NUMBER('12.3456', 10, 1)", + }, + ) + self.validate_all( + "SELECT CAST('3,741.72' AS DECIMAL(6, 2))", + read={ + "snowflake": "SELECT TO_DECIMAL('3,741.72', '9,999.99', 6, 2)", + }, + write={ + "duckdb": "SELECT CAST('3,741.72' AS DECIMAL(6, 2))", + "snowflake": "SELECT TO_DECIMAL('3,741.72', '9,999.99', 6, 2)", + }, + ) self.validate_all( "VAR_POP(x)", read={ From faee7cf72f6bfbc10728b005e4f0e9ab9b2ba000 Mon Sep 17 00:00:00 2001 From: Asha Shankar Date: Wed, 25 Mar 2026 11:16:01 -0700 Subject: [PATCH 02/11] feat(snowflake)!: Transpilation support for TO_NUMBER transpilation. --- sqlglot/generators/snowflake.py | 38 ++++++++++++++--------------- sqlglot/parsers/snowflake.py | 43 +++++---------------------------- 2 files changed, 25 insertions(+), 56 deletions(-) diff --git a/sqlglot/generators/snowflake.py b/sqlglot/generators/snowflake.py index 948ee7ca02..fdb28d1409 100644 --- a/sqlglot/generators/snowflake.py +++ b/sqlglot/generators/snowflake.py @@ -529,13 +529,6 @@ class SnowflakeGenerator(generator.Generator): exp.ToFile: lambda self, e: self.func( f"{'TRY_' if e.args.get('safe') else ''}TO_FILE", e.this, e.args.get("path") ), - exp.ToNumber: lambda self, e: self.func( - f"{'TRY_' if e.args.get('safe') else ''}TO_NUMBER", - e.this, - e.args.get("format"), - e.args.get("precision"), - e.args.get("scale"), - ), exp.JSONFormat: rename_func("TO_JSON"), exp.PartitionedByProperty: lambda self, e: f"PARTITION BY {self.sql(e, 'this')}", exp.PercentileCont: transforms.preprocess([transforms.add_within_group_for_percentiles]), @@ -712,26 +705,18 @@ def datatype_sql(self, expression: exp.DataType) -> str: return super().datatype_sql(expression) def tonumber_sql(self, expression: exp.ToNumber) -> str: - """ - Generate TO_NUMBER SQL, omitting default precision/scale for roundtrips. - - When precision=38 and scale=0 (Snowflake defaults set by parser), - omit them from output to preserve original SQL format. - """ + """Generate TO_NUMBER SQL, omitting default precision/scale for roundtrips.""" precision = expression.args.get("precision") scale = expression.args.get("scale") - # Check if these are the default values (38, 0) set by parser + # Omit default (38, 0) for roundtrip preservation is_default = ( - precision - and scale - and isinstance(precision, exp.Literal) - and isinstance(scale, exp.Literal) + isinstance(precision, exp.Literal) and precision.name == "38" + and isinstance(scale, exp.Literal) and scale.name == "0" ) - # Omit defaults for roundtrip preservation return self.func( "TO_NUMBER", expression.this, @@ -754,6 +739,21 @@ def cast_sql(self, expression: exp.Cast, safe_prefix: t.Optional[str] = None) -> if expression.is_type(exp.DType.GEOMETRY): return self.func("TO_GEOMETRY", expression.this) + # Convert CAST to DECIMAL/NUMERIC to TO_NUMBER + if expression.is_type(exp.DType.DECIMAL): + # Extract precision and scale from DECIMAL(p, s) + params = expression.to.expressions or [] + precision = params[0].this if len(params) >= 1 and isinstance(params[0], exp.DataTypeParam) else None + scale = params[1].this if len(params) >= 2 and isinstance(params[1], exp.DataTypeParam) else None + + to_number = exp.ToNumber( + this=expression.this, + precision=precision, + scale=scale, + safe=isinstance(expression, exp.TryCast), + ) + return self.tonumber_sql(to_number) + return super().cast_sql(expression, safe_prefix=safe_prefix) def trycast_sql(self, expression: exp.TryCast) -> str: diff --git a/sqlglot/parsers/snowflake.py b/sqlglot/parsers/snowflake.py index a04b85123f..5a7fc0c053 100644 --- a/sqlglot/parsers/snowflake.py +++ b/sqlglot/parsers/snowflake.py @@ -306,51 +306,20 @@ def _build_try_to_number(args: t.List[exp.Expr]) -> exp.Expr: def _build_to_number(args: t.List[exp.Expr]) -> exp.ToNumber: - """ - Build TO_NUMBER with Snowflake default precision/scale of (38, 0). - - TO_NUMBER signature: (expr, [format], [precision], [scale]) - - If 1 arg: expr only → defaults precision=38, scale=0 - - If 2 args: expr, precision (no format) → scale defaults to 0 - - If 3 args: expr, precision, scale (no format) - - If 4 args: expr, format, precision, scale - - Format is a string pattern (e.g., '999.99'), precision/scale are integers. - """ - expr = seq_get(args, 0) - arg1 = seq_get(args, 1) - arg2 = seq_get(args, 2) - arg3 = seq_get(args, 3) + """Build TO_NUMBER with Snowflake default precision/scale of (38, 0).""" + expr, arg1, arg2, arg3 = (seq_get(args, i) for i in range(4)) # Determine if arg1 is format (string) or precision (number) - # Format is typically a string literal like '999.99' has_format = arg1 and arg1.is_string + format_arg, precision, scale = (arg1, arg2, arg3) if has_format else (None, arg1, arg2) - if has_format: - # args = [expr, format, precision, scale] - format_arg = arg1 - precision = arg2 - scale = arg3 - else: - # args = [expr, precision, scale] (no format) - format_arg = None - precision = arg1 - scale = arg2 - - # Set Snowflake defaults when precision/scale are not specified + # Set Snowflake defaults when not specified if precision is None and scale is None: - precision = exp.Literal.number(38) - scale = exp.Literal.number(0) + precision, scale = exp.Literal.number(38), exp.Literal.number(0) elif precision and scale is None: - # If only precision provided, scale defaults to 0 scale = exp.Literal.number(0) - return exp.ToNumber( - this=expr, - format=format_arg, - precision=precision, - scale=scale, - ) + return exp.ToNumber(this=expr, format=format_arg, precision=precision, scale=scale) def _show_parser(*args: t.Any, **kwargs: t.Any) -> t.Callable[[SnowflakeParser], exp.Show]: From 7a73fe1f56d1f3f39f5445702e1f61a60218266d Mon Sep 17 00:00:00 2001 From: Asha Shankar Date: Thu, 26 Mar 2026 17:36:32 -0700 Subject: [PATCH 03/11] feat(snowflake)!: Transpilation support for TO_NUMBER transpilation. --- sqlglot/generators/duckdb.py | 10 +++++-- sqlglot/generators/snowflake.py | 48 ++++++++++++++++++++++--------- sqlglot/parsers/snowflake.py | 50 ++++++++++++++------------------- tests/dialects/test_duckdb.py | 2 +- 4 files changed, 64 insertions(+), 46 deletions(-) diff --git a/sqlglot/generators/duckdb.py b/sqlglot/generators/duckdb.py index 62aa162eaa..4b2b4acb94 100644 --- a/sqlglot/generators/duckdb.py +++ b/sqlglot/generators/duckdb.py @@ -2352,21 +2352,27 @@ def tobinary_sql(self, expression: exp.ToBinary) -> str: result = self.func("TO_BINARY", value) return f"TRY({result})" if is_safe else result + @unsupported_args("format") def tonumber_sql(self, expression: exp.ToNumber) -> str: """ Snowflake's TO_NUMBER without precision/scale defaults to NUMBER(38, 0), which truncates decimals. The parser sets these defaults at parse time. Always cast to DECIMAL(precision, scale) using the values from the AST. + + Oracle's TO_NUMBER without precision/scale should convert to DOUBLE. """ precision = expression.args.get("precision") scale = expression.args.get("scale") # Build DECIMAL type with precision and scale from AST - # Parser ensures defaults (38, 0) are set when not specified if precision and scale: + # Snowflake parser ensures defaults (38, 0) are set when not specified decimal_type = exp.DataType.build(f"DECIMAL({precision.name}, {scale.name})") + elif precision is None and scale is None: + # Oracle or other dialects that don't set defaults - convert to DOUBLE + decimal_type = exp.DataType.build("DOUBLE") else: - # Fallback if somehow precision/scale are missing (shouldn't happen) + # Fallback for partial specification decimal_type = exp.DataType.build("DECIMAL(38, 0)") return self.sql(exp.cast(expression.this, decimal_type)) diff --git a/sqlglot/generators/snowflake.py b/sqlglot/generators/snowflake.py index fdb28d1409..1c2af06df1 100644 --- a/sqlglot/generators/snowflake.py +++ b/sqlglot/generators/snowflake.py @@ -717,8 +717,10 @@ def tonumber_sql(self, expression: exp.ToNumber) -> str: and scale.name == "0" ) + func_name = "TRY_TO_NUMBER" if expression.args.get("safe") else "TO_NUMBER" + return self.func( - "TO_NUMBER", + func_name, expression.this, expression.args.get("format"), None if is_default else precision, @@ -739,20 +741,38 @@ def cast_sql(self, expression: exp.Cast, safe_prefix: t.Optional[str] = None) -> if expression.is_type(exp.DType.GEOMETRY): return self.func("TO_GEOMETRY", expression.this) - # Convert CAST to DECIMAL/NUMERIC to TO_NUMBER - if expression.is_type(exp.DType.DECIMAL): - # Extract precision and scale from DECIMAL(p, s) - params = expression.to.expressions or [] - precision = params[0].this if len(params) >= 1 and isinstance(params[0], exp.DataTypeParam) else None - scale = params[1].this if len(params) >= 2 and isinstance(params[1], exp.DataTypeParam) else None + # Convert CAST to DECIMAL/NUMERIC to TO_NUMBER only for string inputs + # Don't convert TryCast - it's handled by trycast_sql + if expression.is_type(exp.DType.DECIMAL) and not isinstance(expression, exp.TryCast): + value = expression.this + + # Annotate types if not already done + if value.type is None: + from sqlglot.optimizer.annotate_types import annotate_types + + value = annotate_types(value, dialect=self.dialect) + + # Only convert to TO_NUMBER for string inputs + if value.is_string or value.is_type(*exp.DataType.TEXT_TYPES): + # Extract precision and scale from DECIMAL(p, s) + params = expression.to.expressions or [] + precision = ( + params[0].this + if len(params) >= 1 and isinstance(params[0], exp.DataTypeParam) + else None + ) + scale = ( + params[1].this + if len(params) >= 2 and isinstance(params[1], exp.DataTypeParam) + else None + ) - to_number = exp.ToNumber( - this=expression.this, - precision=precision, - scale=scale, - safe=isinstance(expression, exp.TryCast), - ) - return self.tonumber_sql(to_number) + to_number = exp.ToNumber( + this=value, + precision=precision, + scale=scale, + ) + return self.tonumber_sql(to_number) return super().cast_sql(expression, safe_prefix=safe_prefix) diff --git a/sqlglot/parsers/snowflake.py b/sqlglot/parsers/snowflake.py index 5a7fc0c053..55d20c3888 100644 --- a/sqlglot/parsers/snowflake.py +++ b/sqlglot/parsers/snowflake.py @@ -295,33 +295,6 @@ def _build_generator(args: t.List) -> exp.Generator: return exp.Generator(**gen_args) -def _build_try_to_number(args: t.List[exp.Expr]) -> exp.Expr: - return exp.ToNumber( - this=seq_get(args, 0), - format=seq_get(args, 1), - precision=seq_get(args, 2), - scale=seq_get(args, 3), - safe=True, - ) - - -def _build_to_number(args: t.List[exp.Expr]) -> exp.ToNumber: - """Build TO_NUMBER with Snowflake default precision/scale of (38, 0).""" - expr, arg1, arg2, arg3 = (seq_get(args, i) for i in range(4)) - - # Determine if arg1 is format (string) or precision (number) - has_format = arg1 and arg1.is_string - format_arg, precision, scale = (arg1, arg2, arg3) if has_format else (None, arg1, arg2) - - # Set Snowflake defaults when not specified - if precision is None and scale is None: - precision, scale = exp.Literal.number(38), exp.Literal.number(0) - elif precision and scale is None: - scale = exp.Literal.number(0) - - return exp.ToNumber(this=expr, format=format_arg, precision=precision, scale=scale) - - def _show_parser(*args: t.Any, **kwargs: t.Any) -> t.Callable[[SnowflakeParser], exp.Show]: def _parse(self: SnowflakeParser) -> exp.Show: return self._parse_show_snowflake(*args, **kwargs) @@ -655,7 +628,16 @@ class SnowflakeParser(parser.Parser): "TRY_TO_BOOLEAN": lambda args: exp.ToBoolean(this=seq_get(args, 0), safe=True), "TRY_TO_DATE": _build_datetime("TRY_TO_DATE", exp.DType.DATE, safe=True), **dict.fromkeys( - ("TRY_TO_DECIMAL", "TRY_TO_NUMBER", "TRY_TO_NUMERIC"), _build_try_to_number + ("TRY_TO_DECIMAL", "TRY_TO_NUMBER", "TRY_TO_NUMERIC"), + lambda args: exp.ToNumber( + this=seq_get(args, 0), + format=seq_get(args, 1) if len(args) in (2, 4) else None, + precision=(seq_get(args, 2) if len(args) in (2, 4) else seq_get(args, 1)) + or exp.Literal.number(38), + scale=(seq_get(args, 3) if len(args) in (2, 4) else seq_get(args, 2)) + or exp.Literal.number(0), + safe=True, + ), ), "TRY_TO_DOUBLE": lambda args: exp.ToDouble( this=seq_get(args, 0), format=seq_get(args, 1), safe=True @@ -676,7 +658,17 @@ class SnowflakeParser(parser.Parser): ), "TO_CHAR": build_timetostr_or_tochar, "TO_DATE": _build_datetime("TO_DATE", exp.DType.DATE), - **dict.fromkeys(("TO_DECIMAL", "TO_NUMBER", "TO_NUMERIC"), _build_to_number), + **dict.fromkeys( + ("TO_DECIMAL", "TO_NUMBER", "TO_NUMERIC"), + lambda args: exp.ToNumber( + this=seq_get(args, 0), + format=seq_get(args, 1) if len(args) in (2, 4) else None, + precision=(seq_get(args, 2) if len(args) in (2, 4) else seq_get(args, 1)) + or exp.Literal.number(38), + scale=(seq_get(args, 3) if len(args) in (2, 4) else seq_get(args, 2)) + or exp.Literal.number(0), + ), + ), "TO_TIME": _build_datetime("TO_TIME", exp.DType.TIME), "TO_TIMESTAMP": _build_datetime("TO_TIMESTAMP", exp.DType.TIMESTAMP), "TO_TIMESTAMP_LTZ": _build_datetime("TO_TIMESTAMP_LTZ", exp.DType.TIMESTAMPLTZ), diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py index ff1cc794e1..dc53025915 100644 --- a/tests/dialects/test_duckdb.py +++ b/tests/dialects/test_duckdb.py @@ -804,7 +804,7 @@ def test_duckdb(self): }, write={ "duckdb": "SELECT CAST('3,741.72' AS DECIMAL(6, 2))", - "snowflake": "SELECT TO_DECIMAL('3,741.72', '9,999.99', 6, 2)", + "snowflake": "SELECT TO_NUMBER('3,741.72', 6, 2)", # Format is lost during transpilation }, ) self.validate_all( From 130f4403f0b4460f725c940be26dd213a0855633 Mon Sep 17 00:00:00 2001 From: geooo109 Date: Fri, 27 Mar 2026 17:16:49 +0200 Subject: [PATCH 04/11] refactor --- sqlglot/generators/duckdb.py | 15 +------------ sqlglot/generators/snowflake.py | 33 ---------------------------- sqlglot/parsers/snowflake.py | 39 +++++++++++++++++++-------------- tests/dialects/test_duckdb.py | 17 ++++++-------- 4 files changed, 30 insertions(+), 74 deletions(-) diff --git a/sqlglot/generators/duckdb.py b/sqlglot/generators/duckdb.py index 4b2b4acb94..872923c3d3 100644 --- a/sqlglot/generators/duckdb.py +++ b/sqlglot/generators/duckdb.py @@ -2354,26 +2354,13 @@ def tobinary_sql(self, expression: exp.ToBinary) -> str: @unsupported_args("format") def tonumber_sql(self, expression: exp.ToNumber) -> str: - """ - Snowflake's TO_NUMBER without precision/scale defaults to NUMBER(38, 0), - which truncates decimals. The parser sets these defaults at parse time. - Always cast to DECIMAL(precision, scale) using the values from the AST. - - Oracle's TO_NUMBER without precision/scale should convert to DOUBLE. - """ precision = expression.args.get("precision") scale = expression.args.get("scale") - # Build DECIMAL type with precision and scale from AST if precision and scale: - # Snowflake parser ensures defaults (38, 0) are set when not specified decimal_type = exp.DataType.build(f"DECIMAL({precision.name}, {scale.name})") - elif precision is None and scale is None: - # Oracle or other dialects that don't set defaults - convert to DOUBLE - decimal_type = exp.DataType.build("DOUBLE") else: - # Fallback for partial specification - decimal_type = exp.DataType.build("DECIMAL(38, 0)") + decimal_type = exp.DataType.build("DOUBLE") return self.sql(exp.cast(expression.this, decimal_type)) diff --git a/sqlglot/generators/snowflake.py b/sqlglot/generators/snowflake.py index 1c2af06df1..651be5e1b9 100644 --- a/sqlglot/generators/snowflake.py +++ b/sqlglot/generators/snowflake.py @@ -741,39 +741,6 @@ def cast_sql(self, expression: exp.Cast, safe_prefix: t.Optional[str] = None) -> if expression.is_type(exp.DType.GEOMETRY): return self.func("TO_GEOMETRY", expression.this) - # Convert CAST to DECIMAL/NUMERIC to TO_NUMBER only for string inputs - # Don't convert TryCast - it's handled by trycast_sql - if expression.is_type(exp.DType.DECIMAL) and not isinstance(expression, exp.TryCast): - value = expression.this - - # Annotate types if not already done - if value.type is None: - from sqlglot.optimizer.annotate_types import annotate_types - - value = annotate_types(value, dialect=self.dialect) - - # Only convert to TO_NUMBER for string inputs - if value.is_string or value.is_type(*exp.DataType.TEXT_TYPES): - # Extract precision and scale from DECIMAL(p, s) - params = expression.to.expressions or [] - precision = ( - params[0].this - if len(params) >= 1 and isinstance(params[0], exp.DataTypeParam) - else None - ) - scale = ( - params[1].this - if len(params) >= 2 and isinstance(params[1], exp.DataTypeParam) - else None - ) - - to_number = exp.ToNumber( - this=value, - precision=precision, - scale=scale, - ) - return self.tonumber_sql(to_number) - return super().cast_sql(expression, safe_prefix=safe_prefix) def trycast_sql(self, expression: exp.TryCast) -> str: diff --git a/sqlglot/parsers/snowflake.py b/sqlglot/parsers/snowflake.py index 55d20c3888..e011bc9c21 100644 --- a/sqlglot/parsers/snowflake.py +++ b/sqlglot/parsers/snowflake.py @@ -39,6 +39,26 @@ def _build_approx_top_k(args: t.List) -> exp.ApproxTopK: return exp.ApproxTopK.from_arg_list(args) +def _build_to_number(args: t.List, safe: bool = False) -> exp.ToNumber: + second_arg = seq_get(args, 1) + if second_arg and second_arg.is_number: + fmt = None + precision = second_arg + scale = seq_get(args, 2) or exp.Literal.number(0) + else: + fmt = second_arg + precision = seq_get(args, 2) or exp.Literal.number(38) + scale = seq_get(args, 3) or exp.Literal.number(0) + + return exp.ToNumber( + this=seq_get(args, 0), + format=fmt, + precision=precision, + scale=scale, + safe=safe, + ) + + def _build_date_from_parts(args: t.List) -> exp.DateFromParts: return exp.DateFromParts( year=seq_get(args, 0), @@ -629,15 +649,7 @@ class SnowflakeParser(parser.Parser): "TRY_TO_DATE": _build_datetime("TRY_TO_DATE", exp.DType.DATE, safe=True), **dict.fromkeys( ("TRY_TO_DECIMAL", "TRY_TO_NUMBER", "TRY_TO_NUMERIC"), - lambda args: exp.ToNumber( - this=seq_get(args, 0), - format=seq_get(args, 1) if len(args) in (2, 4) else None, - precision=(seq_get(args, 2) if len(args) in (2, 4) else seq_get(args, 1)) - or exp.Literal.number(38), - scale=(seq_get(args, 3) if len(args) in (2, 4) else seq_get(args, 2)) - or exp.Literal.number(0), - safe=True, - ), + lambda args: _build_to_number(args, safe=True), ), "TRY_TO_DOUBLE": lambda args: exp.ToDouble( this=seq_get(args, 0), format=seq_get(args, 1), safe=True @@ -660,14 +672,7 @@ class SnowflakeParser(parser.Parser): "TO_DATE": _build_datetime("TO_DATE", exp.DType.DATE), **dict.fromkeys( ("TO_DECIMAL", "TO_NUMBER", "TO_NUMERIC"), - lambda args: exp.ToNumber( - this=seq_get(args, 0), - format=seq_get(args, 1) if len(args) in (2, 4) else None, - precision=(seq_get(args, 2) if len(args) in (2, 4) else seq_get(args, 1)) - or exp.Literal.number(38), - scale=(seq_get(args, 3) if len(args) in (2, 4) else seq_get(args, 2)) - or exp.Literal.number(0), - ), + lambda args: _build_to_number(args), ), "TO_TIME": _build_datetime("TO_TIME", exp.DType.TIME), "TO_TIMESTAMP": _build_datetime("TO_TIMESTAMP", exp.DType.TIMESTAMP), diff --git a/tests/dialects/test_duckdb.py b/tests/dialects/test_duckdb.py index dc53025915..4f16a54f5a 100644 --- a/tests/dialects/test_duckdb.py +++ b/tests/dialects/test_duckdb.py @@ -776,7 +776,6 @@ def test_duckdb(self): }, ) - # TO_NUMBER transpilation from Snowflake to DuckDB self.validate_all( "SELECT CAST('12.3456' AS DECIMAL(38, 0))", read={ @@ -784,29 +783,27 @@ def test_duckdb(self): }, write={ "duckdb": "SELECT CAST('12.3456' AS DECIMAL(38, 0))", - "snowflake": "SELECT TO_NUMBER('12.3456')", }, ) self.validate_all( - "SELECT CAST('12.3456' AS DECIMAL(10, 1))", + "SELECT CAST('12.3456' AS DECIMAL(10, 0))", read={ - "snowflake": "SELECT TO_NUMBER('12.3456', 10, 1)", + "snowflake": "SELECT TO_NUMBER('12.3456', 10)", }, write={ - "duckdb": "SELECT CAST('12.3456' AS DECIMAL(10, 1))", - "snowflake": "SELECT TO_NUMBER('12.3456', 10, 1)", + "duckdb": "SELECT CAST('12.3456' AS DECIMAL(10, 0))", }, ) self.validate_all( - "SELECT CAST('3,741.72' AS DECIMAL(6, 2))", + "SELECT CAST('12.3456' AS DECIMAL(10, 2))", read={ - "snowflake": "SELECT TO_DECIMAL('3,741.72', '9,999.99', 6, 2)", + "snowflake": "SELECT TO_NUMBER('12.3456', 10, 2)", }, write={ - "duckdb": "SELECT CAST('3,741.72' AS DECIMAL(6, 2))", - "snowflake": "SELECT TO_NUMBER('3,741.72', 6, 2)", # Format is lost during transpilation + "duckdb": "SELECT CAST('12.3456' AS DECIMAL(10, 2))", }, ) + self.validate_all( "VAR_POP(x)", read={ From 7334c765485401f1663017f674bc40a35368a73f Mon Sep 17 00:00:00 2001 From: geooo109 Date: Fri, 27 Mar 2026 17:28:57 +0200 Subject: [PATCH 05/11] ref 2 --- sqlglot/generators/duckdb.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sqlglot/generators/duckdb.py b/sqlglot/generators/duckdb.py index 872923c3d3..2d4000e56c 100644 --- a/sqlglot/generators/duckdb.py +++ b/sqlglot/generators/duckdb.py @@ -2352,17 +2352,21 @@ def tobinary_sql(self, expression: exp.ToBinary) -> str: result = self.func("TO_BINARY", value) return f"TRY({result})" if is_safe else result - @unsupported_args("format") def tonumber_sql(self, expression: exp.ToNumber) -> str: + fmt = expression.args.get("format") precision = expression.args.get("precision") scale = expression.args.get("scale") - if precision and scale: - decimal_type = exp.DataType.build(f"DECIMAL({precision.name}, {scale.name})") - else: - decimal_type = exp.DataType.build("DOUBLE") + if not fmt and precision and scale: + return self.sql( + exp.cast( + expression.this, + f"DECIMAL({self.sql(precision)}, {self.sql(scale)})", + dialect="duckdb", + ) + ) - return self.sql(exp.cast(expression.this, decimal_type)) + return super().tonumber_sql(expression) def _greatest_least_sql(self, expression: exp.Greatest | exp.Least) -> str: """ From 5d6d224180545110f59c495886ca75baf8d07578 Mon Sep 17 00:00:00 2001 From: geooo109 Date: Fri, 27 Mar 2026 17:40:10 +0200 Subject: [PATCH 06/11] ref 3 --- sqlglot/generators/duckdb.py | 4 +--- sqlglot/generators/snowflake.py | 2 -- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/sqlglot/generators/duckdb.py b/sqlglot/generators/duckdb.py index 2d4000e56c..373238aab3 100644 --- a/sqlglot/generators/duckdb.py +++ b/sqlglot/generators/duckdb.py @@ -2360,9 +2360,7 @@ def tonumber_sql(self, expression: exp.ToNumber) -> str: if not fmt and precision and scale: return self.sql( exp.cast( - expression.this, - f"DECIMAL({self.sql(precision)}, {self.sql(scale)})", - dialect="duckdb", + expression.this, f"DECIMAL({precision.name}, {scale.name})", dialect="duckdb" ) ) diff --git a/sqlglot/generators/snowflake.py b/sqlglot/generators/snowflake.py index 651be5e1b9..93ba772cb7 100644 --- a/sqlglot/generators/snowflake.py +++ b/sqlglot/generators/snowflake.py @@ -705,11 +705,9 @@ def datatype_sql(self, expression: exp.DataType) -> str: return super().datatype_sql(expression) def tonumber_sql(self, expression: exp.ToNumber) -> str: - """Generate TO_NUMBER SQL, omitting default precision/scale for roundtrips.""" precision = expression.args.get("precision") scale = expression.args.get("scale") - # Omit default (38, 0) for roundtrip preservation is_default = ( isinstance(precision, exp.Literal) and precision.name == "38" From 7a7dda98d46b8d15abd2927d94fb3b42b7312e1e Mon Sep 17 00:00:00 2001 From: geooo109 Date: Fri, 27 Mar 2026 17:59:22 +0200 Subject: [PATCH 07/11] sf tests --- tests/dialects/test_snowflake.py | 54 ++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py index 797a90c30c..70e510cf90 100644 --- a/tests/dialects/test_snowflake.py +++ b/tests/dialects/test_snowflake.py @@ -502,6 +502,31 @@ def test_snowflake(self): self.validate_identity("TO_NUMBER(expr)") self.validate_identity("TO_NUMBER(expr, fmt)") self.validate_identity("TO_NUMBER(expr, fmt, precision, scale)") + + ast = self.validate_identity("TO_NUMBER('12.3456')") + self.assertIsInstance(ast, exp.ToNumber) + self.assertIsNone(ast.args.get("format")) + self.assertEqual(ast.args.get("precision").name, "38") + self.assertEqual(ast.args.get("scale").name, "0") + + ast = self.validate_identity("TO_NUMBER('12.3456', 10, 1)") + self.assertIsInstance(ast, exp.ToNumber) + self.assertIsNone(ast.args.get("format")) + self.assertEqual(ast.args.get("precision").name, "10") + self.assertEqual(ast.args.get("scale").name, "1") + + ast = self.validate_identity("TO_NUMBER('12.3456', '99.99')") + self.assertIsInstance(ast, exp.ToNumber) + self.assertEqual(ast.args.get("format").name, "99.99") + self.assertEqual(ast.args.get("precision").name, "38") + self.assertEqual(ast.args.get("scale").name, "0") + + ast = self.validate_identity("TO_NUMBER('12.3456', '99.99', 10, 1)") + self.assertIsInstance(ast, exp.ToNumber) + self.assertEqual(ast.args.get("format").name, "99.99") + self.assertEqual(ast.args.get("precision").name, "10") + self.assertEqual(ast.args.get("scale").name, "1") + self.validate_identity("TO_DECFLOAT('123.456')") self.validate_identity("TO_DECFLOAT('1,234.56', '999,999.99')") self.validate_identity("TRY_TO_DECFLOAT('123.456')") @@ -545,6 +570,35 @@ def test_snowflake(self): self.validate_identity("TRY_TO_NUMBER('123.45')") self.validate_identity("TRY_TO_NUMBER('123.45', '999.99')") self.validate_identity("TRY_TO_NUMBER('123.45', '999.99', 10, 2)") + + ast = self.validate_identity("TRY_TO_NUMBER('12.3456')") + self.assertIsInstance(ast, exp.ToNumber) + self.assertIsNone(ast.args.get("format")) + self.assertEqual(ast.args.get("precision").name, "38") + self.assertEqual(ast.args.get("scale").name, "0") + self.assertTrue(ast.args.get("safe")) + + ast = self.validate_identity("TRY_TO_NUMBER('12.3456', 10, 1)") + self.assertIsInstance(ast, exp.ToNumber) + self.assertIsNone(ast.args.get("format")) + self.assertEqual(ast.args.get("precision").name, "10") + self.assertEqual(ast.args.get("scale").name, "1") + self.assertTrue(ast.args.get("safe")) + + ast = self.validate_identity("TRY_TO_NUMBER('12.3456', '99.99')") + self.assertIsInstance(ast, exp.ToNumber) + self.assertEqual(ast.args.get("format").name, "99.99") + self.assertEqual(ast.args.get("precision").name, "38") + self.assertEqual(ast.args.get("scale").name, "0") + self.assertTrue(ast.args.get("safe")) + + ast = self.validate_identity("TRY_TO_NUMBER('12.3456', '99.99', 10, 1)") + self.assertIsInstance(ast, exp.ToNumber) + self.assertEqual(ast.args.get("format").name, "99.99") + self.assertEqual(ast.args.get("precision").name, "10") + self.assertEqual(ast.args.get("scale").name, "1") + self.assertTrue(ast.args.get("safe")) + self.validate_identity("TO_NUMERIC('123.45')", "TO_NUMBER('123.45')") self.validate_identity("TO_NUMERIC('123.45', '999.99')", "TO_NUMBER('123.45', '999.99')") self.validate_identity( From 8e270c294435f92caf18dfd09fa0f908db9ad47e Mon Sep 17 00:00:00 2001 From: geooo109 Date: Fri, 27 Mar 2026 18:02:02 +0200 Subject: [PATCH 08/11] tests final --- tests/dialects/test_snowflake.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py index 70e510cf90..135542330f 100644 --- a/tests/dialects/test_snowflake.py +++ b/tests/dialects/test_snowflake.py @@ -527,6 +527,12 @@ def test_snowflake(self): self.assertEqual(ast.args.get("precision").name, "10") self.assertEqual(ast.args.get("scale").name, "1") + ast = self.validate_identity("TO_NUMBER('12.3456', 3, 0)") + self.assertIsInstance(ast, exp.ToNumber) + self.assertIsNone(ast.args.get("format")) + self.assertEqual(ast.args.get("precision").name, "3") + self.assertEqual(ast.args.get("scale").name, "0") + self.validate_identity("TO_DECFLOAT('123.456')") self.validate_identity("TO_DECFLOAT('1,234.56', '999,999.99')") self.validate_identity("TRY_TO_DECFLOAT('123.456')") @@ -599,6 +605,13 @@ def test_snowflake(self): self.assertEqual(ast.args.get("scale").name, "1") self.assertTrue(ast.args.get("safe")) + ast = self.validate_identity("TRY_TO_NUMBER('12.3456', 3, 0)") + self.assertIsInstance(ast, exp.ToNumber) + self.assertIsNone(ast.args.get("format")) + self.assertEqual(ast.args.get("precision").name, "3") + self.assertEqual(ast.args.get("scale").name, "0") + self.assertTrue(ast.args.get("safe")) + self.validate_identity("TO_NUMERIC('123.45')", "TO_NUMBER('123.45')") self.validate_identity("TO_NUMERIC('123.45', '999.99')", "TO_NUMBER('123.45', '999.99')") self.validate_identity( From e0b21643e7421e21cba34f4b9bde0147e0e73bbe Mon Sep 17 00:00:00 2001 From: geooo109 Date: Fri, 27 Mar 2026 18:05:45 +0200 Subject: [PATCH 09/11] tests 4 --- tests/dialects/test_snowflake.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py index 135542330f..27c990f43a 100644 --- a/tests/dialects/test_snowflake.py +++ b/tests/dialects/test_snowflake.py @@ -527,7 +527,7 @@ def test_snowflake(self): self.assertEqual(ast.args.get("precision").name, "10") self.assertEqual(ast.args.get("scale").name, "1") - ast = self.validate_identity("TO_NUMBER('12.3456', 3, 0)") + ast = self.validate_identity("TO_NUMBER('12.3456', 3)", "TO_NUMBER('12.3456', 3, 0)") self.assertIsInstance(ast, exp.ToNumber) self.assertIsNone(ast.args.get("format")) self.assertEqual(ast.args.get("precision").name, "3") @@ -605,7 +605,9 @@ def test_snowflake(self): self.assertEqual(ast.args.get("scale").name, "1") self.assertTrue(ast.args.get("safe")) - ast = self.validate_identity("TRY_TO_NUMBER('12.3456', 3, 0)") + ast = self.validate_identity( + "TRY_TO_NUMBER('12.3456', 3)", "TRY_TO_NUMBER('12.3456', 3, 0)" + ) self.assertIsInstance(ast, exp.ToNumber) self.assertIsNone(ast.args.get("format")) self.assertEqual(ast.args.get("precision").name, "3") From 48063589d1f736eb6edffea75ef3dcf6fa2fc39f Mon Sep 17 00:00:00 2001 From: geooo109 Date: Fri, 27 Mar 2026 18:50:15 +0200 Subject: [PATCH 10/11] ref --- sqlglot/generators/snowflake.py | 18 ++++++++++-------- tests/dialects/test_snowflake.py | 16 ++++++---------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/sqlglot/generators/snowflake.py b/sqlglot/generators/snowflake.py index 93ba772cb7..38a1c7c180 100644 --- a/sqlglot/generators/snowflake.py +++ b/sqlglot/generators/snowflake.py @@ -708,12 +708,14 @@ def tonumber_sql(self, expression: exp.ToNumber) -> str: precision = expression.args.get("precision") scale = expression.args.get("scale") - is_default = ( - isinstance(precision, exp.Literal) - and precision.name == "38" - and isinstance(scale, exp.Literal) - and scale.name == "0" - ) + default_precision = isinstance(precision, exp.Literal) and precision.name == "38" + default_scale = isinstance(scale, exp.Literal) and scale.name == "0" + + if default_precision and default_scale: + precision = None + scale = None + elif default_scale: + scale = None func_name = "TRY_TO_NUMBER" if expression.args.get("safe") else "TO_NUMBER" @@ -721,8 +723,8 @@ def tonumber_sql(self, expression: exp.ToNumber) -> str: func_name, expression.this, expression.args.get("format"), - None if is_default else precision, - None if is_default else scale, + precision, + scale, ) def timestampfromparts_sql(self, expression: exp.TimestampFromParts) -> str: diff --git a/tests/dialects/test_snowflake.py b/tests/dialects/test_snowflake.py index 27c990f43a..f72b92ccb0 100644 --- a/tests/dialects/test_snowflake.py +++ b/tests/dialects/test_snowflake.py @@ -499,9 +499,8 @@ def test_snowflake(self): self.validate_identity( "TO_DECIMAL(expr, fmt, precision, scale)", "TO_NUMBER(expr, fmt, precision, scale)" ) - self.validate_identity("TO_NUMBER(expr)") - self.validate_identity("TO_NUMBER(expr, fmt)") - self.validate_identity("TO_NUMBER(expr, fmt, precision, scale)") + self.validate_identity("TO_NUMBER(expr, 38, 0)", "TO_NUMBER(expr)") + self.validate_identity("TO_NUMBER(expr, 38)", "TO_NUMBER(expr)") ast = self.validate_identity("TO_NUMBER('12.3456')") self.assertIsInstance(ast, exp.ToNumber) @@ -527,7 +526,7 @@ def test_snowflake(self): self.assertEqual(ast.args.get("precision").name, "10") self.assertEqual(ast.args.get("scale").name, "1") - ast = self.validate_identity("TO_NUMBER('12.3456', 3)", "TO_NUMBER('12.3456', 3, 0)") + ast = self.validate_identity("TO_NUMBER('12.3456', 3)") self.assertIsInstance(ast, exp.ToNumber) self.assertIsNone(ast.args.get("format")) self.assertEqual(ast.args.get("precision").name, "3") @@ -573,9 +572,8 @@ def test_snowflake(self): self.validate_identity("TRY_TO_FILE(object_col)") self.validate_identity("TRY_TO_FILE('file.csv')") self.validate_identity("TRY_TO_FILE('file.csv', 'relativepath/')") - self.validate_identity("TRY_TO_NUMBER('123.45')") - self.validate_identity("TRY_TO_NUMBER('123.45', '999.99')") - self.validate_identity("TRY_TO_NUMBER('123.45', '999.99', 10, 2)") + self.validate_identity("TRY_TO_NUMBER(expr, 38, 0)", "TRY_TO_NUMBER(expr)") + self.validate_identity("TRY_TO_NUMBER(expr, 38)", "TRY_TO_NUMBER(expr)") ast = self.validate_identity("TRY_TO_NUMBER('12.3456')") self.assertIsInstance(ast, exp.ToNumber) @@ -605,9 +603,7 @@ def test_snowflake(self): self.assertEqual(ast.args.get("scale").name, "1") self.assertTrue(ast.args.get("safe")) - ast = self.validate_identity( - "TRY_TO_NUMBER('12.3456', 3)", "TRY_TO_NUMBER('12.3456', 3, 0)" - ) + ast = self.validate_identity("TRY_TO_NUMBER('12.3456', 3)") self.assertIsInstance(ast, exp.ToNumber) self.assertIsNone(ast.args.get("format")) self.assertEqual(ast.args.get("precision").name, "3") From 7944560d91829e2fadd7d552f23d6e7500459a9d Mon Sep 17 00:00:00 2001 From: George Sittas Date: Mon, 6 Apr 2026 13:52:42 +0300 Subject: [PATCH 11/11] Sync w/ integration tests Signed-off-by: George Sittas --- sqlglot-integration-tests | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlglot-integration-tests b/sqlglot-integration-tests index ead3ade9d4..ea97cd804a 160000 --- a/sqlglot-integration-tests +++ b/sqlglot-integration-tests @@ -1 +1 @@ -Subproject commit ead3ade9d4a8d212a044f950bf2e5e078043e4e7 +Subproject commit ea97cd804a409d5e28155d49d89ee69a8924e167