Skip to content

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

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

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.86%. Comparing base (9e4292e) to head (2959e13).
⚠️ Report is 42 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #45912      +/-   ##
==========================================
+ Coverage   84.73%   84.86%   +0.13%     
==========================================
  Files        3069     3074       +5     
  Lines      205771   208832    +3061     
  Branches    48396    49070     +674     
==========================================
+ Hits       174354   177229    +2875     
- Misses      28123    28270     +147     
- Partials     3294     3333      +39     
Flag Coverage Δ
hive 34.37% <18.18%> (-0.14%) ⬇️
mysql 52.94% <18.18%> (+0.34%) ⬆️
postgres 52.96% <18.18%> (+0.35%) ⬆️
presto 36.28% <18.18%> (+0.04%) ⬆️
python 87.90% <100.00%> (+0.13%) ⬆️
sqlite 52.73% <18.18%> (+0.36%) ⬆️
superset-extensions-cli 91.24% <ø> (?)
unit 82.73% <100.00%> (+0.21%) ⬆️

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.

@mikebridge
mikebridge marked this pull request as ready for review November 2, 2026 15:01
The page must keep different rows from the ones the opposite placement would.
"""
assert query.order, "null_placement fixture requires an order"
first: tuple[object, ...] = query.order[0]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The required case only inspects query.order[0], so a provider that ignores NULL placement on later OrderTerms can pass despite the per-term SDK contract.

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes · 🏷️ Incomplete implementation

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset-core/src/superset_core/semantic_layers/testing.py
**Line:** 303:303
**Comment:**
	*Incomplete Implementation: The required case only inspects `query.order[0]`, so a provider that ignores NULL placement on later `OrderTerm`s can pass despite the per-term SDK contract.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codeant-ai-for-open-source

Copy link
Copy Markdown
Contributor

CodeAnt  CodeAnt PR Risk

🟡  Medium Risk

  • The PR needs attention before merging.
  • The required conformance check validates NULL placement only on the first order term, so providers can ignore placement on later keys and still pass.

Assessed commit: 2959e13c86b3

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant