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
603 lines
22 KiB
Python
603 lines
22 KiB
Python
"""
|
|
Tests for migration 0005: Add document_id column to research_resources.
|
|
|
|
Tests cover:
|
|
- Column creation with correct type and nullability
|
|
- Index creation (ix_research_resources_document_id)
|
|
- Full migration chain from empty database
|
|
- Idempotency (running migrations multiple times)
|
|
- Downgrade behavior (column and index removal)
|
|
- Data preservation during upgrade and downgrade
|
|
- Edge cases (missing table, pre-existing column, in-memory database)
|
|
"""
|
|
|
|
from alembic import command
|
|
import pytest
|
|
from sqlalchemy import create_engine, inspect, text
|
|
|
|
from local_deep_research.database.alembic_runner import (
|
|
get_alembic_config,
|
|
get_current_revision,
|
|
get_head_revision,
|
|
needs_migration,
|
|
run_migrations,
|
|
)
|
|
|
|
|
|
def _run_upgrade_to(engine, revision):
|
|
"""Run migrations up to a specific 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):
|
|
"""Run downgrade to a specific revision."""
|
|
config = get_alembic_config(engine)
|
|
with engine.begin() as conn:
|
|
config.attributes["connection"] = conn
|
|
command.downgrade(config, revision)
|
|
|
|
|
|
def _get_columns(engine, table_name):
|
|
"""Get a dict of {column_name: column_info} for a table."""
|
|
insp = inspect(engine)
|
|
if not insp.has_table(table_name):
|
|
return {}
|
|
return {col["name"]: col for col in insp.get_columns(table_name)}
|
|
|
|
|
|
def _get_indexes_by_name(engine, table_name):
|
|
"""Get a dict of {index_name: [column_names]} for a table."""
|
|
insp = inspect(engine)
|
|
if not insp.has_table(table_name):
|
|
return {}
|
|
return {
|
|
idx["name"]: idx["column_names"]
|
|
for idx in insp.get_indexes(table_name)
|
|
if idx["name"] is not None
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def fresh_engine(tmp_path):
|
|
"""Create a fresh SQLite engine (empty database, no tables)."""
|
|
db_path = tmp_path / "fresh_0005_test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
yield engine
|
|
engine.dispose()
|
|
|
|
|
|
@pytest.fixture
|
|
def migrated_to_0004_engine(tmp_path):
|
|
"""Create a database migrated to revision 0004 (before document_id)."""
|
|
db_path = tmp_path / "migrated_0004_test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
_run_upgrade_to(engine, "0004")
|
|
yield engine
|
|
engine.dispose()
|
|
|
|
|
|
@pytest.fixture
|
|
def fully_migrated_engine(tmp_path):
|
|
"""Create a database migrated up to revision 0005 (this file's target).
|
|
|
|
Stops at 0005 instead of head — the downgrade tests below would
|
|
otherwise have to roll back through migration 0010, which is
|
|
documented as non-reversible (raises NotImplementedError). Every
|
|
test in this file is scoped to 0005 behaviour.
|
|
"""
|
|
db_path = tmp_path / "fully_migrated_0005_test.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
_run_upgrade_to(engine, "0005")
|
|
yield engine
|
|
engine.dispose()
|
|
|
|
|
|
class TestMigration0005UpgradeColumn:
|
|
"""Tests that verify document_id column creation on upgrade."""
|
|
|
|
def test_document_id_column_exists(self, fully_migrated_engine):
|
|
"""document_id column should exist on research_resources after migration."""
|
|
columns = _get_columns(fully_migrated_engine, "research_resources")
|
|
assert "document_id" in columns
|
|
|
|
def test_document_id_column_is_nullable(self, fully_migrated_engine):
|
|
"""document_id should be nullable (existing rows have no value)."""
|
|
columns = _get_columns(fully_migrated_engine, "research_resources")
|
|
assert columns["document_id"]["nullable"] is True
|
|
|
|
def test_document_id_column_type_is_varchar(self, fully_migrated_engine):
|
|
"""document_id should be VARCHAR(36) to hold UUIDs."""
|
|
columns = _get_columns(fully_migrated_engine, "research_resources")
|
|
col_type = str(columns["document_id"]["type"])
|
|
assert "VARCHAR" in col_type or "CHAR" in col_type
|
|
|
|
def test_document_id_index_exists(self, fully_migrated_engine):
|
|
"""Index ix_research_resources_document_id should exist."""
|
|
indexes = _get_indexes_by_name(
|
|
fully_migrated_engine, "research_resources"
|
|
)
|
|
assert "ix_research_resources_document_id" in indexes
|
|
assert indexes["ix_research_resources_document_id"] == ["document_id"]
|
|
|
|
def test_document_id_defaults_to_null(self, fully_migrated_engine):
|
|
"""Inserting a row without document_id should default to NULL."""
|
|
engine = fully_migrated_engine
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_resources "
|
|
"(research_id, title, url, created_at) "
|
|
"VALUES ('test-rh-1', 'Test Resource', "
|
|
"'https://example.com', '2026-01-01')"
|
|
)
|
|
)
|
|
|
|
with engine.connect() as conn:
|
|
result = conn.execute(
|
|
text(
|
|
"SELECT document_id FROM research_resources "
|
|
"WHERE research_id = 'test-rh-1'"
|
|
)
|
|
).fetchone()
|
|
assert result[0] is None
|
|
|
|
def test_document_id_can_store_uuid(self, fully_migrated_engine):
|
|
"""document_id should accept a UUID string value."""
|
|
engine = fully_migrated_engine
|
|
test_uuid = "abcdef01-2345-6789-abcd-ef0123456789"
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_resources "
|
|
"(research_id, title, url, created_at, document_id) "
|
|
"VALUES ('test-rh-2', 'Linked Resource', "
|
|
"'https://example.com', '2026-01-01', :doc_id)"
|
|
),
|
|
{"doc_id": test_uuid},
|
|
)
|
|
|
|
with engine.connect() as conn:
|
|
result = conn.execute(
|
|
text(
|
|
"SELECT document_id FROM research_resources "
|
|
"WHERE research_id = 'test-rh-2'"
|
|
)
|
|
).fetchone()
|
|
assert result[0] == test_uuid
|
|
|
|
|
|
class TestMigration0005UpgradeFromPrior:
|
|
"""Tests that verify the upgrade path for databases missing document_id.
|
|
|
|
Migration 0001 uses Base.metadata.create_all() which includes document_id
|
|
from the current model. To test the real-world scenario (database created
|
|
before document_id was added to the model), we manually create the table
|
|
WITHOUT document_id and stamp at 0004.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def legacy_engine(self, tmp_path):
|
|
"""Create a database with research_resources missing document_id.
|
|
|
|
This simulates a database created before commit 2033f977e added
|
|
document_id to the ResearchResource model.
|
|
"""
|
|
db_path = tmp_path / "legacy_no_docid.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
|
|
with engine.begin() as conn:
|
|
# Create research_history (needed for FK)
|
|
conn.execute(
|
|
text(
|
|
"CREATE TABLE research_history ("
|
|
"id VARCHAR(36) PRIMARY KEY, "
|
|
"query TEXT NOT NULL, "
|
|
"mode TEXT NOT NULL, "
|
|
"status TEXT NOT NULL, "
|
|
"created_at TEXT NOT NULL"
|
|
")"
|
|
)
|
|
)
|
|
# Create research_resources WITHOUT document_id
|
|
conn.execute(
|
|
text(
|
|
"CREATE TABLE research_resources ("
|
|
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
"research_id VARCHAR(36) NOT NULL "
|
|
" REFERENCES research_history(id) ON DELETE CASCADE, "
|
|
"title TEXT, "
|
|
"url TEXT, "
|
|
"content_preview TEXT, "
|
|
"source_type TEXT, "
|
|
"metadata JSON, "
|
|
"created_at VARCHAR NOT NULL"
|
|
")"
|
|
)
|
|
)
|
|
# Stamp at 0004 so migration 0005 runs
|
|
conn.execute(
|
|
text(
|
|
"CREATE TABLE alembic_version "
|
|
"(version_num VARCHAR(32) NOT NULL)"
|
|
)
|
|
)
|
|
conn.execute(text("INSERT INTO alembic_version VALUES ('0004')"))
|
|
|
|
yield engine
|
|
engine.dispose()
|
|
|
|
def test_upgrade_adds_column_to_legacy_table(self, legacy_engine):
|
|
"""Upgrading a database missing document_id should add the column."""
|
|
columns = _get_columns(legacy_engine, "research_resources")
|
|
assert "document_id" not in columns
|
|
|
|
_run_upgrade_to(legacy_engine, "0005")
|
|
|
|
columns = _get_columns(legacy_engine, "research_resources")
|
|
assert "document_id" in columns
|
|
|
|
def test_upgrade_adds_index_to_legacy_table(self, legacy_engine):
|
|
"""Upgrading should create the document_id index."""
|
|
_run_upgrade_to(legacy_engine, "0005")
|
|
|
|
indexes = _get_indexes_by_name(legacy_engine, "research_resources")
|
|
assert "ix_research_resources_document_id" in indexes
|
|
|
|
def test_revision_is_0005_after_upgrade(self, legacy_engine):
|
|
"""Current revision should be 0005 after targeted upgrade."""
|
|
_run_upgrade_to(legacy_engine, "0005")
|
|
assert get_current_revision(legacy_engine) == "0005"
|
|
|
|
def test_existing_data_preserved_after_upgrade(self, legacy_engine):
|
|
"""Rows inserted before upgrade should survive with NULL document_id."""
|
|
with legacy_engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_history "
|
|
"(id, query, mode, status, created_at) "
|
|
"VALUES ('rh-legacy', 'old query', 'quick', "
|
|
"'completed', '2025-06-01')"
|
|
)
|
|
)
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_resources "
|
|
"(research_id, title, url, created_at) "
|
|
"VALUES ('rh-legacy', 'Old Resource', "
|
|
"'https://old.com', '2025-06-01')"
|
|
)
|
|
)
|
|
|
|
_run_upgrade_to(legacy_engine, "0005")
|
|
|
|
with legacy_engine.connect() as conn:
|
|
result = conn.execute(
|
|
text(
|
|
"SELECT title, url, document_id "
|
|
"FROM research_resources "
|
|
"WHERE research_id = 'rh-legacy'"
|
|
)
|
|
).fetchone()
|
|
assert result[0] == "Old Resource"
|
|
assert result[1] == "https://old.com"
|
|
assert result[2] is None
|
|
|
|
def test_idempotent_on_fresh_db(self, migrated_to_0004_engine):
|
|
"""On a fresh DB (0001 already created document_id), 0005 is a no-op."""
|
|
engine = migrated_to_0004_engine
|
|
|
|
# 0001's create_all already added document_id from current model
|
|
columns_before = _get_columns(engine, "research_resources")
|
|
assert "document_id" in columns_before
|
|
|
|
_run_upgrade_to(engine, "0005")
|
|
|
|
columns_after = _get_columns(engine, "research_resources")
|
|
assert "document_id" in columns_after
|
|
assert get_current_revision(engine) == "0005"
|
|
|
|
|
|
class TestMigration0005FromFreshDatabase:
|
|
"""Tests that verify the full migration chain on a fresh database."""
|
|
|
|
def test_fresh_db_full_migration_creates_column(self, fresh_engine):
|
|
"""Running all migrations on empty DB should create document_id."""
|
|
run_migrations(fresh_engine)
|
|
|
|
columns = _get_columns(fresh_engine, "research_resources")
|
|
assert "document_id" in columns
|
|
|
|
def test_head_revision_is_real_id(self):
|
|
"""get_head_revision() returns a real 4-digit revision id."""
|
|
head = get_head_revision()
|
|
assert head is not None and head.isdigit() and len(head) == 4
|
|
|
|
def test_current_revision_is_head_after_migrate(self, fresh_engine):
|
|
"""After full migration, current revision should match head."""
|
|
run_migrations(fresh_engine)
|
|
assert get_current_revision(fresh_engine) == get_head_revision()
|
|
|
|
def test_needs_migration_false_after_full_upgrade(self, fresh_engine):
|
|
"""After full migration, needs_migration() should return False."""
|
|
run_migrations(fresh_engine)
|
|
assert needs_migration(fresh_engine) is False
|
|
|
|
|
|
class TestMigration0005Idempotency:
|
|
"""Tests that verify safe re-runs of migration."""
|
|
|
|
def test_run_migrations_twice_no_error(self, fresh_engine):
|
|
"""Calling run_migrations() twice should not raise."""
|
|
run_migrations(fresh_engine)
|
|
run_migrations(fresh_engine)
|
|
|
|
def test_column_unchanged_after_double_migration(self, fresh_engine):
|
|
"""Column should be identical after running migrations twice."""
|
|
run_migrations(fresh_engine)
|
|
columns_first = _get_columns(fresh_engine, "research_resources")
|
|
|
|
run_migrations(fresh_engine)
|
|
columns_second = _get_columns(fresh_engine, "research_resources")
|
|
|
|
assert ("document_id" in columns_first) == (
|
|
"document_id" in columns_second
|
|
)
|
|
|
|
def test_pre_existing_column_not_duplicated(self, tmp_path):
|
|
"""If document_id already exists (fresh DB), migration is a no-op."""
|
|
db_path = tmp_path / "pre_existing_col.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
|
|
try:
|
|
# Full migration creates the column via 0001's create_all
|
|
run_migrations(engine)
|
|
|
|
columns_before = _get_columns(engine, "research_resources")
|
|
assert "document_id" in columns_before
|
|
|
|
# Downgrade to 0004 and re-upgrade to test the migration path
|
|
# when the column was already added by create_all
|
|
# (This tests the idempotency guard)
|
|
col_count_before = len(columns_before)
|
|
|
|
# Running migrations again should be a no-op
|
|
run_migrations(engine)
|
|
|
|
columns_after = _get_columns(engine, "research_resources")
|
|
assert len(columns_after) == col_count_before
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
class TestMigration0005Downgrade:
|
|
"""Tests for rollback behavior."""
|
|
|
|
def test_downgrade_to_0004_removes_column(self, fully_migrated_engine):
|
|
"""Downgrade from 0005 to 0004 should remove document_id."""
|
|
_run_downgrade_to(fully_migrated_engine, "0004")
|
|
|
|
columns = _get_columns(fully_migrated_engine, "research_resources")
|
|
assert "document_id" not in columns
|
|
|
|
def test_downgrade_to_0004_removes_index(self, fully_migrated_engine):
|
|
"""Downgrade should remove ix_research_resources_document_id."""
|
|
_run_downgrade_to(fully_migrated_engine, "0004")
|
|
|
|
indexes = _get_indexes_by_name(
|
|
fully_migrated_engine, "research_resources"
|
|
)
|
|
assert "ix_research_resources_document_id" not in indexes
|
|
|
|
def test_downgrade_preserves_table(self, fully_migrated_engine):
|
|
"""research_resources table should still exist after downgrade."""
|
|
_run_downgrade_to(fully_migrated_engine, "0004")
|
|
|
|
insp = inspect(fully_migrated_engine)
|
|
assert insp.has_table("research_resources")
|
|
|
|
def test_downgrade_preserves_other_columns(self, fully_migrated_engine):
|
|
"""Other columns (title, url, etc.) should survive downgrade."""
|
|
_run_downgrade_to(fully_migrated_engine, "0004")
|
|
|
|
columns = _get_columns(fully_migrated_engine, "research_resources")
|
|
for col in ("id", "research_id", "title", "url", "created_at"):
|
|
assert col in columns, f"Column {col} lost during downgrade"
|
|
|
|
def test_downgrade_then_upgrade_roundtrip(self, fully_migrated_engine):
|
|
"""Downgrade to 0004 then upgrade back to 0005 should restore column."""
|
|
engine = fully_migrated_engine
|
|
|
|
_run_downgrade_to(engine, "0004")
|
|
assert get_current_revision(engine) == "0004"
|
|
|
|
_run_upgrade_to(engine, "0005")
|
|
assert get_current_revision(engine) == "0005"
|
|
|
|
columns = _get_columns(engine, "research_resources")
|
|
assert "document_id" in columns
|
|
|
|
indexes = _get_indexes_by_name(engine, "research_resources")
|
|
assert "ix_research_resources_document_id" in indexes
|
|
|
|
|
|
class TestMigration0005DataPreservation:
|
|
"""Tests that verify migration is non-destructive."""
|
|
|
|
def test_existing_rows_preserved_after_upgrade(
|
|
self, migrated_to_0004_engine
|
|
):
|
|
"""Data in research_resources should survive the migration."""
|
|
engine = migrated_to_0004_engine
|
|
|
|
# Insert a research_history row first (FK constraint)
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_history "
|
|
"(id, query, mode, status, created_at) "
|
|
"VALUES ('rh-preserve', 'test query', 'quick', "
|
|
"'completed', '2026-01-01')"
|
|
)
|
|
)
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_resources "
|
|
"(research_id, title, url, created_at) "
|
|
"VALUES ('rh-preserve', 'Preserved Resource', "
|
|
"'https://example.com', '2026-01-01')"
|
|
)
|
|
)
|
|
|
|
_run_upgrade_to(engine, "0005")
|
|
|
|
with engine.connect() as conn:
|
|
result = conn.execute(
|
|
text(
|
|
"SELECT title, url, document_id "
|
|
"FROM research_resources "
|
|
"WHERE research_id = 'rh-preserve'"
|
|
)
|
|
).fetchone()
|
|
assert result is not None
|
|
assert result[0] == "Preserved Resource"
|
|
assert result[1] == "https://example.com"
|
|
assert result[2] is None # New column defaults to NULL
|
|
|
|
def test_data_preserved_after_downgrade(self, fully_migrated_engine):
|
|
"""Non-document_id data should survive downgrade."""
|
|
engine = fully_migrated_engine
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_history "
|
|
"(id, query, mode, status, created_at) "
|
|
"VALUES ('rh-down', 'downgrade query', 'detailed', "
|
|
"'completed', '2026-01-01')"
|
|
)
|
|
)
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_resources "
|
|
"(research_id, title, url, created_at, document_id) "
|
|
"VALUES ('rh-down', 'Will Lose DocID', "
|
|
"'https://example.com', '2026-01-01', 'some-uuid')"
|
|
)
|
|
)
|
|
|
|
_run_downgrade_to(engine, "0004")
|
|
|
|
with engine.connect() as conn:
|
|
result = conn.execute(
|
|
text(
|
|
"SELECT title, url FROM research_resources "
|
|
"WHERE research_id = 'rh-down'"
|
|
)
|
|
).fetchone()
|
|
assert result is not None
|
|
assert result[0] == "Will Lose DocID"
|
|
assert result[1] == "https://example.com"
|
|
|
|
|
|
class TestMigration0005EdgeCases:
|
|
"""Tests for edge cases and robustness."""
|
|
|
|
def test_missing_research_resources_table(self, tmp_path):
|
|
"""Migration should be a no-op when research_resources doesn't exist."""
|
|
db_path = tmp_path / "no_resources_table.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
|
|
try:
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"CREATE TABLE alembic_version "
|
|
"(version_num VARCHAR(32) NOT NULL)"
|
|
)
|
|
)
|
|
conn.execute(
|
|
text("INSERT INTO alembic_version VALUES ('0004')")
|
|
)
|
|
|
|
_run_upgrade_to(engine, "0005")
|
|
assert get_current_revision(engine) == "0005"
|
|
|
|
insp = inspect(engine)
|
|
assert not insp.has_table("research_resources")
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_in_memory_database(self):
|
|
"""Migration should work on in-memory SQLite database."""
|
|
engine = create_engine("sqlite:///:memory:")
|
|
|
|
try:
|
|
run_migrations(engine)
|
|
|
|
assert get_current_revision(engine) == get_head_revision()
|
|
|
|
columns = _get_columns(engine, "research_resources")
|
|
assert "document_id" in columns
|
|
|
|
indexes = _get_indexes_by_name(engine, "research_resources")
|
|
assert "ix_research_resources_document_id" in indexes
|
|
finally:
|
|
engine.dispose()
|
|
|
|
def test_document_id_queryable_after_upgrade(self, migrated_to_0004_engine):
|
|
"""Queries filtering on document_id should work after upgrade."""
|
|
engine = migrated_to_0004_engine
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_history "
|
|
"(id, query, mode, status, created_at) "
|
|
"VALUES ('rh-query', 'query test', 'quick', "
|
|
"'completed', '2026-01-01')"
|
|
)
|
|
)
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO research_resources "
|
|
"(research_id, title, url, created_at) "
|
|
"VALUES ('rh-query', 'Resource A', "
|
|
"'https://a.com', '2026-01-01')"
|
|
)
|
|
)
|
|
|
|
_run_upgrade_to(engine, "0005")
|
|
|
|
# Set document_id on the row
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"UPDATE research_resources SET document_id = 'doc-uuid-1' "
|
|
"WHERE research_id = 'rh-query'"
|
|
)
|
|
)
|
|
|
|
# Query using the new column (mimics library_service.py join condition)
|
|
with engine.connect() as conn:
|
|
result = conn.execute(
|
|
text(
|
|
"SELECT title FROM research_resources "
|
|
"WHERE document_id = 'doc-uuid-1'"
|
|
)
|
|
).fetchall()
|
|
assert len(result) == 1
|
|
assert result[0][0] == "Resource A"
|
|
|
|
# NULL document_id query
|
|
result = conn.execute(
|
|
text(
|
|
"SELECT COUNT(*) FROM research_resources "
|
|
"WHERE document_id IS NULL"
|
|
)
|
|
).scalar()
|
|
assert result == 0 # We updated the only row
|