mikebridge commented on code in PR #41076: URL: https://github.com/apache/superset/pull/41076#discussion_r3574996320
########## superset/migrations/versions/2026-05-28_19-50_56cd24c07170_add_versioning_tables.py: ########## @@ -0,0 +1,562 @@ +# 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. +"""add_versioning_tables + +Creates the full schema backing entity versioning in a single +migration: + +1. ``version_transaction`` — audit log keyed by Continuum's per-flush + transaction id (plus a Postgres-specific id sequence). +2. **Parent shadow tables** mirroring each versioned entity's columns: + ``dashboards_version`` / ``slices_version`` / ``tables_version``. +3. ``version_changes`` — field-level diff log keyed to a + ``(transaction, entity)`` pair; each row describes one atomic change + (one field or one child-collection element) that occurred during a + save. +4. **Child shadow tables** for the collections Continuum auto-registers + when ``__versioned__`` is applied to ``TableColumn`` / ``SqlMetric`` + and the ``slices`` exclude is removed from + ``Dashboard.__versioned__``: ``table_columns_version`` / + ``sql_metrics_version`` / ``dashboard_slices_version``. + +All shadow tables follow the validity-strategy shape (mirrored columns ++ ``transaction_id`` / ``end_transaction_id`` / ``operation_type`` +bookkeeping with FKs to ``version_transaction.id``). The current +version row has ``end_transaction_id = NULL``. + +This migration replaces three iterative migrations from the spike phase +(``56cd24c07170``, ``e1f3c5a7b9d0``, ``f7a2b3c4d5e6``) that captured the +same schema in three steps as the feature was developed. Compacting +gives downstream operators one migration to apply / reverse and one +review surface. The ``revision`` hash is reused from the original first +migration so anyone still tracking the chain by that hash lands on the +same logical change set. + +Generated by hand because the current Continuum + Alembic-autogenerate +interaction trips on the renamed ``transaction`` -> ``version_transaction`` +table key (``KeyError`` lookups in ``table_key_to_table``). Column +inventories were sourced from the live model ``__table__`` definitions +and ``version_class(...).__table__`` / Continuum association metadata. + +Primary key choice. Both ``version_transaction.id`` and +``version_changes.id`` are ``BigInteger`` autoincrement — a deliberate +carveout from the project's UUID-PK convention for new models (see +``CLAUDE.md`` §"UUID Migration"). ``version_transaction`` is keyed +externally by SQLAlchemy-Continuum via +``nextval('version_transaction_id_seq')`` on every INSERT; matching +that contract is required for ``versioning_manager`` to function. +``version_changes`` follows the same shape because the user-facing +identity is the ``(transaction_id, entity_kind, entity_id, sequence)`` +composite unique key, not the row id; the API surfaces a deterministic +UUIDv5 ``version_uuid`` derived from ``entity.uuid`` and +``transaction_id`` for stable external references. + +Revision ID: 56cd24c07170 +Revises: 2bee73611e32 +Create Date: 2026-05-28 19:50:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op +from sqlalchemy_utils import UUIDType + +from superset.utils.core import MediumText + +revision = "56cd24c07170" +# Stacked on the composite-PK association-tables change (2bee73611e32) so the +# Continuum shadow tables this migration creates can mirror the +# composite-PK shape of the live association tables. If that change +# is removed from the stack, this should be reverted to "ce6bd21901ab". +down_revision = "2bee73611e32" + + +def upgrade() -> None: + bind = op.get_bind() + + # ------------------------------------------------------------------ + # version_transaction + # + # Audit log for each versioning event. Continuum emits + # ``nextval('version_transaction_id_seq')`` on every INSERT, so the + # sequence must exist before the table on Postgres. SQLite/MySQL + # ignore the explicit CREATE SEQUENCE (they auto-increment natively). + # ------------------------------------------------------------------ + if bind.dialect.name == "postgresql": + op.execute("CREATE SEQUENCE IF NOT EXISTS version_transaction_id_seq") + + op.create_table( Review Comment: Addressed by subsequent commits; the flagged code is no longer current after the rebase onto master. Resolving as outdated. ########## superset/migrations/versions/2026-05-28_19-50_56cd24c07170_add_versioning_tables.py: ########## @@ -0,0 +1,562 @@ +# 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. +"""add_versioning_tables + +Creates the full schema backing entity versioning in a single +migration: + +1. ``version_transaction`` — audit log keyed by Continuum's per-flush + transaction id (plus a Postgres-specific id sequence). +2. **Parent shadow tables** mirroring each versioned entity's columns: + ``dashboards_version`` / ``slices_version`` / ``tables_version``. +3. ``version_changes`` — field-level diff log keyed to a + ``(transaction, entity)`` pair; each row describes one atomic change + (one field or one child-collection element) that occurred during a + save. +4. **Child shadow tables** for the collections Continuum auto-registers + when ``__versioned__`` is applied to ``TableColumn`` / ``SqlMetric`` + and the ``slices`` exclude is removed from + ``Dashboard.__versioned__``: ``table_columns_version`` / + ``sql_metrics_version`` / ``dashboard_slices_version``. + +All shadow tables follow the validity-strategy shape (mirrored columns ++ ``transaction_id`` / ``end_transaction_id`` / ``operation_type`` +bookkeeping with FKs to ``version_transaction.id``). The current +version row has ``end_transaction_id = NULL``. + +This migration replaces three iterative migrations from the spike phase +(``56cd24c07170``, ``e1f3c5a7b9d0``, ``f7a2b3c4d5e6``) that captured the +same schema in three steps as the feature was developed. Compacting +gives downstream operators one migration to apply / reverse and one +review surface. The ``revision`` hash is reused from the original first +migration so anyone still tracking the chain by that hash lands on the +same logical change set. + +Generated by hand because the current Continuum + Alembic-autogenerate +interaction trips on the renamed ``transaction`` -> ``version_transaction`` +table key (``KeyError`` lookups in ``table_key_to_table``). Column +inventories were sourced from the live model ``__table__`` definitions +and ``version_class(...).__table__`` / Continuum association metadata. + +Primary key choice. Both ``version_transaction.id`` and +``version_changes.id`` are ``BigInteger`` autoincrement — a deliberate +carveout from the project's UUID-PK convention for new models (see +``CLAUDE.md`` §"UUID Migration"). ``version_transaction`` is keyed +externally by SQLAlchemy-Continuum via +``nextval('version_transaction_id_seq')`` on every INSERT; matching +that contract is required for ``versioning_manager`` to function. +``version_changes`` follows the same shape because the user-facing +identity is the ``(transaction_id, entity_kind, entity_id, sequence)`` +composite unique key, not the row id; the API surfaces a deterministic +UUIDv5 ``version_uuid`` derived from ``entity.uuid`` and +``transaction_id`` for stable external references. + +Revision ID: 56cd24c07170 +Revises: 2bee73611e32 +Create Date: 2026-05-28 19:50:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op +from sqlalchemy_utils import UUIDType + +from superset.utils.core import MediumText + +revision = "56cd24c07170" +# Stacked on the composite-PK association-tables change (2bee73611e32) so the +# Continuum shadow tables this migration creates can mirror the +# composite-PK shape of the live association tables. If that change +# is removed from the stack, this should be reverted to "ce6bd21901ab". +down_revision = "2bee73611e32" + + +def upgrade() -> None: + bind = op.get_bind() + + # ------------------------------------------------------------------ + # version_transaction + # + # Audit log for each versioning event. Continuum emits + # ``nextval('version_transaction_id_seq')`` on every INSERT, so the + # sequence must exist before the table on Postgres. SQLite/MySQL + # ignore the explicit CREATE SEQUENCE (they auto-increment natively). + # ------------------------------------------------------------------ + if bind.dialect.name == "postgresql": + op.execute("CREATE SEQUENCE IF NOT EXISTS version_transaction_id_seq") + + op.create_table( + "version_transaction", + sa.Column( + "id", + sa.BigInteger(), + sa.Sequence("version_transaction_id_seq"), + primary_key=True, + autoincrement=True, + nullable=False, + ), + sa.Column("issued_at", sa.DateTime(), nullable=True), + sa.Column("remote_addr", sa.String(50), nullable=True), + sa.Column("user_id", sa.Integer(), nullable=True), + # ``action_kind`` carries the high-level avenue that produced + # this transaction (``restore`` / ``import`` / ``clone``). + # ``NULL`` is the default "ordinary save" — most rows leave + # this empty. Commands set + # ``session.info["_versioning_action_kind"]`` before commit; + # the change-record listener stamps the value here. Parallel + # to ``version_changes.entity_kind`` and ``version_changes.kind`` + # — the schema's third ``*_kind`` column, at transaction scope. + sa.Column("action_kind", sa.String(32), nullable=True), + ) + + if bind.dialect.name == "postgresql": + op.execute( + "ALTER SEQUENCE version_transaction_id_seq OWNED BY version_transaction.id" + ) + + # ------------------------------------------------------------------ + # dashboards_version + # ------------------------------------------------------------------ + op.create_table( + "dashboards_version", + sa.Column("uuid", UUIDType(binary=True), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dashboard_title", sa.String(500), nullable=True), + # ``MediumText()`` mirrors the live column type — on MySQL plain + # ``TEXT`` caps at 64 KB, which large dashboards exceed; an + # oversized live write would then fail the shadow INSERT under + # ``STRICT_TRANS_TABLES`` (or silently truncate without it) and + # corrupt the history. Postgres ``TEXT`` is unbounded and SQLite + # ignores the length annotation so this is MySQL-driven. + sa.Column("position_json", MediumText(), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("css", MediumText(), nullable=True), + sa.Column("theme_id", sa.Integer(), nullable=True), + sa.Column("certified_by", sa.Text(), nullable=True), + sa.Column("certification_details", sa.Text(), nullable=True), + sa.Column("json_metadata", MediumText(), nullable=True), + sa.Column("slug", sa.String(255), nullable=True), + sa.Column("published", sa.Boolean(), nullable=True), + sa.Column("is_managed_externally", sa.Boolean(), nullable=True), + sa.Column("external_url", sa.Text(), nullable=True), + sa.Column("transaction_id", sa.BigInteger(), nullable=False), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id"), + sa.ForeignKeyConstraint( + ["transaction_id"], + ["version_transaction.id"], + name="fk_dashboards_version_transaction_id", + ), + sa.ForeignKeyConstraint( + ["end_transaction_id"], + ["version_transaction.id"], + name="fk_dashboards_version_end_transaction_id", + ), + ) + op.create_index( Review Comment: Addressed by subsequent commits; the flagged code is no longer current after the rebase onto master. Resolving as outdated. ########## superset/migrations/versions/2026-05-28_19-50_56cd24c07170_add_versioning_tables.py: ########## @@ -0,0 +1,562 @@ +# 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. +"""add_versioning_tables + +Creates the full schema backing entity versioning in a single +migration: + +1. ``version_transaction`` — audit log keyed by Continuum's per-flush + transaction id (plus a Postgres-specific id sequence). +2. **Parent shadow tables** mirroring each versioned entity's columns: + ``dashboards_version`` / ``slices_version`` / ``tables_version``. +3. ``version_changes`` — field-level diff log keyed to a + ``(transaction, entity)`` pair; each row describes one atomic change + (one field or one child-collection element) that occurred during a + save. +4. **Child shadow tables** for the collections Continuum auto-registers + when ``__versioned__`` is applied to ``TableColumn`` / ``SqlMetric`` + and the ``slices`` exclude is removed from + ``Dashboard.__versioned__``: ``table_columns_version`` / + ``sql_metrics_version`` / ``dashboard_slices_version``. + +All shadow tables follow the validity-strategy shape (mirrored columns ++ ``transaction_id`` / ``end_transaction_id`` / ``operation_type`` +bookkeeping with FKs to ``version_transaction.id``). The current +version row has ``end_transaction_id = NULL``. + +This migration replaces three iterative migrations from the spike phase +(``56cd24c07170``, ``e1f3c5a7b9d0``, ``f7a2b3c4d5e6``) that captured the +same schema in three steps as the feature was developed. Compacting +gives downstream operators one migration to apply / reverse and one +review surface. The ``revision`` hash is reused from the original first +migration so anyone still tracking the chain by that hash lands on the +same logical change set. + +Generated by hand because the current Continuum + Alembic-autogenerate +interaction trips on the renamed ``transaction`` -> ``version_transaction`` +table key (``KeyError`` lookups in ``table_key_to_table``). Column +inventories were sourced from the live model ``__table__`` definitions +and ``version_class(...).__table__`` / Continuum association metadata. + +Primary key choice. Both ``version_transaction.id`` and +``version_changes.id`` are ``BigInteger`` autoincrement — a deliberate +carveout from the project's UUID-PK convention for new models (see +``CLAUDE.md`` §"UUID Migration"). ``version_transaction`` is keyed +externally by SQLAlchemy-Continuum via +``nextval('version_transaction_id_seq')`` on every INSERT; matching +that contract is required for ``versioning_manager`` to function. +``version_changes`` follows the same shape because the user-facing +identity is the ``(transaction_id, entity_kind, entity_id, sequence)`` +composite unique key, not the row id; the API surfaces a deterministic +UUIDv5 ``version_uuid`` derived from ``entity.uuid`` and +``transaction_id`` for stable external references. + +Revision ID: 56cd24c07170 +Revises: 2bee73611e32 +Create Date: 2026-05-28 19:50:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op +from sqlalchemy_utils import UUIDType + +from superset.utils.core import MediumText + +revision = "56cd24c07170" +# Stacked on the composite-PK association-tables change (2bee73611e32) so the +# Continuum shadow tables this migration creates can mirror the +# composite-PK shape of the live association tables. If that change +# is removed from the stack, this should be reverted to "ce6bd21901ab". +down_revision = "2bee73611e32" + + +def upgrade() -> None: + bind = op.get_bind() + + # ------------------------------------------------------------------ + # version_transaction + # + # Audit log for each versioning event. Continuum emits + # ``nextval('version_transaction_id_seq')`` on every INSERT, so the + # sequence must exist before the table on Postgres. SQLite/MySQL + # ignore the explicit CREATE SEQUENCE (they auto-increment natively). + # ------------------------------------------------------------------ + if bind.dialect.name == "postgresql": + op.execute("CREATE SEQUENCE IF NOT EXISTS version_transaction_id_seq") + + op.create_table( + "version_transaction", + sa.Column( + "id", + sa.BigInteger(), + sa.Sequence("version_transaction_id_seq"), + primary_key=True, + autoincrement=True, + nullable=False, + ), + sa.Column("issued_at", sa.DateTime(), nullable=True), + sa.Column("remote_addr", sa.String(50), nullable=True), + sa.Column("user_id", sa.Integer(), nullable=True), + # ``action_kind`` carries the high-level avenue that produced + # this transaction (``restore`` / ``import`` / ``clone``). + # ``NULL`` is the default "ordinary save" — most rows leave + # this empty. Commands set + # ``session.info["_versioning_action_kind"]`` before commit; + # the change-record listener stamps the value here. Parallel + # to ``version_changes.entity_kind`` and ``version_changes.kind`` + # — the schema's third ``*_kind`` column, at transaction scope. + sa.Column("action_kind", sa.String(32), nullable=True), + ) + + if bind.dialect.name == "postgresql": + op.execute( + "ALTER SEQUENCE version_transaction_id_seq OWNED BY version_transaction.id" + ) + + # ------------------------------------------------------------------ + # dashboards_version + # ------------------------------------------------------------------ + op.create_table( + "dashboards_version", + sa.Column("uuid", UUIDType(binary=True), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("dashboard_title", sa.String(500), nullable=True), + # ``MediumText()`` mirrors the live column type — on MySQL plain + # ``TEXT`` caps at 64 KB, which large dashboards exceed; an + # oversized live write would then fail the shadow INSERT under + # ``STRICT_TRANS_TABLES`` (or silently truncate without it) and + # corrupt the history. Postgres ``TEXT`` is unbounded and SQLite + # ignores the length annotation so this is MySQL-driven. + sa.Column("position_json", MediumText(), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("css", MediumText(), nullable=True), + sa.Column("theme_id", sa.Integer(), nullable=True), + sa.Column("certified_by", sa.Text(), nullable=True), + sa.Column("certification_details", sa.Text(), nullable=True), + sa.Column("json_metadata", MediumText(), nullable=True), + sa.Column("slug", sa.String(255), nullable=True), + sa.Column("published", sa.Boolean(), nullable=True), + sa.Column("is_managed_externally", sa.Boolean(), nullable=True), + sa.Column("external_url", sa.Text(), nullable=True), + sa.Column("transaction_id", sa.BigInteger(), nullable=False), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id"), + sa.ForeignKeyConstraint( + ["transaction_id"], + ["version_transaction.id"], + name="fk_dashboards_version_transaction_id", + ), + sa.ForeignKeyConstraint( + ["end_transaction_id"], + ["version_transaction.id"], + name="fk_dashboards_version_end_transaction_id", + ), + ) + op.create_index( + "ix_dashboards_version_end_transaction_id", + "dashboards_version", + ["end_transaction_id"], + ) + op.create_index( + "ix_dashboards_version_operation_type", + "dashboards_version", + ["operation_type"], + ) + op.create_index( + "ix_dashboards_version_transaction_id", + "dashboards_version", + ["transaction_id"], + ) + + # ------------------------------------------------------------------ + # slices_version (Charts) + # ------------------------------------------------------------------ + op.create_table( + "slices_version", + sa.Column("uuid", UUIDType(binary=True), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("slice_name", sa.String(250), nullable=True), + sa.Column("datasource_id", sa.Integer(), nullable=True), + sa.Column("datasource_type", sa.String(200), nullable=True), + sa.Column("datasource_name", sa.String(2000), nullable=True), + sa.Column("viz_type", sa.String(250), nullable=True), + sa.Column("params", MediumText(), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("cache_timeout", sa.Integer(), nullable=True), + sa.Column("certified_by", sa.Text(), nullable=True), + sa.Column("certification_details", sa.Text(), nullable=True), + sa.Column("is_managed_externally", sa.Boolean(), nullable=True), + sa.Column("external_url", sa.Text(), nullable=True), + sa.Column("transaction_id", sa.BigInteger(), nullable=False), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id"), + sa.ForeignKeyConstraint( + ["transaction_id"], + ["version_transaction.id"], + name="fk_slices_version_transaction_id", + ), + sa.ForeignKeyConstraint( + ["end_transaction_id"], + ["version_transaction.id"], + name="fk_slices_version_end_transaction_id", + ), + ) + op.create_index( + "ix_slices_version_end_transaction_id", + "slices_version", + ["end_transaction_id"], + ) + op.create_index( + "ix_slices_version_operation_type", + "slices_version", + ["operation_type"], + ) + op.create_index( + "ix_slices_version_transaction_id", + "slices_version", + ["transaction_id"], + ) + + # ------------------------------------------------------------------ + # tables_version (SqlaTable / Datasets) + # ------------------------------------------------------------------ + op.create_table( + "tables_version", + sa.Column("uuid", UUIDType(binary=True), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("default_endpoint", sa.Text(), nullable=True), + sa.Column("is_featured", sa.Boolean(), nullable=True), + sa.Column("filter_select_enabled", sa.Boolean(), nullable=True), + sa.Column("offset", sa.Integer(), nullable=True), + sa.Column("cache_timeout", sa.Integer(), nullable=True), + sa.Column("params", sa.String(1000), nullable=True), + sa.Column("is_managed_externally", sa.Boolean(), nullable=True), + sa.Column("external_url", sa.Text(), nullable=True), + sa.Column("table_name", sa.String(250), nullable=True), + sa.Column("main_dttm_col", sa.String(250), nullable=True), + sa.Column("currency_code_column", sa.String(250), nullable=True), + sa.Column("database_id", sa.Integer(), nullable=True), + sa.Column("fetch_values_predicate", sa.Text(), nullable=True), + sa.Column("schema", sa.String(255), nullable=True), + sa.Column("catalog", sa.String(256), nullable=True), + sa.Column("sql", MediumText(), nullable=True), + sa.Column("is_sqllab_view", sa.Boolean(), nullable=True), + sa.Column("template_params", sa.Text(), nullable=True), + sa.Column("extra", sa.Text(), nullable=True), + sa.Column("normalize_columns", sa.Boolean(), nullable=True), + sa.Column("always_filter_main_dttm", sa.Boolean(), nullable=True), + sa.Column("folders", sa.JSON(), nullable=True), + sa.Column("transaction_id", sa.BigInteger(), nullable=False), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id"), + sa.ForeignKeyConstraint( + ["transaction_id"], + ["version_transaction.id"], + name="fk_tables_version_transaction_id", + ), + sa.ForeignKeyConstraint( + ["end_transaction_id"], + ["version_transaction.id"], + name="fk_tables_version_end_transaction_id", + ), + ) + op.create_index( + "ix_tables_version_end_transaction_id", + "tables_version", + ["end_transaction_id"], + ) + op.create_index( + "ix_tables_version_operation_type", + "tables_version", + ["operation_type"], + ) + op.create_index( + "ix_tables_version_transaction_id", + "tables_version", + ["transaction_id"], + ) + + # ------------------------------------------------------------------ + # version_changes + # + # Field-level diff log keyed to a (transaction, entity) pair. Each + # row describes one atomic change (one field or one child-collection + # element) that occurred to one entity during a save. + # + # ``(entity_kind, entity_id)`` is a polymorphic reference: depending + # on ``entity_kind`` (``"chart"`` / ``"dashboard"`` / ``"dataset"``) + # the ``entity_id`` is the integer PK on ``slices`` / ``dashboards`` / + # ``tables`` respectively. SQL has no native polymorphic FK, so the + # constraint is intentionally omitted — cleanup relies on the + # ``CASCADE`` from ``version_transaction.id`` plus command-layer + # ordering for entity deletes (the command that hard-deletes the + # entity runs inside the same transaction that prunes its history). + # A bare ``DELETE FROM <entity_table> WHERE id = X`` outside that + # transactional boundary leaves orphan ``version_changes`` rows + # whose ``entity_id`` references a vanished row — the read-side + # tombstone-state lookup handles this gracefully. + # ------------------------------------------------------------------ + op.create_table( + "version_changes", + sa.Column( + "id", + sa.BigInteger(), + primary_key=True, + autoincrement=True, + nullable=False, + ), + sa.Column( + "transaction_id", + sa.BigInteger(), + sa.ForeignKey("version_transaction.id", ondelete="CASCADE"), + nullable=False, + ), + sa.Column( + "entity_kind", + sa.String(length=32), + nullable=False, + ), + sa.Column( + "entity_id", + sa.Integer(), + nullable=False, + ), + sa.Column( + # Integer, not SmallInteger: per-entity sequence within one + # transaction is assigned by unbounded enumerate(); a + # pathological diff (e.g. a giant position_json rewrite) could + # overflow SmallInteger's 32767 on Postgres/MySQL. + "sequence", + sa.Integer(), + nullable=False, + ), + sa.Column( + "kind", + sa.String(length=32), + nullable=False, + ), + # ``operation`` is the per-record verb: ``add`` / ``remove`` / + # ``move`` / ``edit``. ``move`` only fires for layout records; + # the other three apply across every emit site. Made explicit + # so consumers don't have to infer the verb from ``from_value`` + # / ``to_value`` null-tests or from ``path[0]`` for layout records. + sa.Column( + "operation", + sa.String(length=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", + ), + ) + op.create_index( + "ix_version_changes_kind", + "version_changes", + ["kind"], + ) + op.create_index( + "ix_version_changes_entity", + "version_changes", + ["entity_kind", "entity_id"], + ) + + # ------------------------------------------------------------------ + # table_columns_version + # ------------------------------------------------------------------ + op.create_table( + "table_columns_version", + sa.Column("uuid", UUIDType(binary=True), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("column_name", sa.String(255), nullable=True), + sa.Column("verbose_name", sa.String(1024), nullable=True), + sa.Column("is_active", sa.Boolean(), nullable=True), + sa.Column("type", sa.Text(), nullable=True), + sa.Column("advanced_data_type", sa.String(255), nullable=True), + sa.Column("groupby", sa.Boolean(), nullable=True), + sa.Column("filterable", sa.Boolean(), nullable=True), + sa.Column("description", MediumText(), nullable=True), + sa.Column("table_id", sa.Integer(), nullable=True), + sa.Column("is_dttm", sa.Boolean(), nullable=True), + sa.Column("expression", MediumText(), nullable=True), + sa.Column("python_date_format", sa.String(255), nullable=True), + sa.Column("datetime_format", sa.String(100), nullable=True), + sa.Column("extra", sa.Text(), nullable=True), + sa.Column("transaction_id", sa.BigInteger(), nullable=False), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id"), + sa.ForeignKeyConstraint( + ["transaction_id"], + ["version_transaction.id"], + name="fk_table_columns_version_transaction_id", + ), + sa.ForeignKeyConstraint( + ["end_transaction_id"], + ["version_transaction.id"], + name="fk_table_columns_version_end_transaction_id", + ), + ) + op.create_index( + "ix_table_columns_version_end_transaction_id", + "table_columns_version", + ["end_transaction_id"], + ) + op.create_index( + "ix_table_columns_version_operation_type", + "table_columns_version", + ["operation_type"], + ) + op.create_index( + "ix_table_columns_version_transaction_id", + "table_columns_version", + ["transaction_id"], + ) + + # ------------------------------------------------------------------ + # sql_metrics_version + # ------------------------------------------------------------------ + op.create_table( + "sql_metrics_version", + sa.Column("uuid", UUIDType(binary=True), nullable=True), + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("metric_name", sa.String(255), nullable=True), + sa.Column("verbose_name", sa.String(1024), nullable=True), + sa.Column("metric_type", sa.String(32), nullable=True), + sa.Column("description", MediumText(), nullable=True), + sa.Column("d3format", sa.String(128), nullable=True), + sa.Column("currency", sa.JSON(), nullable=True), + sa.Column("warning_text", sa.Text(), nullable=True), + sa.Column("table_id", sa.Integer(), nullable=True), + sa.Column("expression", MediumText(), nullable=True), + sa.Column("extra", sa.Text(), nullable=True), + sa.Column("transaction_id", sa.BigInteger(), nullable=False), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("id", "transaction_id"), + sa.ForeignKeyConstraint( + ["transaction_id"], + ["version_transaction.id"], + name="fk_sql_metrics_version_transaction_id", + ), + sa.ForeignKeyConstraint( + ["end_transaction_id"], + ["version_transaction.id"], + name="fk_sql_metrics_version_end_transaction_id", + ), + ) + op.create_index( + "ix_sql_metrics_version_end_transaction_id", + "sql_metrics_version", + ["end_transaction_id"], + ) + op.create_index( + "ix_sql_metrics_version_operation_type", + "sql_metrics_version", + ["operation_type"], + ) + op.create_index( + "ix_sql_metrics_version_transaction_id", + "sql_metrics_version", + ["transaction_id"], + ) + + # ------------------------------------------------------------------ + # dashboard_slices_version (M2M association) + # + # The live ``dashboard_slices`` table is reshaped to a + # composite PK on ``(dashboard_id, slice_id)`` — no surrogate ``id``. + # Continuum auto-mirrors the live columns into the shadow Table at + # ``make_versioned()`` time, so the shadow's SQLAlchemy metadata + # also has no ``id``. The DB shadow PK is the natural composite key + # plus Continuum's bookkeeping (``transaction_id``, ``operation_type``); + # ``operation_type`` is included because a single transaction can in + # principle produce both INSERT and DELETE shadows for the same + # ``(dashboard_id, slice_id)`` pair (slice removed and re-added in + # one save). + # + # If that reshape is removed from the stack, the live table reverts to + # carrying its surrogate ``id`` and this migration would need to + # match. + # ------------------------------------------------------------------ + op.create_table( + "dashboard_slices_version", + sa.Column("dashboard_id", sa.Integer(), nullable=False), + sa.Column("slice_id", sa.Integer(), nullable=False), + sa.Column("transaction_id", sa.BigInteger(), nullable=False), + sa.Column("end_transaction_id", sa.BigInteger(), nullable=True), + sa.Column("operation_type", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint( + "dashboard_id", "slice_id", "transaction_id", "operation_type" + ), + sa.ForeignKeyConstraint( + ["transaction_id"], + ["version_transaction.id"], + name="fk_dashboard_slices_version_transaction_id", + ), + sa.ForeignKeyConstraint( + ["end_transaction_id"], + ["version_transaction.id"], + name="fk_dashboard_slices_version_end_transaction_id", + ), + ) + op.create_index( + "ix_dashboard_slices_version_end_transaction_id", + "dashboard_slices_version", + ["end_transaction_id"], + ) + op.create_index( + "ix_dashboard_slices_version_operation_type", + "dashboard_slices_version", + ["operation_type"], + ) + op.create_index( + "ix_dashboard_slices_version_transaction_id", + "dashboard_slices_version", + ["transaction_id"], + ) + + +def downgrade() -> None: + # Drop in reverse dependency order: children with FKs to + # ``version_transaction`` drop first; ``version_transaction`` and its + # sequence drop last. + op.drop_table("dashboard_slices_version") Review Comment: Addressed by subsequent commits; the flagged code is no longer current after the rebase onto master. Resolving as outdated. ########## superset/initialization/__init__.py: ########## @@ -619,6 +619,181 @@ def init_extensions(self) -> None: extension.manifest.id, ) + @staticmethod + def _remove_continuum_write_listeners() -> None: + """Detach SQLAlchemy-Continuum's own write listeners. + + ``make_versioned()`` runs unconditionally at import of + ``superset.extensions`` and registers Continuum's mapper, session, + and engine listeners — the ones that write shadow rows and + ``version_transaction`` rows on every flush. Skipping only the + custom baseline/change-record listeners would leave those running, + so with the kill-switch off the shadow tables would silently keep + accumulating, contradicting the documented contract. + + This is deliberately a *targeted subset* of + ``sqlalchemy_continuum.remove_versioning()``: that helper also + calls ``manager.reset()``, which clears ``version_class_map`` — + and ``version_class()`` would then silently return the live model + class, breaking the read-only ``/versions/`` endpoints this flag + promises to keep working. + + Idempotent: guarded on a representative listener so repeated app + initializations in one process (test fixtures) don't raise on + double-removal. + """ + # pylint: disable=import-outside-toplevel + import sqlalchemy as sa + from sqlalchemy_continuum import versioning_manager + + if not sa.event.contains( + sa.orm.Mapper, "after_insert", versioning_manager.track_inserts + ): + return # already detached by a prior init + versioning_manager.remove_operations_tracking(sa.orm.Mapper) + versioning_manager.remove_session_tracking(sa.orm.session.Session) + sa.event.remove( + sa.engine.Engine, + "before_execute", + versioning_manager.track_association_operations, + ) + sa.event.remove( + sa.engine.Engine, "rollback", versioning_manager.clear_connection + ) + sa.event.remove( + sa.engine.Engine, + "set_connection_execution_options", + versioning_manager.track_cloned_connections, + ) + + # Belt-and-suspenders: flip Continuum's master option off as well. + # Every write listener checks ``manager.options['versioning']`` before + # doing work (manager.py / unit_of_work.py), so if a future Continuum + # version registers an additional write listener this detach does not + # know to remove, that listener still no-ops. ``version_class()`` reads + # from ``version_class_map`` and ignores this option, so the read-only + # ``/versions/`` endpoints are unaffected. + versioning_manager.options["versioning"] = False + + # Verify the known write listeners are actually gone. A Continuum + # upgrade that renamed a handler would make the removals above silently + # miss, leaving capture half-on while we report "disabled"; surface + # that rather than booting in a contradictory state. + if sa.event.contains( + sa.orm.Mapper, "after_insert", versioning_manager.track_inserts + ): + logger.warning( + "versioning: Continuum write listeners still attached after " + "detach; capture may not be fully disabled. This usually means " + "the pinned sqlalchemy-continuum version changed how it " + "registers listeners." + ) + + def init_versioning(self) -> None: + """Register SQLAlchemy-Continuum baseline and retention listeners. Review Comment: Addressed by subsequent commits; the flagged code is no longer current after the rebase onto master. Resolving as outdated. ########## superset/initialization/__init__.py: ########## @@ -619,6 +619,181 @@ def init_extensions(self) -> None: extension.manifest.id, ) + @staticmethod + def _remove_continuum_write_listeners() -> None: + """Detach SQLAlchemy-Continuum's own write listeners. + + ``make_versioned()`` runs unconditionally at import of + ``superset.extensions`` and registers Continuum's mapper, session, + and engine listeners — the ones that write shadow rows and + ``version_transaction`` rows on every flush. Skipping only the + custom baseline/change-record listeners would leave those running, + so with the kill-switch off the shadow tables would silently keep + accumulating, contradicting the documented contract. + + This is deliberately a *targeted subset* of + ``sqlalchemy_continuum.remove_versioning()``: that helper also + calls ``manager.reset()``, which clears ``version_class_map`` — + and ``version_class()`` would then silently return the live model + class, breaking the read-only ``/versions/`` endpoints this flag + promises to keep working. + + Idempotent: guarded on a representative listener so repeated app + initializations in one process (test fixtures) don't raise on + double-removal. + """ + # pylint: disable=import-outside-toplevel + import sqlalchemy as sa + from sqlalchemy_continuum import versioning_manager + + if not sa.event.contains( + sa.orm.Mapper, "after_insert", versioning_manager.track_inserts + ): + return # already detached by a prior init + versioning_manager.remove_operations_tracking(sa.orm.Mapper) + versioning_manager.remove_session_tracking(sa.orm.session.Session) + sa.event.remove( + sa.engine.Engine, + "before_execute", + versioning_manager.track_association_operations, + ) + sa.event.remove( + sa.engine.Engine, "rollback", versioning_manager.clear_connection + ) + sa.event.remove( + sa.engine.Engine, + "set_connection_execution_options", + versioning_manager.track_cloned_connections, + ) + + # Belt-and-suspenders: flip Continuum's master option off as well. + # Every write listener checks ``manager.options['versioning']`` before + # doing work (manager.py / unit_of_work.py), so if a future Continuum + # version registers an additional write listener this detach does not + # know to remove, that listener still no-ops. ``version_class()`` reads + # from ``version_class_map`` and ignores this option, so the read-only + # ``/versions/`` endpoints are unaffected. + versioning_manager.options["versioning"] = False + + # Verify the known write listeners are actually gone. A Continuum + # upgrade that renamed a handler would make the removals above silently + # miss, leaving capture half-on while we report "disabled"; surface + # that rather than booting in a contradictory state. + if sa.event.contains( + sa.orm.Mapper, "after_insert", versioning_manager.track_inserts + ): + logger.warning( + "versioning: Continuum write listeners still attached after " + "detach; capture may not be fully disabled. This usually means " + "the pinned sqlalchemy-continuum version changed how it " + "registers listeners." + ) + + def init_versioning(self) -> None: + """Register SQLAlchemy-Continuum baseline and retention listeners. + + Must be called after all versioned model classes have been imported so + that VERSIONED_MODELS can be populated and configure_mappers() has run. + + ``ENABLE_VERSIONING_CAPTURE`` (ships default ``False``) gates the two + before-flush listener registrations. The flag is operational, not + feature: with it off the infrastructure is inert (no save writes + shadow rows); flipping it on activates capture. The switch also lets + an operator who observes a versioning-induced regression (e.g. a + save-path slowdown attributable to the change-record listener) + disable capture in ``superset_config.py`` and restart workers — a + 30-second recovery instead of revert-and-redeploy. Shadow tables + already created by the migration stay; they just stop accumulating + new rows. + + The fallback here is ``False`` so that any app-factory path that + does not load ``superset.config`` (some test factories, embedded + use) stays inert by default rather than silently enabling capture. + """ + if not self.config.get("ENABLE_VERSIONING_CAPTURE", False): + logger.warning( + "versioning: ENABLE_VERSIONING_CAPTURE is False; " + "skipping baseline + change-record listener registration " + "and detaching Continuum's write listeners. Save-path " + "capture is disabled; existing shadow tables and " + "/versions/ endpoints continue to work read-only." + ) + self._remove_continuum_write_listeners() + return + + # Symmetric with the OFF branch's ``options['versioning'] = False``: + # re-assert it on here so capture is restored even if a prior app + # init in the same process (multi-app / test reentrancy) flipped the + # process-global Continuum option off. Without this, an OFF app + # initialized before an ON app would leave the option False and the + # baseline listener — which gates on it — would silently write no + # baselines despite capture being "enabled". + from sqlalchemy_continuum import versioning_manager + + versioning_manager.options["versioning"] = True Review Comment: Addressed by subsequent commits; the flagged code is no longer current after the rebase onto master. Resolving as outdated. ########## superset/datasets/api.py: ########## @@ -440,17 +504,69 @@ def put(self, pk: int) -> Response: # This validates custom Schema with custom validations except ValidationError as error: return self.response_400(message=error.messages) + + # Live version identifiers before the update (empty + query-free when + # ``ENABLE_VERSIONING_CAPTURE`` is off). + old_info = current_entity_version_info(SqlaTable, pk) + try: + # Two commands, two commits, two Continuum transactions for an + # ``override_columns`` save — deliberately NOT merged into one + # transaction. A single-transaction design was attempted and + # reverted: ``DBEventLogger`` writes request logs through the + # SHARED scoped session and calls ``commit()`` / + # ``rollback()`` on it mid-request (superset/utils/log.py), + # so any save held uncommitted across a logged sub-action can + # be committed half-done (Postgres/MySQL) or rolled back + # entirely on a transient logger failure (SQLite's + # "database is locked"). Until the event logger gets its own + # session, per-command commit boundaries are the only shape + # whose failure modes are honest. Consequence the + # version-history UI must tolerate: one logical save can + # surface as two version transactions stamped the same second. changed_model = UpdateDatasetCommand(pk, item, override_columns).run() + # Capture the post-update identifiers BEFORE the refresh: + # RefreshDatasetCommand commits its own transaction, so reading + # afterwards would attribute the refresh's version to the + # user's update (and old→new would span two transactions). + new_info = current_entity_version_info( + SqlaTable, changed_model.id, changed_model.uuid + ) + etag_version_uuid = new_info.version_uuid if override_columns: RefreshDatasetCommand(pk).run() - response = self.response(200, id=changed_model.id, result=item) + # The ETag must reflect the entity's *current live* version, + # which after the refresh is the refresh's transaction — + # re-read it rather than reusing the pre-refresh uuid. + etag_version_uuid = current_entity_etag_uuid( + SqlaTable, changed_model.id, changed_model.uuid + ) + response = self.response( + 200, + id=changed_model.id, + result=item, + old_version=old_info.version, + new_version=new_info.version, + old_transaction_id=old_info.transaction_id, + new_transaction_id=new_info.transaction_id, + old_version_uuid=old_info.version_uuid, + new_version_uuid=new_info.version_uuid, + ) + set_version_etag(response, etag_version_uuid) except DatasetNotFoundError: response = self.response_404() Review Comment: Addressed by subsequent commits; the flagged code is no longer current after the rebase onto master. Resolving as outdated. ########## scripts/seed_junction_load.py: ########## @@ -0,0 +1,679 @@ +#!/usr/bin/env python3 +# 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. +# +# ---------------------------------------------------------------------- +# Stress-test data generator for the composite-PK migration (sc-105349). +# +# Bulk-inserts synthetic parent rows and many-to-many junction rows for +# the eight association tables that the composite-PK migration touches. Review Comment: Addressed by subsequent commits; the flagged code is no longer current after the rebase onto master. Resolving as outdated. ########## scripts/seed_junction_load.py: ########## @@ -0,0 +1,679 @@ +#!/usr/bin/env python3 +# 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. +# +# ---------------------------------------------------------------------- +# Stress-test data generator for the composite-PK migration (sc-105349). +# +# Bulk-inserts synthetic parent rows and many-to-many junction rows for +# the eight association tables that the composite-PK migration touches. +# Useful for measuring migration runtime at varying scales — run this at +# 100K / 1M / 5M / 10M rows and time the migration at each scale to +# verify the O(N log N) extrapolation. +# +# Idempotent: rerunning with the same target is a no-op; rerunning with +# a higher target adds rows up to the new total. Batched bulk INSERTs +# (10K rows per statement) make it fast on Postgres, MySQL, and SQLite. +# +# Usage (inside the Superset container): +# +# docker exec superset-superset-1 \\ +# /app/.venv/bin/python /app/scripts/seed_junction_load.py \\ +# --dashboard-slices 1000000 \\ +# --slice-user 100000 \\ +# --dashboard-user 100000 +# +# Run with no flags for the defaults shown below. Use ``--dry-run`` to +# print the planned inserts without writing anything. +# +# The script connects via Superset's standard ``DATABASE_*`` env vars +# (or ``SUPERSET__SQLALCHEMY_DATABASE_URI`` if set), so it works +# automatically inside the Superset container regardless of which +# metadata DB backend is in use. + +from __future__ import annotations + +import argparse +import logging +import os +import sys +import time +from contextlib import contextmanager +from typing import Iterator +from uuid import uuid4 + +import sqlalchemy as sa +from sqlalchemy.engine import Connection, Engine + +logger = logging.getLogger("seed_junction_load") + +# Bulk INSERT batch size. Larger values = fewer statements but more memory. +BATCH = 10_000 + +# Default per-junction-table target row counts. Tuned to mimic the shape +# of a large multi-team Superset install. Override via CLI flags. +DEFAULTS: dict[str, int] = { + "dashboard_slices": 1_000_000, + "slice_user": 100_000, + "dashboard_user": 100_000, + "dashboard_roles": 10_000, +} + +# (junction_table, fk1_col, fk2_col, parent1_table, parent2_table) +# parents reference id columns; we generate (fk1, fk2) pairs by sampling +# from the parents' existing IDs. +JUNCTIONS: list[tuple[str, str, str, str, str]] = [ + ("dashboard_slices", "dashboard_id", "slice_id", "dashboards", "slices"), + ("slice_user", "user_id", "slice_id", "ab_user", "slices"), + ("dashboard_user", "user_id", "dashboard_id", "ab_user", "dashboards"), + ("dashboard_roles", "dashboard_id", "role_id", "dashboards", "ab_role"), +] + +# Junction tables that originally carried ``UNIQUE(fk1, fk2)`` and therefore +# cannot accept duplicate ``(fk1, fk2)`` pairs even on the pre-migration +# (downgrade) schema. The other JUNCTIONS allow duplicates pre-migration. +# Only ``dashboard_slices`` is listed: the migration's other UNIQUE table +# (``report_schedule_user``) is not in JUNCTIONS — this script doesn't seed +# it — so listing it here would imply coverage that doesn't exist. Add it +# alongside a JUNCTIONS entry if that table ever gets seeded. +JUNCTIONS_WITH_UNIQUE: set[str] = {"dashboard_slices"} + + +# ---------------------------------------------------------------------- +# Connection setup +# ---------------------------------------------------------------------- + + +def build_engine() -> Engine: + """Build a SQLAlchemy engine from Superset env vars.""" + if uri := os.environ.get("SUPERSET__SQLALCHEMY_DATABASE_URI"): + logger.info("Using SUPERSET__SQLALCHEMY_DATABASE_URI from env") + return sa.create_engine(uri) + + try: + dialect = os.environ["DATABASE_DIALECT"] + user = os.environ["DATABASE_USER"] + password = os.environ["DATABASE_PASSWORD"] + host = os.environ["DATABASE_HOST"] + port = os.environ["DATABASE_PORT"] + db = os.environ["DATABASE_DB"] + except KeyError as exc: + sys.exit( + f"Missing env var {exc}; either set DATABASE_DIALECT/USER/PASSWORD/" + f"HOST/PORT/DB or SUPERSET__SQLALCHEMY_DATABASE_URI before running." + ) + + uri = f"{dialect}://{user}:{password}@{host}:{port}/{db}" Review Comment: Addressed by subsequent commits; the flagged code is no longer current after the rebase onto master. Resolving as outdated. -- 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]
