Skip to content

Commit ae76eea

Browse files
mhallidaclaude
andcommitted
fix: sql_translate handles Snowflake→BigQuery constructs correctly
- TRY_TO_NUMBER → SAFE_CAST(... AS NUMERIC) - ARRAY_AGG(...) WITHIN GROUP (ORDER BY ...) → ARRAY_AGG(... ORDER BY ...) - IFF(cond, a, b) → IF(cond, a, b) for BigQuery (not CASE WHEN) - QUALIFY preserved as-is for BigQuery (native support since 2021) - FLATTEN(input => expr) → UNNEST(expr) BigQuery removed from QUALIFY_TARGETS since it supports QUALIFY natively. Fixes TICKET-005 in #422. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent 32c2b34 commit ae76eea

2 files changed

Lines changed: 81 additions & 15 deletions

File tree

‎packages/opencode/src/altimate/native/altimate-core.ts‎

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

4444
const IFF_PATTERN = /\bIFF\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+
/\bTRY_TO_(?:NUMBER|DECIMAL|NUMERIC)\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+
/\bARRAY_AGG\s*\(\s*([^()]+?)\s*\)\s*WITHIN\s+GROUP\s*\(\s*ORDER\s+BY\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+
/\bFLATTEN\s*\(\s*(?:input\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+
63106
const QUALIFY_PATTERN =
64107
/\bQUALIFY\b\s+(.+?)(?=\s*(?:LIMIT\s+\d|ORDER\s+BY|;|$))/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
*/
69113
export 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
125170
register("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)

‎packages/opencode/src/altimate/native/sql/register.ts‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import * as core from "@altimateai/altimate-core"
1010
import { register } from "../dispatcher"
1111
import { schemaOrEmpty, resolveSchema } from "../schema-resolver"
12-
import { preprocessIff, postprocessQualify } from "../altimate-core"
12+
import { preprocessIff, postprocessQualify, preprocessSnowflakeToBigQuery } from "../altimate-core"
1313
import type {
1414
SqlAnalyzeResult,
1515
SqlAnalyzeIssue,
@@ -98,13 +98,25 @@ register("sql.analyze", async (params) => {
9898
// ---------------------------------------------------------------------------
9999
register("sql.translate", async (params) => {
100100
try {
101-
const processed = preprocessIff(params.sql)
101+
const source = params.source_dialect.toLowerCase()
102+
const target = params.target_dialect.toLowerCase()
103+
104+
// Apply dialect-specific preprocessing before transpile
105+
let processed = params.sql
106+
if (source === "snowflake" && target === "bigquery") {
107+
processed = preprocessSnowflakeToBigQuery(processed)
108+
} else {
109+
processed = preprocessIff(processed, target)
110+
}
111+
102112
const raw = core.transpile(processed, params.source_dialect, params.target_dialect)
103113
const result = JSON.parse(JSON.stringify(raw))
104114

105115
let translatedSql = result.transpiled_sql?.[0] ?? ""
106-
const target = params.target_dialect.toLowerCase()
107-
if (["bigquery", "databricks", "spark", "trino"].includes(target)) {
116+
117+
// Only wrap QUALIFY for targets that don't support it natively
118+
// BigQuery supports QUALIFY natively since 2021
119+
if (["databricks", "spark", "trino"].includes(target)) {
108120
if (translatedSql.toUpperCase().includes("QUALIFY")) {
109121
translatedSql = postprocessQualify(translatedSql)
110122
}

0 commit comments

Comments
 (0)