Phase 7B: per-plugin Alembic chains for bundled plugins
Each of the six bundled plugins (computers, equipment, network,
notifications, printers, usb) now has its own Alembic chain with a
baseline migration. Sister sites adopting one of these plugins can
manage its schema via `flask plugin migrate <name>` instead of relying
on db.create_all to bootstrap everything.
Existing single-site deploys that bootstrap via db.create_all continue
to work unchanged. The chains coexist; the bootstrap path stays the
operator's choice.
Framework
- shopdb/plugins/alembic_template.py: shared env.py logic + helpers.
PLUGIN_TABLE_OWNERS pins which tables belong to which plugin (explicit
registry, not import-side-effect). _get_plugin_metadata filters
db.metadata to only the named plugin's tables. create_plugin_tables /
drop_plugin_tables emit DDL via SQLAlchemy CreateTable so the table
definitions stay sourced from the models, not duplicated.
- shopdb/plugins/__init__.py: PluginManager.upgrade_all_plugins() runs
pending migrations across every discovered plugin and returns a status
dict. Idempotent (Alembic skips applied revisions).
CLI
- `flask plugin upgrade-all` runs pending migrations for every plugin.
Used on a fresh deploy after the core schema is in place.
Per-plugin scaffolding
- plugins/{computers,equipment,network,notifications,printers,usb}/
migrations/{alembic.ini, env.py, script.py.mako, versions/0001_baseline.py}
- Each env.py is a 5-line shim that sets PLUGIN_NAME and delegates to
the shared template. Each 0001_baseline calls create_plugin_tables(name)
/ drop_plugin_tables(name); no duplication of column definitions.
Tests
- tests/test_plugin_migrations.py (18 cases): every bundled plugin has
an entry in PLUGIN_TABLE_OWNERS, has the on-disk Alembic scaffolding,
and the filtered MetaData contains every owned table (catches drift
between the template's table list and what the models declare).
- 129 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
51
tests/test_plugin_migrations.py
Normal file
51
tests/test_plugin_migrations.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Per-plugin Alembic chain wiring tests.
|
||||
|
||||
Pins the bundled-plugin migration setup so a future refactor that breaks
|
||||
the scaffolding fails fast with a clear test rather than a confusing
|
||||
runtime error on a fresh deploy.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from shopdb.plugins.alembic_template import (
|
||||
PLUGIN_TABLE_OWNERS,
|
||||
_get_plugin_metadata,
|
||||
)
|
||||
|
||||
|
||||
BUNDLED_PLUGINS = ('computers', 'equipment', 'network', 'notifications', 'printers', 'usb')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('plugin', BUNDLED_PLUGINS)
|
||||
def test_bundled_plugin_has_table_owner_entry(plugin):
|
||||
"""Every bundled plugin appears in PLUGIN_TABLE_OWNERS with at least
|
||||
one table; otherwise its baseline migration would be a no-op."""
|
||||
assert plugin in PLUGIN_TABLE_OWNERS
|
||||
assert len(PLUGIN_TABLE_OWNERS[plugin]) > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize('plugin', BUNDLED_PLUGINS)
|
||||
def test_bundled_plugin_has_migrations_dir(plugin):
|
||||
"""Each bundled plugin has the on-disk Alembic scaffolding."""
|
||||
root = Path(__file__).resolve().parent.parent / 'plugins' / plugin / 'migrations'
|
||||
assert (root / 'env.py').is_file(), f"{plugin}/migrations/env.py missing"
|
||||
assert (root / 'alembic.ini').is_file(), f"{plugin}/migrations/alembic.ini missing"
|
||||
assert (root / 'script.py.mako').is_file(), f"{plugin}/migrations/script.py.mako missing"
|
||||
versions = root / 'versions'
|
||||
assert versions.is_dir(), f"{plugin}/migrations/versions missing"
|
||||
baseline = versions / '0001_baseline.py'
|
||||
assert baseline.is_file(), f"{plugin}/migrations/versions/0001_baseline.py missing"
|
||||
|
||||
|
||||
@pytest.mark.parametrize('plugin', BUNDLED_PLUGINS)
|
||||
def test_plugin_metadata_has_all_owned_tables(plugin, app):
|
||||
"""The MetaData filtered to a plugin's owned tables actually contains
|
||||
every table named in PLUGIN_TABLE_OWNERS. Catches drift between the
|
||||
template's table list and what the models declare."""
|
||||
with app.app_context():
|
||||
md = _get_plugin_metadata(plugin)
|
||||
owned = set(PLUGIN_TABLE_OWNERS[plugin])
|
||||
present = set(md.tables.keys())
|
||||
missing = owned - present
|
||||
assert not missing, f"Plugin {plugin}: tables in PLUGIN_TABLE_OWNERS not in metadata: {missing}"
|
||||
Reference in New Issue
Block a user