Skip to content

fix(migrations): use numeric ordering to allow 3-digit migrations#198

Open
Lutherwaves wants to merge 1 commit into
mainfrom
fix/numeric-migration-sort
Open

fix(migrations): use numeric ordering to allow 3-digit migrations#198
Lutherwaves wants to merge 1 commit into
mainfrom
fix/numeric-migration-sort

Conversation

@Lutherwaves

@Lutherwaves Lutherwaves commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Closes #197.

Problem

Migration files are named <id>__<description>.yaml. The runner derives each migration id by parsing the numeric prefix and gates application on a numeric high-water mark (apply when id > mark). But it chose run order with sort.Strings(keys) — a lexical sort.

Lexical and numeric order agree only while every id has the same digit width. The first 3-digit migration (100__…) sorts before 10__…..99__… lexically (the char after 100 is _ > any digit). So on a fresh database (mark = 0) the runner:

  1. applies 01..09 (mark → 9),
  2. applies 100, 101 next in lexical order (mark → 101),
  3. reaches 10..99, whose ids are now below the mark, and silently skips all of them — no error, recorded as success.

Incrementally migrated databases (mark already ≥ 99) are unaffected, so this only bites fresh environments: new dev setups, CI, ephemeral test databases.

Fix

Order keys by their parsed numeric id — the same value the apply gate uses — with a lexical tie-break for equal ids and a lexical fallback for unparseable prefixes (so ordering stays deterministic; a bad id is still surfaced by the existing per-migration check). migrationID is extracted as the single parse helper shared by both the ordering and the gate, so they can no longer disagree.

Tests

storage/migration_internal_test.go:

  • TestSortMigrationKeysOrdersNumericallyNotLexically — mixed 1/2/3-digit ids incl. a tie on 100; asserts 1,2,9,10,99,100,100 order (would fail under sort.Strings).
  • TestSortMigrationKeysFallsBackToLexicalForUnparseablePrefix — non-numeric prefixes sort deterministically without panicking.

go build ./..., go vet ./storage/, and go test ./storage/ all pass.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Fixed migration execution ordering to prioritize numeric IDs from migration filenames, using deterministic lexical ordering when IDs can’t be parsed or are equal.
  • Tests

    • Added coverage for numeric sorting, deterministic lexical fallback for unparseable prefixes, and validation of the required filename format.

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Migration execution now orders files by parsed numeric ID rather than lexical filename order. Shared parsing is used for both sorting and the applied-ID gate, with deterministic fallback behavior and tests for numeric ordering and malformed filenames.

Changes

Numeric-aware migration ordering

Layer / File(s) Summary
Numeric ID extraction and sorting implementation
storage/migration.go
migrationID parses numeric filename prefixes, and sortMigrationKeys orders keys numerically with deterministic lexical fallback.
Apply sorting in runMigrations loop
storage/migration.go
Collected keys are sorted before execution, and the applied-ID gate uses the shared parser.
Sorting behavior validation
storage/migration_internal_test.go
Tests cover numeric ordering, equal-ID ordering, unparseable prefixes, and the required double-underscore separator.

Estimated code review effort: 2 (Simple) | ~15 minutes

Suggested reviewers: ayashjorden, deanefrati

Poem

I’m a rabbit who hops through the rows,
Numeric order is how magic now flows.
No skipped little changes, no lexical race,
Each migration finds its proper place.
Nibble, sort, and deploy!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: switching migration ordering to numeric IDs for 3-digit migrations.
Linked Issues check ✅ Passed The changes match #197 by sorting migrations numerically with deterministic fallback and sharing that parser with the apply gate.
Out of Scope Changes check ✅ Passed The code and tests stay within the migration-ordering fix and add no unrelated functional changes.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@storage/migration_internal_test.go`:
- Around line 116-123: The helper function slicesIndex is redundant—replace all
calls to slicesIndex(s, v) with slices.Index(s, v) from the standard library and
add the "slices" import; then remove the slicesIndex function entirely. Locate
usages in tests (any references to slicesIndex) and update them to slices.Index,
update the import block to include "slices", and delete the slicesIndex helper
function to avoid duplicate boilerplate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d48fbc4b-56b1-451d-9a47-870f7e281670

📥 Commits

Reviewing files that changed from the base of the PR and between 1323ff6 and 946e881.

📒 Files selected for processing (2)
  • storage/migration.go
  • storage/migration_internal_test.go

Comment thread storage/migration_internal_test.go Outdated
@Lutherwaves
Lutherwaves force-pushed the fix/numeric-migration-sort branch from 946e881 to 1faea2d Compare June 3, 2026 10:54
The runner derived each migration id from the numeric filename prefix and
gated application on a numeric high-water mark, but chose run order with
sort.Strings — a lexical sort. The two agree only while every id is the same
width. The first 3-digit migration ("100__") sorts before "10__".."99__"
lexically, so on a fresh database the runner applies 100/101 first, advances
the numeric mark past 99, then silently skips migrations 10..99. Incrementally
migrated databases (mark already >= 99) are unaffected, so this only bites
fresh environments: new dev setups, CI, ephemeral test databases.

Sort by the parsed numeric id — the same value the apply gate uses — with a
lexical tie-break, so run order and gate stay consistent regardless of digit
count and zero-padding is no longer load-bearing. Extract migrationID as the
shared parse helper used by both paths.

Fixes #197

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Lutherwaves
Lutherwaves force-pushed the fix/numeric-migration-sort branch from 1faea2d to f6eff91 Compare June 3, 2026 11:03
@Lutherwaves Lutherwaves self-assigned this Jun 3, 2026
@Lutherwaves Lutherwaves added bug Something isn't working enhancement New feature or request go Pull requests that update go code labels Jun 3, 2026
@Lutherwaves

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@storage/migration_internal_test.go`:
- Around line 104-115: Update sortMigrationKeys so its comparator is total for
mixed inputs: place keys with parseable numeric prefixes before unparseable
keys, compare numeric keys numerically and invalid keys lexically. Extend
TestSortMigrationKeysFallsBackToLexicalForUnparseablePrefix with mixed
valid/invalid cases that verify valid keys always precede invalid keys and
invalid keys are deterministic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ed732d4-1685-4c62-ba92-6dbda1fe8637

📥 Commits

Reviewing files that changed from the base of the PR and between 946e881 and f6eff91.

📒 Files selected for processing (2)
  • storage/migration.go
  • storage/migration_internal_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • storage/migration.go

Comment on lines +104 to +115
func TestSortMigrationKeysFallsBackToLexicalForUnparseablePrefix(t *testing.T) {
// Keys whose prefix is not a number must still sort deterministically
// rather than panic; runMigrations reports the bad id when it gets there.
keys := []string{"10__ok", "bad__name", "2__ok", "another"}
sortMigrationKeys(keys)

// Numeric-prefixed keys keep numeric order among themselves; the rest are
// ordered lexically relative to whatever they compare against.
if got := slices.Index(keys, "2__ok"); got > slices.Index(keys, "10__ok") {
t.Fatalf("2__ok should precede 10__ok, got order %v", keys)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Demonstrate transitivity violation in the current upstream sortMigrationKeys

cat << 'EOF' > test_sort.go
package main

import (
	"fmt"
	"slices"
	"strconv"
	"strings"
)

func migrationID(key string) (int, error) {
	prefix, _, ok := strings.Cut(key, "__")
	if !ok {
		return 0, fmt.Errorf("err")
	}
	return strconv.Atoi(prefix)
}

func sortMigrationKeys(keys []string) {
	slices.SortStableFunc(keys, func(a, b string) int {
		idA, errA := migrationID(a)
		idB, errB := migrationID(b)
		if errA != nil || errB != nil {
			return strings.Compare(a, b)
		}
		if idA != idB {
			if idA < idB {
				return -1
			}
			return 1
		}
		return strings.Compare(a, b)
	})
}

func main() {
	// Demonstrating inconsistent sorting based on initial order due to transitivity violation
	k1 := []string{"10__ok", "2__ok", "1_err"}
	sortMigrationKeys(k1)
	fmt.Printf("Sorted starting from A,B,C: %v\n", k1)
	
	k2 := []string{"1_err", "2__ok", "10__ok"}
	sortMigrationKeys(k2)
	fmt.Printf("Sorted starting from C,B,A: %v\n", k2)
}
EOF
go run test_sort.go

Repository: tink3rlabs/magic

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant files first.
git ls-files 'storage/*' | sort
echo '---'
ast-grep outline storage/migration.go --view expanded || true
echo '---'
ast-grep outline storage/migration_internal_test.go --view expanded || true
echo '---'
# Show the relevant sections with line numbers.
sed -n '1,240p' storage/migration.go | cat -n
echo '---'
sed -n '1,220p' storage/migration_internal_test.go | cat -n

Repository: tink3rlabs/magic

Length of output: 15125


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files 'storage/*' | sort
echo '---'
ast-grep outline storage/migration.go --view expanded || true
echo '---'
ast-grep outline storage/migration_internal_test.go --view expanded || true
echo '---'
sed -n '1,240p' storage/migration.go | cat -n
echo '---'
sed -n '1,220p' storage/migration_internal_test.go | cat -n

Repository: tink3rlabs/magic

Length of output: 15125


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,220p' storage/migration.go | cat -n

Repository: tink3rlabs/magic

Length of output: 7269


Make the migration-key comparator total for mixed inputs
The current fallback to strings.Compare when either side is unparseable breaks transitivity (10__ok < 1_err, 1_err < 2__ok, but 10__ok > 2__ok). That can make slices.SortStableFunc produce orderings that depend on slice state. Compare parseable and unparseable keys in separate buckets (valid first, invalid last) and add a mixed-key test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@storage/migration_internal_test.go` around lines 104 - 115, Update
sortMigrationKeys so its comparator is total for mixed inputs: place keys with
parseable numeric prefixes before unparseable keys, compare numeric keys
numerically and invalid keys lexically. Extend
TestSortMigrationKeysFallsBackToLexicalForUnparseablePrefix with mixed
valid/invalid cases that verify valid keys always precede invalid keys and
invalid keys are deterministic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Migration runner sorts files lexically but gates on numeric id — 3-digit migrations silently skip earlier ones

1 participant