test_material_number_migration.py 3.2 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
  1. """Migration tests for the spool material_number column (#2870).
  2. A legacy database whose spool table predates the column must gain it on
  3. upgrade, existing rows must read back as NULL, and re-running the migration
  4. must be a no-op (idempotent _safe_execute).
  5. """
  6. from __future__ import annotations
  7. import pytest
  8. from sqlalchemy import text
  9. from sqlalchemy.ext.asyncio import create_async_engine
  10. from backend.app.core.database import run_migrations
  11. @pytest.fixture(autouse=True)
  12. def force_sqlite_dialect(monkeypatch):
  13. from backend.app.core import db_dialect
  14. monkeypatch.setattr(db_dialect, "is_sqlite", lambda: True)
  15. monkeypatch.setattr(db_dialect, "is_postgres", lambda: False)
  16. from backend.app.core import database as database_module
  17. monkeypatch.setattr(database_module, "is_sqlite", lambda: True)
  18. def _register_all_models():
  19. import backend.app.models # noqa: F401
  20. from backend.app.models import ( # noqa: F401
  21. external_link,
  22. location,
  23. print_log,
  24. print_queue,
  25. project_bom,
  26. slot_preset,
  27. spoolman_k_profile,
  28. spoolman_slot_assignment,
  29. virtual_printer,
  30. )
  31. @pytest.fixture
  32. async def engine_with_legacy_spool_table():
  33. """create_all builds the current schema; dropping the column afterwards
  34. reproduces a database from a Bambuddy version that predates #2870."""
  35. from backend.app.core.database import Base
  36. _register_all_models()
  37. engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
  38. async with engine.begin() as conn:
  39. await conn.run_sync(Base.metadata.create_all)
  40. await conn.execute(text("ALTER TABLE spool DROP COLUMN material_number"))
  41. await conn.execute(
  42. text(
  43. """
  44. INSERT INTO spool (
  45. material, label_weight, core_weight,
  46. weight_used, weight_used_baseline, weight_locked
  47. )
  48. VALUES ('PLA', 1000, 250, 0, 0, 0)
  49. """
  50. )
  51. )
  52. yield engine
  53. await engine.dispose()
  54. async def test_migration_adds_material_number_column(engine_with_legacy_spool_table):
  55. async with engine_with_legacy_spool_table.begin() as conn:
  56. await run_migrations(conn)
  57. async with engine_with_legacy_spool_table.connect() as conn:
  58. rows = (await conn.execute(text("SELECT id, material, material_number FROM spool"))).all()
  59. assert len(rows) == 1
  60. # Pre-existing rows read back with NULL, not an error or a default.
  61. assert rows[0].material_number is None
  62. async def test_migration_is_idempotent(engine_with_legacy_spool_table):
  63. async with engine_with_legacy_spool_table.begin() as conn:
  64. await run_migrations(conn)
  65. # A value written after the first run must survive the second run — the
  66. # duplicate ALTER TABLE is swallowed, not applied destructively.
  67. async with engine_with_legacy_spool_table.begin() as conn:
  68. await conn.execute(text("UPDATE spool SET material_number = '15'"))
  69. async with engine_with_legacy_spool_table.begin() as conn:
  70. await run_migrations(conn)
  71. async with engine_with_legacy_spool_table.connect() as conn:
  72. value = (await conn.execute(text("SELECT material_number FROM spool"))).scalar_one()
  73. assert value == "15"