Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions .github/workflows/build_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ jobs:
run: supabase link --project-ref ${{ env.SUPABASE_PROJECT_ID }}
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_TOKEN }}
- name: Repair squashed Supabase migration history
run: bash scripts/repair-supabase-squashed-baseline.sh --linked
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_TOKEN }}
- name: Apply Supabase Migrations
run: supabase db push
- name: Update functions
Expand Down
65 changes: 57 additions & 8 deletions scripts/check-supabase-migration-order.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ extract_timestamp() {
fi
}

count_nonempty_lines() {
local value="$1"

if [[ -z "$value" ]]; then
echo 0
return
fi

printf '%s\n' "$value" | awk 'NF { count++ } END { print count + 0 }'
}

resolve_target_branch() {
if [[ -n "${GITHUB_BASE_REF:-}" ]]; then
echo "${GITHUB_BASE_REF}"
Expand Down Expand Up @@ -86,6 +97,8 @@ if [[ -s "${base_timestamps_file}" ]]; then
fi

status=0
current_migration_files="$(find supabase/migrations -maxdepth 1 -type f -name '*.sql' | sort)"
current_migration_count="$(count_nonempty_lines "$current_migration_files")"

modified_files="$(git diff --name-only --diff-filter=MR "${base_ref}...HEAD" -- 'supabase/migrations/*.sql')"
if [[ -n "$modified_files" ]]; then
Expand Down Expand Up @@ -114,18 +127,54 @@ if [[ -n "$modified_files" ]]; then
fi
fi

added_files="$(git diff --name-only --diff-filter=A "${base_ref}...HEAD" -- 'supabase/migrations/*.sql')"
deleted_files="$(git diff --name-only --diff-filter=D "${base_ref}...HEAD" -- 'supabase/migrations/*.sql')"
if [[ -n "$deleted_files" ]]; then
echo '❌ Existing Supabase migrations were deleted in this change.'
echo ' Supabase migrations must remain append-only.'
while IFS= read -r file; do
[[ -z "$file" ]] && continue
echo " - $file"
done <<< "$deleted_files"
status=1
allow_full_squash=0
remaining_migration_file=''
remaining_migration_rewritten=0

if [[ -z "$added_files" && "$current_migration_count" == '1' ]]; then
remaining_migration_file="$current_migration_files"
remaining_timestamp="$(extract_timestamp "$remaining_migration_file" || true)"
if ! git diff --quiet "${base_ref}...HEAD" -- "$remaining_migration_file"; then
remaining_migration_rewritten=1
fi

if [[ "$remaining_migration_rewritten" == '1' && -n "$remaining_timestamp" && "$remaining_timestamp" == "$latest_base_timestamp" ]]; then
deleted_latest_or_newer_files=''

while IFS= read -r file; do
[[ -z "$file" ]] && continue

ts="$(extract_timestamp "$file" || true)"
if [[ -z "$ts" || "$ts" == "$latest_base_timestamp" || 10#$ts > 10#$latest_base_timestamp ]]; then
deleted_latest_or_newer_files+="${file}"$'\n'
fi
done <<< "$deleted_files"

if [[ -z "$deleted_latest_or_newer_files" ]]; then
allow_full_squash=1
fi
fi
fi

if [[ "$allow_full_squash" == '1' ]]; then
echo "⚠️ Allowing intentional Supabase migration squash into baseline: ${remaining_migration_file}"
else
echo '❌ Existing Supabase migrations were deleted in this change.'
echo ' Supabase migrations must remain append-only except for a full baseline squash.'
if [[ -n "$remaining_migration_file" && "$remaining_migration_rewritten" != '1' ]]; then
echo " The remaining migration was not rewritten: ${remaining_migration_file}"
fi
while IFS= read -r file; do
[[ -z "$file" ]] && continue
echo " - $file"
done <<< "$deleted_files"
status=1
fi
fi

added_files="$(git diff --name-only --diff-filter=A "${base_ref}...HEAD" -- 'supabase/migrations/*.sql')"
if [[ -n "$added_files" ]]; then
: > "${added_timestamps_file}"

Expand Down
204 changes: 204 additions & 0 deletions scripts/repair-supabase-squashed-baseline.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#!/usr/bin/env bash

set -euo pipefail

baseline_version='20260608143906'
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
repair_sql="${repo_root}/supabase/repair/${baseline_version}_pre_squash_repair.sql"
reverted_versions_file="${repo_root}/supabase/repair/${baseline_version}_reverted_versions.txt"

usage() {
cat <<'USAGE'
Usage:
bash scripts/repair-supabase-squashed-baseline.sh --linked
bash scripts/repair-supabase-squashed-baseline.sh --local
bash scripts/repair-supabase-squashed-baseline.sh --db-url "$SUPABASE_DB_URL"

If needed, applies the final pre-squash migration, marks deleted historical
migration rows as reverted, and marks the squashed baseline version as applied.

Set SUPABASE_WORKDIR to pass a custom Supabase --workdir, for example when
validating against this repo's worktree-isolated local Supabase stack.
USAGE
}

if [[ $# -lt 1 ]]; then
usage
exit 1
fi

target_args=()
reverted_versions=()
pre_squash_history_output=''
baseline_applied=false
case "$1" in
--linked|--local)
target_args=("$1")
shift
;;
--db-url)
if [[ $# -lt 2 || -z "${2:-}" ]]; then
usage
exit 1
fi
target_args=("--db-url" "$2")
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
usage
exit 1
;;
esac

if [[ $# -ne 0 ]]; then
usage
exit 1
fi

while IFS= read -r version || [[ -n "$version" ]]; do
[[ -z "$version" ]] && continue
[[ "$version" =~ ^# ]] && continue

if [[ ! "$version" =~ ^[0-9]{14}$ ]]; then
echo "Invalid migration version in ${reverted_versions_file}: ${version}" >&2
exit 1
fi

reverted_versions+=("$version")
done < "$reverted_versions_file"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if [[ "${#reverted_versions[@]}" -eq 0 ]]; then
echo "No reverted migration versions found in ${reverted_versions_file}." >&2
exit 1
fi

run_supabase() {
if command -v supabase >/dev/null 2>&1; then
if [[ -n "${SUPABASE_WORKDIR:-}" ]]; then
supabase "$@" --workdir "$SUPABASE_WORKDIR"
else
supabase "$@"
fi
return
fi

if [[ -n "${SUPABASE_WORKDIR:-}" ]]; then
bunx supabase "$@" --workdir "$SUPABASE_WORKDIR"
else
bunx supabase "$@"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

repair_versions() {
local status="$1"
shift

if [[ "$#" -eq 0 ]]; then
return
fi

run_supabase migration repair "${target_args[@]}" --status "$status" "$@"
}

baseline_is_applied() {
local applied_output
if ! applied_output="$(run_supabase db query "${target_args[@]}" -o json "select exists(select 1 from supabase_migrations.schema_migrations where version = '${baseline_version}') as applied;")"; then
echo "Could not read Supabase migration history for ${baseline_version}." >&2
exit 1
fi

grep -Eq '"applied"[[:space:]]*:[[:space:]]*true' <<< "$applied_output"
}

has_migration_history_table() {
local history_table_output
if ! history_table_output="$(run_supabase db query "${target_args[@]}" -o json "select to_regclass('supabase_migrations.schema_migrations') is not null as has_history_table;")"; then
echo "Could not inspect Supabase migration history table." >&2
exit 1
fi

grep -Eq '"has_history_table"[[:space:]]*:[[:space:]]*true' <<< "$history_table_output"
}

has_existing_capgo_schema() {
local schema_output
if ! schema_output="$(run_supabase db query "${target_args[@]}" -o json "select to_regclass('public.apps') is not null as has_capgo_schema;")"; then
echo "Could not inspect existing Capgo schema." >&2
exit 1
fi

grep -Eq '"has_capgo_schema"[[:space:]]*:[[:space:]]*true' <<< "$schema_output"
}

has_pre_squash_history() {
grep -Eq '"has_old_history"[[:space:]]*:[[:space:]]*true' <<< "$pre_squash_history_output"
}

has_complete_pre_squash_history() {
grep -Eq '"has_complete_old_history"[[:space:]]*:[[:space:]]*true' <<< "$pre_squash_history_output"
}

load_pre_squash_history() {
local versions_sql
printf -v versions_sql "'%s'," "${reverted_versions[@]}"
versions_sql="${versions_sql%,}"

if ! pre_squash_history_output="$(run_supabase db query "${target_args[@]}" -o json "select count(*) > 0 as has_old_history, count(*) = ${#reverted_versions[@]} as has_complete_old_history from supabase_migrations.schema_migrations where version in (${versions_sql});")"; then
echo "Could not inspect deleted Supabase migration history rows." >&2
exit 1
fi
}

if ! has_migration_history_table; then
echo "Supabase migration history table does not exist; skipping squash repair for a fresh database."
exit 0
fi

load_pre_squash_history
if baseline_is_applied; then
baseline_applied=true
fi

if ! has_pre_squash_history; then
if [[ "$baseline_applied" == true ]]; then
echo "No deleted pre-squash migration history found and squashed baseline is already applied; skipping squash repair."
exit 0
fi

if has_existing_capgo_schema; then
echo "Existing Capgo schema found, but deleted pre-squash migration history and squashed baseline marker are both missing. Aborting to avoid applying the squashed baseline to an existing database." >&2
exit 1
fi

echo "No deleted pre-squash migration history found; skipping squash repair for a fresh database."
exit 0
fi

if [[ "$baseline_applied" != true ]] && ! has_complete_pre_squash_history; then
final_pre_squash_version="${reverted_versions[$((${#reverted_versions[@]} - 1))]}"
echo "Deleted pre-squash migration history is incomplete; expected ${#reverted_versions[@]} versions through ${final_pre_squash_version}. Aborting to avoid marking a partial database as squashed." >&2
exit 1
fi

if [[ "$baseline_applied" == true ]]; then
echo "Squashed baseline ${baseline_version} is already marked applied; skipping schema repair SQL."
else
run_supabase db query "${target_args[@]}" --file "$repair_sql"
fi

repair_versions applied "$baseline_version"

chunk=()
for version in "${reverted_versions[@]}"; do
chunk+=("$version")
if [[ "${#chunk[@]}" -ge 50 ]]; then
repair_versions reverted "${chunk[@]}"
chunk=()
fi
done

repair_versions reverted "${chunk[@]}"
2 changes: 1 addition & 1 deletion scripts/supabase-worktree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ function ensureWorktreeSupabaseDir(repoRoot: string): { workdir: string, cfg: Re
// Symlink everything except config.toml so we can safely rewrite ports + project_id per worktree.
const repoSupaDir = resolve(cfg.repoRoot, 'supabase')
const repoTemplatesDir = resolve(repoSupaDir, 'templates')
for (const entry of ['functions', 'migrations', 'schemas', 'tests', 'seed.sql', 'migration_guide.md', '.gitignore']) {
for (const entry of ['functions', 'migrations', 'repair', 'schemas', 'tests', 'seed.sql', 'migration_guide.md', '.gitignore']) {
const src = resolve(repoSupaDir, entry)
if (!existsSync(src))
continue
Expand Down
39 changes: 39 additions & 0 deletions supabase/migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,42 @@ This command will clear all data and revert schema changes made to the local dat


By following these steps, you can safely add and deploy Supabase migration changes to your project's database schema.

### Squashed Baseline Repair

The `20260608143906` migration is a squashed schema baseline. Fresh databases
should apply it normally. Existing databases that already applied the deleted
historical migrations must not run that baseline against an existing schema.

If an existing database has the old migration history, run this repair flow
before the normal deployment that contains the squashed migration. Run it even
when `20260608143906` is already marked as applied so the deleted historical
versions are removed from the migration history:

```bash
bash scripts/repair-supabase-squashed-baseline.sh --linked
```

For a database reached by connection string instead of a linked project, replace
`--linked` with `--db-url "$SUPABASE_DB_URL"`.

On a database that already has `20260608143906` marked as applied, this command
does not apply schema SQL. It only removes the deleted historical versions from
the migration history and keeps `20260608143906` marked as applied.

On a fresh database, or on a database that no longer has any deleted pre-squash
migration history rows, the script exits without changing schema or migration
history. The normal `supabase db push` flow then applies the squashed baseline
for fresh projects.

If the script finds only part of the deleted pre-squash migration history, it
aborts instead of marking the squashed baseline as applied. That protects
databases that are not yet at the final pre-squash schema from skipping the
baseline by mistake.

The repair marks the squashed baseline as applied before deleting old migration
history rows. If a retry ever sees an existing Capgo schema with both the old
history rows and the squashed baseline marker missing, it aborts instead of
letting `supabase db push` apply the baseline to that existing schema.
If the baseline marker is present and some old rows remain, rerunning the script
continues removing the remaining old rows.
Loading
Loading