Compare commits

..

3 Commits

Author SHA1 Message Date
Mikhail Chusavitin
688b87e98d Add SQL migration file format, Docker testing, and authoring pitfalls to go-database contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 22:45:37 +03:00
Mikhail Chusavitin
52444350c1 Automate migration backups and add session rollback on failure
- Replace operator-driven backup requirement with automatic migration engine responsibility
- Full DB backup when new migrations are detected, before any step runs
- Per-table backup before each migration step affecting that table
- Session rollback (or per-table restore) on any migration failure
- Update local-first-recovery to reflect automatic backup requirement

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 00:05:22 +03:00
Mikhail Chusavitin
747c42499d Add build version display contract
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 17:49:55 +03:00
3 changed files with 176 additions and 19 deletions

View File

@@ -0,0 +1,66 @@
# Contract: Build Version Display
Version: 1.0
## Purpose
Every web application must display the current build version in the page footer so that users and support staff can identify exactly which version is running.
---
## Rule
The build version **must** be visible in the footer on every page of the web application.
---
## Requirements
- The version is shown in the footer on **all** pages, including error pages (404, 500, etc.).
- The version string is injected at **build time** — it is never hardcoded in source and never fetched at runtime.
- The version value comes from a single authoritative source (e.g. `package.json`, `version.go`, a CI environment variable). It is not duplicated manually.
- Format: any human-readable string that uniquely identifies the build — a semver tag, a git commit SHA, or a combination (e.g. `1.4.2`, `1.4.2-abc1234`, `abc1234`).
- The version text must be legible but visually subordinate — use a muted color and small font size so it does not compete with page content.
---
## Recommended implementation
**Frontend (JS/TS build tools)**
Expose the version through an environment variable at build time and reference it in the footer component:
```ts
// vite.config.ts / webpack.config.js
define: {
__APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? "dev"),
}
// Footer component
<footer>v{__APP_VERSION__}</footer>
```
**Go (server-rendered HTML)**
Inject via `-ldflags` at build time and pass to the template:
```go
// main.go
var Version = "dev"
// Build: go build -ldflags "-X main.Version=1.4.2"
```
```html
<!-- base template -->
<footer>v{{ .Version }}</footer>
```
---
## What is NOT allowed
- Omitting the version from any page, including error pages.
- Fetching the version from an API endpoint at runtime (network dependency for a static value).
- Hardcoding a version string in source code.
- Storing the version in more than one place.

View File

@@ -1,6 +1,6 @@
# Contract: Database Patterns (Go / MySQL / MariaDB)
Version: 1.7
Version: 1.9
## MySQL Transaction Cursor Safety (CRITICAL)
@@ -104,32 +104,123 @@ items, _ := repo.GetItemsByPricelistIDs(ids) // 1 query with WHERE id IN (...)
// then group in Go
```
## Backup Before Any DB Change
## Automatic Backup During Migration
Any operation that changes persisted database state must have a fresh backup taken immediately before execution.
This applies to:
- Go migrations
- Manual SQL runbooks
- Data backfills and repair scripts
- Imports, bulk updates, and bulk deletes
- Admin tools or one-off operator commands
The migration engine is responsible for all backup steps. The operator must never be required to take a backup manually.
Backup naming, storage, archive format, retention, and restore-readiness must follow the `backup-management` contract.
### Full DB Backup on New Migrations
When the migration engine detects that new (unapplied) migrations exist, it must take a full database backup before applying any of them.
Rules:
- No schema change or data mutation is allowed on a non-ephemeral database without a current backup.
- "Small" or "safe" changes are not exceptions.
- The operator must know how to restore from that backup before applying the change.
- If a migration or script is intended for production/staging, the rollout instructions must state the backup step explicitly.
- The backup taken before a migration must be triggered by the application's own backup mechanism, not by assuming `mysql`, `mysqldump`, or other DB client tools exist on the user's machine.
- Before a migration starts, double-check that backup output resolves outside the git worktree and is not tracked or staged in git.
- The full backup must complete and be verified before the first migration step runs.
- The backup must be triggered by the application's own backup mechanism; do not assume `mysql`, `mysqldump`, `pg_dump`, or any other external DB client tool is present on the operator's machine.
- Before creating the backup, verify that the backup output path resolves outside the git worktree and is not tracked or staged in git.
### Per-Table Backup Before Each Table Migration
Before applying a migration step that affects a specific table, take a targeted backup of that table.
Rules:
- A per-table backup must be created immediately before the migration step that modifies that table.
- If a single migration step touches multiple tables, back up each affected table before the step runs.
- Per-table backups are in addition to the full DB backup; they are not a substitute for it.
### Session Rollback on Failure
If any migration step fails during a session, the engine must roll back all migrations applied in that session.
Rules:
- "Session" means all migration steps started in a single run of the migration engine.
- On failure, roll back every step applied in the current session in reverse order before surfacing the error.
- If rollback of a step is not possible (e.g., the operation is not reversible in MySQL without the per-table backup), restore from the per-table backup taken before that step.
- After rollback or restore, the database must be in the same state as before the session started.
- The engine must emit structured diagnostics that identify which step failed, which steps were rolled back, and the final database state.
## Migration Policy
- For local-first desktop applications, startup and migration recovery must follow the `local-first-recovery` contract.
- Migrations are numbered sequentially and never modified after merge.
- Trigger, take, and verify a fresh backup through the application-owned backup mechanism before applying migrations to any non-ephemeral database.
- Each migration must be reversible where possible (document rollback in a comment).
- Never rename a column in one migration step — add new, backfill, drop old across separate deploys.
- Auto-apply migrations on startup is acceptable for internal tools; document if used.
## SQL Migration File Format
Every `.sql` migration file must begin with a structured header block:
```sql
-- Tables affected: supplier, lot_log
-- recovery.not-started: No action required.
-- recovery.partial: DELETE FROM parts_log WHERE created_by = 'migration';
-- recovery.completed: Same as partial.
-- verify: No orphaned supplier_code | SELECT supplier_code FROM parts_log pl LEFT JOIN supplier s ON s.supplier_code = pl.supplier_code WHERE s.supplier_code IS NULL LIMIT 1
-- verify: No empty supplier_code | SELECT supplier_name FROM supplier WHERE supplier_code = '' LIMIT 1
```
**`-- Tables affected:`** — comma-separated list of tables the migration touches. Used by the backup engine to take a targeted pre-migration backup. Omit only if no table can be identified; the engine falls back to full DB backup.
**`-- recovery.*:`** — human-readable rollback SQL for each migration state (`not-started`, `partial`, `completed`). Executed manually by an operator if automatic restore fails. Must be correct, copy-pasteable SQL.
**`-- verify:`** — post-migration assertion query. Format: `-- verify: <description> | <SQL>`. The engine runs the query after all statements in the file succeed. If the query returns **any row**, the migration is considered failed and is rolled back. Write the query so it returns a row only when something is **wrong**:
```sql
-- verify: Orphaned FK refs | SELECT id FROM child c LEFT JOIN parent p ON p.id = c.parent_id WHERE p.id IS NULL LIMIT 1
-- ^ returns a row = bad ^ returns nothing = good
```
- Verify queries must filter out NULL/empty values that would cause false positives: add `AND col IS NOT NULL AND col != ''`.
- A migration is only recorded as applied after all verify checks pass.
- Verify checks are not a substitute for testing; they are a last-resort safety net on production.
## Pre-Production Migration Testing in Docker
Before applying a set of new migrations to production, always validate them against a copy of the production database in a local MariaDB Docker container that matches the production version and collation.
```bash
# Start container matching production (MariaDB 11.8, utf8mb4_uca1400_ai_ci)
docker run -d --name pf_test \
-e MYSQL_ROOT_PASSWORD=test -e MYSQL_DATABASE=RFQ_LOG \
mariadb:11.8 --character-set-server=utf8mb4 --collation-server=utf8mb4_uca1400_ai_ci
# Load production dump
docker exec -i pf_test mariadb -uroot -ptest RFQ_LOG < prod_dump.sql
# Run migrations via pfs (uses real migration engine + verify checks, no backup)
./pfs -migrate-dsn "root:test@tcp(127.0.0.1:3306)/RFQ_LOG?parseTime=true&charset=utf8mb4&multiStatements=true" \
-no-backup -verbose
```
The `-migrate-dsn` flag connects to the given DSN, runs all pending migrations, runs verify checks, and exits. No config file, no server, no browser.
**Rules:**
- Always test on a dump of the **current production database**, not a fixture — schema drift and real data distributions expose bugs that fixtures miss.
- The Docker container must use the same MariaDB version and `--collation-server` as production.
- Each migration file is executed as a **single session** so `SET FOREIGN_KEY_CHECKS = 0` applies to all its statements. Never test by running statements from a migration file individually across separate sessions — the session variable will reset between them.
- If any migration fails in Docker, fix the SQL before touching production. Do not rely on "it will be different in production."
## SQL Migration Authoring — Common Pitfalls
**Semicolons inside string literals break naive splitters.**
The migration engine uses a quote-aware statement splitter. Do not rely on external tools that split on bare `;`. When writing supplier/product names with punctuation, use commas — not semicolons — as separators in string literals. A semicolon inside `'COMPANY; LTD'` will break any naive `split(";")` approach.
**`SET FOREIGN_KEY_CHECKS = 0` only applies to the current session.**
This is a session variable. If statements run in separate connections (e.g. via individual subprocess calls), FK checks are re-enabled for each new connection. Always run an entire migration file as one session. The pfs migration engine runs all statements in a file on the same GORM db handle, which reuses the same connection.
**Verify queries must exclude NULL values.**
A query like `SELECT c.col FROM child c LEFT JOIN parent p ON p.id = c.id WHERE p.id IS NULL` will return rows with `c.col = NULL` if the child table has rows with a NULL FK value. Add `AND c.col IS NOT NULL AND c.col != ''` to avoid false failures.
**Catch-all INSERT for referential integrity before adding FK constraints.**
When adding a FK constraint to a table that previously had no FK (legacy data may have orphaned references), add a catch-all step before the constraint:
```sql
-- Ensure every value referenced in child table exists in parent before adding FK.
INSERT IGNORE INTO parent (name)
SELECT DISTINCT c.fk_col FROM child c
LEFT JOIN parent p ON p.name = c.fk_col
WHERE p.name IS NULL AND c.fk_col IS NOT NULL AND c.fk_col != '';
```
This is not a hack — it repairs data that was valid before the constraint existed. Never delete orphaned child rows unless data loss is acceptable.

View File

@@ -1,6 +1,6 @@
# Contract: Local-First Recovery
Version: 1.1
Version: 1.2
## Purpose
@@ -53,7 +53,7 @@ For protected user data, destructive reset is forbidden.
Rules:
- Do not drop, truncate, or recreate protected tables as a recovery shortcut.
- Backup-before-change is mandatory and must follow the `backup-management` contract.
- Backup-before-change is mandatory, must be performed automatically by the migration engine (never by the operator), and must follow the `backup-management` and `go-database` contracts.
- Validate-before-migrate is mandatory.
- Migration logic must use fail-safe semantics: stop before applying a risky destructive step when invariants are broken or input is invalid.
- The application must emit explicit diagnostics that identify the blocked table, migration step, and reason.