123 lines
5.1 KiB
Bash
123 lines
5.1 KiB
Bash
|
|
#!/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
|