Consolidate bundled plugin schema into the core migration chain

The core chain already owns and reproduces the full bundled schema (deploys run
`flask db upgrade` only). The per-plugin Alembic baselines duplicated those
tables, so `flask plugin upgrade-all` would conflict - a footgun. Remove the 6
bundled plugin migration dirs; the per-plugin Alembic helpers
(alembic_template, PluginMigrationRunner) remain for external/filesystem
plugins. upgrade-all now cleanly no-ops for bundled plugins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-06-26 14:48:07 -04:00
parent 919e0b8600
commit 4f1638bda0
28 changed files with 7 additions and 662 deletions

View File

@@ -1,30 +0,0 @@
[alembic]
script_location = .
prepend_sys_path = .
file_template = %%(rev)s_%%(slug)s
[logging]
keys = root
[loggers]
keys = root
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
qualname =
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = INFO
formatter = generic
[formatter_generic]
format = %%(levelname)-5.5s [%%(name)s] %%(message)s

View File

@@ -1,12 +0,0 @@
"""Alembic env.py for the computers plugin.
Thin shim that sets PLUGIN_NAME then delegates to the shared template at
shopdb.plugins.alembic_template, which filters MetaData to only this
plugin's tables and runs Alembic against the Flask app's engine.
"""
import os
os.environ['PLUGIN_NAME'] = 'computers'
from shopdb.plugins.alembic_template import run_migrations
run_migrations()

View File

@@ -1,23 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,27 +0,0 @@
"""computers plugin: baseline schema
Creates every table owned by the computers plugin per
shopdb.plugins.alembic_template.PLUGIN_TABLE_OWNERS. The table definitions
are derived from the SQLAlchemy models at migration runtime so this stays
in lockstep with the model layer without duplication.
Revision ID: 0001_baseline_computers
Revises:
Create Date: 2026-05-30
"""
from shopdb.plugins.alembic_template import create_plugin_tables, drop_plugin_tables
revision = '0001_baseline_computers'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
create_plugin_tables('computers')
def downgrade():
drop_plugin_tables('computers')

View File

@@ -1,44 +0,0 @@
"""Add PC hardware make/model + installed-app version string
Adds computers.vendorid + computers.modelnumberid (PCs carry vendor/model like
equipment) and computerinstalledapps.installedversion (raw version string from
automated collection, when there is no curated AppVersion). Supports moving the
PC form and the collector off the legacy Machine model onto the asset/computer
model (ADR-001).
Revision ID: 0002_pc_hardware
Revises: 0001_baseline_computers
Create Date: 2026-06-26
"""
from alembic import op
import sqlalchemy as sa
revision = '0002_pc_hardware'
down_revision = '0001_baseline_computers'
branch_labels = None
depends_on = None
def upgrade():
with op.batch_alter_table('computers') as batch_op:
batch_op.add_column(sa.Column('vendorid', sa.Integer(), nullable=True))
batch_op.add_column(sa.Column('modelnumberid', sa.Integer(), nullable=True))
batch_op.create_foreign_key('fk_computers_vendor', 'vendors',
['vendorid'], ['vendorid'])
batch_op.create_foreign_key('fk_computers_model', 'models',
['modelnumberid'], ['modelnumberid'])
with op.batch_alter_table('computerinstalledapps') as batch_op:
batch_op.add_column(sa.Column('installedversion', sa.String(length=100),
nullable=True))
def downgrade():
with op.batch_alter_table('computerinstalledapps') as batch_op:
batch_op.drop_column('installedversion')
with op.batch_alter_table('computers') as batch_op:
batch_op.drop_constraint('fk_computers_model', type_='foreignkey')
batch_op.drop_constraint('fk_computers_vendor', type_='foreignkey')
batch_op.drop_column('modelnumberid')
batch_op.drop_column('vendorid')

View File

@@ -1,30 +0,0 @@
[alembic]
script_location = .
prepend_sys_path = .
file_template = %%(rev)s_%%(slug)s
[logging]
keys = root
[loggers]
keys = root
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
qualname =
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = INFO
formatter = generic
[formatter_generic]
format = %%(levelname)-5.5s [%%(name)s] %%(message)s

View File

@@ -1,12 +0,0 @@
"""Alembic env.py for the equipment plugin.
Thin shim that sets PLUGIN_NAME then delegates to the shared template at
shopdb.plugins.alembic_template, which filters MetaData to only this
plugin's tables and runs Alembic against the Flask app's engine.
"""
import os
os.environ['PLUGIN_NAME'] = 'equipment'
from shopdb.plugins.alembic_template import run_migrations
run_migrations()

View File

@@ -1,23 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,27 +0,0 @@
"""equipment plugin: baseline schema
Creates every table owned by the equipment plugin per
shopdb.plugins.alembic_template.PLUGIN_TABLE_OWNERS. The table definitions
are derived from the SQLAlchemy models at migration runtime so this stays
in lockstep with the model layer without duplication.
Revision ID: 0001_baseline_equipment
Revises:
Create Date: 2026-05-30
"""
from shopdb.plugins.alembic_template import create_plugin_tables, drop_plugin_tables
revision = '0001_baseline_equipment'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
create_plugin_tables('equipment')
def downgrade():
drop_plugin_tables('equipment')

View File

@@ -1,30 +0,0 @@
[alembic]
script_location = .
prepend_sys_path = .
file_template = %%(rev)s_%%(slug)s
[logging]
keys = root
[loggers]
keys = root
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
qualname =
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = INFO
formatter = generic
[formatter_generic]
format = %%(levelname)-5.5s [%%(name)s] %%(message)s

View File

@@ -1,12 +0,0 @@
"""Alembic env.py for the network plugin.
Thin shim that sets PLUGIN_NAME then delegates to the shared template at
shopdb.plugins.alembic_template, which filters MetaData to only this
plugin's tables and runs Alembic against the Flask app's engine.
"""
import os
os.environ['PLUGIN_NAME'] = 'network'
from shopdb.plugins.alembic_template import run_migrations
run_migrations()

View File

@@ -1,23 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,27 +0,0 @@
"""network plugin: baseline schema
Creates every table owned by the network plugin per
shopdb.plugins.alembic_template.PLUGIN_TABLE_OWNERS. The table definitions
are derived from the SQLAlchemy models at migration runtime so this stays
in lockstep with the model layer without duplication.
Revision ID: 0001_baseline_network
Revises:
Create Date: 2026-05-30
"""
from shopdb.plugins.alembic_template import create_plugin_tables, drop_plugin_tables
revision = '0001_baseline_network'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
create_plugin_tables('network')
def downgrade():
drop_plugin_tables('network')

View File

@@ -1,30 +0,0 @@
[alembic]
script_location = .
prepend_sys_path = .
file_template = %%(rev)s_%%(slug)s
[logging]
keys = root
[loggers]
keys = root
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
qualname =
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = INFO
formatter = generic
[formatter_generic]
format = %%(levelname)-5.5s [%%(name)s] %%(message)s

View File

@@ -1,12 +0,0 @@
"""Alembic env.py for the notifications plugin.
Thin shim that sets PLUGIN_NAME then delegates to the shared template at
shopdb.plugins.alembic_template, which filters MetaData to only this
plugin's tables and runs Alembic against the Flask app's engine.
"""
import os
os.environ['PLUGIN_NAME'] = 'notifications'
from shopdb.plugins.alembic_template import run_migrations
run_migrations()

View File

@@ -1,23 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,27 +0,0 @@
"""notifications plugin: baseline schema
Creates every table owned by the notifications plugin per
shopdb.plugins.alembic_template.PLUGIN_TABLE_OWNERS. The table definitions
are derived from the SQLAlchemy models at migration runtime so this stays
in lockstep with the model layer without duplication.
Revision ID: 0001_baseline_notifications
Revises:
Create Date: 2026-05-30
"""
from shopdb.plugins.alembic_template import create_plugin_tables, drop_plugin_tables
revision = '0001_baseline_notifications'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
create_plugin_tables('notifications')
def downgrade():
drop_plugin_tables('notifications')

View File

@@ -1 +0,0 @@
"""Printers plugin migrations."""

View File

@@ -1,30 +0,0 @@
[alembic]
script_location = .
prepend_sys_path = .
file_template = %%(rev)s_%%(slug)s
[logging]
keys = root
[loggers]
keys = root
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
qualname =
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = INFO
formatter = generic
[formatter_generic]
format = %%(levelname)-5.5s [%%(name)s] %%(message)s

View File

@@ -1,12 +0,0 @@
"""Alembic env.py for the printers plugin.
Thin shim that sets PLUGIN_NAME then delegates to the shared template at
shopdb.plugins.alembic_template, which filters MetaData to only this
plugin's tables and runs Alembic against the Flask app's engine.
"""
import os
os.environ['PLUGIN_NAME'] = 'printers'
from shopdb.plugins.alembic_template import run_migrations
run_migrations()

View File

@@ -1,23 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,27 +0,0 @@
"""printers plugin: baseline schema
Creates every table owned by the printers plugin per
shopdb.plugins.alembic_template.PLUGIN_TABLE_OWNERS. The table definitions
are derived from the SQLAlchemy models at migration runtime so this stays
in lockstep with the model layer without duplication.
Revision ID: 0001_baseline_printers
Revises:
Create Date: 2026-05-30
"""
from shopdb.plugins.alembic_template import create_plugin_tables, drop_plugin_tables
revision = '0001_baseline_printers'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
create_plugin_tables('printers')
def downgrade():
drop_plugin_tables('printers')

View File

@@ -1,44 +0,0 @@
"""printers plugin: add modelsupplies table
Data-driven model -> toner/drum/waste part-number mapping. Replaces the old
hardcoded supply_parts table.
Revision ID: 0002_modelsupplies_printers
Revises: 0001_baseline_printers
Create Date: 2026-06-25
"""
import sqlalchemy as sa
from alembic import op
revision = '0002_modelsupplies_printers'
down_revision = '0001_baseline_printers'
branch_labels = None
depends_on = None
def upgrade():
op.create_table(
'modelsupplies',
sa.Column('modelsupplyid', sa.Integer, primary_key=True),
sa.Column('modelnumberid', sa.Integer,
sa.ForeignKey('models.modelnumberid'), nullable=False),
sa.Column('supplytype', sa.String(20), nullable=False, server_default='toner'),
sa.Column('color', sa.String(20), nullable=False, server_default='none'),
sa.Column('capacitytier', sa.String(20), nullable=False, server_default='standard'),
sa.Column('partnumber', sa.String(50), nullable=False),
sa.Column('marketingname', sa.String(120)),
sa.Column('pageyield', sa.Integer),
sa.Column('notes', sa.Text),
sa.Column('createddate', sa.DateTime, nullable=False),
sa.Column('modifieddate', sa.DateTime, nullable=False),
sa.Column('isactive', sa.Boolean, nullable=False, server_default=sa.true()),
sa.UniqueConstraint('modelnumberid', 'partnumber', name='uq_modelsupply_part'),
)
op.create_index('idx_modelsupplies_modelnumberid', 'modelsupplies', ['modelnumberid'])
def downgrade():
op.drop_index('idx_modelsupplies_modelnumberid', table_name='modelsupplies')
op.drop_table('modelsupplies')

View File

@@ -1,30 +0,0 @@
[alembic]
script_location = .
prepend_sys_path = .
file_template = %%(rev)s_%%(slug)s
[logging]
keys = root
[loggers]
keys = root
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = INFO
handlers = console
qualname =
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = INFO
formatter = generic
[formatter_generic]
format = %%(levelname)-5.5s [%%(name)s] %%(message)s

View File

@@ -1,12 +0,0 @@
"""Alembic env.py for the usb plugin.
Thin shim that sets PLUGIN_NAME then delegates to the shared template at
shopdb.plugins.alembic_template, which filters MetaData to only this
plugin's tables and runs Alembic against the Flask app's engine.
"""
import os
os.environ['PLUGIN_NAME'] = 'usb'
from shopdb.plugins.alembic_template import run_migrations
run_migrations()

View File

@@ -1,23 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@@ -1,27 +0,0 @@
"""usb plugin: baseline schema
Creates every table owned by the usb plugin per
shopdb.plugins.alembic_template.PLUGIN_TABLE_OWNERS. The table definitions
are derived from the SQLAlchemy models at migration runtime so this stays
in lockstep with the model layer without duplication.
Revision ID: 0001_baseline_usb
Revises:
Create Date: 2026-05-30
"""
from shopdb.plugins.alembic_template import create_plugin_tables, drop_plugin_tables
revision = '0001_baseline_usb'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
create_plugin_tables('usb')
def downgrade():
drop_plugin_tables('usb')

View File

@@ -1,11 +1,10 @@
"""Per-plugin Alembic chain wiring tests.
"""Plugin table-ownership 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.
Bundled plugin schema is owned by the core migration chain (deploys run
`flask db upgrade` only, which reproduces the full schema). The per-plugin
Alembic helpers remain for external/filesystem plugins; these tests pin the
PLUGIN_TABLE_OWNERS registry those helpers consume.
"""
from pathlib import Path
import pytest
from shopdb.plugins.alembic_template import (
@@ -20,29 +19,16 @@ BUNDLED_PLUGINS = ('computers', 'equipment', 'network', 'notifications', 'printe
@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."""
one table, documenting which tables it contributes to the schema."""
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."""
registry and what the models declare."""
with app.app_context():
md = _get_plugin_metadata(plugin)
owned = set(PLUGIN_TABLE_OWNERS[plugin])