Repository navigation
dialect/sql/schema: one process applies a directory at a time, and an edited file it ran is refused - #17
Merged
Conversation
… edited file it ran is refused A deployment applies its migrations on every start, and so does each replica of it, and nothing kept two from applying one directory at once. On PostgreSQL both made the history table, and the second was refused for a duplicate catalog row; both ran a pending file's statements, and the second to reach one that cannot run twice failed -- and atlas wrote its failure over the record of the one that ran the file, so the history said the file stopped halfway and every start after ran it again, failed again, and did not start. `Apply` now holds a session advisory lock, keyed by the schema and the history table, from before the history table is made until the files have run. The other process waits, and then finds nothing to do. SQLite is not locked: a file several processes migrate is not what this is for. And a file a database ran is not run again, so one edited afterwards, its sum written again with `Rehash`, changed nothing where it had run and everything where it ran next, with nothing to say the two differ. Atlas compares a file's statements only while it is halfway, and `atlas.sum` is rewritten from the files as they are. The history keeps the hash each file had when it ran, so `Pending`, `Apply` and `Current` now refuse a directory that no longer agrees with it, with `ErrEdited`. The unit job gets a PostgreSQL service for the test that needs several sessions; the schema migration tests make a database of their own on it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxtnpRW533bdErnW8QPhsk
… not on Create `Schema.Create` drops a foreign key or a check the schema does not declare: that is how it retires one the schema stopped declaring, since an edge pointed at another table is a foreign key of a new name and the old one, left behind, would refuse every row the new one allows. It cannot tell those from one the extra directory made, so a database kept in step by Create loses that directory's foreign keys and checks the next time Create runs, and the directory's history says the file ran, so nothing makes them again. (Checked on PostgreSQL 17: a CHECK and a foreign key added beside the schema are gone after the next Create.) Leaving them alone in Create, as `Check` does, would keep the stale one too, so the package says what the directory needs instead: a database migrated from files. An app that migrates with Create states what the schema cannot in statements it runs after every Create, each safe to run again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxtnpRW533bdErnW8QPhsk
lesomnus
added a commit
to lesomnus/payday
that referenced
this pull request
Oct 23, 2026
…it calls, and a test links it to say so payday's own sandbox was 63 MB, and 24 MB of it was methods nothing calls. The generated client's runtime.go makes the ent schema's fields at init, for their defaults and value scanners, and every field it made listed its Go type's methods with reflect.Type.Method, by an index the compiler cannot see. In a program that can reach such a call the linker has to assume that any exported method of any type converted to an interface may be called by name, so it keeps all of them, in every package. The page worked, and no list of packages showed it: the call was in one the sandbox has to link. An app with no default of its own has paid it since #87. protoc-gen-orm-ent began writing a declared default as the column's (`.Default(v)`, protobuf-orm/protoc-gen-orm-ent#8), and payday's Audit declares one on `domain`, so since then every app's runtime.go reads the schema at init -- for Access's `domain` as well, since #112. khala's sandbox measured 25.8 MB on payday 6bb6d9c and 46.9 MB on c0f5e7d. This app paid it longer, for its Holder's value scanner. protobuf-orm/ent#20 (4d86cf3) lists the methods in its loader, which only codegen runs, and takes RType.Implements out of the exported API: it has reflect.Type's name and signature, which the linker keeps on any type converted to an interface, and the same call in its loop. What ent generates is the same but for its stamp of which ent made it (`internal/ent/runtime/runtime.go`), the one file apptest's regeneration moves; an app takes the fix with payday's go.mod, and regenerates for the stamp. The bump brings protobuf-orm/ent#17 too -- one process applies a migration directory at a time, and an edited file it ran is refused -- which payday does not call. TestTheSandboxLetsTheLinkerPruneMethods links this app's sandbox as it ships, under GOOS=js with grpcnotrace, with the linker's dump (-dumpdep), and fails on any function in it marked for calling a method by index, with the chain the linker took to it. On the ent before this it fails at field.methods, reached from runtime.go's init through schema.Access.Fields, field.Json and goType. Measured, `GOOS=js GOARCH=wasm go build -tags grpcnotrace ./wasm`: - this app: 62,994,314 B before, 39,013,431 B after (gzip -9: 10.99 MB, 7.61 MB). Without runtime.go's init at all it is 38,628,342 B. - roster c4ae61c, with this payday replaced in: 89,958,712 B before, 56,631,026 B after (gzip -9: 15.47 MB, 11.03 MB). docs: client.md says what a method called by index costs a sandbox, runtime.md's table of what enforces what has the test, and migrating.md says an app regenerates and what it gets back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxtnpRW533bdErnW8QPhsk
lesomnus
added a commit
to lesomnus/payday
that referenced
this pull request
Oct 23, 2026
…it calls, and a test links it to say so payday's own sandbox was 63 MB, and 24 MB of it was methods nothing calls. The generated client's runtime.go makes the ent schema's fields at init, for their defaults and value scanners, and every field it made listed its Go type's methods with reflect.Type.Method, by an index the compiler cannot see. In a program that can reach such a call the linker has to assume that any exported method of any type converted to an interface may be called by name, so it keeps all of them, in every package. The page worked, and no list of packages showed it: the call was in one the sandbox has to link. An app with no default of its own has paid it since #87. protoc-gen-orm-ent began writing a declared default as the column's (`.Default(v)`, protobuf-orm/protoc-gen-orm-ent#8), and payday's Audit declares one on `domain`, so since then every app's runtime.go reads the schema at init -- for Access's `domain` as well, since #112. khala's sandbox measured 25.8 MB on payday 6bb6d9c and 46.9 MB on c0f5e7d. This app paid it longer, for its Holder's value scanner. protobuf-orm/ent#20 (4d86cf3) lists the methods in its loader, which only codegen runs, and takes RType.Implements out of the exported API: it has reflect.Type's name and signature, which the linker keeps on any type converted to an interface, and the same call in its loop. What ent generates is the same but for its stamp of which ent made it (`internal/ent/runtime/runtime.go`), the one file apptest's regeneration moves; an app takes the fix with payday's go.mod, and regenerates for the stamp. The bump brings protobuf-orm/ent#17 too -- one process applies a migration directory at a time, and an edited file it ran is refused -- which payday does not call. TestTheSandboxLetsTheLinkerPruneMethods links this app's sandbox as it ships, under GOOS=js with grpcnotrace, with the linker's dump (-dumpdep), and fails on any function in it marked for calling a method by index, with the chain the linker took to it. On the ent before this it fails at field.methods, reached from runtime.go's init through schema.Access.Fields, field.Json and goType. Measured, `GOOS=js GOARCH=wasm go build -tags grpcnotrace ./wasm`: - this app: 62,994,314 B before, 39,013,431 B after (gzip -9: 10.99 MB, 7.61 MB). Without runtime.go's init at all it is 38,628,342 B. - roster c4ae61c, with this payday replaced in: 89,958,712 B before, 56,631,026 B after (gzip -9: 15.47 MB, 11.03 MB). docs: client.md says what a method called by index costs a sandbox, runtime.md's table of what enforces what has the test, and migrating.md says an app regenerates and what it gets back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KxtnpRW533bdErnW8QPhsk
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
For lesomnus/payday#33's directory (#7), and for the planned one beside it. Both are applied by
Migrations.Apply, on every start, and by every replica that starts.What was wrong
Applyor in atlas'sExecutorlocks; the Atlas CLI takesschema.Lockeritself. On PostgreSQL:duplicate key value violates unique constraint "pg_type_typname_nsp_index", despiteIF NOT EXISTS.relation "once" already exists), and its deferred revision write replaces the winner's record.applied=1 total=2 error="… already exists". Every laterApplyreruns the file, fails the same way, and the deployment cannot start until someone fixes the row by hand.atlas.sumis whateverRehashlast wrote. So an edit plus a rehash changed nothing in a database that had already run the file, and changed what ran in the next database migrated. Nothing reported the difference.What changed
Applyholds a session advisory lock (pg_advisory_lock) from before it creates the history table until the files have run.ctxallows, then finds nothing pending.os.TempDir()keyed by name, which would serialize unrelated databases, and several processes migrating one SQLite file is not a supported setup.ErrEdited.Pending,ApplyandCurrentcompare each fully applied revision's stored hash with the directory's hash for that file, and refuse a mismatch, naming the files.unitjob gets apostgres:17service. The schema migration step getsENT_SCHEMA_POSTGRES; its tests create a database of their own and drop it.dialect/sql/schema/integrationrequireslib/pqv1.10.7, the versionentc/integrationalready pins.And what the directory needs: files, not
Schema.CreateThe second commit records why
Schema.Createkeeps dropping undeclared foreign keys and checks. One option was to make its default skip matchCheck's, so it would leave them alone. I did not, because Create drops them on purpose: that is how it retires a constraint the schema stopped declaring.<table>_<ref>_<edge>). Left in place, the old key would refuse every row the new one allows.CHECKand a foreign key added beside the schema are gone after the next Create.So the package doc and
Extranow say the directory is for a database migrated from files. An app that migrates with Create instead runs its DDL after every Create, with each statement safe to run again; rove'scli/pg.sqlworks this way.Tests
TestApplyIsOneAtATime(PostgreSQL; skipped withoutENT_SCHEMA_POSTGRES): two pools apply the same directory concurrently.SELECT pg_sleep(0.5); CREATE TABLE once ….applied = totalwith no error.TestEdited(SQLite):Pending,ApplyandCurrent, and the error names it;TestApplyIsOneAtATimefail, three runs out of three (the catalog duplicate, thenrelation "once" already exists);TestEditedfails.go test -race ./...passes indialectand, with PostgreSQL 17, indialect/sql/schema/integration..github/lint.shis clean on the changed files.🤖 Generated with Claude Code
https://claude.ai/code/session_01KxtnpRW533bdErnW8QPhsk