|
| 1 | +#!/usr/bin/env bash |
| 2 | +# check-query-contraction.sh — query-contraction check. |
| 3 | +# |
| 4 | +# Compiles a PREVIOUS release's sqlc queries against the CURRENT schema (the |
| 5 | +# migration files under go/core/pkg/migrations) and fails if a migration on this |
| 6 | +# branch removed, renamed, or retyped a column or table that an older query still |
| 7 | +# references — a change that would break that release's code against the new |
| 8 | +# schema (the windowed-contraction invariant). |
| 9 | +# |
| 10 | +# It checks two targets, deduplicated: |
| 11 | +# A) the latest released tag reachable from HEAD — the in-line previous release; |
| 12 | +# catches a contraction introduced during the current line's development. |
| 13 | +# B) the previous stable line's latest patch (release/vX.Y.x tip, via |
| 14 | +# prev-stable-version.sh) — the supported rollback-window floor. |
| 15 | +# Today these usually resolve to the same tag (one compile); they diverge once a |
| 16 | +# new minor releases or the stable line gets a backport patch. |
| 17 | +# |
| 18 | +# Static: no database and no cluster. sqlc derives the schema from the migration |
| 19 | +# files (see go/core/internal/database/sqlc.yaml), so "does every old query still |
| 20 | +# type-check against the new schema" is answerable offline. It catches |
| 21 | +# column/table/type-shape contraction; semantic breaks (a new NOT NULL, a |
| 22 | +# tightened constraint, an index/ordering change) are out of scope for a static |
| 23 | +# check and belong to a runtime regression suite. |
| 24 | +# |
| 25 | +# Inputs (env): |
| 26 | +# TARGET_VERSIONS space-separated versions without leading 'v' to check |
| 27 | +# instead of the auto-derived A/B (for local runs). |
| 28 | +# SQLC sqlc binary to use (default: sqlc on PATH). |
| 29 | +# REMOTE git remote for target B (default: origin). |
| 30 | +set -euo pipefail |
| 31 | + |
| 32 | +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 33 | +repo_root="$(git -C "$here" rev-parse --show-toplevel)" |
| 34 | +sqlc_bin="${SQLC:-sqlc}" |
| 35 | +queries_path="go/core/internal/database/queries" |
| 36 | +core_migrations="$repo_root/go/core/pkg/migrations/core" |
| 37 | +vector_migrations="$repo_root/go/core/pkg/migrations/vector" |
| 38 | + |
| 39 | +# Resolve the target versions. |
| 40 | +targets=() |
| 41 | +if [ -n "${TARGET_VERSIONS:-}" ]; then |
| 42 | + read -ra targets <<<"${TARGET_VERSIONS}" |
| 43 | +else |
| 44 | + a="$(git -C "$repo_root" describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || true)" |
| 45 | + b="$("$here/prev-stable-version.sh" 2>/dev/null || true)" |
| 46 | + [ -n "${a}" ] && targets+=("${a}") |
| 47 | + [ -n "${b}" ] && targets+=("${b}") |
| 48 | +fi |
| 49 | +if [ "${#targets[@]}" -eq 0 ]; then |
| 50 | + echo "ERROR: no contraction target versions resolved; ensure tags are fetched and a release branch exists, or set TARGET_VERSIONS." >&2 |
| 51 | + exit 1 |
| 52 | +fi |
| 53 | +# Deduplicate, preserving order (version strings are space- and glob-free). |
| 54 | +targets=($(printf '%s\n' "${targets[@]}" | awk 'NF && !seen[$0]++')) |
| 55 | + |
| 56 | +workroot="$(mktemp -d)" |
| 57 | +trap 'rm -rf "$workroot"' EXIT |
| 58 | + |
| 59 | +# A target resolved (non-empty version) but whose tag is absent locally means |
| 60 | +# the checkout didn't fetch tags — a misconfiguration that would otherwise let |
| 61 | +# the whole check pass having compiled nothing. Track the two outcomes so the |
| 62 | +# post-loop guard can fail on that case while still allowing a legitimately |
| 63 | +# empty run (every resolved target predates the sqlc query set). |
| 64 | +compiled=0 |
| 65 | +missing_tag=0 |
| 66 | + |
| 67 | +check_target() { |
| 68 | + local prev="$1" |
| 69 | + local prev_tag="v${prev}" |
| 70 | + |
| 71 | + if ! git -C "$repo_root" rev-parse -q --verify "refs/tags/${prev_tag}" >/dev/null; then |
| 72 | + echo "NOTE: tag ${prev_tag} not present locally; skipping (fetch tags to include it)." |
| 73 | + missing_tag=$((missing_tag + 1)) |
| 74 | + return 0 |
| 75 | + fi |
| 76 | + if [ -z "$(git -C "$repo_root" ls-tree "$prev_tag" -- "$queries_path" 2>/dev/null)" ]; then |
| 77 | + echo "NOTE: ${prev_tag} has no ${queries_path}; skipping (predates the sqlc query set)." |
| 78 | + return 0 |
| 79 | + fi |
| 80 | + |
| 81 | + # Self-contained sqlc project: sqlc resolves schema/queries relative to the |
| 82 | + # config file, so stage everything under a per-target dir. Current migrations |
| 83 | + # supply the schema; the previous release supplies the queries. |
| 84 | + local wd="$workroot/$prev" |
| 85 | + mkdir -p "$wd/schema/core" "$wd/schema/vector" "$wd/queries" "$wd/gen" "$wd/prev" |
| 86 | + cp "$core_migrations"/*.sql "$wd/schema/core/" |
| 87 | + cp "$vector_migrations"/*.sql "$wd/schema/vector/" |
| 88 | + git -C "$repo_root" archive "$prev_tag" "$queries_path" | tar -x -C "$wd/prev" |
| 89 | + cp "$wd/prev/$queries_path"/*.sql "$wd/queries/" |
| 90 | + |
| 91 | + # Minimal config: the go_type overrides in the real sqlc.yaml only affect the |
| 92 | + # generated Go types, not whether a query type-checks against the schema. |
| 93 | + cat >"$wd/sqlc.yaml" <<'EOF' |
| 94 | +version: "2" |
| 95 | +sql: |
| 96 | + - engine: "postgresql" |
| 97 | + schema: ["schema/core", "schema/vector"] |
| 98 | + queries: "queries" |
| 99 | + gen: |
| 100 | + go: |
| 101 | + package: "dbgen" |
| 102 | + out: "gen" |
| 103 | +EOF |
| 104 | + |
| 105 | + echo "=== Contraction check: queries@${prev_tag} vs current schema ===" |
| 106 | + ( cd "$wd" && "$sqlc_bin" compile -f sqlc.yaml ) |
| 107 | + echo "OK: ${prev_tag} queries still type-check against the current schema." |
| 108 | + compiled=$((compiled + 1)) |
| 109 | +} |
| 110 | + |
| 111 | +for t in "${targets[@]}"; do |
| 112 | + check_target "$t" |
| 113 | +done |
| 114 | + |
| 115 | +# Guard against a vacuous green: if nothing compiled because resolved targets had |
| 116 | +# no local tag, the checkout almost certainly didn't fetch tags. Fail loudly |
| 117 | +# rather than report success on an empty run. An all-predate run (no missing |
| 118 | +# tags) is legitimately empty and stays green. |
| 119 | +if [ "$compiled" -eq 0 ]; then |
| 120 | + if [ "$missing_tag" -gt 0 ]; then |
| 121 | + echo "ERROR: no targets compiled — ${missing_tag} resolved version(s) had no local tag; fetch tags (fetch-depth: 0, fetch-tags: true) so the contraction check actually runs." >&2 |
| 122 | + exit 1 |
| 123 | + fi |
| 124 | + echo "NOTE: no targets compiled; all resolved versions predate the sqlc query set." |
| 125 | +fi |
0 commit comments