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.

$ pip install sqlquack
Try the playground Read the docs View on GitHub
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.

01 · Check

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.

How checking works →

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
02 · Convert

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.

How conversion works →

-- 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
03 · Refuse precisely

A refusal you can act on

No verified rule, no conversion. The refusal underlines the construct and says what to change.

Every diagnostic code →

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
04 · Decide what to accept

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.

Configure the policy →

# 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
05 · Measure a project

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.

Run a census →

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
06 · Use it from Python

The same engine, as a library

The same engine as the CLI, with typed results and diagnostics.

Python API →

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.