-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
[cli] Improve migration creation #312
Conversation
* Validates migration version on creation, to avoid creation of duplicated versions * Uses `os.OpenFile` with `O_CREATE|O_EXCL` to create files to avoid file collisions * Uses `filepath.Join` to concatenate paths, making `cleanPath()` not necessary * Prints generated filenames * Fixes golang-migrate#238 * Supersedes golang-migrate#250
Pull Request Test Coverage Report for Build 570
💛 - Coveralls |
6377852
to
7fa10e1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me
|
||
createCmd(*dirPtr, startTime, *formatPtr, name, *extPtr, seq, seqDigits) | ||
if err := createCmd(*dirPtr, startTime, *formatPtr, name, *extPtr, seq, seqDigits, true); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider putting this true
in a named variable, so that it's understandable without checking method definition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of the other *Cmd()
methods return an error. The new method signature where an error is returned is cleaner, it's more important to be consistent. Either use log.fatalErr()
in createCmd()
or update the other *Cmd()
methods to also return an error.
padding := seqDigits - len(nextSeqStr) | ||
if padding > 0 { | ||
nextSeqStr = strings.Repeat("0", padding) + nextSeqStr | ||
version := fmt.Sprintf("%0[2]*[1]d", nextSeq, seqDigits) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's pretty neat
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to not implementing our own padding logic!
I currently can't create migrations on windows, hoping this can resolve that issue. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the PR and the great writeup describing your changes!
Please see the feedback below.
|
||
createCmd(*dirPtr, startTime, *formatPtr, name, *extPtr, seq, seqDigits) | ||
if err := createCmd(*dirPtr, startTime, *formatPtr, name, *extPtr, seq, seqDigits, true); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of the other *Cmd()
methods return an error. The new method signature where an error is returned is cleaner, it's more important to be consistent. Either use log.fatalErr()
in createCmd()
or update the other *Cmd()
methods to also return an error.
padding := seqDigits - len(nextSeqStr) | ||
if padding > 0 { | ||
nextSeqStr = strings.Repeat("0", padding) + nextSeqStr | ||
version := fmt.Sprintf("%0[2]*[1]d", nextSeq, seqDigits) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 to not implementing our own padding logic!
}{ | ||
{dir: "", expectedCleanDir: ""}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using filepath
to build paths should make cleanDir()
obsolete. Can you verify that the behavior for these test cases is the same?
log.fatalErr(err) | ||
func createFile(filename string) error { | ||
// create exclusive (fails if file already exists) | ||
f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment that os.Create()
specifies 0666
as the FileMode
, so we're doing the same
Is there any chance for this PR to be merged soon? |
@r3code It might be quicker to create a new PR that references this one and supersedes it. Make your PR still has the original commits from this PR. |
Superseded by #352 |
Instead of returning a file not found error when no more changes are available, return a no change message. Fixes golang-migrate#35 Fixes golang-migrate#312
Instead of returning a file not found error when no more changes are available, return a no change message. Fixes golang-migrate#35 Fixes golang-migrate#312
This PR adds validation to migration versions when creating migrations and improves/refactors creation in general. As a "side-effect", it fixes #238.
Rationale
migrate create
doesn't validate migration versions when creating files. This causes issues when creating files that results in same-version migrations. It's easy to replicate this behavior:I accidentally hit the first case when converting an already existing database schema into migrations, using a very similar loop, iterating over table names, creating migrations and redirecting the output of
pg_dump -st $table
into each "up" migration. It didn't occur to me that versions would be colliding. When I tried to runmigrate up
, a very obscure "unable to parse file" error popped up. Reading the source code explained it.Since it's much harder to refactor the whole
source
package to show useful error messages (Migrations.Append() isn't supposed to returnerror
with useful messages instead ofbool
?), I went for the simpler "warn the kids that the stove is hot" solution.Solution & Improvements
A refactoring of the migration creation code first generates the
version
component and then checks if any files with that version already exists on disk.Here's the corresponding outputs to the commands above, using the fixed version:
os.OpenFile
withO_CREATE|O_EXCL
to create files to avoid file collisionsThis would be very difficult to cause, but I can imagine some bogus uses of
parallel
+migrate
that would case this to happen (in cases someone want to generate hundreds of migrations faster, but the script contains bugs, for example).filepath
functions to manipulate paths, makingcleanPath()
not necessaryI see that in the discussion of #238 and the proposed fix for it in 9f6c7e5 and #250, there's a lot of string manipulation gymnastics to handle cross-platform path manipulation.
filepath
is the cross-platform solution for this and all its functions should be used when doing real, on disk, OS specific, path manipulation.So this PR properly fixes #238 and supersedes #250.
This can be debatable, but popular migration generators (active record, django, etc) print the filenames out, so the developer knows which file was created. It's also convenient to simply copy-paste the name of the file to open it for editing.
Tests
The PR updates the migration creation tests, but I'm not able to run the whole suite because I'm on Fedora 31 which ships the kernel with cgroupsv2 enabled and docker doesn't work with it (as of now).
go test ./internal/cli/...
does work though.And I quickly tested the built CLI on a Windows VM: https://gist.github.com/13k/1483f0c377991252b486fd604fd5acee