sadpandajoe opened a new pull request, #45317:
URL: https://github.com/apache/superset/pull/45317

   ### SUMMARY
   
   The negated filter operators `!=`, `NOT LIKE` and `NOT ILIKE` on a 
SQLAlchemy-backed dataset also removed rows whose column value is NULL, not 
only the named values. For example `state != 'CA'` dropped the `CA` rows and 
the rows where `state` is NULL. Only the named values should be excluded, which 
is also how a `NOT IN` filter should behave (see #45305).
   
   `ExploreMixin.get_sqla_query` emitted the bare SQLAlchemy operator for these 
filters. Under SQL three-valued logic `NULL != 'CA'` and `NULL NOT LIKE '%C%'` 
are not true, so the `WHERE` clause discarded the NULL rows. With this change a 
negated predicate with a non-NULL operand is built as `<column> IS NULL OR 
<negated predicate>`:
   
   - scalar `!=`, `NOT LIKE` and `NOT ILIKE`; the NULL check is applied to the 
column itself, not to the string cast used for pattern matching on non-string 
columns;
   - whole-array `!=` and `NOT IN` on multi-value columns (compile-level, 
defensive);
   - `!=` and `NOT IN` on advanced data type columns (internet address, port), 
applied to the plugin's translated predicate;
   - the `!=` clause built by the MCP dashboard scope SQL, so it agrees with 
the dataset path.
   
   The NULL check goes through the engine spec's `handle_null_filter`, so 
engines with their own NULL syntax are honored. An explicit NULL operand keeps 
its meaning: `!= NULL` is still `IS NOT NULL`, and a `NOT IN` list that 
contains NULL still excludes NULL rows. `=`, `IN`, `LIKE`, `ILIKE` and the 
comparison operators are unchanged, and so is plain `NOT IN`, which #45305 
handles. Metric filters (HAVING) share the same code path, so `metric != x` now 
also keeps rows whose aggregate is NULL.
   
   `UPDATING.md` documents the behavior change, because saved charts and 
reports that use these operators can now return additional NULL rows.
   
   ### Root Cause
   
   - **Verified:** the final comparison/pattern branch of `get_sqla_query` 
passed `!=` straight to `BaseEngineSpec.handle_comparison_filter` (`sqla_col != 
value`) and built `NOT LIKE` / `NOT ILIKE` with `sqla_col.not_like(eq)` / 
`sqla_col.not_ilike(eq)`. Running real `SqlaTable.get_sqla_query` queries 
against SQLite with rows `CA, NY, TX, NULL`: `!= 'CA'` returned `NY, TX`, `NOT 
LIKE '%C%'` and `NOT ILIKE '%c%'` returned `NY, TX`; the NULL row was missing 
in each. The same held for numeric, boolean, temporal, calculated and ad-hoc 
SQL columns, for pattern matching on numeric and UUID columns, and for a metric 
in HAVING.
   - **Verified:** `handle_comparison_filter` is defined only in 
`BaseEngineSpec` and has two callers (`get_sqla_query` and the MCP dashboard 
scope SQL); no engine overrides it. `handle_null_filter` is overridden only by 
the Kusto KQL spec.
   - **History:** each operator was added as a 1:1 mapping to the SQLAlchemy 
operator (`!=` in 2017, `NOT LIKE` in 2024, `NOT ILIKE` in 2026) without 
considering NULL column values; no commit states an intent to drop NULL rows.
   - **Not changed here:** plain `NOT IN` and the MCP `_in_list_clause` 
(#45305); the Explore Custom SQL text generated for simple filters in the 
frontend, which still excludes NULL rows and is left for a follow-up; the 
semantic layer, whose NULL handling is separate and intentionally pinned by its 
own tests.
   
   ### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
   
   Not applicable: backend query generation only, no UI change. Regression 
guard output (real `get_sqla_query` executed on SQLite with a NULL row, plus 
compile-level cases for array columns):
   
   - Before the fix: 44 of the new guard cases fail with `Extra items in the 
right set: None` (the NULL row is missing) or a missing NULL check in the 
compiled SQL; the 29 pins for behavior that must not change pass.
   - After the fix: all 73 guard cases pass; `tests/unit_tests/models` and 
`tests/unit_tests/mcp_service` pass (11590 passed, 12 skipped).
   
   ### TESTING INSTRUCTIONS
   
   1. Run the new tests:
      `pytest tests/unit_tests/models/helpers_test.py 
tests/unit_tests/models/test_multivalue_filter.py 
tests/unit_tests/mcp_service/test_dashboard_scope_sql.py -k "negated or 
unchanged_null or engine_spec_hook or advanced_data_type or metric_filters"`.
   2. Manual check: create a chart on a dataset whose text column has some NULL 
values, add the filter `state != 'CA'` (or `NOT LIKE '%C%'` through a custom 
filter), and confirm only the matching values are excluded while NULL rows 
remain. A `!= NULL` filter still returns only non-NULL rows.
   
   ### ADDITIONAL INFORMATION
   - [ ] Has associated issue:
   - [ ] Required feature flags:
   - [ ] Changes UI
   - [ ] Includes DB Migration (follow approval process in 
[SIP-59](https://github.com/apache/superset/issues/13351))
     - [ ] Migration is atomic, supports rollback & is backwards-compatible
     - [ ] Confirm DB migration upgrade and downgrade tested
     - [ ] Runtime estimates and downtime expectations provided
   - [ ] Introduces new feature or API
   - [ ] Removes existing feature or API
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to