Files
superset2/tests/unit_tests/daos/test_version_dao.py
Mike Bridge 59045f8cfe refactor(versioning): split VersionDAO into queries + restore modules
VersionDAO carried five distinct concerns under one class — UUID
derivation, version metadata queries, change-record loading,
single-version snapshot retrieval, and restore orchestration. Bob's
"and" test (the clean-code review flagged this as the next structural
fix after the dead-code purge) gives ~600 lines of "queries about
versioned state of one entity AND the workflow that mutates it."

Splits the read and write sides into purpose-built modules:

- ``superset/versioning/queries.py`` — UUID derivation
  (``VERSION_UUID_NAMESPACE``, ``derive_version_uuid``) + read-side
  helpers (``find_active_by_uuid``, ``current_version_number``,
  ``current_live_transaction_id``, ``current_live_version_uuid``,
  ``list_versions``, ``resolve_version_uuid``, ``get_version``,
  ``list_change_records_batch``). ~475 lines.

- ``superset/versioning/restore.py`` — write-side (``restore_version``,
  ``_stamp_audit_fields_for_restore``, ``_RESTORE_RELATIONS``).
  ~140 lines. Depends only on ``queries.find_active_by_uuid`` and
  ``utils.single_flush_scope``.

- ``superset/daos/version.py`` — collapsed to an ~85-line backward-compat
  façade that re-exports both modules under a single ``VersionDAO``
  class via ``staticmethod`` aliases. The module also re-exports
  ``VERSION_UUID_NAMESPACE`` and ``derive_version_uuid`` at module level
  so the ~10 existing callers (api.py handlers, command classes, the
  ETag emitter, integration tests) don't have to change their imports.
  New code is encouraged to import from the sub-modules directly.

The functions themselves are unchanged byte-for-byte aside from
internal call sites being rewritten from ``VersionDAO.foo`` to the bare
function name (since they now live as module-level functions, not
class methods).

One unit-test mock target moved: ``test_restore_version_returns_none_for_unknown_entity``
now patches ``superset.versioning.restore.find_active_by_uuid`` (the
actual call site) instead of ``VersionDAO.find_active_by_uuid`` (which
is now just an alias).

Each of the three modules now has one reason to change. When the
sc-103157 soft-delete pass adds the ``deleted_at IS NULL`` filter to
``find_active_by_uuid``, it touches only ``queries.py``. When a
per-entity-type restore Strategy replaces the string-keyed
``_RESTORE_RELATIONS`` dispatch, it touches only ``restore.py``.
2026-05-19 18:42:06 -06:00

98 lines
3.8 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.
"""Unit tests for ``VersionDAO``.
Exercises the pure helpers (``derive_version_uuid``) and the
``restore_version`` control-flow branches that can be covered with mocks
alone. Full round-trip scalar restore / audit stamping / non-destructive
behaviour is covered by the integration tests in
``tests/integration_tests/{charts,dashboards,datasets}/version_history_tests.py``
— those need a real Continuum stack and live DB, which unit tests here
deliberately avoid.
"""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from uuid import UUID
from superset.daos.version import (
derive_version_uuid,
VERSION_UUID_NAMESPACE,
VersionDAO,
)
# ---------------------------------------------------------------------------
# derive_version_uuid
# ---------------------------------------------------------------------------
def test_derive_version_uuid_is_deterministic():
entity = UUID("14f48794-ebfa-4f60-a26a-582c49132f1b")
assert derive_version_uuid(entity, 42) == derive_version_uuid(entity, 42)
def test_derive_version_uuid_differs_across_tx():
entity = UUID("14f48794-ebfa-4f60-a26a-582c49132f1b")
assert derive_version_uuid(entity, 1) != derive_version_uuid(entity, 2)
def test_derive_version_uuid_differs_across_entities():
tx = 42
a = UUID("14f48794-ebfa-4f60-a26a-582c49132f1b")
b = UUID("b388a396-cbca-4299-a443-3e41e870e2c2")
assert derive_version_uuid(a, tx) != derive_version_uuid(b, tx)
def test_derive_version_uuid_is_v5():
"""UUIDs must be version 5 — changing this is a breaking change."""
entity = UUID("14f48794-ebfa-4f60-a26a-582c49132f1b")
result = derive_version_uuid(entity, 1)
assert result.version == 5
def test_derive_version_uuid_uses_fixed_namespace():
"""Asserts the namespace constant hasn't drifted (changing it
invalidates every cached version_uuid — see the constant's comment)."""
assert VERSION_UUID_NAMESPACE == UUID("7a6f5d9b-4c3b-5d8e-9a1c-0e2b4c6d8f10")
# ---------------------------------------------------------------------------
# restore_version control-flow — unknown entity / out-of-range version
# ---------------------------------------------------------------------------
@patch("superset.versioning.restore.find_active_by_uuid", return_value=None)
def test_restore_version_returns_none_for_unknown_entity(mock_find):
"""Unknown entity UUID → caller raises 404."""
result = VersionDAO.restore_version(
MagicMock(__name__="Dashboard"),
UUID("00000000-0000-0000-0000-000000000000"),
0,
)
assert result is None
# Out-of-range version_num (the lookup query returns None) is verified
# end-to-end in the integration tests
# (``test_restore_returns_404_for_unknown_version_uuid`` in the three
# {charts,dashboards,datasets}/version_history_tests.py suites). A pure
# unit-level version of that test would require mocking the full
# SQLAlchemy expression tree — including ``ver_cls.operation_type != 0``
# — which is fragile and doesn't add coverage beyond what the
# integration path already provides.