The SQL surface
SQL runs on DuckDB and the result says so. This emulator does not claim Snowflake SQL compatibility; it claims that statements real Snowflake accepts are either answered or refused by name, and parity is that list, generated by running each one against the built image.
The boundary
Section titled “The boundary”A construct belongs in this emulator if and only if real Snowflake accepts it. Not “if a consumer wants it” — a convenience Snowflake does not offer would make a consumer that works here fail on the real thing, which is worse than failing in both places because it fails later and somewhere else.
The other direction is the same rule read backwards: SQL that Snowflake would reject is the product’s defect, not this emulator’s, and adding a Spark spelling here to make a model run would hide it.
Refusal is a feature
Section titled “Refusal is a feature”Until v0.1.4 it was not one. The duckdb CLI the image pins exits 0 after refusing a statement — it writes the diagnosis to stderr and stops — so an unparseable statement produced empty stdout, which rendered as the acknowledgement shape:
{"success": true, "data": {"rowtype": [{"name": "status"}], "rowset": [["ok"]]}}THIS IS NOT SQL AT ALL answered ok. So did CREATE TASK, Time Travel,
TO_DATE, MERGE — eighteen constructs, none of them implemented, none of
them saying so. parity.md at the time listed four of them as not implemented
and said nothing about the rest.
It survived a day of measurement because a developer’s duckdb exits 1. A probe run against a host build reported honest failures the shipped image did not give, and the difference was credited to the code. stderr is the signal now, and the parity probe runs against the image for the same reason.
What is answered
Section titled “What is answered”Ordinary SELECT with CTEs and window functions; QUALIFY; LISTAGG,
ARRAY_AGG, TRY_CAST; IFF, NVL, NVL2, ZEROIFNULL, NULLIFZERO;
TO_DATE, TO_VARCHAR, TO_CHAR, TO_TIMESTAMP, PARSE_JSON,
ARRAY_SIZE, ARRAY_GENERATE_RANGE, CHARINDEX; DATEDIFF; TABLE(GENERATOR (ROWCOUNT => n)) with SEQ4(); SHOW TABLES, DESCRIBE TABLE,
information_schema.columns, SHOW FUNCTIONS, CREATE SCHEMA.
Plus stages, semi-structured, and tasks and streams.
Three-part names, and where a schema really is
Section titled “Three-part names, and where a schema really is”TEST_DB is a fiction — the engine has no such catalog — so the database
qualifier is removed before a statement reaches it. Two things about how:
Only the session’s own database is removed, and only from a three-part
name. TEST_DB.GOLD.orders becomes GOLD.orders; OTHER_DB.PUBLIC.t is left
whole, so the engine can say plainly that OTHER_DB does not exist rather than
answering about a table that was never asked for. A struct access is a.b.c
too, and the database test is what keeps v.customer.email intact.
The schema survives. Every schema used to be flattened into the default
one, which meant TEST_DB.SILVER.orders and TEST_DB.GOLD.orders were a
single table — selecting from the silver one returned the gold row, with no
error anywhere. They are two tables now, and GOLD.orders names the same one
as TEST_DB.GOLD.orders, as it would on an account.
PUBLIC is the exception, and deliberately: Snowflake’s default schema is
PUBLIC and the engine’s is main, so the two are treated as one idea.
SHOW OBJECTS maps it back, reporting PUBLIC for anything in the default
schema and the real name for anything else.
Any schema works. It was once a list of four — PUBLIC, GOLD, SILVER,
MAIN, three of them this family’s own names — and a project whose models
lived in bronze or staging could not run here at all, for a reason its
author had no way to see.
DATEADD, and one honest limit
Section titled “DATEADD, and one honest limit”DATEADD(day | week | hour | minute | second, n, d) works and keeps the type
Snowflake returns — DATE + INTEGER is a DATE in DuckDB, so a day offset
stays a date.
MONTH, QUARTER and YEAR are refused. Every DuckDB spelling of them
widens a DATE to a TIMESTAMP, and SQL unifies a CASE to one type, so
there is no expression that returns a DATE for a DATE and a TIMESTAMP for a
TIMESTAMP. Answering with the wrong type is worse than not answering: a value
that is right and a type that is a lie is invisible to every row check.
A TIMESTAMP given to the day form is an error here where Snowflake would
answer. That is a divergence, and it is recorded rather than smoothed over.
Still refused
Section titled “Still refused”MERGE, OBJECT_CONSTRUCT, PUT, REMOVE, INFER_SCHEMA, Time Travel,
CLONE, GRANT and roles, stored procedures. Each names itself when asked.