627 lines
22 KiB
Python
627 lines
22 KiB
Python
from pathlib import Path
|
|
|
|
from alembic import command
|
|
from alembic.config import Config
|
|
from sqlalchemy import create_engine, text
|
|
|
|
import migrations
|
|
|
|
|
|
def _alembic_config(database_url: str) -> Config:
|
|
config = Config()
|
|
config.set_main_option(
|
|
"script_location", str(Path(__file__).resolve().parents[2] / "alembic")
|
|
)
|
|
config.set_main_option("sqlalchemy.url", database_url)
|
|
return config
|
|
|
|
|
|
def test_legacy_database_with_theme_is_stamped_past_theme_migration(
|
|
tmp_path, monkeypatch
|
|
):
|
|
database_url = f"sqlite:///{tmp_path / 'legacy.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(
|
|
text("CREATE TABLE presentations (id TEXT PRIMARY KEY, theme JSON)")
|
|
)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
stamped_revisions = []
|
|
monkeypatch.setattr(
|
|
migrations.command,
|
|
"stamp",
|
|
lambda _config, revision: stamped_revisions.append(revision),
|
|
)
|
|
|
|
migrations._stamp_legacy_database_if_needed(
|
|
_alembic_config(database_url), database_url
|
|
)
|
|
|
|
assert stamped_revisions == [migrations.REVISION_BEFORE_TEMPLATE_CREATE_INFO]
|
|
|
|
|
|
def test_upgrade_from_baseline_stamp_skips_existing_theme_column(tmp_path):
|
|
database_url = f"sqlite:///{tmp_path / 'baseline-stamped.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(
|
|
text("CREATE TABLE presentations (id TEXT PRIMARY KEY, theme JSON)")
|
|
)
|
|
connection.execute(
|
|
text("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)")
|
|
)
|
|
connection.execute(
|
|
text("INSERT INTO alembic_version (version_num) VALUES (:revision)"),
|
|
{"revision": migrations.LEGACY_BASELINE_REVISION},
|
|
)
|
|
|
|
command.upgrade(_alembic_config(database_url), "head")
|
|
|
|
with engine.connect() as connection:
|
|
version = connection.execute(
|
|
text("SELECT version_num FROM alembic_version")
|
|
).scalar_one()
|
|
columns = {
|
|
row[1]
|
|
for row in connection.execute(text("PRAGMA table_info(presentations)"))
|
|
}
|
|
tables = {
|
|
row[0]
|
|
for row in connection.execute(
|
|
text("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
)
|
|
}
|
|
|
|
assert version == migrations.REVISION_ASYNC_TASKS
|
|
assert "theme" in columns
|
|
assert "fonts" in columns
|
|
assert "async_tasks" in tables
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_upgrade_from_theme_stamp_skips_existing_template_create_infos_table(tmp_path):
|
|
database_url = f"sqlite:///{tmp_path / 'template-table-exists.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(
|
|
text("CREATE TABLE presentations (id TEXT PRIMARY KEY, theme JSON)")
|
|
)
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE template_create_infos (
|
|
id CHAR(32) NOT NULL,
|
|
fonts JSON,
|
|
pptx_url VARCHAR,
|
|
slide_htmls JSON NOT NULL,
|
|
slide_image_urls JSON NOT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id)
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
text("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)")
|
|
)
|
|
connection.execute(
|
|
text("INSERT INTO alembic_version (version_num) VALUES (:revision)"),
|
|
{"revision": migrations.REVISION_BEFORE_TEMPLATE_CREATE_INFO},
|
|
)
|
|
|
|
command.upgrade(_alembic_config(database_url), "head")
|
|
|
|
with engine.connect() as connection:
|
|
version = connection.execute(
|
|
text("SELECT version_num FROM alembic_version")
|
|
).scalar_one()
|
|
tables = {
|
|
row[0]
|
|
for row in connection.execute(
|
|
text("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
)
|
|
}
|
|
|
|
assert version == migrations.REVISION_ASYNC_TASKS
|
|
assert "template_create_infos" in tables
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_upgrade_from_template_stamp_skips_existing_chat_history_table(tmp_path):
|
|
database_url = f"sqlite:///{tmp_path / 'chat-table-exists.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(text("CREATE TABLE presentations (id TEXT PRIMARY KEY)"))
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE chat_history_messages (
|
|
id CHAR(32) NOT NULL,
|
|
presentation_id CHAR(32) NOT NULL,
|
|
conversation_id CHAR(32) NOT NULL,
|
|
position INTEGER NOT NULL,
|
|
role VARCHAR NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
tool_calls JSON,
|
|
PRIMARY KEY (id)
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
text("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)")
|
|
)
|
|
connection.execute(
|
|
text("INSERT INTO alembic_version (version_num) VALUES (:revision)"),
|
|
{"revision": migrations.REVISION_TEMPLATE_CREATE_INFO},
|
|
)
|
|
|
|
command.upgrade(_alembic_config(database_url), "head")
|
|
|
|
with engine.connect() as connection:
|
|
version = connection.execute(
|
|
text("SELECT version_num FROM alembic_version")
|
|
).scalar_one()
|
|
indexes = {
|
|
row[1]
|
|
for row in connection.execute(
|
|
text("PRAGMA index_list(chat_history_messages)")
|
|
)
|
|
}
|
|
tables = {
|
|
row[0]
|
|
for row in connection.execute(
|
|
text("SELECT name FROM sqlite_master WHERE type = 'table'")
|
|
)
|
|
}
|
|
template_columns = {
|
|
row[1]
|
|
for row in connection.execute(text("PRAGMA table_info(template_v2)"))
|
|
}
|
|
|
|
assert version == migrations.REVISION_ASYNC_TASKS
|
|
assert {
|
|
"ix_chat_history_messages_conversation_id",
|
|
"ix_chat_history_messages_position",
|
|
"ix_chat_history_messages_presentation_id",
|
|
"ix_chat_history_messages_template_v2_id",
|
|
}.issubset(indexes)
|
|
assert "template_v2" in tables
|
|
assert "components" in template_columns
|
|
assert "cluster_candidates" not in template_columns
|
|
assert "clusters" not in template_columns
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_consolidated_migration_adds_presentation_version(tmp_path):
|
|
database_url = f"sqlite:///{tmp_path / 'presentation-version.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE presentations (
|
|
id TEXT PRIMARY KEY,
|
|
content VARCHAR NOT NULL,
|
|
n_slides INTEGER NOT NULL,
|
|
language VARCHAR NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO presentations (id, content, n_slides, language)
|
|
VALUES ('p1', 'content', 1, 'English')
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(text("CREATE TABLE slides (id TEXT PRIMARY KEY)"))
|
|
connection.execute(
|
|
text("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)")
|
|
)
|
|
connection.execute(
|
|
text("INSERT INTO alembic_version (version_num) VALUES (:revision)"),
|
|
{"revision": migrations.REVISION_CHAT_HISTORY},
|
|
)
|
|
|
|
command.upgrade(_alembic_config(database_url), "head")
|
|
|
|
with engine.connect() as connection:
|
|
version = connection.execute(
|
|
text("SELECT version_num FROM alembic_version")
|
|
).scalar_one()
|
|
presentation_version = connection.execute(
|
|
text("SELECT version FROM presentations WHERE id = 'p1'")
|
|
).scalar_one()
|
|
version_column = next(
|
|
row
|
|
for row in connection.execute(text("PRAGMA table_info(presentations)"))
|
|
if row[1] == "version"
|
|
)
|
|
slide_columns = {
|
|
row[1]
|
|
for row in connection.execute(text("PRAGMA table_info(slides)"))
|
|
}
|
|
|
|
assert version == migrations.REVISION_ASYNC_TASKS
|
|
assert presentation_version == "v1-standard"
|
|
assert version_column[3] == 1
|
|
assert version_column[4] is None
|
|
assert "ui" in slide_columns
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_unversioned_database_with_chat_history_stamps_before_template_v2(
|
|
tmp_path, monkeypatch
|
|
):
|
|
database_url = f"sqlite:///{tmp_path / 'legacy-chat.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(text("CREATE TABLE presentations (id TEXT PRIMARY KEY)"))
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE chat_history_messages (
|
|
id CHAR(32) NOT NULL,
|
|
presentation_id CHAR(32) NOT NULL,
|
|
conversation_id CHAR(32) NOT NULL,
|
|
position INTEGER NOT NULL,
|
|
role VARCHAR NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id)
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
stamped_revisions = []
|
|
monkeypatch.setattr(
|
|
migrations.command,
|
|
"stamp",
|
|
lambda _config, revision: stamped_revisions.append(revision),
|
|
)
|
|
|
|
migrations._stamp_legacy_database_if_needed(
|
|
_alembic_config(database_url), database_url
|
|
)
|
|
|
|
assert stamped_revisions == [migrations.REVISION_CHAT_HISTORY]
|
|
|
|
|
|
def test_upgrade_from_template_v2_revision_adds_slide_ui(tmp_path):
|
|
database_url = f"sqlite:///{tmp_path / 'slide-ui.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(text("CREATE TABLE slides (id TEXT PRIMARY KEY)"))
|
|
connection.execute(
|
|
text("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)")
|
|
)
|
|
connection.execute(
|
|
text("INSERT INTO alembic_version (version_num) VALUES (:revision)"),
|
|
{"revision": migrations.REVISION_TEMPLATE_V2},
|
|
)
|
|
|
|
command.upgrade(_alembic_config(database_url), "head")
|
|
|
|
with engine.connect() as connection:
|
|
version = connection.execute(
|
|
text("SELECT version_num FROM alembic_version")
|
|
).scalar_one()
|
|
slide_columns = {
|
|
row[1]
|
|
for row in connection.execute(text("PRAGMA table_info(slides)"))
|
|
}
|
|
|
|
assert version == migrations.REVISION_ASYNC_TASKS
|
|
assert "ui" in slide_columns
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_upgrade_from_font_uploads_revision_converts_template_v2_ids_to_strings(
|
|
tmp_path,
|
|
):
|
|
database_url = f"sqlite:///{tmp_path / 'template-v2-string-ids.db'}"
|
|
template_id = "12345678123456781234567812345678"
|
|
expected_template_id = "12345678-1234-5678-1234-567812345678"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE template_v2 (
|
|
id CHAR(32) NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
description VARCHAR,
|
|
raw_layouts JSON,
|
|
components JSON,
|
|
merged_components JSON,
|
|
layouts JSON,
|
|
assets JSON,
|
|
created_at DATETIME NOT NULL,
|
|
updated_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id)
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO template_v2 (
|
|
id, name, layouts, created_at, updated_at
|
|
)
|
|
VALUES (
|
|
:template_id,
|
|
'Legacy V2',
|
|
'{"layouts": []}',
|
|
'2026-07-09 00:00:00',
|
|
'2026-07-09 00:00:00'
|
|
)
|
|
"""
|
|
),
|
|
{"template_id": template_id},
|
|
)
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE chat_history_messages (
|
|
id CHAR(32) NOT NULL,
|
|
presentation_id CHAR(32),
|
|
template_v2_id CHAR(32),
|
|
conversation_id CHAR(32) NOT NULL,
|
|
position INTEGER NOT NULL,
|
|
role VARCHAR NOT NULL,
|
|
content TEXT NOT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
tool_calls JSON,
|
|
PRIMARY KEY (id)
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
INSERT INTO chat_history_messages (
|
|
id,
|
|
template_v2_id,
|
|
conversation_id,
|
|
position,
|
|
role,
|
|
content,
|
|
created_at
|
|
)
|
|
VALUES (
|
|
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
:template_id,
|
|
'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
1,
|
|
'user',
|
|
'hello',
|
|
'2026-07-09 00:00:00'
|
|
)
|
|
"""
|
|
),
|
|
{"template_id": template_id},
|
|
)
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE INDEX ix_chat_history_messages_template_v2_id
|
|
ON chat_history_messages (template_v2_id)
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
text("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)")
|
|
)
|
|
connection.execute(
|
|
text("INSERT INTO alembic_version (version_num) VALUES (:revision)"),
|
|
{"revision": migrations.REVISION_FONT_UPLOADS},
|
|
)
|
|
|
|
command.upgrade(_alembic_config(database_url), "head")
|
|
|
|
with engine.connect() as connection:
|
|
version = connection.execute(
|
|
text("SELECT version_num FROM alembic_version")
|
|
).scalar_one()
|
|
stored_template_id = connection.execute(
|
|
text("SELECT id FROM template_v2")
|
|
).scalar_one()
|
|
stored_chat_template_id = connection.execute(
|
|
text("SELECT template_v2_id FROM chat_history_messages")
|
|
).scalar_one()
|
|
template_id_type = next(
|
|
row[2]
|
|
for row in connection.execute(text("PRAGMA table_info(template_v2)"))
|
|
if row[1] == "id"
|
|
)
|
|
chat_template_id_type = next(
|
|
row[2]
|
|
for row in connection.execute(
|
|
text("PRAGMA table_info(chat_history_messages)")
|
|
)
|
|
if row[1] == "template_v2_id"
|
|
)
|
|
template_columns = {
|
|
row[1]
|
|
for row in connection.execute(text("PRAGMA table_info(template_v2)"))
|
|
}
|
|
|
|
assert version == migrations.REVISION_ASYNC_TASKS
|
|
assert stored_template_id == expected_template_id
|
|
assert stored_chat_template_id == expected_template_id
|
|
assert template_id_type == "VARCHAR"
|
|
assert chat_template_id_type == "VARCHAR"
|
|
assert "is_default" in template_columns
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
def test_unversioned_database_with_old_template_v2_stamps_before_consolidated(
|
|
tmp_path, monkeypatch
|
|
):
|
|
database_url = f"sqlite:///{tmp_path / 'legacy-template-v2.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE template_v2 (
|
|
id CHAR(32) NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
raw_layouts JSON,
|
|
layouts JSON NOT NULL,
|
|
created_at DATETIME NOT NULL,
|
|
updated_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id)
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
stamped_revisions = []
|
|
monkeypatch.setattr(
|
|
migrations.command,
|
|
"stamp",
|
|
lambda _config, revision: stamped_revisions.append(revision),
|
|
)
|
|
|
|
migrations._stamp_legacy_database_if_needed(
|
|
_alembic_config(database_url), database_url
|
|
)
|
|
|
|
assert stamped_revisions == [migrations.REVISION_CHAT_HISTORY]
|
|
|
|
|
|
def test_unversioned_database_with_template_v2_artifacts_stamps_before_consolidated(
|
|
tmp_path, monkeypatch
|
|
):
|
|
database_url = f"sqlite:///{tmp_path / 'legacy-template-v2-artifacts.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(text("CREATE TABLE presentations (id TEXT PRIMARY KEY)"))
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE template_v2 (
|
|
id CHAR(32) NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
raw_layouts JSON,
|
|
layouts JSON NOT NULL,
|
|
cluster_candidates JSON,
|
|
clusters JSON,
|
|
components JSON,
|
|
created_at DATETIME NOT NULL,
|
|
updated_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id)
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
finally:
|
|
engine.dispose()
|
|
|
|
stamped_revisions = []
|
|
monkeypatch.setattr(
|
|
migrations.command,
|
|
"stamp",
|
|
lambda _config, revision: stamped_revisions.append(revision),
|
|
)
|
|
|
|
migrations._stamp_legacy_database_if_needed(
|
|
_alembic_config(database_url), database_url
|
|
)
|
|
|
|
assert stamped_revisions == [migrations.REVISION_CHAT_HISTORY]
|
|
|
|
|
|
def test_removed_intermediate_revision_upgrades_through_consolidated_migration(
|
|
tmp_path,
|
|
):
|
|
database_url = f"sqlite:///{tmp_path / 'removed-template-v2-revision.db'}"
|
|
engine = create_engine(database_url)
|
|
try:
|
|
with engine.begin() as connection:
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE presentations (
|
|
id TEXT PRIMARY KEY,
|
|
version VARCHAR NOT NULL
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
text(
|
|
"""
|
|
CREATE TABLE template_v2 (
|
|
id CHAR(32) NOT NULL,
|
|
name VARCHAR NOT NULL,
|
|
raw_layouts JSON,
|
|
layouts JSON NOT NULL,
|
|
cluster_candidates JSON,
|
|
clusters JSON,
|
|
components JSON,
|
|
created_at DATETIME NOT NULL,
|
|
updated_at DATETIME NOT NULL,
|
|
PRIMARY KEY (id)
|
|
)
|
|
"""
|
|
)
|
|
)
|
|
connection.execute(
|
|
text("CREATE TABLE alembic_version (version_num VARCHAR(32) NOT NULL)")
|
|
)
|
|
connection.execute(
|
|
text("INSERT INTO alembic_version (version_num) VALUES ('2d7c8f9a0b1c')")
|
|
)
|
|
|
|
config = _alembic_config(database_url)
|
|
migrations._repair_orphan_alembic_revision(config, database_url)
|
|
command.upgrade(config, "head")
|
|
|
|
with engine.connect() as connection:
|
|
version = connection.execute(
|
|
text("SELECT version_num FROM alembic_version")
|
|
).scalar_one()
|
|
template_columns = {
|
|
row[1]
|
|
for row in connection.execute(text("PRAGMA table_info(template_v2)"))
|
|
}
|
|
|
|
assert version == migrations.REVISION_ASYNC_TASKS
|
|
assert {"description", "components", "assets"}.issubset(template_columns)
|
|
assert "is_default" in template_columns
|
|
assert "cluster_candidates" not in template_columns
|
|
assert "clusters" not in template_columns
|
|
finally:
|
|
engine.dispose()
|