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

   ### SUMMARY
   
   Consolidates the four-PR partition-filter-mapping stack (#43757, #43758, 
#43759, #43760) into one PR, and **replaces its four new database columns with 
a storage interface, so the feature ships with no Alembic migration.** The 
migration was the only thing blocking deployment, and it gated the other three 
PRs behind it.
   
   Datasets on Hadoop-family engines are commonly partitioned on a *technical* 
column — an epoch integer, a lowercased region key — that no analyst would 
filter on. Unless a query carries a predicate on that column the engine scans 
every partition, and the only workaround today is hand-writing the predicate as 
custom SQL in a virtual dataset, which pushes a performance concern onto every 
chart author and takes the dataset out of the physical/syncable path.
   
   A dataset owner names one partition column, one business column whose 
filters are mirrored onto it, a value transform (a SQL expression containing 
`:value`), and whether that transform preserves ordering. Superset then appends 
an equivalent predicate on the partition column to every query: chart authors 
change nothing and queries prune. Behind `PARTITION_FILTER_MAPPING`, off by 
default.
   
   #### The storage interface
   
   This follows the pattern already two screens away in 
`superset/datasets/api.py`, which exposes four properties over `tables.extra` 
(`is_certified`, `certified_by`, `certification_details`, `warning_markdown`) 
through `show_columns`, with a comment saying why. Here the same blob carries 
`partition_filter_mapping`.
   
   Behind `PartitionMappingStore` — `load`/`save`, one adapter today 
(`ExtraJsonPartitionMappingStore`) and one that activates with the migration, 
selected by `PARTITION_MAPPING_STORE`. The model, the DAO, validation, the 
query rewriter and the preview endpoint are written against the port and never 
learn which store answered. A store-swap test runs the behavioural surface 
against a second, dict-backed implementation, so "we can move this" is a claim 
with evidence rather than an intention.
   
   `tables.extra` turned out to be a reasonable home rather than only an 
expedient one:
   
   - it is already in `SqlaTable.export_fields` and already one of 
`ExportDatasetsCommand`'s `JSON_KEYS`, so a mapping travels through 
**import/export with no bundle-format change**;
   - Continuum already versions the column, so **dataset version history and 
restore work untouched** — and no shadow-table migration is needed, which is 
the part the repo documents as unreliable to autogenerate;
   - the dataset editor round-trips dataset-level `extra` verbatim, and its one 
mutating path (`setDatasetCertification`) merges rather than rebuilds, so an 
unknown key survives a save.
   
   It has to be the *dataset*-level blob, though, never `table_columns.extra` — 
`buildExtraJsonObject` rebuilds every column's and metric's `extra` from two 
keys on every editor save.
   
   #### Two details worth a look
   
   **Transforms are keyed by column name, not flattened onto the mapping.** 
Flattening is unsound: an owner who declares a transform on `A` (then 
`main_dttm_col`) and later re-points `main_dttm_col` to `B` would have `B`'s 
values mirrored through `A`'s expression. Keyed, `B` has no transform and the 
mapping goes inactive — which is exactly what four real columns would do. That 
parity is what makes the later migration a mechanical per-column copy.
   
   **The model's four attributes are read-only properties, and `DatasetDAO` 
extracts the mapping from a payload before anything is applied and writes it 
after everything is.** The editor's PUT carries both the typed 
`partition_column` field *and* a verbatim copy of `extra`, and under this store 
those are the same bytes — so a `setattr` loop that happens to apply `extra` 
last silently discards the typed field, and which order that is depends on 
marshmallow's field ordering, not on anything visible at the call site. 
Read-only properties make that loss impossible rather than merely unlikely; 
`set_partition_mapping` is the one supported write. There is a test for each 
assignment raising, so nobody reintroduces setters later.
   
   #### Two bugs in the original stack, fixed here
   
   - `_transform_is_usable` omitted the non-deterministic denylist, so a 
transform calling `now()` that reached storage without passing 
`UpdateDatasetCommand` — via create, via import, or via the free-text Extra 
box, which every dataset owner can reach — *was* mirrored at query time and its 
result cached for a day. Replaced by `is_transform_active`, which is 
`validate_transform` with the messages discarded and therefore cannot drift 
from the save path. Net −13 lines.
   - `columns.partition_value_transform` was missing from the dataset GET 
payload while `DatasourceModal` PUT it back for every column, so **every 
dataset-editor save cleared the transform.**
   
   Also new here: `partition_column` is a physical column name, so it joins 
`DASHBOARD_DATASET_INACCESSIBLE_FIELDS` alongside `columns` and `sql`; 
`DatasetPutSchema.extra` gains a structural validator for the mapping key, 
because the Extra box is a second door into the same storage and a mistyped 
value there would otherwise be silently ignored; and 
`SqlaTable.update_from_object` preserves the mapping when a legacy payload 
omits `extra`.
   
   #### The follow-up migration
   
   Not just `ADD COLUMN`. In one revision it must: add the four columns to 
`tables`/`table_columns` **and** to the `*_version` shadow tables; backfill in 
a Python batch loop over rows whose `extra` holds the key (not raw JSON SQL — 
migrations run on sqlite/MySQL/Postgres), leaving a blob it cannot parse in 
place rather than dropping an owner's configuration; strip the key; **delete 
the facade properties and declare the real columns in the same commit** (a 
property and a `Column` of the same name cannot coexist); fill in 
`ColumnPartitionMappingStore`; add the four names to `export_fields`; flip 
`PARTITION_MAPPING_STORE`. `ColumnPartitionMappingStore`'s docstring carries 
that list, and its `NotImplementedError` names the migration.
   
   It is deliberately not implemented now: it would read 
`table.partition_column`, which today is the facade property, which calls the 
store — unbounded recursion. Written against private attribute names instead it 
could only ever be exercised against a hand-built stand-in, which proves 
nothing about the parts that are actually hard.
   
   ### BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF
   
   N/A — no UI. As in the original stack, there are no editor form fields for 
this; a mapping is configured through the REST API or the Extra box, and the 
preview endpoint ships available but uncalled. The docs say so explicitly.
   
   ### TESTING INSTRUCTIONS
   
   ```bash
   pytest tests/unit_tests/connectors/sqla/partition_mapping_test.py \
          tests/unit_tests/connectors/sqla/partition_mapping_storage_test.py \
          tests/unit_tests/models/partition_mirroring_test.py \
          tests/unit_tests/datasets/partition_mapping_serialization_test.py \
          tests/unit_tests/datasets/partition_mapping_preview_test.py \
          tests/unit_tests/dao/dataset_test.py \
          tests/unit_tests/commands/dataset/update_test.py \
          tests/unit_tests/sql/parse_tests.py
   ```
   
   `partition_mapping_storage_test.py` is the file to read first: 
round-tripping, defensive parsing of a blob an owner typed by hand, the memo, 
read-only enforcement, the factory, and the store-swap parity test.
   
   Two tests in `tests/unit_tests/dao/dataset_test.py` are the ones this design 
exists for:
   `test_a_typed_field_beats_a_stale_extra_in_the_same_payload` and
   `test_a_mapping_written_through_extra_alone_lands`. Both fail if the funnel 
is ever "simplified" back into property setters.
   
   Manually, with `PARTITION_FILTER_MAPPING: True`:
   
   1. `PUT /api/v1/dataset/<id>` with `partition_column`, and 
`partition_value_transform` + `partition_transform_is_monotonic` on a column.
   2. `SELECT extra FROM tables WHERE id = <id>` — the blob holds 
`partition_filter_mapping` and every pre-existing key is intact.
   3. Open the dataset editor, change a column description, save. Re-check step 
2: the mapping is still there. *This is the ordering hazard; if the funnel were 
wrong, this is where the mapping would disappear.*
   4. Add a certification and save. Both the certification and the mapping are 
present.
   5. Build a chart filtered on the mapped column; **View query** shows the 
mirrored predicate on the partition column.
   6. Export the dataset to a zip, delete it, re-import — the mapping comes 
back.
   7. Type `{"partition_filter_mapping": 42}` into the Extra box and save — 
rejected with a readable message rather than silently ignored.
   8. Set `PARTITION_MAPPING_STORE = "columns"` and restart — a clear 
`NotImplementedError` naming the migration, not a mystery 500.
   
   Verified: 2949 backend unit tests green across the touched surface, plus a 
full `tests/unit_tests` sweep at 8479 passed / 4 failed, those 4 being a 
pre-existing `mcp_service` trio-parameterised failure confirmed identical on 
pristine `master`. Frontend `partitionMapping.test.ts` green. `ruff check` and 
`ruff format --check` clean. `docs/static/resources/openapi.json` regenerated 
with `superset update-api-docs` (+165 lines, purely additive).
   
   `git diff origin/master -- superset/migrations/` is empty, which is the 
point.
   
   ### ADDITIONAL INFORMATION
   - [ ] Has associated issue:
   - [x] Required feature flags: `PARTITION_FILTER_MAPPING` (off by default)
   - [ ] 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
   - [x] Introduces new feature or API
   - [ ] Removes existing feature or API
   
   #### Notes for review
   
   - **Message catalogs are not regenerated here.** Nothing in 
`.pre-commit-config.yaml` or CI enforces extraction, and 1,900 lines of 
generated churn would make this unreviewable. Happy to add it as a follow-up or 
to this PR, whichever you prefer.
   - `superset/connectors/sqla/partition_mapping.py` was left as one module 
with the new storage layer as a sibling, `partition_mapping_storage.py`, rather 
than split into a package. The split is defensible on size alone, but it would 
have turned a reviewable diff against the original stack into a 
moved-everything diff, and it carries a specific trap: 
`partition_mirroring_test.py` patches 
`...partition_mapping.evaluate_transform`, which a package split silently turns 
into a no-op that fails as a confusing assertion error.
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


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