Home / Documentation / Semantics and data rules
Semantics and data rules
The compiler lowers each program to DuckDB SQL and executes it in an in-memory DuckDB. These are the behaviors the tests pin down.
NULLs and logic
filter,having, and join conditions keep only rows where the condition isTRUE;FALSEandNULLdrop the row. Useis nullrather than= null.and/orfollow SQL three-valued logic (null and falseisfalse,null or trueistrue).COUNT(*)counts rows;COUNT(expr)counts non-NULL values.SUM,AVG,MIN, andMAXignore NULL inputs and return NULL for an empty aggregate input.- Sorting NULLs follows DuckDB: they order as the largest values (last when ascending, first when descending).
Division and numeric types
/returns a number (float) for numeric operands, matching DuckDB:7 / 2is3.5(and-7 / 2is-3.5);%is integer remainder for integer operands.- PostgreSQL, Redshift, and SQLite truncate integer division to
3; when porting SQL that relied on truncation, convert explicitly — there is no integer-division operator yet.
CSV loading and type inference
Before loading a CSV, the compiler scans the header and all rows itself, then tells DuckDB the inferred types. Inference is per column, not per cell:
- a column whose non-empty values are all integers becomes
BIGINT; all numeric (including decimals) becomesDOUBLE; - a leading-zero integer such as
02134or00501, a mixed/dirty value, or any non-numeric text keeps the whole columnVARCHAR; - ISO
YYYY-MM-DDand timestamp columns infer asDATE/TIMESTAMP; - empty/whitespace-only cells become NULL; literal
null,none, andnanstrings stay text; - duplicate (case-insensitive) headers and rows with the wrong field count are rejected with an error naming the file.
Override inference when needed:
unsequel run query.unsequel --delimiter ';' --column-type orders.postal_code=text
Types accepted for --column-type: text, integer, number, boolean, date, time, timestamp, and DuckDB decimal(p,s). Parquet input does not accept overrides, and the delimiter option applies to every CSV loaded in that command.
Column naming and ambiguity
- Qualified names use the source label: the alias if given, otherwise the last segment of the table name.
- After a join, a bare name that exists on both sides is an error — qualify it.
select *exposes a colliding right column as<label>_<column>. select *, name = expressionis allowed, but the added name must not duplicate a live column.- Projections replace the addressable scope: after
select order_id, a later stage addressesorder_id, notorders.order_id. - A
group bykey can be a bare column orname = expression; qualified group keys are written as named expressions.
Sort order and pagination
sortrecords an internal row ordinal. Filters, derives, windows, joins, projections, and pagination preserve it; the final query orders by that ordinal, then removes it from visible output.group by,distinct, set operators, andsqlreset the ordering boundary. Add a finalsortwhen output order matters.- Pagination applies against the established order; without a sort, which rows
takereturns is engine-defined. ORDER BYordinals are not part of the language; sort by a name or expression listed at that point.
Set operators and nested programs
uniondeduplicates,union allkeeps duplicates;intersectandexceptfollow SQL. Both sides must have the same column count (checked unless a side issql-opaque); names and order follow the left side.withdefinitions are program-scoped, may reference earlier definitions, and are resolved before schema tables infrom/join.- Subquery sources must be aliased; the analyzer computes the alias's column families, so type checks behave like a table.
The sql hatch
- One query per stage, starting with
SELECTorWITH;;outside strings/comments is rejected. - The previous relation is
__input__; named tables remain visible. Substitution never touches string literals or comments. - Its output columns are opaque: later stages are not schema-checked, and
joinafter a hatch is rejected.
Engine boundaries
The production compiler targets DuckDB only. Type errors, unsupported expressions, and runtime data problems surface as clean error: lines. Known boundaries relative to standard SQL and to the Python prototype:
- DuckDB is the execution engine, so function and type behavior is DuckDB's, not a portable ANSI subset.
- Floating-point division by zero can yield infinity in DuckDB while the Python memory engine returns NULL; JSON numbers are rendered differently (for example
192.0vs192). - Ties in
ORDER BYare engine-defined; add a unique tie-breaker key when deterministic output matters. LIKEis case-sensitive in DuckDB and in the Python engine; the two engines are intended to agree on the shared subset, with conformance probes in the repository.