Privacy-Period-Tracker/scripts/schema-guard.sh

123 lines
5.1 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# A Room entity may not change without the database version changing with it.
#
# ## The incident that motivated this
#
# Written 2026-08-18, immediately after the check it replaces was proved to be
# green over exactly the failure it claimed to catch.
#
# `core/database/src/test/.../SchemaTest.kt` compares the committed schema JSON
# against the SQL the current code produces, and it looked like a drift guard.
# It is not one, for a reason that is invisible until you try it: **Room
# regenerates the schema export during compilation**, overwriting
# `schemas/<db>/1.json` in place. So by the time any test runs, the "committed"
# file already describes the changed entity, both sides of the comparison agree,
# and the test passes.
#
# It was proved by adding a column to `PeriodRecordEntity` without touching
# `PeriodDatabase.VERSION`. The suite stayed green and the committed schema
# silently gained the column. On a device that is a crash on update, in front of
# a user, after shipping — `Room cannot verify the data integrity`.
#
# GUARDS.md §1 exists for precisely this: a guard nobody has watched fail is not
# yet evidence.
#
# ## What this does instead
#
# It asks git, which Room cannot overwrite. After a build has regenerated the
# exports, an EXISTING schema file that now differs from its committed contents
# means an entity changed under a version that has already shipped. A NEW file
# — `2.json` appearing — is a version bump, which is the correct way to change a
# schema and is allowed.
#
# bash scripts/schema-guard.sh # assumes exports are current
# bash scripts/schema-guard.sh --build # regenerate them first
#
# ## Exit codes
#
# 0 every committed schema file is unchanged, and one exists for the current
# version
# 1 a schema file changed without a version bump, or none exists for the
# current version
# 2 NOTHING WAS CHECKED — no schema directory, or git could not be read.
# **Two is not a pass** and no hook or CI step may treat it as one.
set -uo pipefail
cd "$(git rev-parse --show-toplevel 2>/dev/null)" || {
printf '\033[1mschema-guard:\033[0m not a git repository — nothing could be checked.\n' >&2
exit 2
}
say() { printf '\033[1mschema-guard:\033[0m %s\n' "$*" >&2; }
SCHEMA_DIR="core/database/schemas"
VERSION_SRC="core/database/src/main/kotlin/dev/privacyllc/period/core/database/PeriodDatabase.kt"
if [ "${1:-}" = "--build" ]; then
say "regenerating the schema exports…"
./gradlew --quiet :core:database:assembleDebug >/dev/null 2>&1 || {
say "the build failed, so the exports are not current and nothing was checked."
exit 2
}
fi
if [ ! -d "$SCHEMA_DIR" ]; then
say "$SCHEMA_DIR does not exist. Either the database module moved or the"
say "schema export was switched off — both mean this check did not run."
exit 2
fi
# The declared version, read from the source rather than assumed.
version=$(grep -oP 'const val VERSION\s*=\s*\K[0-9]+' "$VERSION_SRC" 2>/dev/null)
if [ -z "$version" ]; then
say "could not read PeriodDatabase.VERSION from $VERSION_SRC — nothing checked."
exit 2
fi
expected="$SCHEMA_DIR/dev.privacyllc.period.core.database.PeriodDatabase/${version}.json"
if [ ! -f "$expected" ]; then
say "database version is $version but $expected does not exist."
say "The version was bumped without exporting the schema, so no migration"
say "into it can ever be validated. Build once and commit the export."
exit 1
fi
# Tracked files under the schema directory whose contents differ from HEAD.
if ! modified=$(git diff --name-only -- "$SCHEMA_DIR" 2>/dev/null); then
say "could not read git status for $SCHEMA_DIR — nothing checked."
exit 2
fi
staged=$(git diff --cached --name-only --diff-filter=M -- "$SCHEMA_DIR" 2>/dev/null)
changed=$(printf '%s\n%s\n' "$modified" "$staged" | grep -v '^$' | sort -u)
if [ -n "$changed" ]; then
say "a schema file that is already committed has CHANGED:"
printf ' %s\n' $changed >&2
say ""
say "That means an entity changed under database version $version, which has"
say "already been exported. Room overwrote the export during the build, so"
say "nothing else will notice — including the unit tests."
say ""
say "Either revert the entity change, or bump PeriodDatabase.VERSION, write"
say "the migration, and let a NEW schema file be exported beside this one."
say "Then add its row to the migration table in docs/architecture/README.md,"
say "in the same commit."
exit 1
fi
# An untracked file here is a new version, which is the correct way to change a
# schema. Reported rather than passed over silently: it needs a migration.
untracked=$(git ls-files --others --exclude-standard -- "$SCHEMA_DIR" 2>/dev/null)
if [ -n "$untracked" ]; then
say "new schema file(s), so this is a version bump:"
printf ' %s\n' $untracked >&2
say "Commit them, write the migration, and add the row to"
say "docs/architecture/README.md's migration table in the same commit."
fi
count=$(find "$SCHEMA_DIR" -name '*.json' | wc -l)
say "ok — $count committed schema file(s) unchanged; version $version is exported."
exit 0