Skip to content

fix(sqla): parenthesize calculated column expressions in SELECT/GROUP BY/ORDER BY and series-limit queries - #43864

Open
rebenitez1802 wants to merge 2 commits into
masterfrom
rebenitez1802/parenthesize-calculated-or-filters
Open

fix(sqla): parenthesize calculated column expressions in SELECT/GROUP BY/ORDER BY and series-limit queries#43864
rebenitez1802 wants to merge 2 commits into
masterfrom
rebenitez1802/parenthesize-calculated-or-filters

Conversation

@rebenitez1802

Copy link
Copy Markdown
Contributor

SUMMARY

A calculated column's expression becomes SQL via literal_column(expr) with no parentheses. When the expression contains a low-precedence boolean operator (e.g. state = 'CA' OR state = 'NY'), the bare OR leaks into the surrounding operator's precedence.

#39793 fixed this for WHERE/HAVING filters only. The same column used as a dimension / GROUP BY / ORDER BY / metric was still emitted unparenthesized, and — the real correctness bug — so were the series-limit (top-N) prequery predicate and the series-limit JOIN ON, where <calc col> = value mis-parsed:

state = 'CA' OR state = 'NY' = 1   ->   state = 'CA' OR (state = 'NY' = 1)

changing which groups the top-N prequery selects and the join membership whenever a boolean/OR calculated column is used as a series dimension.

Fix: wrap the expression branch of both column converters (ExploreMixin.convert_tbl_column_to_sqla_col and TableColumn.get_sqla_col) in Grouping(...). Every downstream clause (SELECT, GROUP BY, ORDER BY, both _get_top_groups, the JOIN ON, COUNT(DISTINCT ...)) is built from the converter output, so one wrap at the source parenthesizes them all. Physical (non-expression) columns are left untouched.

The existing WHERE/HAVING filter wrap is narrowed to adhoc expressions only (registered calculated columns are now parenthesized by the converter) with an _is_parenthesized guard that prevents a redundant ((...)) double-wrap for adhoc columns that reference a saved calculated column. Adhoc SQL-expression columns referenced by label are now parenthesized too, matching inline adhoc columns. SQL Lab virtual datasets (Query) inherit the same converter and are covered.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A (backend query generation). Example — a boolean calculated column is_ca_or_ny = state = 'CA' OR state = 'NY' used as a series (top-N) dimension:

Before:

... ON state = 'CA' OR state = 'NY' = is_ca_or_ny__ ...

After:

... ON (state = 'CA' OR state = 'NY') = is_ca_or_ny__ ...

TESTING INSTRUCTIONS

New unit tests cover SELECT / GROUP BY / ORDER BY (alias variants), the series-limit JOIN ON and top-N prequery predicate (correctness regressions that fail without this change), adhoc calc-column-reference filters, adhoc-by-label filters, SQL Lab Query, and legacy COUNT(DISTINCT ...):

pytest tests/unit_tests/models/helpers_test.py tests/unit_tests/connectors/sqla/models_test.py

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

@bito-code-review

bito-code-review Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #3bac2b

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset/connectors/sqla/models.py - 1
    • Temporal path not parenthesized · Line 1240-1240
      The `Grouping` wrap here fixes precedence leaks in the SELECT/aggregate path, but the temporal path is not covered: `get_time_filter` (helpers.py:3994) calls `get_timestamp_expression` (models.py:1307), which still returns a bare `literal_column`. A calculated time column containing a bare `OR` then renders as `state='CA' OR (state='NY' >= ...)` in the range filter (helpers.py:4055). Consider wrapping there too.
Review Details
  • Files reviewed - 4 · Commit Range: 793a47c..793a47c
    • superset/connectors/sqla/models.py
    • superset/models/helpers.py
    • tests/unit_tests/connectors/sqla/models_test.py
    • tests/unit_tests/models/helpers_test.py
  • Files skipped - 1
    • UPDATING.md - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@netlify

netlify Bot commented Sep 4, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

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

@rebenitez1802
rebenitez1802 force-pushed the rebenitez1802/parenthesize-calculated-or-filters branch from 793a47c to 41f67dd Compare September 4, 2026 13:26
# Parenthesize calculated-column expressions so a bare boolean
# operator (e.g. OR) inside the expression cannot leak into the
# surrounding operator precedence (e.g. COUNT(DISTINCT ...)).
col = Grouping(literal_column(expression, type_=type_))

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: get_timestamp_expression still emits calculated temporal expressions without this grouping, so time-series queries can retain the same boolean precedence bug. [incomplete implementation]

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes

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/connectors/sqla/models.py
**Line:** 1240:1240
**Comment:**
	*Incomplete Implementation: `get_timestamp_expression` still emits calculated temporal expressions without this grouping, so time-series queries can retain the same boolean precedence bug.

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
👍 | 👎

@bito-code-review

Copy link
Copy Markdown
Contributor

The flagged issue is correct. The get_timestamp_expression method in superset/connectors/sqla/models.py currently bypasses the Grouping wrapper applied to other calculated columns, which can lead to operator precedence bugs in time-series queries. To resolve this, you should wrap the expression returned by get_timestamp_expression in a Grouping object, similar to how other calculated columns are handled in the PR.

Would you like me to implement this fix for get_timestamp_expression? I can also check the rest of the PR comments if you would like to address other issues as well.

superset/connectors/sqla/models.py

from sqlalchemy.sql.elements import ColumnClause, Grouping, TextClause

# ... inside get_timestamp_expression ...
return Grouping(literal_column(expression, type_=type_))

@rebenitez1802
rebenitez1802 force-pushed the rebenitez1802/parenthesize-calculated-or-filters branch from 41f67dd to acc2573 Compare September 4, 2026 14:08
@pull-request-size pull-request-size Bot added size/XL and removed size/L labels Sep 4, 2026
@codecov

codecov Bot commented Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.61538% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.43%. Comparing base (d9b201d) to head (dc2ca4f).

Files with missing lines Patch % Lines
superset/connectors/sqla/models.py 50.00% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #43864      +/-   ##
==========================================
- Coverage   79.45%   79.43%   -0.03%     
==========================================
  Files        2895     2895              
  Lines      168167   167990     -177     
  Branches    38995    38895     -100     
==========================================
- Hits       133624   133442     -182     
- Misses      32044    32051       +7     
+ Partials     2499     2497       -2     
Flag Coverage Δ
hive 37.77% <15.38%> (-0.01%) ⬇️
mysql 57.49% <38.46%> (+<0.01%) ⬆️
postgres 57.52% <38.46%> (+<0.01%) ⬆️
presto 39.66% <23.07%> (-0.01%) ⬇️
python 83.89% <84.61%> (+0.01%) ⬆️
sqlite 57.20% <38.46%> (-0.01%) ⬇️
unit 74.42% <84.61%> (+0.02%) ⬆️

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.

… BY/ORDER BY and series-limit queries

A calculated column's expression is turned into SQL via literal_column()
with no parentheses. When the expression contains a low-precedence boolean
operator (e.g. `state = 'CA' OR state = 'NY'`), the bare OR leaks into the
surrounding operator's precedence. PR #39793 fixed this for WHERE/HAVING
filters only; the same column used as a dimension / GROUP BY / ORDER BY /
metric, and -- the real correctness bug -- the series-limit (top-N)
prequery predicate and JOIN ON, were still emitted unparenthesized, so
`<calc> = value` mis-parsed as `state = 'CA' OR (state = 'NY' = value)`,
changing top-N group membership and join predicates.

Wrap the expression branch of both column converters
(ExploreMixin.convert_tbl_column_to_sqla_col and TableColumn.get_sqla_col)
in Grouping(...); every downstream clause is built from the converter
output, so one wrap at the source parenthesizes SELECT, GROUP BY, ORDER BY,
both _get_top_groups, the JOIN ON, and COUNT(DISTINCT ...) at once. Physical
columns are left untouched. Narrow the existing WHERE/HAVING filter wrap to
adhoc expressions only (registered calculated columns are now parenthesized
by the converter) and add an _is_parenthesized guard so converter-wrapped
columns are not double-wrapped. Adhoc SQL-expression columns referenced by
label are parenthesized too, matching inline adhoc columns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rebenitez1802
rebenitez1802 force-pushed the rebenitez1802/parenthesize-calculated-or-filters branch from acc2573 to f61a60c Compare September 4, 2026 15:45
@bito-code-review

bito-code-review Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #79727a

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset/models/helpers.py - 1
    • Filter wrap regression · Line 4919-4924
      The new condition only wraps when `is_adhoc_sqla_col` is set, dropping the old `(col_obj and col_obj.expression)` case. A calculated column with a `filter_grain` reaches the loop via `get_timestamp_expression`, which returns a bare `Label(literal_column(expr))` (not a `Grouping`), so it is no longer parenthesized. If the engine's grain expression adds no parens (e.g. `::` cast), the OR-precedence bug this PR fixes can reappear. Consider `(is_adhoc_sqla_col or (col_obj and col_obj.expression))`.
  • tests/unit_tests/models/helpers_test.py - 1
    • Disallowed dynamic Any type annotation · Line 3070-3070
      `_calc_table` returns `Any`; use a concrete type like `SqlaTable` (imported under `TYPE_CHECKING`). The same `ANN401` issue exists at line 3090 for the `sqla_query` parameter in `_compile`.
Review Details
  • Files reviewed - 7 · Commit Range: f61a60c..dc2ca4f
    • superset/connectors/sqla/models.py
    • superset/models/helpers.py
    • tests/integration_tests/datasource_tests.py
    • tests/integration_tests/db_engine_specs/base_engine_spec_tests.py
    • tests/unit_tests/connectors/sqla/models_test.py
    • tests/unit_tests/jinja_context_test.py
    • tests/unit_tests/models/helpers_test.py
  • Files skipped - 1
    • UPDATING.md - Reason: Filter setting
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

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