ADR-014: schema-lean per-site (investigation + idempotent create_plugin_tables)
Cross-plugin FK blocker ADR-013 cited is already resolved: the FKs into machines were held only by dead legacy tables (machinerelationships, printerdata, installedapps, communications.machineid) that existing migrations 7a01/7c01 already drop. No live plugin table hard-FKs another plugin. Schema-lean is unblocked. Enabling change: create_plugin_tables now skips already-existing tables (idempotent) so a plugin anchor can create its tables on a fresh lean install and no-op on a database that has them from the pre-cutover baseline. The load-bearing baseline lift is staged as ADR-014 Phase 2.
This commit is contained in:
@@ -23,6 +23,7 @@ Architecture decisions live in `docs/adr/`. Read those before making schema or c
|
|||||||
- ADR-011: Machines rename + modeltypes retyping - ACCEPTED
|
- ADR-011: Machines rename + modeltypes retyping - ACCEPTED
|
||||||
- ADR-012: GE-Enforce manifest ownership in shopdb - ACCEPTED
|
- ADR-012: GE-Enforce manifest ownership in shopdb - ACCEPTED
|
||||||
- ADR-013: Plugin catalog, curated shelf, and lean per-site builds - PROPOSED
|
- ADR-013: Plugin catalog, curated shelf, and lean per-site builds - PROPOSED
|
||||||
|
- ADR-014: Schema-lean per-site builds (retire cross-plugin FKs, lift plugin tables) - PROPOSED
|
||||||
|
|
||||||
## Coding convention
|
## Coding convention
|
||||||
|
|
||||||
|
|||||||
118
docs/adr/ADR-014-schema-lean-per-site.md
Normal file
118
docs/adr/ADR-014-schema-lean-per-site.md
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
# 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(<name>)` / `drop_plugin_tables(<name>)` (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.
|
||||||
@@ -26,6 +26,7 @@ Each ADR captures a single architectural decision: the context, the decision its
|
|||||||
| [011](ADR-011-machines-rename.md) | Machines rename + modeltypes retyping | ACCEPTED |
|
| [011](ADR-011-machines-rename.md) | Machines rename + modeltypes retyping | ACCEPTED |
|
||||||
| [012](ADR-012-geenforce-manifest-ownership.md) | GE-Enforce manifest ownership in shopdb | ACCEPTED |
|
| [012](ADR-012-geenforce-manifest-ownership.md) | GE-Enforce manifest ownership in shopdb | ACCEPTED |
|
||||||
| [013](ADR-013-plugin-catalog-and-lean-builds.md) | Plugin catalog, curated shelf, and lean per-site builds | PROPOSED |
|
| [013](ADR-013-plugin-catalog-and-lean-builds.md) | Plugin catalog, curated shelf, and lean per-site builds | PROPOSED |
|
||||||
|
| [014](ADR-014-schema-lean-per-site.md) | Schema-lean per-site builds (retire cross-plugin FKs, lift plugin tables) | PROPOSED |
|
||||||
|
|
||||||
## Authoring
|
## Authoring
|
||||||
|
|
||||||
|
|||||||
@@ -85,21 +85,24 @@ def _get_plugin_metadata(plugin_name: str) -> MetaData:
|
|||||||
|
|
||||||
|
|
||||||
def create_plugin_tables(plugin_name: str):
|
def create_plugin_tables(plugin_name: str):
|
||||||
"""Emit CreateTable DDL for every table this plugin owns. Idempotent
|
"""Create every table this plugin owns, sourced from the SQLAlchemy models
|
||||||
via Alembic's batch_op.create_table behavior (raises if exists; the
|
(not duplicated DDL). IDEMPOTENT: a table that already exists is skipped, so
|
||||||
baseline migration is meant to run against an empty schema).
|
this is safe on an existing database that has the table from the pre-cutover
|
||||||
|
core baseline as well as on a fresh install (ADR-014 Phase 2).
|
||||||
|
|
||||||
Called from each plugin's 0001_baseline.py upgrade() so the table
|
Called from each plugin's 0001 baseline.py upgrade().
|
||||||
definitions stay sourced from the SQLAlchemy models rather than being
|
|
||||||
duplicated in handwritten Alembic ops.
|
|
||||||
"""
|
"""
|
||||||
from alembic import op
|
from alembic import op
|
||||||
|
from sqlalchemy import inspect
|
||||||
from sqlalchemy.schema import CreateTable
|
from sqlalchemy.schema import CreateTable
|
||||||
|
|
||||||
md = _get_plugin_metadata(plugin_name)
|
md = _get_plugin_metadata(plugin_name)
|
||||||
bind = op.get_bind()
|
bind = op.get_bind()
|
||||||
|
existing = set(inspect(bind).get_table_names())
|
||||||
# Sort by FK dependency so parent tables are created first.
|
# Sort by FK dependency so parent tables are created first.
|
||||||
for table in md.sorted_tables:
|
for table in md.sorted_tables:
|
||||||
|
if table.name in existing:
|
||||||
|
continue
|
||||||
op.execute(str(CreateTable(table).compile(dialect=bind.dialect)))
|
op.execute(str(CreateTable(table).compile(dialect=bind.dialect)))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ rsync -a --delete \
|
|||||||
--exclude 'venv' \
|
--exclude 'venv' \
|
||||||
--exclude 'node_modules' \
|
--exclude 'node_modules' \
|
||||||
--exclude 'frontend/dist*' \
|
--exclude 'frontend/dist*' \
|
||||||
|
--exclude 'frontend/src/.plugins-staged' \
|
||||||
|
--exclude 'frontend/src/router/routes.gen.js' \
|
||||||
--exclude 'instance' \
|
--exclude 'instance' \
|
||||||
--exclude '.env' \
|
--exclude '.env' \
|
||||||
--exclude '__pycache__' \
|
--exclude '__pycache__' \
|
||||||
|
|||||||
Reference in New Issue
Block a user