@@ -1233,3 +1233,83 @@ def test_macro_coerce_literal_type(macro_evaluator):
12331233 expression = d .parse_one ("@TEST_LITERAL_TYPE(1.0)" )
12341234 with pytest .raises (MacroEvalError , match = ".*Coercion failed" ):
12351235 macro_evaluator .transform (expression )
1236+
1237+
1238+ def test_generate_surrogate_key_hash_semantics () -> None :
1239+ from sqlmesh .core .macros import generate_surrogate_key
1240+
1241+ # The macro must always build the string-semantics hash expression, never
1242+ # a binary digest, so dialects that model the two separately (Presto and
1243+ # Trino after tobymao/sqlglot#7824) can render the hex-string form.
1244+ # BigQuery's parser maps SHA256 to SHA2Digest, which exercises the
1245+ # conversion on every supported sqlglot version.
1246+ func = generate_surrogate_key (
1247+ MacroEvaluator (dialect = "bigquery" ),
1248+ exp .column ("a" ),
1249+ hash_function = exp .Literal .string ("SHA256" ),
1250+ )
1251+ assert isinstance (func , exp .SHA2 )
1252+
1253+ # The hash argument is annotated as text so generators that wrap an
1254+ # encode around string inputs (TO_UTF8 on Presto/Trino) can do so without
1255+ # a separate annotation pass.
1256+ assert func .this .is_type ("text" )
1257+
1258+ def render (dialect : str , hash_function : str ) -> str :
1259+ sql = f"SELECT @GENERATE_SURROGATE_KEY(a, hash_function := '{ hash_function } ') FROM foo"
1260+ rendered = MacroEvaluator (dialect = dialect ).transform (parse_one (sql , dialect = dialect ))
1261+ assert isinstance (rendered , exp .Expr )
1262+ return rendered .sql (dialect )
1263+
1264+ # Rendered SQL, stable across supported sqlglot versions.
1265+ assert (
1266+ render ("bigquery" , "SHA256" )
1267+ == "SELECT SHA256(CONCAT(COALESCE(CAST(a AS STRING), '_sqlmesh_surrogate_key_null_'))) FROM foo"
1268+ )
1269+ assert (
1270+ render ("duckdb" , "SHA256" )
1271+ == "SELECT SHA256(COALESCE(CAST(a AS TEXT), '_sqlmesh_surrogate_key_null_')) FROM foo"
1272+ )
1273+ assert (
1274+ render ("trino" , "MD5" )
1275+ == "SELECT LOWER(TO_HEX(MD5(TO_UTF8(CAST(COALESCE(CAST(a AS VARCHAR), '_sqlmesh_surrogate_key_null_') AS VARCHAR))))) FROM foo"
1276+ )
1277+
1278+ # The reported bug (#5871): Trino/Presto SHA256/SHA512 surrogate keys must
1279+ # be the hex-string form, not a bare SHA256(varchar). The macro-side
1280+ # fallback produces it under the current sqlglot pin; once sqlglot renders
1281+ # exp.SHA2 this way natively (tobymao/sqlglot#7824), the probe disables
1282+ # the fallback and these assertions hold unchanged.
1283+ # Athena is included: it runs the Trino engine and hits the same
1284+ # sha256(varbinary) failure, but its parser has no SHA256/SHA512 entry, so
1285+ # exp.func hands back exp.Anonymous rather than exp.SHA2/exp.SHA2Digest.
1286+ # That is true on every sqlglot version tested, before and after #7824, so
1287+ # the Anonymous path is not a pin-era workaround the way the probe is.
1288+ for _dialect in ("trino" , "presto" , "athena" ):
1289+ assert (
1290+ render (_dialect , "SHA256" )
1291+ == "SELECT LOWER(TO_HEX(SHA256(TO_UTF8(CAST(COALESCE(CAST(a AS VARCHAR), '_sqlmesh_surrogate_key_null_') AS VARCHAR))))) FROM foo"
1292+ )
1293+ assert (
1294+ render (_dialect , "SHA512" )
1295+ == "SELECT LOWER(TO_HEX(SHA512(TO_UTF8(CAST(COALESCE(CAST(a AS VARCHAR), '_sqlmesh_surrogate_key_null_') AS VARCHAR))))) FROM foo"
1296+ )
1297+
1298+ # Anonymous is sqlglot's catch-all for an unrecognised function name, so
1299+ # the conversion is keyed on the name: an unknown hash_function must pass
1300+ # through untouched rather than be reinterpreted as a SHA-2 digest.
1301+ assert (
1302+ render ("athena" , "MYHASH" )
1303+ == "SELECT MYHASH(CAST(COALESCE(CAST(a AS VARCHAR), '_sqlmesh_surrogate_key_null_') AS VARCHAR)) FROM foo"
1304+ )
1305+
1306+ # The fallback is scoped to the Presto family: dialects whose bare
1307+ # SHA256(varchar) already returns a hex string are left to sqlglot.
1308+ from sqlmesh .core .macros import _sha2_renders_binary
1309+
1310+ assert not _sha2_renders_binary ("duckdb" )
1311+ assert not _sha2_renders_binary ("bigquery" )
1312+ assert (
1313+ render ("snowflake" , "SHA256" )
1314+ == "SELECT SHA256(CONCAT(COALESCE(CAST(a AS VARCHAR), '_sqlmesh_surrogate_key_null_'))) FROM foo"
1315+ )
0 commit comments