-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Support evolutions scripts prefixed with zeros (01.sql, 001.sql, etc.) #7978
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
Conversation
| def loadResource(db: String, revision: Int) = { | ||
| environment.getExistingFile(Evolutions.fileName(db, revision)).map(f => java.nio.file.Files.newInputStream(f.toPath)).orElse { | ||
| environment.resourceAsStream(Evolutions.resourceName(db, revision)) | ||
| val revisionPadded = List.tabulate(15)(s"${revision}".reverse.padTo(_, "0").reverse.mkString).distinct // 1, 01, 001, ... 000000000001 |
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.
List.tabulate(15)(n => List.fill(n)(0).mkString + revision)Looks simpler to me.
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.
I didn't change this because with your approach we always would add a fixed amount of zeros in front. E.g. if we have revision 7 and 1435 the existence of files with up until following amount of zeros would be checked:
000000000000007.sql
000000000000001435.sql
I think however we just should check up until a fixed total file name length and that is what my padding approach is doing:
00000000000007.sql
00000000001435.sql
Otherwise (probably very very rare cases anyway) people might wonder why 000000000000001435.sql works (same like above) but 0000000000000007.sql doesn't (one zero more than above).
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.
Got it.
I think you can then try:
List.tabulate(15 - revision.toString.length)(n => List.fill(n)(0).mkString + revision)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.
If we're scanning for the first file that matches, then something like this reads better to me:
def loadResource(db: String, revision: Int): Option[InputStream] = {
@tailrec def findPaddedRevisionFile(paddedRevision: String): Option[InputStream] {
if (paddedRevision.length > 15) {
None // Revision string has reached max padding
} else {
{
// Try a file on the filesystem
val filename: String = Evolutions.fileName(db, paddedRevision)
environment.getExistingFile().map(file => java.nio.file.Files.newInputStream(file.toPath))
} orElse {
// Try a resource on the classpath
val resourceName: String = Evolutions.resourceName(db, paddedRevision)
environment.resource(resourceName)
} match {
case None =>
// Add an extra "0" to the padding
findPaddedRevisionFile("0"+revisionString)
case someStream@Some(_) =>
// Found something!
someStream
}
}
}
findPaddedRevisionFile(revision.toString)
}| environment.resourceAsStream(Evolutions.resourceName(db, revision)) | ||
| val revisionPadded = List.tabulate(15)(s"${revision}".reverse.padTo(_, "0").reverse.mkString).distinct // 1, 01, 001, ... 000000000001 | ||
|
|
||
| revisionPadded.flatMap(revision => environment.getExistingFile(Evolutions.fileName(db, revision))).find(_ != None).map(f => java.nio.file.Files.newInputStream(f.toPath)).orElse { |
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.
find(_ != None) -> find(_.isDefined).
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.
@marcospereira Also I did not update this because I use flatMap so _ could be just the File itself (which of course doesn't have a isDefined method)
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.
@mkurz Got it too. You won't need the find then since flatMap removes the Nones for you (it is like flattening an empty list). For example:
val a = List(1, 2, 3, 4, 5)
numbers.flatMap {
case n if n % 2 == 0 => Some(n)
case n => None
}Results in List(2, 4) and not List(None, 2, None, 4, None). So, no need to find. Just use headOption. Finally, maybe we can iterate over the list once like this:
revisionPadded.flatMap { revision =>
environment.getExistingFile(Evolutions.fileName(db, revision)) match {
case Some(file) => Option(java.nio.file.Files.newInputStream(file.toPath))
case None => environment.resource(Evolutions.resourceName(db, revision)).map(_.openStream())
}
}.headOptionThere 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.
@marcospereira If I read your suggested code (to iterate just once) right, it would mean that we would check back and forth between file and resource. Priority in your code would be:
1.sql file > 1.sql resource > 01.sql file > 01.sql resource > 001.sql file > 001.sql resource > .... > 00000000000001.sql file > 00000000000001.sql resource
Whereas priority in my code would be:
1.sql file > 01.sql file > 001.sql file > 0001.sql file > ... > 00000000000001.sql file > 1.sql resource > 01.sql resource > 001.sql resource > 0001.sql resource > ... > 00000000000001.sql resource
So my code checks all files of a revision before even starting looking into a resource for that revision.
I don't say mine is better, just questioning what is the better approach? WDYT? Does it matter somehow?
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.
Hi @mkurz, sorry for taking so long to reply here.
What is the better approach? WDYT? Does it matter somehow?
I think it would be better to go file > resource for each iteration. That looks closer to the existing behavior to me, but I don't have a strong opinion about this. Trying all files and later the resources also sound reasonable to me too because I can't envision a case where users will have both file 01.sql (tried first in your approach) and resource 1.sql (tried after all files in your approach).
WDYT?
|
Since we're changing this, I wonder if we could also add the ability to use other names? see #6919. If you had the ability to use arbitrary names, we could have something like: Is there a real advantage to only allowing numbers? |
|
Rails migrations have a sustainable and simple solution in my opinion:
They are sortable, have good information about when the migration was created, and have a clear name as suggested in #6919. So I would be more inclined to have something like this instead. @mkurz, how would this PR handle a case where we have the following files: This is obviously a mistake, but that is hard to make today since the numbers are plain incremental. WDYT? |
There is no real advantage in only allowing numbers, that's just how it was implemented in Play starting with Play 1 and never got touched anymore. We could (and probably should) allow the possibility to use other names. Turns out this is possible already by implementing your own If you really would have an
That would also back up my idea of having different evolution readers built-in and actived by users so they don't mix different approaches. Pull request updated, ready to reviewed again. |
|
Actually the format could be supported by the current default evolution reader since there is just a random string after the number (which doesn't influence ordering), however the format definitely would need it's own However I will not add support for the former format to this pull request. |
ec198e2 to
17649a9
Compare
I have another opinion here: to me Play needs to have a good (opinionated) default and be extensible. Right now I think we are extensible with a not so good default (just incremental numbers). Given that and the fact we need to offer a transition, I would pick the
And, as I said, users can replace this if they need/want. My intent here is to keep Play itself smaller, with the possibility to have a bigger ecosystem evolved by the community.
Sorry for not taking proper time to better explain this, @mkurz. I was trying to draw the same scenario you did: In this case |
|
|
|
@marcospereira @richdougherty Thank you for your comments! I had a deeper look into this issue and it eventually turned out that we actually do not need to check for a file - checking for resources on the classpath is all it needs: I am pretty sure this file checking is just a leftover from long time ago and therefore I removed it. This doesn't change behaviour at all, also not in production (Again, this file checking was just introduced for DEV mode, which is working now, it was never targeting prod mode.) I am almost certainly 100% sure about that 😉 About the implementation: I also created a test project to test all of this: https://github.com/mkurz/play-evolutions-padded/ (based on a 2.7.0-SNAPSHOT) In the log you get following warnings: So please check again, I think this is done and can be merged 😄 |
|
BTW @marcospereira
Sure we can do that. We just need to implement the |
marcospereira
left a comment
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 your patience here, @mkurz.
This LGTM, but I think we can have some tests here.
You can even test the logging warning by using play.api.libs.logback.LogbackCapturingAppender. See play.api.ModeSpecificLoggerSpec for an example.
|
Alright, I will have a look. |
|
@marcospereira Done - tests added. Please have a look, thanks! |
| "Ignoring evolution script 002.sql, using 2.sql instead already", | ||
| "Ignoring evolution script 005.sql, using 05.sql instead already", | ||
| "Ignoring evolution script 0010.sql, using 010.sql instead already" | ||
| ) |
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.
I'm always impressed when I see logging tests!
richdougherty
left a comment
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.
LGTM
|
@marcospereira I think this pr is also waiting for your approval 😉 |
marcospereira
left a comment
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.
LGTM.
Thank you, @mkurz. And sorry for taking so long to look back at this PR.
|
@marcospereira Thanks! |
Fixes #7976
The order of precedence which file is chosen is
1.sqlif it exists, otherwise01.sqlif it exists, otherwise001.sqland so on - until000000000001.sql.Can be backported because it's backwards compatible.