Rename the equipment domain to machines; retype the models catalog (ADR-011)
All checks were successful
CI / backend (push) Successful in 23s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s

The equipment plugin is now the machines plugin, ending the UI-vs-code
vocabulary split while the contract is pre-1.0 and nothing external
depends on the old names.

- plugins/equipment -> plugins/machines: manifest, class, /api/machines,
  machines.* permissions, registry key (with an auto-migrating load shim
  for existing installs).
- Tables: equipment -> machines (equipmentid -> machineid) and
  equipmenttypes -> machinetypes, renamed in the plugin's own migration
  chain (machines0002rename), idempotent for both upgrading and fresh
  installs.
- The legacy core machinetypes lookup actually types the vendor MODELS
  catalog, so it is renamed losslessly to modeltypes
  (models.modeltypeid, /api/modeltypes, Model Types settings page)
  rather than collapsed, freeing the machinetypes name. Core migration
  7d17_machines_rename also flips data in place: assettypes row
  equipment -> machine, auditlog entitytype, identifier_/search_
  settings keys, permission rows, and renames alembic_version_equipment.
- Frontend: machinesApi/modeltypesApi, item.machine response shape,
  assettype value compares 'equipment' -> 'machine' (map, search,
  custom fields, relationships), routes machines.js with plugin gating
  retagged, /print/machine-badge, Machine Types (subtypes) and Model
  Types (catalog) settings pages, machines-by-type report id.
- Docs swept; ADRs left as history per the authoring rule.

Upgrade: flask db upgrade then flask plugin upgrade-all.

Verified: dev DB flipped live (262 machines, 35 modeltypes, 95 models
retyped, zero equipment tables remain); fresh scratch-MySQL install
produces the new names; 341 tests green; naming/style green; frontend
builds; live E2E on machines list/detail, PC relationships, map,
reports, and both settings pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-11 15:17:42 -04:00
parent 3c43c8d5c8
commit 48d3160bc5
84 changed files with 4755 additions and 4317 deletions

View File

@@ -0,0 +1,14 @@
"""Alembic environment for the machines plugin migration chain.
Delegates to the shared runner in shopdb.plugins.alembic_template, which
filters the metadata to this plugin's tables and drives Alembic against the
per-plugin version table alembic_version_machines. See ADR-008 for the
ownership cutover between the core chain and per-plugin chains.
"""
import os
os.environ['PLUGIN_NAME'] = 'machines'
from shopdb.plugins.alembic_template import run_migrations # noqa: E402
run_migrations()

View File

@@ -0,0 +1,24 @@
"""${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 identifiers, used by Alembic.
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

@@ -0,0 +1,32 @@
"""machines plugin anchor (ownership cutover; authored as equipment).
Stamp-only no-op. The core Alembic chain (baseline .. 7d16_directoryemployees)
already created every table this plugin owns at the cutover point, so there is
nothing to build here. This revision gives the plugin's own chain a base that
`flask plugin upgrade-all` can stamp into alembic_version_machines. From this
anchor forward, new machines schema changes land as 000N revisions in this
directory, never in the core chain. See ADR-008.
The revision id keeps its original 'equipment0001anchor' string: ids are
arbitrary and existing installs already carry it in their version table
(which the core chain renames to alembic_version_machines).
"""
from alembic import op # noqa: F401
import sqlalchemy as sa # noqa: F401
# revision identifiers, used by Alembic.
revision = 'equipment0001anchor'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# No-op: the core chain owns every table that exists at the cutover.
pass
def downgrade():
# No-op: this anchor never unwinds core-owned tables.
pass

View File

@@ -0,0 +1,168 @@
"""Rename equipment tables to machines
Plugin half of the approved equipment -> machines rename (the core half is
migrations/versions/7d17_machines_rename.py):
equipment -> machines (equipmentid -> machineid,
equipmenttypeid -> machinetypeid)
equipmenttypes -> machinetypes (equipmenttypeid -> machinetypeid,
equipmenttype -> machinetype)
Runs on BOTH upgraded installs and fresh installs: the core baseline creates
these tables under their old names (frozen DDL), so this revision always does
the physical rename. Idempotent: skips when the tables already carry the new
names (e.g. test DBs built by db.create_all() from current models). Refuses
to run while the core machinetypes table still exists - `flask db upgrade`
(which renames it to modeltypes) must land first.
Revision ID: machines0002rename
Revises: equipment0001anchor
"""
from alembic import op
import sqlalchemy as sa
revision = 'machines0002rename'
down_revision = 'equipment0001anchor'
branch_labels = None
depends_on = None
def _fk_names(insp, table, referred_table):
# find FK constraint names on table pointing at referred_table
return [fk['name'] for fk in insp.get_foreign_keys(table)
if fk.get('referred_table') == referred_table and fk.get('name')]
def upgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
tables = set(insp.get_table_names())
if 'equipment' not in tables and 'equipmenttypes' not in tables:
# already renamed, or fresh schema built straight from current models
return
if 'machinetypes' in tables:
# core chain still owns the machinetypes name (not yet renamed to
# modeltypes) - renaming equipmenttypes onto it would collide
raise RuntimeError(
"Core migration 7d17_machines_rename has not run: the legacy "
"machinetypes table still exists. Run `flask db upgrade` before "
"`flask plugin upgrade-all`."
)
if bind.dialect.name == 'mysql':
_upgrade_mysql(bind, insp)
else:
_upgrade_generic(insp)
def _upgrade_mysql(bind, insp):
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0")
# snapshot index names before the renames invalidate the table name
machine_indexes = {ix['name'] for ix in insp.get_indexes('equipment')}
# drop the equipment -> equipmenttypes FK before renaming the parent cols
for name in _fk_names(insp, 'equipment', 'equipmenttypes'):
bind.exec_driver_sql(f"ALTER TABLE equipment DROP FOREIGN KEY {name}")
bind.exec_driver_sql("RENAME TABLE equipmenttypes TO machinetypes")
bind.exec_driver_sql("RENAME TABLE equipment TO machines")
bind.exec_driver_sql(
"ALTER TABLE machinetypes "
"CHANGE equipmenttypeid machinetypeid INT NOT NULL AUTO_INCREMENT, "
"CHANGE equipmenttype machinetype VARCHAR(100) NOT NULL, "
"DROP KEY equipmenttype, "
"ADD UNIQUE KEY machinetype (machinetype)"
)
parts = [
"CHANGE equipmentid machineid INT NOT NULL AUTO_INCREMENT",
"CHANGE equipmenttypeid machinetypeid INT NULL",
]
# rename the model-declared index names to match the Machine model
if 'idx_equipment_type' in machine_indexes:
parts += ["DROP KEY idx_equipment_type",
"ADD KEY idx_machine_type (machinetypeid)"]
if 'idx_equipment_vendor' in machine_indexes:
parts += ["DROP KEY idx_equipment_vendor",
"ADD KEY idx_machine_vendor (vendorid)"]
if 'ix_equipment_assetid' in machine_indexes:
parts += ["DROP KEY ix_equipment_assetid",
"ADD UNIQUE KEY ix_machines_assetid (assetid)"]
parts += [
"ADD CONSTRAINT fk_machines_machinetypeid "
"FOREIGN KEY (machinetypeid) REFERENCES machinetypes (machinetypeid)"
]
bind.exec_driver_sql("ALTER TABLE machines " + ", ".join(parts))
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1")
def _upgrade_generic(insp):
# SQLite (and anything else): batch rename via ALTER ... RENAME
op.rename_table('equipmenttypes', 'machinetypes')
op.rename_table('equipment', 'machines')
with op.batch_alter_table('machinetypes') as batch_op:
batch_op.alter_column('equipmenttypeid', new_column_name='machinetypeid',
existing_type=sa.Integer(), existing_nullable=False)
batch_op.alter_column('equipmenttype', new_column_name='machinetype',
existing_type=sa.String(100),
existing_nullable=False)
with op.batch_alter_table('machines') as batch_op:
batch_op.alter_column('equipmentid', new_column_name='machineid',
existing_type=sa.Integer(), existing_nullable=False)
batch_op.alter_column('equipmenttypeid', new_column_name='machinetypeid',
existing_type=sa.Integer(), existing_nullable=True)
def downgrade():
bind = op.get_bind()
insp = sa.inspect(bind)
tables = set(insp.get_table_names())
if 'machines' not in tables and 'machinetypes' not in tables:
return
if bind.dialect.name != 'mysql':
raise NotImplementedError(
"downgrade implemented for MySQL only (dev/prod dialect)")
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=0")
machine_indexes = {ix['name'] for ix in insp.get_indexes('machines')}
for name in _fk_names(insp, 'machines', 'machinetypes'):
bind.exec_driver_sql(f"ALTER TABLE machines DROP FOREIGN KEY {name}")
bind.exec_driver_sql("RENAME TABLE machinetypes TO equipmenttypes")
bind.exec_driver_sql("RENAME TABLE machines TO equipment")
bind.exec_driver_sql(
"ALTER TABLE equipmenttypes "
"CHANGE machinetypeid equipmenttypeid INT NOT NULL AUTO_INCREMENT, "
"CHANGE machinetype equipmenttype VARCHAR(100) NOT NULL, "
"DROP KEY machinetype, "
"ADD UNIQUE KEY equipmenttype (equipmenttype)"
)
parts = [
"CHANGE machineid equipmentid INT NOT NULL AUTO_INCREMENT",
"CHANGE machinetypeid equipmenttypeid INT NULL",
]
if 'idx_machine_type' in machine_indexes:
parts += ["DROP KEY idx_machine_type",
"ADD KEY idx_equipment_type (equipmenttypeid)"]
if 'idx_machine_vendor' in machine_indexes:
parts += ["DROP KEY idx_machine_vendor",
"ADD KEY idx_equipment_vendor (vendorid)"]
if 'ix_machines_assetid' in machine_indexes:
parts += ["DROP KEY ix_machines_assetid",
"ADD UNIQUE KEY ix_equipment_assetid (assetid)"]
parts += [
"ADD CONSTRAINT equipment_ibfk_2 "
"FOREIGN KEY (equipmenttypeid) REFERENCES equipmenttypes (equipmenttypeid)"
]
bind.exec_driver_sql("ALTER TABLE equipment " + ", ".join(parts))
bind.exec_driver_sql("SET FOREIGN_KEY_CHECKS=1")