Skip to content

fix(render): keep un-aliased NULL unwrapped so NOT survives negation - #2

Open
toddyLee wants to merge 2 commits into
mindsdb:mainfrom
toddyLee:fix/sqlalchemy-render-not-is-null
Open

fix(render): keep un-aliased NULL unwrapped so NOT survives negation#2
toddyLee wants to merge 2 commits into
mindsdb:mainfrom
toddyLee:fix/sqlalchemy-render-not-is-null

Conversation

@toddyLee

Copy link
Copy Markdown

Summary

SqlalchemyRender.to_expression() wrapped every ast.Constant — including NULL — in a .label(). In WHERE operands this turned x IS NULL into a bind-param comparison, which defeats SQLAlchemy's negate optimization, so a negated null predicate compiled to the identical expression and the NOT was silently dropped:

expr = col.operate(operators.is_, null_label)   # x IS :param_1
str(expr.__invert__().compile())                # x IS :param_1  <-- NOT lost
str(col.is_(None).__invert__().compile())       # x IS NOT NULL  <-- correct without the Label

Pushed-down queries therefore returned inverted results with no error. Verified on a live deployment (ClickHouse integration):

SELECT COUNT(*) FROM db.t WHERE code='UAV' AND TypeName IS NOT NULL;   -- 1 row (correct)
SELECT COUNT(*) FROM db.t WHERE code='UAV' AND NOT (TypeName IS NULL); -- 0 rows (wrong, should be 1)
SELECT COUNT(*) FROM db.t WHERE code='UAV' AND NOT (TypeName IS NOT NULL); -- 1 row (wrong, should be 0)

Because the renderer is shared, this affects every integration that pushes SQL through SqlalchemyRender (mysql, postgres, mssql, oracle, snowflake, duckdb, bigquery, databricks, and 30+ community handlers) and is dialect-independent — reproduced rendering with MySQL, PostgreSQL and SQLite dialects.

Fix

Return sa.null() without a label when t.value is None and not t.alias. Labeled behavior is kept for aliased SELECT-list constants (SELECT NULL AS x).

Tests

5 new regression cases in tests/unit/render/test_sqlalchemyrender.py (TestNullPredicateRendering): NOT (x IS NULL), NOT x IS NULL, NOT (x IS NOT NULL), positive-form invariance, and aliased-NULL label preservation. Full file passes 20/20.

Verification

Applied the same one-line fix to a live mindsdb/mindsdb:latest deployment: all probe queries returned correct results afterwards, no regressions on the positive forms.

Fixes mindsdb/mindshub#12491

SqlalchemyRender.to_expression() wrapped every ast.Constant — including
NULL — in a Label. In WHERE operands this turned `x IS NULL` into a
bind-param comparison, defeating SQLAlchemy's negate optimization, so
`NOT (x IS NULL)` compiled identical to `x IS NULL` and pushed-down
queries silently returned inverted results.

Return sa.null() without a label for un-aliased NULL constants; labeled
behavior is kept for aliased SELECT-list constants.

Fixes mindsdb/mindshub#12491
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@toddyLee

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

1 similar comment
@todd-lee

todd-lee commented Sep 1, 2026

Copy link
Copy Markdown

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Sep 1, 2026
@toddyLee

toddyLee commented Sep 1, 2026

Copy link
Copy Markdown
Author

recheck

@toddyLee
toddyLee force-pushed the fix/sqlalchemy-render-not-is-null branch from f4a009b to 9ba32cb Compare September 1, 2026 01:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: SqlalchemyRender silently drops NOT on IS [NOT] NULL predicates — wrong query results across all SqlalchemyRender-based integrations

2 participants