Conversation
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
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
1 similar comment
|
I have read the CLA Document and I hereby sign the CLA |
|
recheck |
f4a009b to
9ba32cb
Compare
to_expression() returned sa.null() for every un-aliased NULL constant, regardless of the operator it lands under: - comparisons raise ArgumentError (only =/!=/is-family accept null), so queries like 'a > last' - whose placeholder is Constant(None) on the first pass - silently failback to str(ast) and produce the invalid 'a > None', surfacing as a duckdb Binder Error (test_last) - 'a = NULL' was auto-rewritten to 'a IS NULL', changing match semantics - un-aliased 'SELECT NULL' lost its NULL column label Restore the always-labeled form and unwrap to sa.null() only under is / is not, the one place where SQLAlchemy's negate optimization needs it to keep NOT (x IS NULL) from compiling as x IS NULL.
SQLAlchemy's __invert__ only produces a real negation when the operand of IS / IS NOT is a dedicated NULL/TRUE/FALSE singleton. For any other operand (bind param, label, column, function, subquery, UNKNOWN) the inverted expression compiled identical to the original, silently dropping the NOT and inverting pushed-down results. Swapping IS <-> IS NOT is not a universal replacement either: PostgreSQL row-valued predicates such as NOT (ROW(1, NULL) IS NULL) are not equivalent to ROW(1, NULL) IS NOT NULL. Comparison negation (NOT (a > b) -> a <= b) stays untouched. The IS TRUE / IS FALSE keywords are preserved on SQLite, where they test truthiness and differ from IS 1 / IS 0. Tests: real LastQuery placeholder-to-injection roundtrip, duckdb row-level assertions, differential testing against sqlite3 for arbitrary IS operands and nested NOT, and cross-dialect NOT preservation.
|
Following up from mindsdb/mindshub#12491 — @akshat-lakhera offered to help there, and the renderer fix is implemented on this branch awaiting review. (GitHub doesn't allow the fork-PR author to send a formal review request, so mentioning here instead.) Feedback welcome, especially on the explicit-unary-NOT approach in |
|
I tested PR #2 locally. On the main branch, SELECT * After switching to this PR's branch, it is correctly rendered as: SELECT * I also ran the SQLAlchemy render test suite successfully. Thank you for the fix! |
Summary
SqlalchemyRender.to_expression()wrapped everyast.Constant— including NULL — in a.label(). In WHERE operands this turnedx IS NULLinto a bind-param comparison, which defeats SQLAlchemy's negate optimization, so a negated null predicate compiled to the identical expression and theNOTwas silently dropped:Pushed-down queries therefore returned inverted results with no error. Verified on a live deployment (ClickHouse integration):
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 whent.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:latestdeployment: all probe queries returned correct results afterwards, no regressions on the positive forms.Fixes mindsdb/mindshub#12491