@@ -981,13 +981,16 @@ def generate_surrogate_key(
981981 # Same split as MD5/MD5Digest: the surrogate key must be a hex string,
982982 # not a binary digest, on every dialect.
983983 func = exp .SHA2 (this = func .this , length = func .args .get ("length" ))
984- elif isinstance (func , exp .Anonymous ) and _is_presto_family (evaluator .dialect ):
985- # Athena runs the Trino engine, so sha256() takes varbinary there too,
986- # but its parser has no SHA256/SHA512 entry: exp.func returns an
987- # Anonymous node, so neither branch above fires and the surrogate key
988- # keeps the bare SHA256(varchar) form reported in #5871. Unlike the
989- # probe below, this is not a pin-era workaround — Athena still parses
990- # to Anonymous on sqlglot versions that carry tobymao/sqlglot#7824.
984+ elif isinstance (func , exp .Anonymous ):
985+ # Some dialects' parsers have no entry for the SHA-2 functions, so
986+ # exp.func returns an Anonymous node and neither branch above fires:
987+ # on MySQL and StarRocks the bare SHA256(varchar) is a runtime error
988+ # because those engines only spell the function SHA2(expr, length),
989+ # and Athena runs the Trino engine where sha256() takes varbinary
990+ # (#5871). Mapping the untyped name to exp.SHA2 with the canonical
991+ # digest length renders the valid call on every one of them. Unlike
992+ # the probe below, this is not a pin-era workaround: these parsers
993+ # hand back Anonymous on every sqlglot version.
991994 #
992995 # Anonymous is the catch-all for every unrecognised function name, and
993996 # hash_function is caller-supplied, so the name is checked rather than
@@ -1013,6 +1016,26 @@ def generate_surrogate_key(
10131016 )
10141017 )
10151018
1019+ if isinstance (func , (exp .MD5 , exp .SHA , exp .SHA2 )) and _renders_varbinary (evaluator .dialect ):
1020+ # T-SQL renders every one of these as HASHBYTES, which returns
1021+ # VARBINARY rather than the hex string the surrogate key promises.
1022+ # CONVERT style 2 strips the 0x prefix and LOWER() restores the
1023+ # lowercase hex the other dialects return, so keys hash identically
1024+ # everywhere. The probe keeps this branch inert if the tsql generator
1025+ # ever emits the conversion itself.
1026+ digest_bits = 128
1027+ if isinstance (func , exp .SHA ):
1028+ digest_bits = 160
1029+ elif isinstance (func , exp .SHA2 ) and func .args .get ("length" ) is not None :
1030+ digest_bits = int (str (func .args ["length" ].name )) # type: ignore[union-attr]
1031+ func = exp .Lower (
1032+ this = exp .Convert (
1033+ this = exp .DataType .build (f"VARCHAR({ digest_bits // 4 } )" ),
1034+ expression = func ,
1035+ style = exp .Literal .number (2 ),
1036+ )
1037+ )
1038+
10161039 return func
10171040
10181041
@@ -1021,6 +1044,11 @@ def generate_surrogate_key(
10211044# Athena is on the list because it runs the Trino engine.
10221045_PRESTO_FAMILY = frozenset ({"presto" , "trino" , "athena" })
10231046
1047+ # Dialects that render every string hash as HASHBYTES, which returns
1048+ # VARBINARY rather than a hex string. Fabric is on the list because it runs
1049+ # the T-SQL engine.
1050+ _TSQL_FAMILY = frozenset ({"tsql" , "fabric" })
1051+
10241052# The SHA-2 digest widths a surrogate key may ask for, by function name.
10251053_SHA2_DIGEST_LENGTHS = {"SHA256" : 256 , "SHA512" : 512 }
10261054
@@ -1030,6 +1058,11 @@ def _is_presto_family(dialect: DialectType) -> bool:
10301058 return (str (dialect ) if dialect else "" ).split ("," )[0 ].strip ().lower () in _PRESTO_FAMILY
10311059
10321060
1061+ def _is_tsql_family (dialect : DialectType ) -> bool :
1062+ """Whether this dialect is T-SQL (MSSQL or Fabric)."""
1063+ return (str (dialect ) if dialect else "" ).split ("," )[0 ].strip ().lower () in _TSQL_FAMILY
1064+
1065+
10331066@lru_cache (maxsize = None )
10341067def _sha2_renders_binary (dialect : DialectType ) -> bool :
10351068 """Whether this dialect renders exp.SHA2 as a bare binary-semantics call.
@@ -1043,6 +1076,21 @@ def _sha2_renders_binary(dialect: DialectType) -> bool:
10431076 return "TO_HEX" not in probe .sql (dialect = dialect )
10441077
10451078
1079+ @lru_cache (maxsize = None )
1080+ def _renders_varbinary (dialect : DialectType ) -> bool :
1081+ """Whether this dialect renders the string hashes as HASHBYTES (VARBINARY).
1082+
1083+ The T-SQL family (MSSQL, Fabric) has no MD5/SHA2 functions: every string
1084+ hash renders as HASHBYTES, which returns VARBINARY instead of the hex
1085+ string the surrogate key promises.
1086+ """
1087+ if not _is_tsql_family (dialect ):
1088+ return False
1089+ probe = exp .MD5 (this = exp .column ("_sqlmesh_probe" ))
1090+ rendered = probe .sql (dialect = dialect )
1091+ return "HASHBYTES" in rendered and "CONVERT" not in rendered
1092+
1093+
10461094@macro ()
10471095def safe_add (_ : MacroEvaluator , * fields : exp .Expr ) -> exp .Case :
10481096 """Adds numbers together, substitutes nulls for 0s and only returns null if all fields are null.
0 commit comments