-
Notifications
You must be signed in to change notification settings - Fork 5
[feat] add support for outside-of-transaction migrations #269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
58c87c3
add support for outside-of-transaction migrations
muir 0914b92
more tests
muir fcfe6c3
more tests
muir 679e56f
shorten readme
muir fc8f426
adjustments, coverage, parallel
muir 28559a4
coverage
muir 7a82e48
more tests
muir 89d9c43
less casting
muir a50657d
more coverage
muir 2c3cb7a
more tests
muir ff1126e
lint
muir 51b9e4e
test updates
muir 387708b
all error generation -> memsql/errors
muir be1f33a
tweak readme to retrigger tests
muir 4406956
refactor for common code and testability
muir 251d748
lint
muir bd2f56c
more coverage
muir 6f25476
more tests
muir 2015e2a
stmtclass
muir 5179895
simplify mysql
muir e941902
simplify mysql.go
muir 1eab28a
cleanup
muir 86fe132
copilot feedback
muir 1583305
wip checkpoint
muir 34a1fc5
redesign
muir df5f867
self review
muir a54e51a
read commited for forced downgrade w/generate
muir d8a6586
more self review
muir 7673390
fake schema everywhere
muir b1b337b
test: consolidate postgres non-tx & classification, mysql generate & …
muir 1d425d2
test(mysql): fold computed mismatch & failure into bad migrations sui…
muir 753bf0e
chore(testing,postgres): simplify FakeSchema cleanup; switch server v…
muir f1bfd33
adjust comments for Script/Generate/Computed; remove GenerateE, a typo
muir 91753e8
clean up / review feedback
muir File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # GitHub Copilot Contribution Rules | ||
|
|
||
| One page. No ambiguity. Follow or reject the change. | ||
|
|
||
| ## Good engineering | ||
| - always prefer to remove code than to add it | ||
| - always look for ways to simplify | ||
|
|
||
| ## Core | ||
| - Remove things completely. No placeholders, stubs, TODO, commentary about removal. History lives only in git. | ||
| - No historical or refactor narration comments. Comments only explain current behavior, invariants, limits, dialect nuances, or (released) API deprecations. | ||
| - ASCII only. No Unicode of any kind. | ||
|
|
||
| ## Public API | ||
| - Public = exported + existed in a tagged release + intentionally documented. | ||
| - Changing/removing: update doc comment only if callers must adapt. | ||
| - Shim only if break is not trivially fixable. Shim must be minimal, delegated, marked `// Deprecated:` with planned removal version, and tested. | ||
| - No shims for anything else. | ||
|
|
||
| ## Testing | ||
| - Coverage target 95-100% for unit-testable code. | ||
| - No mocks. | ||
| - Use testify. | ||
| - Deterministic, table-driven tests. Assert meaningful outcomes only. | ||
| - Disallow: mocking frameworks, log-text assertions (unless log text is contract), sleeps for timing. | ||
| - Prefer errors.Is() over checking error text | ||
|
|
||
| ## Style & Refactors | ||
| - Prefer elimination over abstraction. No commented-out code. No speculative feature flags. | ||
| - Inline vs extract based strictly on clarity or real reuse. | ||
| - Silent refactors do not add commentary. Behavioral changes only update directly affected docs. | ||
|
|
||
| ## Errors | ||
| - Use memsql/errors only. | ||
| - Sentinel errors with errors.String type, augmented with .Errorf | ||
| - Wrap once at subsystem boundaries with context. Do not re-wrap in tight loops. Avoid repeating obvious parameter values. | ||
| - Don't capitalize the first letter | ||
|
|
||
| ## Temporary changes for debugging | ||
| - Mark temporary code with "XXX" | ||
| - Add freely | ||
|
|
||
| ## Checklist (all must be true) | ||
| - [ ] No history / refactor narration comments. | ||
| - [ ] No placeholders or commented-out code left behind. | ||
| - [ ] ASCII only. | ||
| - [ ] Public API changes reviewed; shims only when allowed & tested. | ||
| - [ ] Coverage for unit-testable code >=95% | ||
| - [ ] No mocks. | ||
| - [ ] Error wrapping matches boundary rule. | ||
| - [ ] Tests use testify and do not use t.Errorf() or t.FailNow() | ||
| - [ ] Sentinel errors use errors.String | ||
| - [ ] No temporary (XXX) code remaining for commits | ||
|
|
||
| ## Enforcement | ||
| Non-compliant changes are rejected regardless of technical merit. |
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,9 +4,27 @@ import ( | |
| "context" | ||
| "database/sql" | ||
|
|
||
| "github.com/memsql/errors" | ||
|
|
||
| "github.com/muir/libschema/internal" | ||
| ) | ||
|
|
||
| const ( | ||
| // ErrDataAndDDL indicates a single migration mixes schema changes (DDL) and data | ||
| // manipulation (DML) where that combination is disallowed. | ||
| ErrDataAndDDL errors.String = "migration combines DDL (schema changes) and data manipulation" | ||
|
|
||
| // ErrNonIdempotentDDL indicates a non-transactional migration contains easily | ||
| // guardable DDL lacking an IF (NOT) EXISTS clause. | ||
| ErrNonIdempotentDDL errors.String = "unconditional migration has non-idempotent DDL" | ||
|
|
||
| "github.com/pkg/errors" | ||
| // ErrNonTxMultipleStatements indicates a non-transactional (idempotent) script migration | ||
| // attempted to execute multiple SQL statements when only one is allowed. | ||
| ErrNonTxMultipleStatements errors.String = "non-transactional migration has multiple statements" | ||
|
|
||
| // ErrNonIdempotentNonTx indicates a required-to-be-idempotent non-transactional migration | ||
| // failed idempotency validation heuristics. | ||
| ErrNonIdempotentNonTx errors.String = "non-idempotent non-transactional migration" | ||
| ) | ||
|
|
||
| const DefaultTrackingTable = "libschema.migration_status" | ||
|
|
@@ -46,14 +64,16 @@ type MigrationOption func(Migration) | |
|
|
||
| // Migration defines a single database defintion update. | ||
| type MigrationBase struct { | ||
| Name MigrationName | ||
| async bool | ||
| rawAfter []MigrationName | ||
| order int // overall desired ordring across all libraries, ignores runAfter | ||
| status MigrationStatus | ||
| skipIf func() (bool, error) | ||
| skipRemainingIf func() (bool, error) | ||
| repeatUntilNoOp bool | ||
| Name MigrationName | ||
| async bool | ||
| rawAfter []MigrationName | ||
| order int // overall desired ordring across all libraries, ignores runAfter | ||
| status MigrationStatus | ||
| skipIf func() (bool, error) | ||
| skipRemainingIf func() (bool, error) | ||
| repeatUntilNoOp bool | ||
| nonTransactional bool // set automatically or by ForceNonTransactional / inference | ||
| forcedTx *bool // if not nil, explicitly chosen transactional mode (true=transactional, false=non-transactional) | ||
| } | ||
|
|
||
| func (m MigrationBase) Copy() MigrationBase { | ||
|
|
@@ -190,16 +210,26 @@ func Asynchronous() MigrationOption { | |
| } | ||
| } | ||
|
|
||
| // RepeatUntilNoOp marks a migration as potentially being needed to run multiple times. | ||
| // It will run over and over until the database reports that the migration | ||
| // modified no rows. This can useuflly be combined with Asychnronous. | ||
| // RepeatUntilNoOp marks a migration (Script or Generate) to be re-executed until | ||
| // the underlying driver reports zero rows affected. Use it for single, pure DML | ||
| // statements that progressively transform data (e.g. UPDATE batches that move a | ||
| // limited subset each run). It is NOT a generic looping primitive. | ||
| // | ||
| // Safety / correctness guidelines: | ||
| // - Single statement only - multi-statement scripts can give a meaningless final RowsAffected(). | ||
| // - DML only - avoid DDL (CREATE/ALTER/DROP/REFRESH) or utility commands; most return 0 and will | ||
| // terminate immediately or loop uselessly. | ||
| // - Idempotent per batch - repeating the same statement after partial success must not corrupt data. | ||
| // - Do NOT mix with non-transactional Postgres DDL (concurrent index builds, REFRESH MATERIALIZED VIEW CONCURRENTLY). | ||
| // - If logic needs conditionals or multiple statements, write a Computed migration and loop explicitly. | ||
| // | ||
| // This marking only applies to Script() and Generated() migrations. The migration | ||
| // must be a single statement. | ||
| // Prefer a Computed migration when: | ||
| // - You need to run multiple statements per batch | ||
| // - You must inspect progress with custom queries | ||
| // - RowsAffected() is unreliable or driver-dependent | ||
| // | ||
| // For Computed() migrations, do not use RepeatUntilNoOp. Instead simply write the | ||
| // migration use Driver.DB() to get a database handle and use it to do many little | ||
| // transactions, each one modifying a few rows until there is no more work to do. | ||
| // Future note: libschema may emit debug warnings for obviously unreliable usages | ||
| // (e.g. DDL + RepeatUntilNoOp). Treat the above bullets as normative behavior now. | ||
| func RepeatUntilNoOp() MigrationOption { | ||
| return func(m Migration) { | ||
| m.Base().repeatUntilNoOp = true | ||
|
|
@@ -247,6 +277,58 @@ func SkipRemainingIf(pred func() (bool, error)) MigrationOption { | |
| } | ||
| } | ||
|
|
||
| // ForceNonTransactional forces a migration (Script, Generate, or Computed) to run without | ||
| // a wrapping transaction. By using this you assert the migration is idempotent (safe to retry). | ||
| // Overrides any automatic inference the driver would normally perform. | ||
| // | ||
| // Generate note: For drivers where generation always occurs inside a transaction context (e.g. Postgres | ||
| // Generate with *sql.Tx generator) forcing non-transactional only affects execution of the | ||
| // produced SQL, not the generator callback itself. Drivers will reject impossible combinations (e.g. | ||
| // attempting to force non-transactional on a generator that fundamentally requires *sql.Tx if they cannot | ||
| // safely downgrade). | ||
| func ForceNonTransactional() MigrationOption { | ||
| return func(m Migration) { | ||
| b := m.Base() | ||
| v := false // false means non-transactional | ||
| b.forcedTx = &v | ||
|
Comment on lines
+292
to
+293
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should this use
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, force applies later. |
||
| } | ||
| } | ||
|
|
||
| // ForceTransactional forces a migration to run inside a transaction even if automatic inference | ||
| // would choose non-transactional execution. | ||
| // | ||
| // Generate note: For generator-based migrations whose callback already receives *sql.Tx (Generate) | ||
| // this is effectively a no-op (they are already transactional). For generators that inherently execute | ||
| // outside a transaction (none exist in current public API) forcing transactional would be rejected. | ||
| // | ||
| // WARNING (foot-gun): On MySQL / SingleStore this DOES NOT make DDL atomic. Those engines | ||
| // autocommit each DDL statement regardless of any BEGIN you issue. By forcing transactional mode: | ||
| // - Earlier DML in the same migration may roll back while preceding DDL remains applied. | ||
| // - The bookkeeping UPDATE that records migration completion can fail while schema changes | ||
| // have already been partially or fully applied (leaving the migration marked incomplete and | ||
| // retried later against an already-changed schema). | ||
| // - Mixed DDL + DML ordering inside a forced transactional migration can produce inconsistent, | ||
| // misleading results during retries or partial failures. | ||
| // | ||
| // Use this only if you fully understand these semantics and prefer to retain a transactional wrapper | ||
| // for non-DDL statements. If you simply need safe DDL, prefer writing idempotent statements and let | ||
| // the driver downgrade automatically. | ||
| func ForceTransactional() MigrationOption { | ||
| return func(m Migration) { | ||
| b := m.Base() | ||
| v := true // true means transactional | ||
| b.forcedTx = &v | ||
| } | ||
| } | ||
|
|
||
| // ApplyForceOverride overrides transactionality for any prior force call (ForceTransactional | ||
| // or ForceNonTransactional) | ||
| func (m *MigrationBase) ApplyForceOverride() { | ||
| if m.forcedTx != nil { // forced override present | ||
| m.SetNonTransactional(!*m.forcedTx) | ||
| } | ||
| } | ||
|
|
||
| func (d *Database) DB() *sql.DB { | ||
| return d.db | ||
| } | ||
|
|
@@ -290,6 +372,26 @@ func (m *MigrationBase) HasSkipIf() bool { | |
| return m.skipIf != nil | ||
| } | ||
|
|
||
| // NonTransactional reports if the migration must not be wrapped in a transaction. | ||
| // This is automatically set for drivers (e.g. Postgres) that provide generic | ||
| // migration helpers which infer non-transactional status from the connection | ||
| // type used (e.g. *sql.DB vs *sql.Tx). A migration marked nonTransactional will | ||
| // be executed without an encompassing BEGIN/COMMIT; status recording still | ||
| // occurs within its own small transaction when supported. | ||
| func (m *MigrationBase) NonTransactional() bool { | ||
| return m.nonTransactional | ||
| } | ||
|
|
||
| func (m *MigrationBase) SetNonTransactional(v bool) { | ||
| m.nonTransactional = v | ||
| } | ||
|
|
||
| // ForcedTransactional reports if ForceTransactional() was explicitly called. | ||
| func (m *MigrationBase) ForcedTransactional() bool { return m.forcedTx != nil && *m.forcedTx } | ||
|
|
||
| // ForcedNonTransactional reports if ForceNonTransactional() was explicitly called. | ||
| func (m *MigrationBase) ForcedNonTransactional() bool { return m.forcedTx != nil && !*m.forcedTx } | ||
|
|
||
| func (n MigrationName) String() string { | ||
| return n.Library + ": " + n.Name | ||
| } | ||
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
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.