From 83141bacb749a0cf93340677274ff66bf1da6f03 Mon Sep 17 00:00:00 2001 From: cproudlock Date: Fri, 17 Jul 2026 20:31:48 -0400 Subject: [PATCH] Widen settings.description to TEXT; run seeders in CI The dualpath_single_machine setting description is 257 chars but settings.description was varchar(255). On strict MySQL 8 an over-length insert is a hard error 1406 (Data too long), so `flask seed settings` failed on a fresh install; older/relaxed MySQL truncated silently and hid it. Widen the column to TEXT (matches value, already TEXT) via core migration 7d26. CI only ran `flask db upgrade` + plugin install, never the seeders, so it missed this. Add a seed step to the migrations-mysql job so a seeded row that violates a column constraint fails CI on strict MySQL 8 instead of shipping. --- .github/workflows/ci.yml | 9 +++ CLAUDE.md | 4 +- .../7d26_settings_description_text.py | 61 +++++++++++++++++++ shopdb/core/models/setting.py | 2 +- 4 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 migrations/versions/7d26_settings_description_text.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a914e30..bf5fa5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,15 @@ jobs: flask plugin install "$p" done flask plugin upgrade-all + - name: Seed platform data (catches strict-mode data violations) + # Runs the real seeders on strict MySQL 8. A seeded row that exceeds a + # column width is a hard error 1406 here (not the silent truncation + # older/relaxed MySQL gives), so this is what catches over-length + # setting descriptions and similar before they reach a fresh box. + run: | + flask seed permissions + flask seed settings + flask seed reference-data - name: Assert schema built + utf8mb4 run: | python - <<'PY' diff --git a/CLAUDE.md b/CLAUDE.md index 3516a50..c709fde 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,7 +45,7 @@ Refactor phases 0-5 landed; phase 6 (multi-site distribution readiness) largely - 966 tests, naming/style check green, Gitea Actions CI (backend + naming + frontend build + a migrations-mysql job that runs the real fresh upgrade on utf8mb4 MySQL 8) - `__contract_version__` at 0.13.0 (0.12.0 added the mailer, 0.13.0 the User model, to the plugin surface) (product `__version__` 0.7.0, tags v0.5.0/v0.6.0/v0.7.0 - distinct series, ADR-007) - 12 bundled plugins all satisfy contract: computers, employees, geenforce, knowledgebase, machines, measuringtools, network, notifications, printers, slides, usb, warranty -- Core Alembic chain: baseline `68b3947ae14f` -> head `7d25_drop_redundant_indexes` (32 core migrations). Each plugin owns its own chain (ADR-008); deploy runs `flask db upgrade` then `flask plugin upgrade-all`. Reproducible + idempotent from empty (env.py relaxes session sql_mode so the chain runs on strict MySQL 8). +- Core Alembic chain: baseline `68b3947ae14f` -> head `7d26_settings_description_text` (33 core migrations). Each plugin owns its own chain (ADR-008); deploy runs `flask db upgrade` then `flask plugin upgrade-all`. Reproducible + idempotent from empty (env.py relaxes session sql_mode so the chain runs on strict MySQL 8). - Legacy import: `docs/IMPORT-API.md` is the schema-agnostic import contract; `docs/IMPORT-ADOPTION.md` + `docs/PILOT-DEPLOY.md` cover adopting a site; `scripts/site_imports/wjf/` is the West Jefferson reference loader (all 15 stages, validated end-to-end including on a Windows + MySQL 8 VM). - API is migration-complete: an admin PAT + docs/IMPORT-API.md let a script import the whole legacy DB (X-Import-Mode preserves timestamps). - Pre-1.0 framework; sister sites should pin tight `core_version` ranges until contract reaches 1.0 @@ -135,4 +135,4 @@ Each plugin must have: - `migrations/FIX_LOCATIONONLY_EQUIPMENT_TYPES.md` - LocationOnly equipment type fix - `migrations/PRODUCTION_MIGRATION_GUIDE.md` - production import methods - `migrations/rename_underscore_columns.sql` - one-time rename of snake_case columns to lowercase concatenated (per CONTRIBUTING.md) -- `migrations/versions/` - the core Alembic chain (baseline `68b3947ae14f` -> head `7d25_drop_redundant_indexes`). Run `flask db upgrade` to apply. +- `migrations/versions/` - the core Alembic chain (baseline `68b3947ae14f` -> head `7d26_settings_description_text`). Run `flask db upgrade` to apply. diff --git a/migrations/versions/7d26_settings_description_text.py b/migrations/versions/7d26_settings_description_text.py new file mode 100644 index 0000000..b14f938 --- /dev/null +++ b/migrations/versions/7d26_settings_description_text.py @@ -0,0 +1,61 @@ +"""Widen settings.description to TEXT (was varchar(255)) + +Seeded setting descriptions are explanatory sentences; some exceed 255 chars +(e.g. dualpath_single_machine). On strict MySQL 8 an over-length insert is a +hard error 1406, not a silent truncation, so `flask seed settings` failed on a +fresh box. TEXT removes the ceiling and matches value, which is already TEXT. + +Idempotent guard on current column type; real downgrade back to varchar(255). + +Revision ID: 7d26_settings_description_text +Revises: 7d25_drop_redundant_indexes +Create Date: 2026-07-17 + +""" +from alembic import op +import sqlalchemy as sa + + +revision = '7d26_settings_description_text' +down_revision = '7d25_drop_redundant_indexes' +branch_labels = None +depends_on = None + + +def _description_type(insp): + for c in insp.get_columns('settings'): + if c['name'] == 'description': + return str(c['type']).upper() + return None + + +def upgrade(): + bind = op.get_bind() + insp = sa.inspect(bind) + + if 'settings' not in insp.get_table_names(): + return + coltype = _description_type(insp) + if coltype is None or 'TEXT' in coltype: + return + + op.alter_column('settings', 'description', + existing_type=sa.String(length=255), + type_=sa.Text(), + existing_nullable=True) + + +def downgrade(): + bind = op.get_bind() + insp = sa.inspect(bind) + + if 'settings' not in insp.get_table_names(): + return + coltype = _description_type(insp) + if coltype is None or 'TEXT' not in coltype: + return + + op.alter_column('settings', 'description', + existing_type=sa.Text(), + type_=sa.String(length=255), + existing_nullable=True) diff --git a/shopdb/core/models/setting.py b/shopdb/core/models/setting.py index 8c82c1f..1c209cb 100644 --- a/shopdb/core/models/setting.py +++ b/shopdb/core/models/setting.py @@ -26,7 +26,7 @@ class Setting(db.Model): value = db.Column(db.Text, nullable=True) valuetype = db.Column(db.String(20), default='string') # string, boolean, integer, json category = db.Column(db.String(50), default='general') # For grouping in UI - description = db.Column(db.String(255), nullable=True) + description = db.Column(db.Text, nullable=True) createddate = db.Column(db.DateTime, default=_utcnow) modifieddate = db.Column(db.DateTime, default=_utcnow, onupdate=_utcnow)