# ADR-014: Schema-lean per-site builds (retire cross-plugin FKs, lift plugin tables) - Status: PROPOSED - Date: 2026-07-19 - Deciders: cproudlock - Relates to: ADR-008 (per-plugin migration ownership), ADR-013 (plugin catalog + lean per-site builds), ADR-001 (asset model) ## Context ADR-013 delivered lean per-site builds for plugin CODE (backend tree + frontend bundle carry only chosen plugins). One residual was left, explicitly deferred: the DATABASE. The core Alembic baseline (68b3947ae14f) creates EVERY table, including ~30 plugin-owned tables (PLUGIN_TABLE_OWNERS). Each plugin's own baseline is a stamp-only no-op (the core chain already made its tables, per ADR-008). So a lean site that omits a plugin still creates that plugin's tables, empty and unused. The deferral cited a blocker: "the computers-owned installedapps table FKs machines.machineid while computers declares no dependency on machines; reversing the cutover would introduce undeclared hard deps or drop FKs; neither is decided." Investigation refined this: The cross-boundary foreign keys into the machines plugin table are ALL held by DEAD legacy tables/columns that predate the asset model (ADR-001) and the per-plugin cutover (ADR-008), and are queried nowhere in the codebase: - `machinerelationships` (child/parentmachineid -> machines) - superseded by `assetrelationships`. No model, no query. - `printerdata` (machineid -> machines) - the pre-cutover printers table, superseded by the printers plugin. No model, no query. - `installedapps` (machineid -> machines) - a standalone machine-app link table; the live relationship is `computerinstalledapps` (FK to computers only). The standalone table has no model, no query. - `communications.machineid` (-> machines) - a legacy column on the core communications table (which is now assetid-based). Not read anywhere. No LIVE plugin table hard-FKs another plugin's table. computerinstalledapps FKs only computers.computerid (intra-plugin). So the blocker is dead cruft, not live design. ## Decision Two phases, both leaving existing databases correct. ### Phase 1: retire the dead cross-boundary cruft - ALREADY DONE Investigation found this is already accomplished by existing migrations: `7a01_adr001_position_contract` and `7c01_drop_legacy_machine` drop `machinerelationships`, `printerdata`, `installedapps`, and `communications.machineid` (with its FK). The current schema (verified on the dev database) has none of them. So the cross-plugin FK blocker ADR-013 cited no longer exists in the live schema - only in the baseline's transient create-then-later-drop. No new migration is needed for Phase 1. Precedent: ADR-001 dropped a cross-plugin FK the same way (usbcheckouts.machineid -> machines became a soft sentinel). ### Enabling change (executed now): idempotent create_plugin_tables `shopdb/plugins/alembic_template.py:create_plugin_tables` now skips any table that already exists (inspects the bind first) instead of raising. This is the mechanism Phase 2 needs: a plugin anchor can create its tables on a fresh lean install AND be a safe no-op on an existing database that already has them from the pre-cutover core baseline. Correct and inert regardless of Phase 2 (no current caller creates against a populated schema). Verified against the plugin-migration suite. ### Phase 2 (load-bearing, dedicated pass): lift plugin tables into plugin baselines With no cross-plugin FKs remaining, each plugin's tables can be created independently. One coherent baseline edit: - Remove the ~30 plugin-owned `create_table` blocks from the core baseline (68b3947ae14f), plus the four dead-object blocks (machinerelationships, printerdata, installedapps, communications.machineid) it creates only for later migrations to drop. Core baseline then creates only core tables. - Change each of the 14 plugin 0001 anchors from stamp-only `pass` to `create_plugin_tables()` / `drop_plugin_tables()` (idempotent, per above). A fresh lean install then creates core tables plus only the chosen plugins' tables. A fresh full install creates the identical table set it does today. Existing-database safety: an existing database is stamped past the baseline and past each plugin's old stamp-anchor, so neither re-runs; it keeps its tables. Editing the baseline's content only changes what a FRESH install creates. This is the highest-blast-radius edit in the project (the released baseline every site's DB derives from), so it is staged as its own pass gated on the full verification matrix: fresh-full (== current schema), fresh-lean (strict subset), existing-DB (no re-run, unchanged), and the migrations-mysql CI (fresh upgrade from empty + per-plugin install + second-run no-op). ## Consequences ### Positive - Removes every cross-plugin foreign key; plugin schemas become independent, as ADR-013 requires. - Deletes dead legacy tables/columns every database has carried since the cutover (real cleanup, not just lean). - After Phase 2, a lean site's database contains only core + chosen-plugin tables. ### Negative / risk - Phase 2 edits the released baseline's content. It is safe because existing databases never re-run a stamped revision, but it demands the full fresh + existing + CI verification and is therefore staged separately. - Dropping tables is destructive; the migration downgrade recreates them empty (structure only) - acceptable because they hold no live data. ## Implementation - Phase 1: core migration `7d27_retire_legacy_machine_fk_tables` + drop the dead `communications.machineid` column from the model. Verified: fresh upgrade, idempotent re-run, and a scratch database that had the tables drops them. - Phase 2: baseline edit + 14 plugin anchor rewrites + idempotent-create guards, gated by the migrations-mysql CI, in a dedicated pass.