From dfe4f5d1ed8267ff64df20626db7c7bb2ddc6fbe Mon Sep 17 00:00:00 2001 From: Alex Kosachev Date: Thu, 15 Oct 2026 11:06:12 +0800 Subject: [PATCH 1/2] feat(core-spec): default NULL placement in window ORDER BY Without NULLS FIRST or NULLS LAST, NULL sorts as the largest value: last for ASC, first for DESC. A mapping table lists which engines need the explicit clause to match. Co-Authored-By: Claude Opus 5.5 --- core-spec/expression_language.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core-spec/expression_language.md b/core-spec/expression_language.md index 698690a8..94dd4f11 100644 --- a/core-spec/expression_language.md +++ b/core-spec/expression_language.md @@ -573,6 +573,14 @@ Each `ORDER BY` item has three independent parts: RANK() OVER (ORDER BY revenue DESC NULLS LAST) ``` +Without `NULLS FIRST` or `NULLS LAST`, NULL sorts as the largest value: last for `ASC`, first for `DESC`. Implementations MUST produce this order whatever the native default of the engine; see [Window ORDER BY NULL Placement](#window-order-by-null-placement). + +```sql +-- revenue over three rows: 10, NULL, 5; each comment lists the values numbered 1, 2, 3 +ROW_NUMBER() OVER (ORDER BY revenue) -- 5, 10, NULL +ROW_NUMBER() OVER (ORDER BY revenue DESC) -- NULL, 10, 5 +``` + Frame clause options: - `ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW` @@ -788,6 +796,18 @@ date range. For a row-level shift, add a number of days to the date directly, or Sigma's table-calculation functions (`RowNumber`, `Rank`, `RunningSum`, `RunningAvg`, `Lag`, `Lead`, etc.) resolve their partition/order context from workbook UI configuration (which pivot/table the calculation is attached to) rather than from arguments passed in the formula text itself. Because that context isn't recoverable from the formula string alone, the Sigma converter (`converters/sigma/`) treats these as untranslatable to ANSI SQL and carries the original Sigma formula through in the `SIGMA` dialect only — see `converters/sigma/LIMITATIONS.md`. +### Window ORDER BY NULL Placement + +On an engine whose native NULL placement differs from the Ossie default, an implementation emits the clause explicitly. Where the native default matches, the clause MAY be omitted. Native defaults with no session setting changed: + +| Engine | Native `ASC` | Native `DESC` | Emits when the clause is omitted | +| :---- | :---- | :---- | :---- | +| Snowflake, PostgreSQL | NULLS LAST | NULLS FIRST | nothing | +| BigQuery, Databricks | NULLS FIRST | NULLS LAST | `ASC NULLS LAST`, `DESC NULLS FIRST` | +| ClickHouse, DuckDB | NULLS LAST | NULLS LAST | `DESC NULLS FIRST` | + +Snowflake (`DEFAULT_NULL_ORDERING`) and DuckDB (`default_null_order`) let a session change the native default; an implementation that cannot rely on the setting emits the clause on every `ORDER BY` item. + --- ## Compliance Levels From 6b08d8b3d0fb9a68bfff19dfd96b4b58b27d3501 Mon Sep 17 00:00:00 2001 From: Alex Kosachev Date: Thu, 15 Oct 2026 15:30:51 +0800 Subject: [PATCH 2/2] Address review: one rule per engine, engines without NULLS syntax, own section Co-Authored-By: Claude Opus 5.5 --- core-spec/expression_language.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/core-spec/expression_language.md b/core-spec/expression_language.md index 94dd4f11..e516fc0b 100644 --- a/core-spec/expression_language.md +++ b/core-spec/expression_language.md @@ -796,17 +796,23 @@ date range. For a row-level shift, add a number of days to the date directly, or Sigma's table-calculation functions (`RowNumber`, `Rank`, `RunningSum`, `RunningAvg`, `Lag`, `Lead`, etc.) resolve their partition/order context from workbook UI configuration (which pivot/table the calculation is attached to) rather than from arguments passed in the formula text itself. Because that context isn't recoverable from the formula string alone, the Sigma converter (`converters/sigma/`) treats these as untranslatable to ANSI SQL and carries the original Sigma formula through in the `SIGMA` dialect only — see `converters/sigma/LIMITATIONS.md`. -### Window ORDER BY NULL Placement +--- -On an engine whose native NULL placement differs from the Ossie default, an implementation emits the clause explicitly. Where the native default matches, the clause MAY be omitted. Native defaults with no session setting changed: +## Window ORDER BY NULL Placement -| Engine | Native `ASC` | Native `DESC` | Emits when the clause is omitted | -| :---- | :---- | :---- | :---- | -| Snowflake, PostgreSQL | NULLS LAST | NULLS FIRST | nothing | -| BigQuery, Databricks | NULLS FIRST | NULLS LAST | `ASC NULLS LAST`, `DESC NULLS FIRST` | -| ClickHouse, DuckDB | NULLS LAST | NULLS LAST | `DESC NULLS FIRST` | +When an `ORDER BY` item omits the clause, the Ossie placement is `NULLS LAST` for `ASC` and `NULLS FIRST` for `DESC`. An implementation produces any placement, given or default, as follows: + +- On an engine with `NULLS FIRST` and `NULLS LAST`, it emits the clause. It MAY omit a default placement that the native default already gives. This holds only if the session cannot change that default, or the implementation sets it. Snowflake (`DEFAULT_NULL_ORDERING`) and DuckDB (`default_null_order`) let a session change it. +- On an engine without them, it adds a leading sort key `CASE WHEN order_expr IS NULL THEN 1 ELSE 0 END`. The key is ascending to put NULLs last and descending to put them first. -Snowflake (`DEFAULT_NULL_ORDERING`) and DuckDB (`default_null_order`) let a session change the native default; an implementation that cannot rely on the setting emits the clause on every `ORDER BY` item. +Native defaults: + +| Engine | `NULLS FIRST`, `NULLS LAST` | Native `ASC` | Native `DESC` | +| :---- | :---- | :---- | :---- | +| Oracle, PostgreSQL, Redshift, Snowflake | yes | NULLS LAST | NULLS FIRST | +| BigQuery, Databricks | yes | NULLS FIRST | NULLS LAST | +| ClickHouse, DuckDB, Trino | yes | NULLS LAST | NULLS LAST | +| MySQL, SQL Server | no | NULLS FIRST | NULLS LAST | ---