Skip to content

fix(partition-mapping): refuse a Jinja-templated partition column - #45546

Open
hughhhh wants to merge 5 commits into
masterfrom
hughhhh/pfm-jinja-partition-key
Open

hughhhh wants to merge 5 commits into
masterfrom
hughhhh/pfm-jinja-partition-key

Conversation

@hughhhh

@hughhhh hughhhh commented Oct 20, 2026

Copy link
Copy Markdown
Member

SUMMARY

Stacked on #43870. Addresses the review note there that calculated partition expressions bypass template processing.

The bug. build_mirrored_predicates builds the partition column with convert_tbl_column_to_sqla_col(partition_column) and no template processor. When the partition column is a calculated column whose expression holds Jinja, that expression goes into the predicate unrendered: the query-time validator can't parse {{ ... }}, so it skips the check and returns the text as-is. Reproduced with dt_calc = {{ 'dt_' ~ 'epoch' }}:

  • Preview returns valid: true with emitted_predicate set to ({{ 'dt_' ~ 'epoch' }}) = 1768435200 OR ({{ 'dt_' ~ 'epoch' }}) IS NULL.
  • The chart SQL renders the same column as (dt_epoch) in SELECT and GROUP BY, but leaves it raw in the mirrored WHERE, so the query fails at the engine.

Before this change, only the value transform was checked for Jinja.

The fix: reject it rather than render it. Rendering with the query's template processor would not help:

  • Preview has no chart template context (url_params, filter_values, current user), so it couldn't show what a chart actually emits.
  • A key computed per request isn't a physical partition, so the engine can't prune on it anyway.

Changes:

  • validate_partition_mapping takes a new partition_expression argument and reports a blocking issue on partition_column when it contains Jinja.
  • The REST update command (reads the payload, falling back to the stored column), the legacy /datasource/save/ path, and preview all pass the expression in. _effective_transform is generalized to _effective_column_attribute for this.
  • resolve_partition_mapping declines such a column at query time, to cover imports and rows saved before this check. The Explore summary reports the mapping as inactive.

A calculated partition column without Jinja still mirrors as before.

TESTING INSTRUCTIONS

Run the partition-mapping, dataset command, datasource view, model and MCP dataset unit suites. Locally: 2,523 passed, ruff is clean, and mypy reproducing the CI hook (--check-untyped-defs) is clean on the touched files.

New tests cover each layer:

  • validator: expression, statement and comment Jinja forms
  • resolve_partition_mapping: templated vs. untemplated partition column
  • REST update: stored and payload expressions
  • legacy save: returns 422
  • preview: returns invalid without calling the probe
  • Explore summary: reports inactive

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags: PARTITION_FILTER_MAPPING
  • Changes UI
  • Includes DB Migration
  • Introduces new feature or API
  • Removes existing feature or API

🤖 Generated with Claude Code

@netlify

netlify Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

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

Comment thread superset/commands/dataset/update.py Outdated
if column.get("column_name") == mapped_column:
return column.get("partition_value_transform")
if column.get("column_name") == column_name:
return column.get(attribute)

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: A partial column entry without expression returns None here, while update_columns preserves the stored expression; a Jinja-templated partition column can therefore escape validation.

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes · 🏷️ Api mismatch

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/commands/dataset/update.py
**Line:** 635:635
**Comment:**
	*Api Mismatch: A partial column entry without `expression` returns `None` here, while `update_columns` preserves the stored expression; a Jinja-templated partition column can therefore escape validation.

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed—a PUT whose column entry carries an id but omits expression leaves the stored Jinja expression untouched in _upsert_columns, yet _effective_column_attribute reads None from the payload entry, so the new blocking check is skipped and the save is accepted. The mapping then silently stays inactive at query time instead of the owner seeing the error. Should the payload branch fall back to the persisted column's value when the key is absent?

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.

Yes. The validator should inspect the value the update will leave in storage: if the payload omits expression, fall back to the existing column’s expression; if it explicitly supplies None, treat that as a clear.

For example, a partial entry that omits expression must still validate the stored {{ ... }} expression and block the save. Since column updates can also identify a column by id, the fallback should locate the stored column by ID when the payload does not provide a matching column_name. This closes the validation gap while preserving intentional expression clears.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of d2617fa.

When a matching payload column has an id but omits the requested attribute, the helper now looks up that stored column and returns its persisted attribute value. Explicitly supplied attributes, including None, still come from the payload.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 1360f15.

When a column entry omits the attribute, the helper now looks up the stored value using the same key as the write path: column_name for overrides and id for existing upserts. Entries treated as new still return None.

If that's not right, unresolve this thread and CodeAnt will leave it open.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Claude Opus 5.5 responding on behalf of hughhhh

Fixed: a partial column entry can no longer skip the Jinja check. _effective_column_attribute (superset/commands/dataset/update.py) now reads an omitted attribute from the row the write will actually touch, and an explicit None still counts as a clear.

  • id with no expression (4d0e16b): this falls back to the column stored under that id, as you suggested. test_a_partial_entry_keeps_the_stored_templated_expression covers the block, and test_an_explicit_null_expression_clears_the_stored_template covers the clear.
  • override_columns=true (1360f15): the fallback above still missed this path, because _override_columns matches by column_name and ignores id. An entry with neither id nor expression kept the stored Jinja and passed validation, and a renamed entry with a stale id was refused over an expression its new row wouldn't get. Both now follow the name. test_an_override_entry_keeps_the_stored_template_of_its_name and test_an_override_entry_ignores_a_stale_id failed before the fix and pass now.

The value transform goes through the same helper. The PUT schema requires column_name, so the id-only entry CodeAnt mentions can't reach this code. I've left this thread open for you to check.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 7685947.

When a column entry omits the attribute, the helper now looks up the stored value using the same key as the write path: column_name for overrides and id for existing upserts. Entries treated as new still return None.

If that's not right, unresolve this thread and CodeAnt will leave it open.

@codeant-ai-for-open-source

codeant-ai-for-open-source Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

CodeAnt  CodeAnt PR Risk

🟢  Low Risk

  • The PR appears safe to merge; validation now checks the stored partition expression when partial updates omit it and honors explicit clears.

Assessed commit: 7685947d5fe9

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.29630% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 84.93%. Comparing base (3d9de97) to head (7685947).
⚠️ Report is 8 commits behind head on master.

Files with missing lines Patch % Lines
superset/commands/dataset/update.py 93.75% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master   #45546   +/-   ##
=======================================
  Coverage   84.93%   84.93%           
=======================================
  Files        3077     3077           
  Lines      208205   208247   +42     
  Branches    49105    49113    +8     
=======================================
+ Hits       176834   176881   +47     
+ Misses      28002    27998    -4     
+ Partials     3369     3368    -1     
Flag Coverage Δ
hive 34.18% <7.40%> (-0.01%) ⬇️
mysql 52.15% <7.40%> (-0.02%) ⬇️
postgres 52.17% <7.40%> (-0.02%) ⬇️
presto 35.88% <7.40%> (-0.01%) ⬇️
python 87.89% <96.29%> (+<0.01%) ⬆️
sqlite 51.91% <7.40%> (-0.02%) ⬇️
unit 82.73% <96.29%> (+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.

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Oct 21, 2026
Base automatically changed from hughhhh/pfm-6-explore-indicator to master October 27, 2026 23:09
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Possible DB migration conflict

@hughhhh the base branch master picked up a new database migration under superset/migrations/ after this PR was opened, and this PR adds or modifies migration files too. Alembic migrations form a single linear chain, so if both branch off the same parent revision, merging this PR as-is could leave the migration history with two heads instead of one, which makes superset db upgrade fail with Multiple head revisions are present.

To check and, if needed, fix this:

  1. Rebase this branch onto the latest master.
  2. Run superset db heads. If it lists more than one revision, you have a conflict.
  3. In your new migration's script, update down_revision (and the Revises comment) to point at the other head instead, so the chain has a single head again.

See Merging DB Migrations in the contributing guide for a worked example.

2 similar comments
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Possible DB migration conflict

@hughhhh the base branch master picked up a new database migration under superset/migrations/ after this PR was opened, and this PR adds or modifies migration files too. Alembic migrations form a single linear chain, so if both branch off the same parent revision, merging this PR as-is could leave the migration history with two heads instead of one, which makes superset db upgrade fail with Multiple head revisions are present.

To check and, if needed, fix this:

  1. Rebase this branch onto the latest master.
  2. Run superset db heads. If it lists more than one revision, you have a conflict.
  3. In your new migration's script, update down_revision (and the Revises comment) to point at the other head instead, so the chain has a single head again.

See Merging DB Migrations in the contributing guide for a worked example.

@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

⚠️ Possible DB migration conflict

@hughhhh the base branch master picked up a new database migration under superset/migrations/ after this PR was opened, and this PR adds or modifies migration files too. Alembic migrations form a single linear chain, so if both branch off the same parent revision, merging this PR as-is could leave the migration history with two heads instead of one, which makes superset db upgrade fail with Multiple head revisions are present.

To check and, if needed, fix this:

  1. Rebase this branch onto the latest master.
  2. Run superset db heads. If it lists more than one revision, you have a conflict.
  3. In your new migration's script, update down_revision (and the Revises comment) to point at the other head instead, so the chain has a single head again.

See Merging DB Migrations in the contributing guide for a worked example.

@hughhhh
hughhhh force-pushed the hughhhh/pfm-jinja-partition-key branch from cf34d40 to d2617fa Compare November 3, 2026 16:48
@github-actions github-actions Bot added i18n Namespace | Anything related to localization i18n:spanish Translation related to Spanish language i18n:italian Translation related to Italian language i18n:french Translation related to French language i18n:chinese Translation related to Chinese language i18n:japanese Translation related to Japanese language i18n:russian Translation related to Russian language i18n:korean Translation related to Korean language labels Nov 3, 2026
hughhhh and others added 4 commits November 3, 2026 12:12
The mirrored predicate is built with `convert_tbl_column_to_sqla_col` outside
the chart query's template processor, so a calculated partition column whose
expression carried Jinja reached the SQL as raw `{{ ... }}`. Preview reported
the mapping valid with the raw template in `emitted_predicate`, and every chart
filtered on the mapped column then failed at the engine.

Refuse it rather than render it: preview has no chart template context to
render with, and a key computed per request is not a partition the engine can
prune on anyway.

- `validate_partition_mapping` takes the partition column's expression and
  reports a blocking issue on `partition_column` when it holds Jinja; the REST
  update command, the legacy datasource save and preview all pass it.
- `resolve_partition_mapping` declines such a column at query time (imports,
  older rows), and the Explore summary reports it inactive.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mn entry keeps

A column entry with an `id` but no `expression` leaves the stored expression
in place, because `DatasetDAO._upsert_columns` only sets the keys it is given.
`_effective_column_attribute` read `None` from such an entry, so a stored
Jinja partition key passed validation and the mapping silently stayed inactive.
Fall back to the stored column by id when the key is absent. An explicit
`None` is still a clear. The value transform gets the same fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e catalogs

The new message goes into messages.pot and into every .po catalog as an
untranslated entry. Nothing else in the catalogs changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ite hits

`_effective_column_attribute` assumed `_upsert_columns`' matching, by `id`.
With `override_columns`, `_override_columns` matches by name and ignores the
`id`, so an entry with neither `id` nor `expression` kept the stored Jinja
while validation saw nothing, and a renamed entry with a stale `id` was refused
over an expression its new row would not get.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hughhhh
hughhhh force-pushed the hughhhh/pfm-jinja-partition-key branch from d2617fa to 1360f15 Compare November 3, 2026 17:15
command._properties = {
"columns": [
{"id": 1, "column_name": "event_time"},
{"id": 2, "column_name": "dt_epoch", "description": "partition key"},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only exercises the id-keyed fallback with the name unchanged, so a lookup keyed on column_name instead of id would pass it too. A rename in the same PUT is the case that separates them: stored column id=2 named dt_epoch holds the Jinja, the payload sends {"id": 2, "column_name": "dt_renamed"} with partition_column="dt_renamed" and no expression, and a name-based lookup finds no stored row and returns None, so the templated column is saved. Could you add a case like that asserting a partition_column error mentioning Jinja?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

🤖 Claude Opus 5.5 responding on behalf of hughhhh

Added in 7685947. test_a_renamed_partial_entry_keeps_the_stored_template_of_its_id is your case: stored id=2 named dt_epoch holds the Jinja, the payload sends {"id": 2, "column_name": "dt_renamed"} with partition_column="dt_renamed", and the test asserts a partition_column error mentioning Jinja.

It passes against the current lookup, so _effective_column_attribute didn't need changing. To check that it separates the two, I switched the lookup to column_name, and the test failed while the existing partial-entry test still passed. I've left this thread open for you to check.

@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Nov 3, 2026
…e PUT

A name-based lookup finds no stored row for a renamed partial entry, so the
stored Jinja would be saved under the new name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

i18n:brazilian i18n:chinese Translation related to Chinese language i18n:czech i18n:dutch i18n:french Translation related to French language i18n:italian Translation related to Italian language i18n:japanese Translation related to Japanese language i18n:korean Translation related to Korean language i18n:latvian i18n:persian i18n:portuguese i18n:russian Translation related to Russian language i18n:slovak i18n:spanish Translation related to Spanish language i18n:traditional-chinese i18n:ukrainian i18n Namespace | Anything related to localization size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants