Files
superset2/superset/versioning/changes/table.py
Mike Bridge a899e1db41 feat(versioning): entity-version base infrastructure (gated off, dark launch) (#41176)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:57:05 -07:00

91 lines
4.1 KiB
Python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Schema definition for ``version_changes``.
Declared against the shared ``Model.metadata`` so integration tests
that build schema via ``metadata.create_all()`` pick it up without the
Alembic migration running. Mirrors the shape of the
``56cd24c07170_add_versioning_tables`` migration byte-for-byte. Typed
columns (``sa.JSON`` for path / values) are required so the
connection's bulk-insert path marshals Python lists/dicts into JSON —
a lightweight ``sa.table(...)`` would not carry the type info and
SQLite's driver would reject the ``list`` as an unsupported bind.
The schema lives in its own module to keep the listener and the
shadow-table-query helpers free of schema-construction boilerplate at
import time.
"""
from __future__ import annotations
import sqlalchemy as sa
from flask_appbuilder import Model
_metadata = Model.metadata # pylint: disable=no-member
version_changes_table = sa.Table(
"version_changes",
_metadata,
sa.Column("id", sa.BigInteger, primary_key=True, autoincrement=True),
# ``transaction_id`` references ``version_transaction.id`` at the DB
# level only — the FK + ON DELETE CASCADE live in the Alembic
# migration. Declaring the FK here would fail to resolve at Table
# creation time because ``version_transaction`` is built
# dynamically by SQLAlchemy-Continuum at mapper-configuration time;
# integration tests that materialise schema via ``metadata.create_all``
# before Continuum runs would hit ``NoReferencedTableError``. Same
# pattern as the other versioning tables.
sa.Column("transaction_id", sa.BigInteger, nullable=False),
sa.Column("entity_kind", sa.String(32), nullable=False),
sa.Column("entity_id", sa.Integer, nullable=False),
# Integer, not SmallInteger: matches the migration — per-entity
# sequence within a transaction is assigned by unbounded enumerate().
sa.Column("sequence", sa.Integer, nullable=False),
sa.Column("kind", sa.String(32), nullable=False),
sa.Column("operation", sa.String(16), nullable=False),
sa.Column("path", sa.JSON, nullable=False),
sa.Column("from_value", sa.JSON, nullable=True),
sa.Column("to_value", sa.JSON, nullable=True),
sa.UniqueConstraint(
"transaction_id",
"entity_kind",
"entity_id",
"sequence",
name="uq_version_changes_tx_entity_sequence",
),
sa.Index("ix_version_changes_kind", "kind"),
# No standalone transaction_id index: the UNIQUE constraint above
# leads with transaction_id, so its backing index already serves
# transaction_id-prefix lookups on every dialect.
# Extends to transaction_id so the activity read's
# (entity_kind = ? AND entity_id IN (...) AND transaction_id >= ?)
# filter is index-served. Mirrors the migration byte-for-byte.
sa.Index("ix_version_changes_entity", "entity_kind", "entity_id", "transaction_id"),
extend_existing=True,
)
# Mapping from Python class name to the ``entity_kind`` value written
# to ``version_changes.entity_kind``. The API filters change records
# by this value (``WHERE entity_kind = 'chart'`` for the chart history
# endpoint, etc.) — kept short and user-facing-ish so downstream tools
# consuming the raw table read sensibly.
ENTITY_KIND_BY_CLASS_NAME: dict[str, str] = {
"Slice": "chart",
"Dashboard": "dashboard",
"SqlaTable": "dataset",
}