A versioned migration is a controlled database change with an identity and exact source bytes. A history row says which version was applied and which checksum was reviewed. Editing an already applied file creates drift rather than a new migration.
The native task plans an exact prefix. The real adapter uses a transaction and an advisory transaction lock before reading history, then applies pending SQL and records checksums together. The supplied catalog has two reviewed versions, followed by a deliberately failing third proposal used to prove rollback.
PendingMigrations checks order and exact checksums, then copies only the unapplied suffix. Returning an aliased slice would let a caller mutate the reviewed catalog accidentally. Even a trailing newline changes the checksum because the bytes differ.
The database adapter takes its advisory transaction lock before reading migration history. A failed migration’s DDL and history update roll back together. This small runner is deliberately local and bounded; it is not a full replacement for an established migration tool in a larger application.
package main
import ("fmt";"crypto/sha256")
func main(){
a:=sha256.Sum256([]byte("sql"));b:=sha256.Sum256([]byte("sql\n"));fmt.Println(a==b)
}A checksum does not prove that SQL is safe or correct. It binds an applied version to reviewed bytes. The owner still reviews the migration and its data effects. A new legitimate change belongs in a new version, not an edited historical file.
The failing third proposal creates a table and then divides by zero. The actual lab verifies the table is absent afterward and history still has two versions. Reapplying the unchanged two-version catalog performs no new work.
package main
import ("fmt")
func main(){
catalog:=[]int{1,2};applied:=1;pending:=append([]int{},catalog[applied:]...);pending[0]=99;fmt.Println(catalog,pending)
}Implement exact history-prefix planning and reject edited history. Next use parameters and a transaction around related application writes.
The records survive the database boundary.
This snapshot came from the owned local lab. Choose an imported identity to inspect its actual source and canonical target. Reconciliation covers all fields, before a separate successful application write brings the final note count to four.
- 1 · Schemaowners · notes · events
- 2 · Indexowner lookup
- Reapplyzero pending versions
- Bad version 3DDL/history rolled back
SQLite source · ID 1
{
"id": 1,
"owner_id": 1,
"title": " first note ",
"done": 0,
"cents": 29,
"created_at": "2024-01-01T01:00:00+01:00"
}PostgreSQL canonical row
{
"id": 1,
"owner_id": 1,
"title": "first note",
"done": false,
"cents": 29,
"created_at": "2024-01-01T00:00:00Z"
}done → booleancents → bigintcreated_at → timestamp with time zone3 source rows · 3 target rows · no missing, unexpected or changed identities
Inspect the separate database checks
- Quoted owner name
- 2 matching rows with a bound parameter
- Injection-shaped name
- 0 matches · value remains data
- Next generated identity
- 43 after imported maximum 42
- Failed note/event transaction
- Partial note write rolled back
- Foreign key and cents constraints
- Actual invalid inserts rejected
- Canceled query
- Cancellation observed; pool usable afterward
- Application role
- Nonsuperuser · pool maximum two connections
- Owned cleanup
- Cluster stop confirmed; fixture directory removed