Snowflake syntax the tokenizer knows about
Before any layout happens, PasteKit splits the text using Snowflake’s own quoting rules. That step is what keeps a formatter from wrecking a query, and Snowflake has a few rules of its own:
- Single-quoted strings accept both escapes:
'it''s'and'it\'s'are the same value. - Double quotes wrap case-sensitive identifiers, so
"Order ID"and"SALES"."PUBLIC"."ORDERS"stay exactly as typed. $$ ... $$marks a dollar-quoted string. Stored procedure, UDF andEXECUTE IMMEDIATEbodies inside it are kept verbatim: line breaks, casing and inner comments are not touched.//starts a line comment, in addition to--and/* */.$namesession variables,IDENTIFIER($table_name)and:namebind variables are recognised as single tokens.
Semi-structured data, FLATTEN and QUALIFY
Queries over VARIANT columns are where unformatted Snowflake SQL gets hardest to read. Colon paths and casts like e.payload:customer.id::number are left as one unit, and the cast type follows your keyword-case setting (::NUMBER). LATERAL FLATTEN(input => e.payload:items) i lands on its own line in the FROM list, with the => named argument intact.
QUALIFY is placed as a separate clause after WHERE, which suits the common “keep one row per key” pattern. The window spec inside OVER (...) is split into PARTITION BY and ORDER BY lines.
Not every word is upper-cased. Keyword case only applies to words that sql-formatter lists as Snowflake keywords, so over, desc, within and clone keep the case you typed. If you need uniform casing, type those few in the case you want.
Where Snowflake support stops
Some Snowflake-only syntax is outside what sql-formatter can parse, and it is better to know in advance:
- Stage references such as
@raw.orders_stageor@~/stagedinFROMorCOPY INTOare rejected. In COPY INTO you can put the location in single quotes ('@raw.orders_stage/2026/'), which Snowflake also accepts. ForSELECT $1, $2 FROM @stage, swap in a placeholder table name while formatting. ?placeholders are not accepted by the Snowflake grammar here; use:namebinds instead.- Before a bind variable, the formatter drops the space:
status = :statusbecomesstatus =:status. CREATE OR REPLACE PROCEDUREheaders come out on an awkward line break after CREATE, although the body is preserved.- Snowflake Scripting outside
$$(bare DECLARE / BEGIN blocks) is flattened, andINTO :totalloses its space. Wrapping the block inEXECUTE IMMEDIATE $$ ... $$keeps it exactly as written.
Choosing options for Snowflake worksheets
Worksheets often hold a dozen statements in a row. Blank lines between statements controls the gap (0 to 5) after each semicolon. Pick Tabular, left-aligned under Indent style if you like clause keywords padded into a column; Standard is better for deep CTE nesting. Leading commas and AND / OR at the start of the line suit dbt models that change often in review.
Minify is useful for pasting a query into a Python or JavaScript string: whitespace and comments go, while $$ bodies and quoted strings stay unchanged. The SQL minifier page covers that mode in more detail.
Examples
Flatten order line items from JSON events
Each VARIANT path and :: cast stays on one line, and LATERAL FLATTEN joins the FROM list.
select e.event_id, e.payload:customer.id::number as customer_id, i.value:sku::string as sku, i.value:qty::int as qty, i.value:price::number(10,2) as unit_price from raw.order_events e, lateral flatten(input => e.payload:items) i where e.event_type = 'order_placed' and e.loaded_at >= dateadd(day, -1, current_timestamp())SELECT
e.event_id,
e.payload:customer.id::NUMBER AS customer_id,
i.value:sku::STRING AS sku,
i.value:qty::INT AS qty,
i.value:price::NUMBER(10, 2) AS unit_price
FROM
raw.order_events e,
LATERAL flatten(input => e.payload:items) i
WHERE
e.event_type = 'order_placed'
AND e.loaded_at >= dateadd(day, -1, current_timestamp())
Longest session per user with QUALIFY
The CTE is indented under WITH and QUALIFY gets a clause line of its own, with commas at the start of each line.
with sessions as (select session_id, user_id, min(event_ts) as started_at, max(event_ts) as ended_at, count(*) as events from analytics.web_events where event_date = current_date() - 1 group by session_id, user_id) select user_id, session_id, datediff('second', started_at, ended_at) as duration_s, events from sessions where events > 1 qualify row_number() over (partition by user_id order by events desc) = 1WITH
sessions AS (
SELECT
session_id
, user_id
, min(event_ts) AS started_at
, max(event_ts) AS ended_at
, count(*) AS events
FROM
analytics.web_events
WHERE
event_date = current_date() - 1
GROUP BY
session_id
, user_id
)
SELECT
user_id
, session_id
, datediff('second', started_at, ended_at) AS duration_s
, events
FROM
sessions
WHERE
events > 1
QUALIFY
row_number() over (
PARTITION BY
user_id
ORDER BY
events desc
) = 1
COPY INTO with a quoted stage and session variables
Quoting the stage location lets the load statement format, and the // comment stays at the end of its line.
copy into raw.orders from '@raw.orders_stage/2026/' file_format = (type = 'CSV' skip_header = 1 field_optionally_enclosed_by = '"') on_error = 'CONTINUE';
select count(*) from identifier($target_table) where region = $region // session variablesCOPY INTO raw.orders
FROM
'@raw.orders_stage/2026/' file_format = (
type = 'CSV' skip_header = 1 field_optionally_enclosed_by = '"'
) on_error = 'CONTINUE';
SELECT
count(*)
FROM
identifier($target_table)
WHERE
region = $region // session variables
Scripting block kept inside $$
Only EXECUTE IMMEDIATE is upper-cased; everything between the $$ markers is returned exactly as written.
execute immediate $$
declare
stale_rows number default 0;
begin
delete from inventory.stock_snapshots where snapshot_date < dateadd(day, -90, current_date());
stale_rows := sqlrowcount;
return 'deleted ' || stale_rows;
end;
$$;EXECUTE IMMEDIATE $$
declare
stale_rows number default 0;
begin
delete from inventory.stock_snapshots where snapshot_date < dateadd(day, -90, current_date());
stale_rows := sqlrowcount;
return 'deleted ' || stale_rows;
end;
$$;
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
"@raw.order" is not valid Snowflake syntax | The query reads from a named stage (@raw.orders_stage), which the formatter cannot parse in a FROM or COPY INTO clause. | In COPY INTO, quote the location: FROM ‘@raw.orders_stage/’. In a SELECT over staged files, format with a placeholder table name and put the stage back afterwards. |
"?" is not valid Snowflake syntax | A JDBC/ODBC style ? placeholder was left in the query text. | Replace each ? with a named bind such as :customer_id, or with a sample literal while you format. |
This $$-quoted string is never closedExplained | A procedure or EXECUTE IMMEDIATE body opened with $$ but the closing $$ was cut off when copying. | Add $$ after the final END; of the body, before the terminating semicolon. |
This string is never closed — the ' has no matching 'Explained | Snowflake treats a backslash as an escape, so a Windows path like 'C:\exports' escapes its own closing quote. | Double every backslash in the literal (‘C:\exports\’) or use a $$ string for text full of backslashes. |
Unexpected ')' — there is no matching '(' before itExplained | An extra closing bracket, usually left behind after deleting a FLATTEN call or an OBJECT_CONSTRUCT argument. | Remove the stray ) or restore the call it belonged to. |
Frequently asked questions
Does it format Snowflake stored procedures?
It formats the CREATE PROCEDURE statement around the body, but anything between $$ markers is returned unchanged. To tidy the body itself, paste just the SQL statements inside it and format those.
Does it support Snowflake Scripting?
Scripting inside $$ … $$ (for example in EXECUTE IMMEDIATE) is preserved exactly. Bare DECLARE and BEGIN blocks are formatted statement by statement, without block indentation.
Why are some keywords still lowercase after formatting?
Keyword case only applies to words in the Snowflake keyword list that sql-formatter ships. Words outside that list, such as over, desc and clone, keep the case you typed.
Can I format a query copied from Snowsight?
Yes. Paste it, check the Dialect is Snowflake, press Ctrl+Enter, and copy the result back with Ctrl+Shift+C. The query is processed locally in the page and is not uploaded.
Is the output safe to run in dbt?
Formatting changes layout and casing, not the logic of the query. Jinja tags are another matter: {{ ref(‘orders’) }} is reported as “{{” is not valid Snowflake syntax, so format the compiled SQL from the target folder rather than the model template.