Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@mkurz
Copy link
Member

@mkurz mkurz commented Nov 1, 2017

Fixes #7976

The order of precedence which file is chosen is 1.sql if it exists, otherwise 01.sql if it exists, otherwise 001.sql and so on - until 000000000001.sql.

Can be backported because it's backwards compatible.

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
Copy link
Member

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.

Copy link
Member Author

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).

Copy link
Member

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)

Copy link
Member

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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find(_ != None) -> find(_.isDefined).

Copy link
Member Author

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)

Copy link
Member

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())
  }
}.headOption

Copy link
Member Author

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?

Copy link
Member

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?

@gmethvin
Copy link
Member

gmethvin commented Nov 2, 2017

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:

001-create-foos-table.sql
002-add-foos-bar-field.sql
...

Is there a real advantage to only allowing numbers?

@eximius313
Copy link

@gmethvin the advantage is to have at least partial solution now - #6919 was raised almost year ago

@marcospereira
Copy link
Member

marcospereira commented Nov 2, 2017

Rails migrations have a sustainable and simple solution in my opinion:

Migrations are stored as files in the db/migrate directory, one for each migration class. The name of the file is of the form YYYYMMDDHHMMSS_create_products.rb, that is to say a UTC timestamp identifying the migration followed by an underscore followed by the name of the migration.

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:

evolutions
|_ 001.sql
|_ 0010.sql
|_ 010.sql
|_ 0100.sql

This is obviously a mistake, but that is hard to make today since the numbers are plain incremental.

WDYT?

@mkurz
Copy link
Member Author

mkurz commented Nov 3, 2017

Since we're changing this, I wonder if we could also add the ability to use other names?
Is there a real advantage to only allowing numbers?

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.

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 EvolutionsReader, however it would be nicer if Play comes with various built-in EvolutionsReaders so someone could just switch on the YYYYMMDDHHMMSS_create_products format by disabling the default "number" reader and enabling that specific built-in one.
However for backward compatibility reasons I wouldn't change the default reader. IMHO providing built-in readers should do.
Also I wouldn't make that part of this pull request. This pr is just about enhancing the current default EvolutionsReader.

@marcospereira

evolutions
|_ 001.sql // revision 1
|_ 0010.sql // revision 10, but ignored because 010.sql (see next line) has priority
|_ 010.sql // revision 10, chosen over 0010.sql because it has fewer leading zeros
|_ 0100.sql // revision 100

If you really would have an evolutions folder like this (with exactly that files) only 001.sql would run right now, until you add revision 2-9, then also 010.sql would run (but 0010.sql wouldn't), then you would have to add revision 11-99 so that 0100.sql would run. This isn't any different like it works right now.
Giving 010.sql priority over 0010.sql guarantees backward compatibility because fewer zeros wins (10.sql having highest priority in that case).

This is obviously a mistake, but that is hard to make today since the numbers are plain incremental.

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.

@mkurz
Copy link
Member Author

mkurz commented Nov 3, 2017

Actually the format

001-create-foos-table.sql
002-add-foos-bar-field.sql

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

YYYYMMDDHHMMSS_create_products.sql

definitely would need it's own EvolutionsReader because it's not compatible with the number format one.

However I will not add support for the former format to this pull request.

@mkurz mkurz force-pushed the evolutionsPadding branch from ec198e2 to 17649a9 Compare November 3, 2017 19:32
@marcospereira
Copy link
Member

however it would be nicer if Play comes with various built-in EvolutionsReaders so someone could just switch on the YYYYMMDDHHMMSS_create_products format by disabling the default "number" reader and enabling that specific built-in one.

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 YYYYMMDDHHMMSS_create_products.sql format as the default for new since:

  1. It does not have to count/pad zeros at all
  2. Has clear ordering
  3. Has information information about when the evolution was created
  4. Has an human name part.

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.

If you really would have an evolutions folder like this (with exactly that files) only 001.sql would run right now, until you add revision 2-9, then also 010.sql would run (but 0010.sql wouldn't), then you would have to add revision 11-99 so that 0100.sql would run. This isn't any different like it works right now.

Sorry for not taking proper time to better explain this, @mkurz. I was trying to draw the same scenario you did:

evolutions
|_ 001.sql
|_ 002.sql
|_ 003.sql
|_ ...
|_ 0010.sql
|_ 010.sql
|_ 011.sql
|_ 012.sql
|_ ...
|_ 099.sql
|_ 0100.sql

In this case 0010.sql is just ignored? Without a warning?

@eximius313
Copy link

YYYYMMDDHHMMSS_create_products.sql looks like an overkill for many situations...
How about two formats: extended (with date) and simple (with padding) which is backwards compatibile?

@mkurz
Copy link
Member Author

mkurz commented Nov 18, 2017

@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:
Checking for a file in addition to a resource on the classpath was added a very very long time ago, at the beginning of Play 2 - because of "Better evolutions auto-reload in DEV mode": cbc542d. This file checking was probably added because in DEV mode the /conf folder was not on the classpath back then. As you know it is now, therefore I just can not see the need to check for a file at all - it is just redundant work. The resource() methods even states "The conf directory is included on the classpath, so this may be used to look up resources, relative to the conf directory". So again, this is handled by calling resource(), no need for extra file checking.
In each mode, DEV or PROD via staging, the evolutions are on the classpath. When staging and PlayKeys.externalizeResources := false they are in the generated jar, when true they are in the conf folder of the distribution which is also on the classpath.

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 choose the approach suggested by @richdougherty using a recursion. However I customized the method a bit, so we always check all possible file names, so we can log warnings in case a file already was chosen and overrules others that will be therefore be ignored, as wished by @marcospereira 😉

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)
@marcospereira As you can see I added various evolutions scripts that should get ignored. Each evolution script that actually runs writes it's file name into a applied_evolutions_log table, which content I return when accessing the index action. And that is what it says:

Following evolutions have been applied:
---------------------------------------
001.sql
002.sql
003.sql
004.sql
005.sql
006.sql
7.sql
008.sql
009.sql
010.sql
011.sql
012.sql
00013.sql
014.sql

In the log you get following warnings:

[warn] p.a.d.e.DefaultEvolutionsApi - Ignoring evolution script 07.sql, using 7.sql instead already
[warn] p.a.d.e.DefaultEvolutionsApi - Ignoring evolution script 007.sql, using 7.sql instead already
[warn] p.a.d.e.DefaultEvolutionsApi - Ignoring evolution script 0010.sql, using 010.sql instead already
[warn] p.a.d.e.DefaultEvolutionsApi - Ignoring evolution script 0000013.sql, using 00013.sql instead already
[warn] p.a.d.e.DefaultEvolutionsApi - Ignoring evolution script 000000013.sql, using 00013.sql instead already
[warn] p.a.d.e.DefaultEvolutionsApi - Ignoring evolution script 00000000000013.sql, using 00013.sql instead already

So please check again, I think this is done and can be merged 😄

@mkurz
Copy link
Member Author

mkurz commented Nov 18, 2017

BTW @marcospereira

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 YYYYMMDDHHMMSS_create_products.sql format as the default for new

Sure we can do that. We just need to implement the YYYYMMDDHHMMSS_create_products EvolutionsReader and make it default and mention it in the 2.7 migration guide.
However that should happen in a new pull request 😉
(You say "... and the fact we need to offer a transition" -> for this pull request here we do not need to offer a transition, the padding stuff is fully backward compatible)

Copy link
Member

@marcospereira marcospereira left a 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.

@mkurz
Copy link
Member Author

mkurz commented Nov 24, 2017

Alright, I will have a look.

@mkurz
Copy link
Member Author

mkurz commented Dec 3, 2017

@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"
)
Copy link
Member

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!

Copy link
Member

@richdougherty richdougherty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@mkurz
Copy link
Member Author

mkurz commented Dec 18, 2017

@marcospereira I think this pr is also waiting for your approval 😉

Copy link
Member

@marcospereira marcospereira left a 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 marcospereira merged commit d913616 into playframework:master Jan 9, 2018
@mkurz
Copy link
Member Author

mkurz commented Jan 9, 2018

@marcospereira Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants