Skip to content

fix(semantic-cache): NULL semantics in leftover masks + close the 100% coverage gap#41856

Open
mikebridge wants to merge 2 commits into
apache:sl-cachefrom
mikebridge:pr-40221-coverage-gap
Open

fix(semantic-cache): NULL semantics in leftover masks + close the 100% coverage gap#41856
mikebridge wants to merge 2 commits into
apache:sl-cachefrom
mikebridge:pr-40221-coverage-gap

Conversation

@mikebridge

Copy link
Copy Markdown
Contributor

SUMMARY

Fourth contribution into #40221's branch (sl-cache). The unit-test CI job gates superset/semantic_layers/ at 100% coverage (--cov-fail-under=100), and the branch sits at 86.44% — its own unit-tests (current) is red, and every stacked PR inherits the red regardless of its own coverage. This closes the gap to 100.00% (499 tests), which also directly addresses the long-standing patch-coverage review comment.

Mostly table-driven tests over the untested edges: the full pairwise filter-implication matrix, can_satisfy rejection edges, the leftover-mask operator matrix, LIKE→regex translation, the MAX_ENTRIES trim, serialization helpers, and mapper's complete filter-value coercion matrix (every dtype's accept/reject branches).

One real bug found and fixed (the point of testing these branches): negative leftover masks (NOT_EQUALS/NOT_IN/NOT_LIKE) kept NULL rows — pandas evaluates NaN != x as True, but SQL three-valued logic excludes NULLs from negative predicates. Cache-served results could contain rows the warehouse would have dropped. The masks now AND with notna(), pinned by the operator matrix.

Two findings flagged, not changed:

  • _sql_like_to_regex has no escape-character supportLIKE '100\%' wildcards the %, diverging from warehouses (PostgreSQL escapes with backslash by default). The test pins current behavior; if escape support is added, extend the table.
  • Two provably-dead fallthroughs in _implies_range (both same-direction range-op combos are exhaustively enumerated above them) carry justified # pragma: no cover comments — deleting the dead lines is equally fine if preferred.

TESTING INSTRUCTIONS

pytest --cov=superset/semantic_layers/ tests/unit_tests/semantic_layers/ --cov-fail-under=100 -q
# 499 passed · Required test coverage of 100% reached. Total coverage: 100.00%

Once this merges into sl-cache, the branch's own unit-tests (current) and every stacked PR's (#41824/#41825/#41826) go green.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
  • Introduces new feature or API
  • Removes existing feature or API

🤖 Generated with Claude Code

… in negative masks

The unit-test CI job gates superset/semantic_layers/ at 100% coverage,
and the branch was at 86.44% — its own CI red, and every stacked PR
inheriting the red. This closes the gap to 100.00% (499 tests) with
table-driven tests over the untested edges:

- the pairwise filter-implication matrix (_implies and helpers): every
  IS_NULL / IS_NOT_NULL / EQUALS / NOT_EQUALS / IN / NOT_IN / range
  cross-combination, range-direction and comparability edges
- can_satisfy edges: column-less WHERE rejection, redundant-vs-leftover
  filters on one column, ROLLUP order-on-dropped-dimension rejection,
  group-limit shape mismatch, projection gating
- leftover-mask operator matrix, LIKE->regex translation, ordering with
  unknown columns, rollup with aggregation-less metrics
- store_result's MAX_ENTRIES trim (oldest dropped first)
- key/serialization helpers and the config timeout fallback
- the full filter-value coercion matrix in mapper (every dtype's accept
  and reject branches)

Writing the tests surfaced one REAL bug, fixed here: negative leftover
masks (NOT_EQUALS / NOT_IN / NOT_LIKE) kept NULL rows, where SQL
three-valued logic excludes them — cache-served results could contain
rows the warehouse would have dropped. The masks now AND with notna(),
and the operator matrix pins SQL semantics.

Two provably-dead fallthroughs in _implies_range (both same-direction
range-op combinations are exhaustively enumerated above them) carry
justified no-cover pragmas rather than fabricated tests.

Also flagged (not changed): _sql_like_to_regex has no escape-character
support — LIKE '100\%' treats the percent as a wildcard, diverging from
warehouses (e.g. PostgreSQL) where backslash escapes it. The test pins
current behavior and the divergence is raised on the parent PR.
@bito-code-review

bito-code-review Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped - Branch Excluded

Bito didn't auto-review because the source or target branch is excluded from automatic reviews.
No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.
You can change the branch exclusion settings here, or contact your Bito workspace admin at evan@preset.io.

@netlify

netlify Bot commented Jul 7, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit e1218de
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a4d22cb5ebbce0007416cc4
😎 Deploy Preview https://deploy-preview-41856--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.

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.27%. Comparing base (5581315) to head (2fc8aae).

Additional details and impacted files
@@             Coverage Diff              @@
##           sl-cache   #41856      +/-   ##
============================================
+ Coverage     63.43%   64.27%   +0.84%     
============================================
  Files          2592     2591       -1     
  Lines        138780   138644     -136     
  Branches      32235    32212      -23     
============================================
+ Hits          88038    89120    +1082     
+ Misses        49212    48003    -1209     
+ Partials       1530     1521       -9     
Flag Coverage Δ
hive 39.24% <0.00%> (-0.02%) ⬇️
mysql 58.79% <0.00%> (-0.01%) ⬇️
postgres 58.87% <0.00%> (-0.01%) ⬇️
presto 40.92% <0.00%> (-0.02%) ⬇️
python 60.96% <100.00%> (+1.84%) ⬆️
sqlite 58.51% <0.00%> (-0.01%) ⬇️
unit 100.00% <100.00%> (?)

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.

…mutant

Round-2 review panel on this PR (4 lenses; one ran 15 real mutants —
6 survived) plus its own findings, all resolved:

Production (completing the SQL three-valued-logic family):
1. Positive LIKE now excludes NULL rows: astype(str) stringifies NULLs
   into "nan"/"None"/"NaT" sentinels that spuriously matched patterns
   like 'n%' (empirically reproduced by two reviewers). SQL: NULL LIKE p
   is UNKNOWN. Same guard the PR already applied to NOT_LIKE.
2. Positive IN now excludes NULL rows: pandas isin(None) matches None in
   object columns; SQL IN-lists never match NULL rows even when the list
   contains NULL.
3. _implies None-guards: a new NULL-matching predicate (EQUALS None, or
   an IN set containing None) was wrongly treated as contained by cached
   negative predicates (NOT_EQUALS / NOT_IN) — whose SQL result sets
   contain no NULL rows — so a NULL-lookup query could be served an
   EMPTY cached result. The IS_NOT_NULL branch already guarded this;
   the negative branches now match.

Tests (mutation-hardening — every one of the 6 surviving mutants plus
the 4 new guards now has a killing row, re-validated one by one):
- upper-bound implication False rows (a True-mutant here means silently
  truncated cache serves)
- the (LTE 5, LTE 5) row was theater — shadowed by the equality early
  return; replaced with (LTE 4, LTE 5)
- the mask matrix DataFrame gains a NULL-bearing string row, so all
  three LIKE-family notna() guards are observable; IN-with-NULL-element
  row added
- LIKE regex end-anchor pinned ('a_c' rejects 'a.cd'); the no-escape
  divergence is now pinned by a backslash row instead of only narrated
- the boolean token sets are fully enumerated (the set is the contract)
- None-hole implication rows for the new _implies guards

Hygiene per review: production imports hoisted to the module block,
planning-repo comment references replaced with the PR number, timeless
docstring wording.

Gate: 100.00%, 515 tests.
@mikebridge mikebridge changed the title test(semantic-cache): close the 100% coverage gap; fix NULL semantics in negative masks fix(semantic-cache): NULL semantics in leftover masks + close the 100% coverage gap Jul 7, 2026
@mikebridge

Copy link
Copy Markdown
Contributor Author

Round-2 review (four-lens panel via Claude; one lens ran 15 real mutants against the suite — 6 survived; every claim below verified empirically) — all findings resolved in 2fc8aae86d, and the PR is retitled fix(...) since it carries production data-correctness changes:

The 3VL NULL fix is now complete (round 1 fixed only the negative half):

  • Positive LIKE excluded NULLsastype(str) stringifies NULLs into "nan"/"None"/"NaT" sentinels that spuriously matched patterns like 'n%' (reproduced independently by two lenses). Same notna() guard as NOT_LIKE.
  • Positive IN excludes NULLs — pandas isin(None) matches None in object columns; SQL IN-lists never match NULL rows.
  • _implies None-guards — a NULL-matching query (EQUALS None, or an IN set containing None) was wrongly contained by cached negative predicates, so a NULL-lookup could be served an empty cached result. The IS_NOT_NULL branch already guarded this; the negative branches now match.

Every surviving mutant now dies — re-validated one at a time: the upper-bound looser-containment mutants (which would serve silently truncated results), the shadowed (LTE 5, LTE 5) theater row, the unpinned NOT_LIKE guard, the regex end-anchor, and the boolean token set (now fully enumerated). The mask matrix gained a NULL-bearing row so all three LIKE-family guards are behaviorally observable, and the LIKE no-escape divergence is now pinned by a backslash row rather than only narrated.

Merge-coordination note for this stack (verified by merge replay): this PR and #41824 both append to cache_test.py — a naive union-merge corrupts the file (byte-identical fake-cache bodies act as common context and git splices classes together; reproduced IndentationError). The correct resolution keeps both appended suites wholesale and hand-merges the import block — validated: the union suite runs green. Recommended order: #41824 → this PR (rebased, gate re-run over the union) → #41825/#41826 in any order. I'll handle the rebase when #41824 lands.

Gate: 100.00%, 515 tests.

@rusackas rusackas requested review from betodealmeida and Copilot July 7, 2026 19:42

Copilot AI 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.

Pull request overview

Note

Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.

This PR fixes SQL NULL-semantics mismatches in smart-cache leftover filtering and closes the superset/semantic_layers/ unit-test coverage gap to meet the 100% CI gate.

Changes:

  • Fix NULL-handling for negative leftover masks (NOT_EQUALS, NOT_IN, NOT_LIKE) and for IN / LIKE to better match SQL three-valued logic.
  • Add extensive table-driven unit tests covering filter implication, leftover mask behavior, LIKE→regex translation, cache trimming, and scalar filter-value coercion.
  • Add tests for additional cache helper edges (ordering, rollup skips, key/serialization helpers, timeout fallback, and can_satisfy rejection paths).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
superset/semantic_layers/cache.py Adjusts implication and mask generation logic to align with SQL NULL semantics and adds no-cover markers for unreachable branches.
tests/unit_tests/semantic_layers/cache_test.py Adds broad matrix-style tests to close coverage gaps and validate cache reuse / masking behavior across operators and edge cases.
tests/unit_tests/semantic_layers/mapper_test.py Adds coercion accept/reject matrices for scalar filter-value conversion by dimension dtype.

Comment on lines 522 to 532
if cop == Operator.NOT_EQUALS:
if nop == Operator.NOT_EQUALS:
return nval == cval
if nop == Operator.EQUALS:
return nval != cval
# EQUALS None means IS_NULL; a cached negative predicate's rows
# contain no NULLs (SQL three-valued logic), so it can never
# contain the NULL-matching query.
return nval is not None and nval != cval
if nop == Operator.IN and isinstance(nval, frozenset):
return cval not in nval
return all(v is not None for v in nval) and cval not in nval
return False
Comment on lines 544 to 552
if nop == Operator.NOT_EQUALS:
return cval.issubset({nval})
if nop == Operator.EQUALS:
return nval not in cval
# See the NOT_EQUALS branch above: NULL-matching queries are
# never contained by negative cached predicates.
return nval is not None and nval not in cval
if nop == Operator.IN and isinstance(nval, frozenset):
return cval.isdisjoint(nval)
return all(v is not None for v in nval) and cval.isdisjoint(nval)
return False
Comment on lines 733 to +737
if op == Operator.NOT_EQUALS:
return series != val if val is not None else series.notna()
# SQL three-valued logic: NULL != x is UNKNOWN, so the warehouse
# excludes NULL rows from a negative predicate; pandas would keep
# them (NaN != x is True). Match the warehouse.
return (series != val) & series.notna() if val is not None else series.notna()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants