Skip to content

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

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

mikebridge wants to merge 3 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.83%. Comparing base (9e4292e) to head (762801b).
⚠️ Report is 52 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #45912      +/-   ##
==========================================
+ Coverage   84.73%   84.83%   +0.10%     
==========================================
  Files        3069     3075       +6     
  Lines      205771   207867    +2096     
  Branches    48396    48739     +343     
==========================================
+ Hits       174354   176341    +1987     
- Misses      28123    28166      +43     
- Partials     3294     3360      +66     
Flag Coverage Δ
hive 34.24% <18.18%> (-0.27%) ⬇️
mysql 52.26% <18.18%> (-0.35%) ⬇️
postgres 52.27% <18.18%> (-0.34%) ⬇️
presto 35.95% <18.18%> (-0.29%) ⬇️
python 87.87% <100.00%> (+0.11%) ⬆️
sqlite 52.02% <18.18%> (-0.35%) ⬇️
superset-extensions-cli 91.24% <ø> (?)
unit 82.69% <100.00%> (+0.17%) ⬆️

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 762801b
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6ae9138fc09b3d0c8feaf7f1
😎 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
👍 | 👎

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.

get_table requires "including any term's NULL placement" (view.py:175). Dropping placement only after the first key still passes all 65 conformance tests; add a tied-prefix multi-key witness whose page changes when secondary placement is dropped.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a required secondary-order witness in 762801b. January rows tie on month; with category NULLS LAST, offset 1 / limit 1 keeps the NULL category (total 3). Dropping only the second key’s placement instead keeps A (total 2). The check partitions by the preceding keys, and rejects fixtures whose page cannot distinguish the two placements. There is also a descending/NULLS FIRST mutation check.

  • Before: keeping placement only on the first key passed all 65 existing conformance tests.
  • After: the same mutation fails the new required case while those 65 tests still pass.
  • Verification: 1,985 PR tests passed, with 4 capability skips; changed-file hooks and the pre-push coverage gate passed.

@codeant-ai-for-open-source

codeant-ai-for-open-source Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

CodeAnt  CodeAnt PR Risk

🟢  Low Risk

  • The PR appears safe to merge; the secondary-key conformance fixture detects providers that drop NULL placement within tied prefixes.

Assessed commit: 762801b8a880

@aminghadersohi aminghadersohi left a comment

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.

The NULL-placement witness needs another adjustment.

index: int
row: dict[str, object]
for index, row in enumerate(rows):
groups.setdefault(tuple(row[name] for name in prefix), []).append(index)

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.

STRUCT/LIST prefix dimensions become dict/list values in to_pylist(), so this required case raises TypeError: unhashable type even when ordering is correct. Keep the hashable Arrow scalars as prefix keys.

Suggested change
groups.setdefault(tuple(row[name] for name in prefix), []).append(index)
groups.setdefault(
tuple(full.column(name)[index] for name in prefix), []
).append(index)

Comment on lines +366 to +367
assert sorted(repr(rows[index]) for index in placed[start:stop]) != sorted(
repr(rows[index]) for index in opposite[start:stop]

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.

The full-row check accepts (A,x),(NULL,y),(NULL,z) at offset 1/limit 1, although either placement may return (NULL,y) because NULL ties are unordered. The old key-value check rejected this non-discriminating fixture.

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.

2 participants