@@ -38,33 +38,77 @@ function fail(error: unknown): AltimateCoreResult {
3838}
3939
4040// ---------------------------------------------------------------------------
41- // IFF / QUALIFY transpile transforms (ported from Python guard.py)
41+ // Snowflake → target dialect transpile transforms
4242// ---------------------------------------------------------------------------
4343
4444const IFF_PATTERN = / \b I F F \s * \( ( [ ^ , ( ) ] + ) , \s * ( [ ^ , ( ) ] + ) , \s * ( [ ^ ( ) ] + ) \) / gi
4545
4646/**
47- * Iteratively convert Snowflake IFF(cond, a, b) to
48- * CASE WHEN cond THEN a ELSE b END.
47+ * Convert Snowflake IFF(cond, a, b) to IF(cond, a, b) for BigQuery,
48+ * or CASE WHEN cond THEN a ELSE b END for other targets .
4949 */
50- export function preprocessIff ( sql : string ) : string {
50+ export function preprocessIff ( sql : string , targetDialect ?: string ) : string {
51+ const target = targetDialect ?. toLowerCase ( ) ?? ""
52+ const useIf = target === "bigquery"
5153 let current = sql
5254 for ( let i = 0 ; i < 10 ; i ++ ) {
53- const next = current . replace (
54- IFF_PATTERN ,
55- "CASE WHEN $1 THEN $2 ELSE $3 END" ,
56- )
55+ const next = useIf
56+ ? current . replace ( IFF_PATTERN , "IF($1, $2, $3)" )
57+ : current . replace ( IFF_PATTERN , "CASE WHEN $1 THEN $2 ELSE $3 END" )
5758 if ( next === current ) break
5859 current = next
5960 }
6061 return current
6162}
6263
64+ /**
65+ * Convert Snowflake TRY_TO_NUMBER/TRY_TO_DECIMAL/TRY_TO_NUMERIC to SAFE_CAST for BigQuery.
66+ */
67+ export function preprocessTryToNumber ( sql : string ) : string {
68+ return sql . replace (
69+ / \b T R Y _ T O _ (?: N U M B E R | D E C I M A L | N U M E R I C ) \s * \( \s * ( [ ^ ( ) ] + ?) \s * \) / gi,
70+ "SAFE_CAST($1 AS NUMERIC)" ,
71+ )
72+ }
73+
74+ /**
75+ * Convert Snowflake ARRAY_AGG(expr) WITHIN GROUP (ORDER BY ...) to
76+ * ARRAY_AGG(expr ORDER BY ...) for BigQuery.
77+ */
78+ export function preprocessArrayAggWithinGroup ( sql : string ) : string {
79+ return sql . replace (
80+ / \b A R R A Y _ A G G \s * \( \s * ( [ ^ ( ) ] + ?) \s * \) \s * W I T H I N \s + G R O U P \s * \( \s * O R D E R \s + B Y \s + ( [ ^ ( ) ] + ?) \s * \) / gi,
81+ "ARRAY_AGG($1 ORDER BY $2)" ,
82+ )
83+ }
84+
85+ /**
86+ * Convert Snowflake FLATTEN(input => expr) to UNNEST(expr) for BigQuery.
87+ */
88+ export function preprocessFlatten ( sql : string ) : string {
89+ return sql . replace (
90+ / \b F L A T T E N \s * \( \s * (?: i n p u t \s * = > \s * ) ? ( [ ^ ( ) ] + ?) \s * \) / gi,
91+ "UNNEST($1)" ,
92+ )
93+ }
94+
95+ /**
96+ * Apply all Snowflake → BigQuery preprocessing transforms.
97+ */
98+ export function preprocessSnowflakeToBigQuery ( sql : string ) : string {
99+ let result = preprocessIff ( sql , "bigquery" )
100+ result = preprocessTryToNumber ( result )
101+ result = preprocessArrayAggWithinGroup ( result )
102+ result = preprocessFlatten ( result )
103+ return result
104+ }
105+
63106const QUALIFY_PATTERN =
64107 / \b Q U A L I F Y \b \s + ( .+ ?) (? = \s * (?: L I M I T \s + \d | O R D E R \s + B Y | ; | $ ) ) / is
65108
66109/**
67110 * Wrap QUALIFY clause into outer SELECT for targets that lack native support.
111+ * BigQuery supports QUALIFY natively, so it's excluded.
68112 */
69113export function postprocessQualify ( sql : string ) : string {
70114 const m = QUALIFY_PATTERN . exec ( sql )
@@ -76,7 +120,8 @@ export function postprocessQualify(sql: string): string {
76120 return suffix ? `${ wrapped } ${ suffix } ` : wrapped
77121}
78122
79- const QUALIFY_TARGETS = new Set ( [ "bigquery" , "databricks" , "spark" , "trino" ] )
123+ // BigQuery supports QUALIFY natively — only wrap for dialects that don't
124+ const QUALIFY_TARGETS = new Set ( [ "databricks" , "spark" , "trino" ] )
80125
81126// ---------------------------------------------------------------------------
82127// Handler registrations
@@ -124,12 +169,21 @@ register("altimate_core.safety", async (params) => {
124169// 4. altimate_core.transpile — with IFF/QUALIFY transforms
125170register ( "altimate_core.transpile" , async ( params ) => {
126171 try {
127- const processed = preprocessIff ( params . sql )
172+ const sourceLower = params . from_dialect . toLowerCase ( )
173+ const targetLower = params . to_dialect . toLowerCase ( )
174+
175+ // Apply dialect-specific preprocessing
176+ let processed = params . sql
177+ if ( sourceLower === "snowflake" && targetLower === "bigquery" ) {
178+ processed = preprocessSnowflakeToBigQuery ( processed )
179+ } else {
180+ processed = preprocessIff ( processed , targetLower )
181+ }
182+
128183 const raw = core . transpile ( processed , params . from_dialect , params . to_dialect )
129184 const data = toData ( raw )
130185
131186 // Post-process QUALIFY for targets that lack native support
132- const targetLower = params . to_dialect . toLowerCase ( )
133187 if ( QUALIFY_TARGETS . has ( targetLower ) ) {
134188 // Rust returns transpiled_sql as string[] — use first element
135189 const transpiled = Array . isArray ( data . transpiled_sql )
0 commit comments