Rust-powered · Free and open source · Apache-2.0
Run warehouse SQL locally.
Verified results, or a clear no.
Dialects today Snowflake parse · check · translate to DuckDB
SQLQuack converts only where rules checked against recorded Snowflake results cover the types involved. Everything else is refused, with the exact span and the reason.
sqlquack check orders.sql error[SQ0203]: QUALIFY needs a window function --> orders.sql:5:1 | 5 | QUALIFY revenue > 100 | ^^^^^^^^^^^^^^^^^^^^^ QUALIFY is used without a window | function in the SELECT list or QUALIFY | = help: move the condition to WHERE or HAVING sqlquack convert revenue.sql --schema schema.json SELECT "ORDERS"."CUSTOMER_ID" AS "CUSTOMER_ID", CAST(sum("ORDERS"."TOTAL") AS DECIMAL(24,2)) AS "REVENUE" FROM "ORDERS" AS "ORDERS" WHERE ("ORDERS"."STATUS" = 'shipped') GROUP BY "ORDERS"."CUSTOMER_ID" sqlquack convert: done, converted under the built-in default
Where SQL translation goes wrong
Snowflake rejects it at runtime
Invalid Snowflake SQL only shows up when the query runs on the warehouse, often mid-pipeline.
Tests pass for the wrong engine
On DuckDB, NULLs, decimals and casts behave differently, with no error. Your tests go green locally and the numbers differ in production.
Support depends on the types
A function can convert for one precision and overflow on another. "Supported" means nothing without the types involved.
Best effort means guessing
Most transpilers rewrite what they don't understand and hope. There's no way to tell which parts are guesses.
From Snowflake SQL to DuckDB SQL
Check it, convert it, and know exactly what was refused and why.
Catch what Snowflake would reject
Parses Snowflake SQL and reports the syntax Snowflake would reject. No schema needed. Each error has a code and the exact span; rejections also come with a fix.
sqlquack check error[SQ0201]: Snowflake does not support INTERSECT, EXCEPT or MINUS with ALL --> <stdin>:1:29 | 1 | SELECT order_id FROM orders INTERSECT ALL SELECT order_id FROM orders | ^^^^^^^^^^^^^ Snowflake does not support INTERSECT with ALL | = help: drop ALL to compare distinct rows; only UNION accepts ALL in Snowflake
DuckDB SQL that keeps Snowflake's meaning
Uses your schema's types and only verified rules. NUMBER(12,2) / 3 keeps Snowflake's 8 decimal places.
-- div.sql, with TOTAL as NUMBER(12,2) in schema.json SELECT order_id, total / 3 AS third FROM orders sqlquack convert div.sql --schema schema.json --builtin-policy SELECT "ORDERS"."ORDER_ID" AS "ORDER_ID", (CASE … ELSE CAST(… AS DECIMAL(18,8)) END) AS "THIRD" FROM "ORDERS" AS "ORDERS" sqlquack convert: done, converted under the built-in default
A refusal you can act on
No verified rule, no conversion. The refusal underlines the construct and says what to change.
sqlquack convert due.sql --schema schema.json --builtin-policy error[SQ0008]: the conversion needs a known divergence the policy refuses: `fn:HASH` --> due.sql:2:8 | 2 | HASH(customer_id) AS bucket | ^^^^^^^^^^^^^^^^^ fn:HASH needs known divergence `hash`, | which the policy refuses in this mode | = help: the built-in policy refuses every known divergence; pass --policy with a policy file that allows `hash` … sqlquack convert: refused, 1 refusal under the built-in default
Known divergences are opt-in
Some conversions differ in one documented way, like HASH. The policy file lists each one and whether it's allowed; the built-in default refuses them all.
# sqlquack.toml (excerpt) [divergences.hash] default = "warn" sqlquack convert due.sql --schema schema.json SELECT "ORDERS"."ORDER_ID" AS "ORDER_ID", … AS "BUCKET" FROM "ORDERS" AS "ORDERS" sqlquack convert: relies on known divergence hash at fn:HASH sqlquack convert: done, converted under utc-v1
See what a whole project needs
Point it at a folder of compiled SQL, such as dbt's target/compiled. It counts the constructs, how many convert, and which refusals block the rest.
sqlquack census target/compiled/my_project/models --schema schema.json --builtin-policy Census of 25 snowflake SQL files parsed 25 (25 statements), unparsed 0, empty 0 distinct constructs 21 (4 clause, 4 fn, 1 join, 12 node) files refused: missing rule 0, outside type conditions 0, unresolved name 0, not expressible 0, parse fidelity 0, conversion budget 0, divergence refused 2 convertible: strict 23 …, unit-testable 23 … Refusals by category and construct (top 50) 2 a known divergence the policy refuses: fn:SUM
The same engine, as a library
The same engine as the CLI, with typed results and diagnostics.
import sqlquack result = sqlquack.check(sql="SELECT 1 QUALIFY TRUE") result.diagnostics[0].code # "SQ0203" conversion = sqlquack.convert( sql="SELECT 1 AS total", options=sqlquack.ConvertOptions(builtin_policy=True), ) conversion.converted # True
Each panel shows actual sqlquack output on a small orders example, cut down where marked ….
Measured against real Snowflake
Every number comes from SQL run on real Snowflake. How we measure →
71% of Snowflake syntax verified
2,388 of 3,351 documented forms, checked against real Snowflake.
Rules checked against Snowflake
A rule converts only what real Snowflake results back. Anything else gets a clear no.
Start in a minute
Install it with pip, then check a file or convert it with a schema of your relations.
- Snowflake to DuckDB, for running warehouse SQL against small fixtures locally or in CI.
- No account, server or Snowflake connection needed.
- One binary for the command line, and a Python package with the same engine.
- Free and open source, Apache-2.0.
pip install sqlquack echo "SELECT 1 QUALIFY TRUE" | sqlquack check error[SQ0203]: QUALIFY needs a window function … echo "SELECT 1 AS total" | sqlquack convert --builtin-policy SELECT CAST(1 AS DECIMAL(1,0)) AS "TOTAL" sqlquack convert: done, converted under the built-in default
Try it on your own SQL
Paste Snowflake SQL into the playground, or read the docs for the command line, Python and every diagnostic.