7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
191 lines
6.5 KiB
Python
191 lines
6.5 KiB
Python
"""Tests for migration 0015: drop the dead ``documents.notes`` legacy column.
|
|
|
|
Covers:
|
|
- The drop removes ``notes`` from ``documents``.
|
|
- Other columns, indexes, and row data on the rebuilt table survive.
|
|
- Idempotency (re-run, and tables that never had the column).
|
|
- Missing-table guard.
|
|
- Downgrade re-adds ``notes`` as nullable Text, and a round-trip is stable.
|
|
|
|
(The head-alignment guard moved to the newer 0016 migration's test file, per
|
|
the convention that it lives in the newest migration's tests.)
|
|
|
|
Note on test setup
|
|
==================
|
|
Migration 0001 runs ``Base.metadata.create_all()`` against the CURRENT model,
|
|
which (after this PR) no longer has ``notes`` — so a freshly initialised DB
|
|
never has the column. To pin "0015 is the migration that drops it" we use the
|
|
same pattern as ``test_migration_0005_resource_document_id.py`` /
|
|
``test_migration_0014_*``: hand-build a "legacy" ``documents`` table that still
|
|
has ``notes``, stamp at ``0014``, and run the upgrade against that.
|
|
"""
|
|
|
|
import pytest
|
|
from alembic import command
|
|
from sqlalchemy import create_engine, inspect, text
|
|
|
|
from local_deep_research.database.alembic_runner import (
|
|
get_alembic_config,
|
|
stamp_database,
|
|
)
|
|
|
|
|
|
def _run_upgrade_to(engine, revision):
|
|
config = get_alembic_config(engine)
|
|
with engine.begin() as conn:
|
|
config.attributes["connection"] = conn
|
|
command.upgrade(config, revision)
|
|
|
|
|
|
def _run_downgrade_to(engine, revision):
|
|
config = get_alembic_config(engine)
|
|
with engine.begin() as conn:
|
|
config.attributes["connection"] = conn
|
|
command.downgrade(config, revision)
|
|
|
|
|
|
def _columns(engine, table):
|
|
insp = inspect(engine)
|
|
if not insp.has_table(table):
|
|
return set()
|
|
return {c["name"] for c in insp.get_columns(table)}
|
|
|
|
|
|
def _column_info(engine, table):
|
|
insp = inspect(engine)
|
|
if not insp.has_table(table):
|
|
return {}
|
|
return {c["name"]: c for c in insp.get_columns(table)}
|
|
|
|
|
|
def _index_names(engine, table):
|
|
insp = inspect(engine)
|
|
if not insp.has_table(table):
|
|
return set()
|
|
return {idx["name"] for idx in insp.get_indexes(table) if idx["name"]}
|
|
|
|
|
|
@pytest.fixture
|
|
def legacy_engine(tmp_path):
|
|
"""A DB whose ``documents`` table still has the ``notes`` column.
|
|
|
|
Simulates an installation created before 0015. Stamped at 0014 so 0015's
|
|
upgrade is a clean delta. Includes a named index and a seeded row so the
|
|
batch-mode table rebuild can be checked for column/index/data preservation.
|
|
"""
|
|
db_path = tmp_path / "legacy_pre_0015.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"CREATE TABLE documents ("
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
"document_hash VARCHAR(64) NOT NULL, "
|
|
"title VARCHAR(500), "
|
|
"favorite BOOLEAN NOT NULL DEFAULT 0, "
|
|
"tags TEXT, "
|
|
"notes TEXT, "
|
|
"created_at TEXT NOT NULL"
|
|
")"
|
|
)
|
|
)
|
|
conn.execute(
|
|
text("CREATE INDEX idx_document_hash ON documents (document_hash)")
|
|
)
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO documents "
|
|
"(document_hash, title, favorite, tags, notes, created_at) "
|
|
"VALUES ('hash-1', 'Doc One', 1, '[\"a\"]', "
|
|
"'a stray note', '2026-01-01 00:00:00')"
|
|
)
|
|
)
|
|
|
|
stamp_database(engine, "0014")
|
|
yield engine
|
|
engine.dispose()
|
|
|
|
|
|
class TestMigration0015Upgrade:
|
|
def test_drops_notes_column(self, legacy_engine):
|
|
engine = legacy_engine
|
|
assert "notes" in _columns(engine, "documents")
|
|
_run_upgrade_to(engine, "0015")
|
|
assert "notes" not in _columns(engine, "documents")
|
|
|
|
def test_preserves_other_columns(self, legacy_engine):
|
|
engine = legacy_engine
|
|
_run_upgrade_to(engine, "0015")
|
|
cols = _columns(engine, "documents")
|
|
for expected in {
|
|
"id",
|
|
"document_hash",
|
|
"title",
|
|
"favorite",
|
|
"tags",
|
|
"created_at",
|
|
}:
|
|
assert expected in cols
|
|
|
|
def test_preserves_index(self, legacy_engine):
|
|
engine = legacy_engine
|
|
assert "idx_document_hash" in _index_names(engine, "documents")
|
|
_run_upgrade_to(engine, "0015")
|
|
assert "idx_document_hash" in _index_names(engine, "documents")
|
|
|
|
def test_preserves_row_data(self, legacy_engine):
|
|
engine = legacy_engine
|
|
_run_upgrade_to(engine, "0015")
|
|
with engine.begin() as conn:
|
|
row = conn.execute(
|
|
text(
|
|
"SELECT document_hash, title, favorite, tags "
|
|
"FROM documents WHERE document_hash = 'hash-1'"
|
|
)
|
|
).fetchone()
|
|
assert row is not None
|
|
assert row[0] == "hash-1"
|
|
assert row[1] == "Doc One"
|
|
assert row[2] == 1
|
|
assert row[3] == '["a"]'
|
|
|
|
def test_upgrade_is_idempotent(self, legacy_engine):
|
|
engine = legacy_engine
|
|
_run_upgrade_to(engine, "0015")
|
|
_run_upgrade_to(engine, "0015") # second run is a no-op, no error
|
|
assert "notes" not in _columns(engine, "documents")
|
|
|
|
def test_missing_table_does_not_crash(self, tmp_path):
|
|
"""Upgrade is a no-op when ``documents`` doesn't exist yet."""
|
|
engine = create_engine(f"sqlite:///{tmp_path}/no_documents.db")
|
|
stamp_database(engine, "0014")
|
|
_run_upgrade_to(engine, "0015") # must not raise
|
|
assert not inspect(engine).has_table("documents")
|
|
engine.dispose()
|
|
|
|
|
|
class TestMigration0015Downgrade:
|
|
def test_downgrade_readds_notes_nullable(self, legacy_engine):
|
|
engine = legacy_engine
|
|
_run_upgrade_to(engine, "0015")
|
|
assert "notes" not in _columns(engine, "documents")
|
|
_run_downgrade_to(engine, "0014")
|
|
cols = _column_info(engine, "documents")
|
|
assert "notes" in cols
|
|
assert cols["notes"]["nullable"] is True
|
|
assert "TEXT" in str(cols["notes"]["type"]).upper()
|
|
|
|
def test_downgrade_then_upgrade_roundtrip(self, legacy_engine):
|
|
engine = legacy_engine
|
|
_run_upgrade_to(engine, "0015")
|
|
_run_downgrade_to(engine, "0014")
|
|
_run_upgrade_to(engine, "0015")
|
|
assert "notes" not in _columns(engine, "documents")
|
|
# Seeded row still present after the round-trip.
|
|
with engine.begin() as conn:
|
|
count = conn.execute(
|
|
text("SELECT COUNT(*) FROM documents")
|
|
).scalar()
|
|
assert count == 1
|