Skip to content

feat(semantic-layer): sort NULL dates last in the default order - #45912

Draft
mikebridge wants to merge 2 commits into
apache:masterfrom
mikebridge:sc-128656-null-placement
Draft

mikebridge wants to merge 2 commits into
apache:masterfrom
mikebridge:sc-128656-null-placement

Conversation

@mikebridge

Copy link
Copy Markdown
Contributor

SUMMARY

TL;DR: When a semantic query has no sort order, Superset sorts it by its first date column, oldest first. NULL dates now always go last, both from the provider and from Superset's cache, so a limited chart shows the same rows either way. Upgrading recomputes every cached semantic-layer result once, because the cache key format changes.

Background:

  • A semantic query without an explicit order gets a default: ascending by the first selected date, timestamp or time dimension. The host sends that order to the provider and also uses it to re-sort rows it rolls up from its cache.
  • The SDK never said where NULL values of that dimension go. The cache put them last, while some engines put NULL first in ascending order (BigQuery, MySQL, Databricks; Snowflake depends on a session setting).

Problem:

  • With a limit, those two answers differ. For dates [NULL, January, February] and a limit of 2, a NULL-first provider returns NULL and January, but a cache rollup returns January and February.
  • The provider also couldn't tell the generated default apart from a user's explicit "date ascending", so it couldn't apply a different rule to the default alone.

Changes:

  • SDK (superset-core): an order term can now carry an optional NULL placement.
    • The new OrderTerm(member, direction, nulls=None) still unpacks as a (member, direction) pair, so existing providers keep working unchanged.
    • null_placement(term) reads the placement from either form; None means the provider's default, as before.
    • A term with a placement differs from the plain pair; one without equals it.
  • Host: the generated default is sent as OrderTerm(dimension, ASC, NullPlacement.LAST). Explicit orders are sent exactly as given and keep the provider's NULL placement.
  • Cache: the cache identity includes the placement, so a page cut with NULL values last is never reused for an explicit ascending query, or the reverse. Rolled-up rows sort NULL values last explicitly. The identity format moves to v6, so existing semantic cache entries are recomputed once.
  • Conformance kit: a new required null_placement query case checks that a provider applies a placement before offset/limit. The SQLite reference provider renders NULLS FIRST/LAST.
  • Docs:
    • UPDATING.md covers the provider obligation, the kit case and the one-time cache recompute;
    • the provider testing guide describes the new case.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable; no UI change. For a chart whose time column has NULL values and that has a row limit:

  • Before: a NULL-first provider and a cache hit could show different rows.
  • After: both show the earliest dates, with NULL dates last.

TESTING INSTRUCTIONS

  • pytest superset-core/tests/semantic_layers tests/unit_tests/semantic_layers
  • These tests failed before the change:
    • default_order_test.py::test_default_order_differs_from_an_explicit_ascending_order;
    • test_limited_hit_is_not_shared_between_default_and_explicit_ascending;
    • test_conformance.py::...::test_query_outputs[null_placement] against a provider that ignores the placement.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
  • Introduces new feature or API (OrderTerm, NullPlacement, null_placement in the semantic-layer SDK; a new required conformance-kit case)
  • Removes existing feature or API

Notes for reviewers:

An unordered semantic query is sorted by its first temporal dimension
ascending, but the SDK left NULL placement to the provider while the cache
rollup put NULL values last. With a limit, a provider that sorts NULL first
and a cached rollup could return different rows.

The SDK order term can now carry an optional NULL placement:
OrderTerm(member, direction, nulls) still unpacks as a (member, direction)
pair, so existing providers keep working, and null_placement(term) reads it
from either form. The host sends its generated default as ascending with
NULL values last, which also tells it apart from an explicit ascending order
on the same dimension. Explicit orders are unchanged.

The cache identity includes the placement and moves to v6, so cached
semantic results are recomputed once. The conformance kit adds a required
null_placement case.
dataclasses.asdict and astuple rebuild a tuple from its elements, which
failed for OrderTerm. The term now takes their named-tuple path, keeping its
member and direction; its docstring says the placement is not carried and
to read it with null_placement.

The conformance kit names a missing query case instead of failing with a
bare KeyError, and the provider testing guide asks the null_placement
fixture for distinct key values and a single NULL group, since tied rows may
come back in either order.
@github-actions github-actions Bot added the doc Namespace | Anything related to documentation label Oct 31, 2026
@codecov

codecov Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.75%. Comparing base (9e4292e) to head (2959e13).
⚠️ Report is 20 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #45912      +/-   ##
==========================================
+ Coverage   84.73%   84.75%   +0.02%     
==========================================
  Files        3069     3074       +5     
  Lines      205771   206513     +742     
  Branches    48396    48423      +27     
==========================================
+ Hits       174354   175030     +676     
- Misses      28123    28183      +60     
- Partials     3294     3300       +6     
Flag Coverage Δ
hive 34.49% <18.18%> (-0.02%) ⬇️
mysql 52.60% <18.18%> (-0.01%) ⬇️
postgres 52.61% <18.18%> (+<0.01%) ⬆️
presto 36.21% <18.18%> (-0.02%) ⬇️
python 87.76% <100.00%> (+<0.01%) ⬆️
sqlite 52.36% <18.18%> (-0.01%) ⬇️
superset-extensions-cli 91.24% <ø> (?)
unit 82.53% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
@netlify

netlify Bot commented Oct 31, 2026

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 2959e13
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6ae55b9ba2bffae7b3632883
😎 Deploy Preview https://deploy-preview-45912--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Namespace | Anything related to documentation size/XL

1 participant