├── playapp ├── public │ ├── stylesheets │ │ └── main.css │ ├── images │ │ └── favicon.png │ └── javascripts │ │ └── jquery-1.9.0.min.js ├── conf │ ├── db │ │ └── migration │ │ │ ├── placeholders │ │ │ ├── V2__add_wows.sql │ │ │ └── V1__create_wow_table.sql │ │ │ ├── default │ │ │ ├── V1__Create_person_table.sql │ │ │ └── V2__Add_people.sql │ │ │ ├── secondary │ │ │ ├── V1__create_job_table.sql │ │ │ └── V2__Add_job.sql │ │ │ ├── scriptsDirectory │ │ │ └── V1__create_folder_table.sql │ │ │ └── migration_prefix │ │ │ ├── migration_1__create_project_table.sql │ │ │ └── migration_2__Add_projects.sql │ ├── routes │ ├── logback.xml │ └── application.conf ├── app │ ├── views │ │ ├── index.scala.html │ │ └── main.scala.html │ ├── controllers │ │ └── HomeController.scala │ ├── db │ │ └── migration │ │ │ └── java │ │ │ ├── V2__Add_language.scala │ │ │ └── V1__Create_language_table.scala │ └── loader │ │ └── MyApplicationLoader.scala └── test │ └── com │ └── github │ └── tototoshi │ └── play2 │ └── flyway │ └── PlayModuleSpec.scala ├── .scala-steward.conf ├── project ├── build.properties └── plugins.sbt ├── screenshot1.png ├── screenshot2.png ├── .git-blame-ignore-revs ├── plugin └── src │ ├── test │ ├── resources │ │ └── sample.sql │ └── scala │ │ └── org │ │ └── flywaydb │ │ └── play │ │ ├── WebCommandPathSpec.scala │ │ ├── UrlParserSpec.scala │ │ ├── FileUtilsSpec.scala │ │ └── ConfigReaderSpec.scala │ └── main │ ├── twirl │ └── org │ │ └── flywaydb │ │ └── play │ │ └── views │ │ ├── parts │ │ ├── links.scala.html │ │ ├── header.scala.html │ │ ├── css.scala.html │ │ └── js.scala.html │ │ ├── index.scala.html │ │ └── info.scala.html │ └── scala │ └── org │ └── flywaydb │ └── play │ ├── PlayModule.scala │ ├── FlywayPlayComponents.scala │ ├── InvalidDatabaseRevision.scala │ ├── PlayInitializer.scala │ ├── FlywayConfiguration.scala │ ├── UrlParser.scala │ ├── FileUtils.scala │ ├── WebCommandPath.scala │ ├── FlywayWebCommand.scala │ ├── ConfigReader.scala │ └── Flyways.scala ├── .github ├── dependabot.yml └── workflows │ └── scala.yml ├── .scalafmt.conf ├── .gitignore ├── LICENSE.txt ├── CHANGELOG.md └── README.md /playapp/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.includeScala = "no" -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.7 2 | -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/flyway-play/main/screenshot1.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/flyway-play/main/screenshot2.png -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.7.4 2 | 2f7953d501ddc2d5be104ce6ac72f6dea11763c6 3 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/placeholders/V2__add_wows.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO [[[tableName]]] (id, name) VALUES (1, 'Oh!'); 2 | -------------------------------------------------------------------------------- /playapp/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/playframework/flyway-play/main/playapp/public/images/favicon.png -------------------------------------------------------------------------------- /plugin/src/test/resources/sample.sql: -------------------------------------------------------------------------------- 1 | create table person ( 2 | id int not null, 3 | name varchar(100) not null 4 | ); 5 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/default/V1__Create_person_table.sql: -------------------------------------------------------------------------------- 1 | create table person ( 2 | id int not null, 3 | name varchar(100) not null 4 | ); -------------------------------------------------------------------------------- /playapp/conf/db/migration/secondary/V1__create_job_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE job ( 2 | id integer primary key, 3 | name varchar(200) not null 4 | ); 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/scriptsDirectory/V1__create_folder_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE folder ( 2 | id integer primary key, 3 | name varchar(200) not null 4 | ); 5 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/migration_prefix/migration_1__create_project_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE project ( 2 | id integer primary key, 3 | name varchar(200) not null 4 | ); 5 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/placeholders/V1__create_wow_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE [[[tableName]]] ( 2 | id integer primary key, 3 | name varchar(200) [[[maybe]]] null 4 | ); 5 | -------------------------------------------------------------------------------- /playapp/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 |

4 | @message 5 |

6 | 7 |

8 | This is a test applitcation for flyway plugin 9 |

10 | 11 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.10.2" 2 | runner.dialect = scala213 3 | preset = default 4 | maxColumn = 120 5 | align.openParenCallSite = false 6 | align.tokens = [] 7 | docstrings.style = Asterisk -------------------------------------------------------------------------------- /playapp/conf/db/migration/migration_prefix/migration_2__Add_projects.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO project (id, name) VALUES (1, 'Manhattan project'); 2 | INSERT INTO project (id, name) VALUES (2, 'Apollo program'); 3 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/secondary/V2__Add_job.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO job (id, name) VALUES (1, 'Programmer'); 2 | INSERT INTO job (id, name) VALUES (2, 'Teacher'); 3 | INSERT INTO job (id, name) VALUES (3, 'Exorcist'); 4 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/parts/links.scala.html: -------------------------------------------------------------------------------- 1 | @(dbNames: Seq[String]) 2 | 3 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | plugin/project 4 | project/target 5 | target 6 | tmp 7 | .history 8 | dist 9 | /.idea 10 | /*.iml 11 | /out 12 | /.idea_modules 13 | /.classpath 14 | /.project 15 | /RUNNING_PID 16 | /.settings 17 | .bsp/ 18 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/default/V2__Add_people.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO person (id, name) VALUES (1, 'John'); 2 | INSERT INTO person (id, name) VALUES (2, 'Paul'); 3 | INSERT INTO person (id, name) VALUES (3, 'George'); 4 | INSERT INTO person (id, name) VALUES (4, 'Ringo'); 5 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/parts/header.scala.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/parts/css.scala.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/parts/js.scala.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.9") 2 | 3 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") 4 | 5 | scalacOptions ++= Seq("-deprecation", "-unchecked", "-language:_") 6 | 7 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.12.2") 8 | 9 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") 10 | -------------------------------------------------------------------------------- /playapp/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.HomeController.index 7 | GET /hello controllers.HomeController.hello 8 | 9 | # Map static resources from the /public folder to the /assets URL path 10 | GET /assets/*file controllers.Assets.at(path="/public", file) 11 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(dbNames: Seq[String]) 2 | 3 | 4 | 5 | play-flyway 6 | @parts.css() 7 | 8 | 9 | @parts.header() 10 |
11 | << Back to app 12 |
13 | @parts.links(dbNames) 14 |
15 |
16 | @parts.js() 17 | 18 | 19 | -------------------------------------------------------------------------------- /playapp/app/controllers/HomeController.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.{Inject, Singleton} 4 | 5 | import play.api.mvc._ 6 | 7 | @Singleton 8 | class HomeController @Inject() (controllerComponents: ControllerComponents) 9 | extends AbstractController(controllerComponents) { 10 | 11 | def index = Action { 12 | Ok(views.html.index("Your new application is ready.")) 13 | } 14 | 15 | def hello = Action { 16 | Ok("Hello") 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /playapp/app/db/migration/java/V2__Add_language.scala: -------------------------------------------------------------------------------- 1 | package db.migration.java 2 | 3 | import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} 4 | 5 | class V2__Add_language extends BaseJavaMigration { 6 | 7 | override def migrate(context: Context): Unit = { 8 | val conn = context.getConnection 9 | conn 10 | .createStatement() 11 | .executeUpdate("""insert into language(id, name) values(1, 'SQL'); 12 | |insert into language(id, name) values(2, 'Java'); 13 | """.stripMargin) 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /playapp/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /playapp/app/db/migration/java/V1__Create_language_table.scala: -------------------------------------------------------------------------------- 1 | package db.migration.java 2 | 3 | import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} 4 | 5 | class V1__Create_language_table extends BaseJavaMigration { 6 | 7 | override def migrate(context: Context): Unit = { 8 | val conn = context.getConnection 9 | conn 10 | .createStatement() 11 | .executeUpdate("""create table language ( 12 | | id integer primary key, 13 | | name varchar(100) not null 14 | |);""".stripMargin) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 Toshiyuki Takahashi 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | either express or implied. See the License for the specific language 13 | governing permissions and limitations under the License. -------------------------------------------------------------------------------- /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Scala CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | flyway-version: 17 | - "9.16.0" 18 | - "9.22.3" 19 | java-version: 20 | - "11" 21 | 22 | steps: 23 | - uses: actions/checkout@v6 24 | - name: Set up JDK 25 | uses: actions/setup-java@v5 26 | with: 27 | distribution: temurin 28 | java-version: ${{ matrix.java-version }} 29 | - name: Run tests 30 | run: FLYWAY_PLAY_FLYWAY_VERSION=${{ matrix.flyway-version }} sbt +test 31 | -------------------------------------------------------------------------------- /playapp/app/loader/MyApplicationLoader.scala: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import org.flywaydb.play.FlywayPlayComponents 4 | import play.api._ 5 | import _root_.controllers.HomeController 6 | import play.api.ApplicationLoader.Context 7 | import play.filters.HttpFiltersComponents 8 | 9 | class MyApplicationLoader extends ApplicationLoader { 10 | def load(context: Context): Application = { 11 | new MyComponents(context).application 12 | } 13 | } 14 | 15 | class MyComponents(context: Context) 16 | extends BuiltInComponentsFromContext(context) 17 | with FlywayPlayComponents 18 | with HttpFiltersComponents 19 | with _root_.controllers.AssetsComponents { 20 | flywayPlayInitializer 21 | lazy val applicationController = new HomeController(controllerComponents) 22 | lazy val router = new _root_.router.Routes(httpErrorHandler, applicationController, assets) 23 | } 24 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/PlayModule.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api._ 19 | import play.api.inject._ 20 | 21 | class PlayModule extends Module { 22 | def bindings(environment: Environment, configuration: Configuration): Seq[Binding[PlayInitializer]] = { 23 | Seq(bind[PlayInitializer].toSelf.eagerly()) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/FlywayPlayComponents.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api._ 19 | import play.core._ 20 | 21 | trait FlywayPlayComponents { 22 | def configuration: Configuration 23 | def environment: Environment 24 | def webCommands: WebCommands 25 | 26 | val flyways = new Flyways(configuration, environment) 27 | lazy val flywayPlayInitializer = new PlayInitializer(configuration, environment, flyways, webCommands) 28 | } 29 | -------------------------------------------------------------------------------- /playapp/conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d - %coloredLevel - [%thread] - %logger - %message%n%xException 7 | 8 | 9 | 10 | 100000 11 | 0 12 | 13 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /plugin/src/test/scala/org/flywaydb/play/WebCommandPathSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import org.scalatest.funspec.AnyFunSpec 19 | import org.scalatest.matchers.should.Matchers 20 | 21 | class WebCommandPathSpec extends AnyFunSpec with Matchers { 22 | 23 | describe("PluginConfiguration") { 24 | 25 | describe("migratePath") { 26 | it("construct path to apply migration") { 27 | WebCommandPath.migratePath("foo") should be("/@flyway/foo/migrate") 28 | } 29 | it("extract db to migrate migration") { 30 | val dbName = "/@flyway/foo/migrate" match { 31 | case WebCommandPath.migratePath(db) => Some(db) 32 | case _ => None 33 | } 34 | dbName should be(Some("foo")) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/InvalidDatabaseRevision.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api._ 19 | 20 | case class InvalidDatabaseRevision(db: String, script: String) 21 | extends PlayException.RichDescription( 22 | "Database '" + db + "' needs migration!", 23 | "An SQL script need to be run on your database." 24 | ) { 25 | 26 | def subTitle = "This SQL script must be run:" 27 | def content: String = script 28 | 29 | private val redirectToApply = s""" 30 | document.location = '${WebCommandPath.migratePath(db)}/?redirect=' + encodeURIComponent(location); 31 | """ 32 | 33 | private val redirectToAdmin = s""" 34 | document.location = '/@flyway/' + encodeURIComponent('$db') 35 | """ 36 | 37 | def htmlDescription: String = { 38 | An SQL script will be run on your database - 39 | 40 | 41 | }.mkString 42 | } 43 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/PlayInitializer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import javax.inject._ 19 | import play.api._ 20 | import play.core._ 21 | 22 | @Singleton 23 | class PlayInitializer @Inject() ( 24 | configuration: Configuration, 25 | environment: Environment, 26 | flyways: Flyways, 27 | webCommands: WebCommands 28 | ) { 29 | 30 | def onStart(): Unit = { 31 | val webCommand = new FlywayWebCommand(configuration, environment, flyways) 32 | webCommands.addHandler(webCommand) 33 | 34 | flyways.allDatabaseNames.foreach { dbName => 35 | environment.mode match { 36 | case Mode.Test => 37 | flyways.migrate(dbName) 38 | case Mode.Prod if flyways.config(dbName).auto => 39 | flyways.migrate(dbName) 40 | case Mode.Prod => 41 | flyways.checkState(dbName) 42 | case Mode.Dev => 43 | // Do nothing here. 44 | // In dev mode, FlywayWebCommand handles migration. 45 | } 46 | } 47 | } 48 | 49 | val enabled: Boolean = 50 | !configuration.getOptional[String]("flywayplugin").contains("disabled") 51 | 52 | if (enabled) { 53 | onStart() 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/FlywayConfiguration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | case class FlywayConfiguration( 19 | database: DatabaseConfiguration, 20 | validateOnStart: Boolean, 21 | auto: Boolean, 22 | locations: Seq[String], 23 | encoding: Option[String], 24 | schemas: Seq[String], 25 | table: Option[String], 26 | placeholderReplacement: Option[Boolean], 27 | placeholders: Map[String, String], 28 | placeholderPrefix: Option[String], 29 | placeholderSuffix: Option[String], 30 | sqlMigrationPrefix: Option[String], 31 | repeatableSqlMigrationPrefix: Option[String], 32 | sqlMigrationSeparator: Option[String], 33 | sqlMigrationSuffix: Option[String], 34 | sqlMigrationSuffixes: Seq[String], 35 | ignoreMigrationPatterns: Seq[String], 36 | validateOnMigrate: Option[Boolean], 37 | cleanOnValidationError: Option[Boolean], 38 | cleanDisabled: Option[Boolean], 39 | initOnMigrate: Option[Boolean], 40 | outOfOrder: Option[Boolean], 41 | scriptsDirectory: Option[String], 42 | mixed: Option[Boolean], 43 | group: Option[Boolean] 44 | ) 45 | 46 | case class DatabaseConfiguration(driver: String, url: String, user: String, password: String) 47 | -------------------------------------------------------------------------------- /plugin/src/test/scala/org/flywaydb/play/UrlParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package org.flywaydb.play 16 | 17 | import play.api.Environment 18 | import org.scalatest.funspec.AnyFunSpec 19 | import org.scalatest.matchers.should.Matchers 20 | 21 | class UrlParserSpec extends AnyFunSpec with Matchers { 22 | 23 | val urlParser = new UrlParser(Environment.simple()) 24 | 25 | describe("UrlParser") { 26 | 27 | it("should parse URI that starts with 'postgres:'") { 28 | urlParser.parseUrl("postgres://john:secret@host.example.com/dbname") should be( 29 | ("jdbc:postgresql://host.example.com/dbname", Some("john"), Some("secret")) 30 | ) 31 | } 32 | 33 | it("should parse URI that starts with 'mysql:' and has no extra parameters") { 34 | urlParser.parseUrl("mysql://john:secret@host.example.com/dbname") should be( 35 | ( 36 | "jdbc:mysql://host.example.com/dbname?useUnicode=yes&characterEncoding=UTF-8&connectionCollation=utf8_general_ci", 37 | Some("john"), 38 | Some("secret") 39 | ) 40 | ) 41 | } 42 | 43 | it("should parse URI that starts with 'mysql:' and has parameter(s)") { 44 | urlParser.parseUrl("mysql://john:secret@host.example.com/dbname?foo=bar") should be( 45 | ("jdbc:mysql://host.example.com/dbname?foo=bar", Some("john"), Some("secret")) 46 | ) 47 | } 48 | 49 | it("should return as is for URIs other than 'postgres' or 'mysql' ones") { 50 | urlParser.parseUrl("jdbc:yoursql://host.example.com/dbname") should be( 51 | ("jdbc:yoursql://host.example.com/dbname", None, None) 52 | ) 53 | } 54 | 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/UrlParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api.{Environment, Mode} 19 | 20 | import scala.util.matching.Regex 21 | 22 | /** 23 | * Most of the code is taken from package play.api.db.DB. 24 | */ 25 | class UrlParser(environment: Environment) { 26 | val PostgresFullUrl: Regex = "^postgres://([a-zA-Z0-9_]+):([^@]+)@([^/]+)/([^\\s]+)$".r 27 | val MysqlFullUrl: Regex = "^mysql://([a-zA-Z0-9_]+):([^@]+)@([^/]+)/([^\\s]+)$".r 28 | val MysqlCustomProperties: Regex = ".*\\?(.*)".r 29 | val H2DefaultUrl: Regex = "^jdbc:h2:mem:.+".r 30 | 31 | def parseUrl(url: String): (String, Option[String], Option[String]) = { 32 | 33 | url match { 34 | case PostgresFullUrl(username, password, host, dbname) => 35 | ("jdbc:postgresql://%s/%s".format(host, dbname), Some(username), Some(password)) 36 | case url @ MysqlFullUrl(username, password, host, dbname) => 37 | val defaultProperties = """?useUnicode=yes&characterEncoding=UTF-8&connectionCollation=utf8_general_ci""" 38 | val addDefaultPropertiesIfNeeded = 39 | MysqlCustomProperties.findFirstMatchIn(url).map(_ => "").getOrElse(defaultProperties) 40 | ("jdbc:mysql://%s/%s".format(host, dbname + addDefaultPropertiesIfNeeded), Some(username), Some(password)) 41 | case url @ H2DefaultUrl() if !url.contains("DB_CLOSE_DELAY") => 42 | val jdbcUrl = if (environment.mode == Mode.Dev) { 43 | url + ";DB_CLOSE_DELAY=-1" 44 | } else { 45 | url 46 | } 47 | (jdbcUrl, None, None) 48 | case s: String => 49 | (s, None, None) 50 | } 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/FileUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import java.io.{File, InputStream} 19 | 20 | object FileUtils { 21 | 22 | def readFileToString(filename: File): String = { 23 | val src = scala.io.Source.fromFile(filename, "UTF-8") 24 | try { 25 | src.mkString 26 | } finally { 27 | src.close() 28 | } 29 | } 30 | 31 | def readInputStreamToString(in: InputStream): String = { 32 | val src = scala.io.Source.fromInputStream(in, "UTF-8") 33 | try { 34 | src.mkString 35 | } finally { 36 | src.close() 37 | in.close() 38 | } 39 | } 40 | 41 | def recursiveListFiles(root: File): Seq[File] = { 42 | if (!root.isDirectory) { 43 | throw new IllegalArgumentException(s"root is not a directory") 44 | } 45 | val these = root.listFiles.toSeq 46 | these ++ these.filter(_.isDirectory).flatMap(recursiveListFiles) 47 | } 48 | 49 | def findFile(root: File, filename: String): Option[File] = { 50 | recursiveListFiles(root).dropWhile(f => f.getName != filename).headOption 51 | } 52 | 53 | private def findSourceFile(root: File, className: String, ext: String): Option[File] = { 54 | for { 55 | cls <- className.split("\\.").lastOption 56 | filename = cls + "." + ext 57 | f <- findFile(root, filename) 58 | } yield f 59 | } 60 | 61 | private def findJavaSourceFile(root: File, className: String): Option[File] = { 62 | findSourceFile(root, className, "java") 63 | } 64 | 65 | private def findScalaSourceFile(root: File, className: String): Option[File] = { 66 | findSourceFile(root, className, "scala") 67 | } 68 | 69 | def findJdbcMigrationFile(root: File, className: String): Option[File] = { 70 | findScalaSourceFile(root, className).orElse(findJavaSourceFile(root, className)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/WebCommandPath.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | object WebCommandPath { 19 | private val applyPathRegex = s"""/@flyway/([a-zA-Z0-9_]+)/migrate""".r 20 | private val showInfoPathRegex = """/@flyway/([a-zA-Z0-9_]+)""".r 21 | private val cleanPathRegex = """/@flyway/([a-zA-Z0-9_]+)/clean""".r 22 | private val repairPathRegex = """/@flyway/([a-zA-Z0-9_]+)/repair""".r 23 | private val initPathRegex = """/@flyway/([a-zA-Z0-9_]+)/init/""".r 24 | private val versionedInitPathRegex = """/@flyway/([a-zA-Z0-9_]+)/init/([0-9.]+)""".r 25 | 26 | object migratePath { 27 | 28 | def apply(dbName: String): String = { 29 | s"/@flyway/$dbName/migrate" 30 | } 31 | 32 | def unapply(path: String): Option[String] = { 33 | applyPathRegex.findFirstMatchIn(path).map(_.group(1)) 34 | } 35 | 36 | } 37 | 38 | object showInfoPath { 39 | 40 | def unapply(path: String): Option[String] = { 41 | showInfoPathRegex.findFirstMatchIn(path).map(_.group(1)) 42 | } 43 | 44 | } 45 | 46 | object cleanPath { 47 | 48 | def apply(dbName: String): String = { 49 | s"/@flyway/$dbName/clean" 50 | } 51 | 52 | def unapply(path: String): Option[String] = { 53 | cleanPathRegex.findFirstMatchIn(path).map(_.group(1)) 54 | } 55 | 56 | } 57 | 58 | object repairPath { 59 | 60 | def apply(dbName: String): String = { 61 | s"/@flyway/$dbName/repair" 62 | } 63 | 64 | def unapply(path: String): Option[String] = { 65 | repairPathRegex.findFirstMatchIn(path).map(_.group(1)) 66 | } 67 | 68 | } 69 | 70 | object versionedInitPath { 71 | def apply(dbName: String, version: String): String = { 72 | s"/@flyway/$dbName/init/$version" 73 | } 74 | 75 | def unapply(path: String): Option[(String, String)] = { 76 | versionedInitPathRegex.findFirstMatchIn(path) match { 77 | case None => None 78 | case Some(matched) => Some(matched.group(1), matched.group(2)) 79 | } 80 | } 81 | } 82 | 83 | object initPath { 84 | 85 | def apply(dbName: String): String = { 86 | s"/@flyway/$dbName/init" 87 | } 88 | 89 | def unapply(path: String): Option[String] = { 90 | initPathRegex.findFirstMatchIn(path).map(_.group(1)) 91 | } 92 | 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/info.scala.html: -------------------------------------------------------------------------------- 1 | @(request: play.api.mvc.RequestHeader, 2 | dbName: String, 3 | allMigrationInfo: Seq[org.flywaydb.core.api.MigrationInfo], 4 | scripts: Seq[String], 5 | showManualInsertQuery: Boolean, 6 | schemaTable: String 7 | ) 8 | 9 | @import org.flywaydb.play.WebCommandPath 10 | 11 | @withRedirectParam(path: String) = { 12 | @{path}?redirect=@{java.net.URLEncoder.encode(request.path, "utf-8")} 13 | } 14 | 15 | @statusText(info: org.flywaydb.core.api.MigrationInfo) = { 16 | @if(info.getState.isApplied) { 17 | applied 18 | } else { 19 | @if(info.getState.isResolved) { 20 | resolved 21 | } 22 | } 23 | @if(info.getState.isFailed) { 24 | failed 25 | } 26 | } 27 | 28 | @insertSql(schemaTable: String, info: org.flywaydb.core.api.MigrationInfo) = { 29 |

--- Manual insert ---

30 |

31 | If you need to apply your migrations manually use this SQL to update your flyway schema table. 32 |

33 |
INSERT INTO @{schemaTable}(version_rank, installed_rank, version, description, type, script, checksum, installed_by, installed_on, execution_time, success)
34 | SELECT MAX(version_rank)+1, MAX(installed_rank)+1, '@{info.getVersion}', '@{info.getDescription}', 'SQL', '@{info.getScript}', @{info.getChecksum}, 'Manually', NOW(), 0, 1 from @schemaTable;
35 | } 36 | 37 | 38 | 39 | play-flyway 40 | @parts.css() 41 | 42 | 43 | @parts.header() 44 |
45 | << Back to app 46 |

Database: @dbName

47 | migrate 48 | repair 49 | clean 50 | 51 |
52 | 55 | 66 |
67 | @allMigrationInfo.zip(scripts).map { case (info, script) => 68 |

69 |

70 | @info.getScript (@statusText(info)) 71 |

72 |
@script
73 | @if(showManualInsertQuery) { 74 | @insertSql(schemaTable, info) 75 | } 76 |

77 | } 78 |
79 | @parts.js() 80 | 81 | 82 | -------------------------------------------------------------------------------- /playapp/conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # If you deploy your application to several instances be sure to use the same key! 8 | play.http.secret.key="xU/2nEHY?Ot85T][Ix_Sh;yPo;adCA1Na 34 | flyways.migrate(dbName) 35 | sbtLink.forceReload() 36 | Some(Redirect(getRedirectUrlFromRequest(request))) 37 | case WebCommandPath.cleanPath(dbName) => 38 | flyways.clean(dbName) 39 | Some(Redirect(getRedirectUrlFromRequest(request))) 40 | case WebCommandPath.repairPath(dbName) => 41 | flyways.repair(dbName) 42 | Some(Redirect(getRedirectUrlFromRequest(request))) 43 | case WebCommandPath.versionedInitPath(dbName, version) => 44 | flyways.baseline(dbName, version) 45 | Some(Redirect(getRedirectUrlFromRequest(request))) 46 | case WebCommandPath.showInfoPath(dbName) => 47 | val allMigrationInfo: Seq[MigrationInfo] = flyways.allMigrationInfo(dbName) 48 | val scriptsDirectory = 49 | configuration.getOptional[String](s"db.$dbName.migration.scriptsDirectory").getOrElse(dbName) 50 | val scripts: Seq[String] = allMigrationInfo.map { info => 51 | environment 52 | .resourceAsStream(s"${flyways.flywayPrefixToMigrationScript}/$scriptsDirectory/${info.getScript}") 53 | .map { in => 54 | FileUtils.readInputStreamToString(in) 55 | } 56 | .orElse { 57 | for { 58 | script <- FileUtils.findJdbcMigrationFile(environment.rootPath, info.getScript) 59 | } yield FileUtils.readFileToString(script) 60 | } 61 | .getOrElse("") 62 | } 63 | val showManualInsertQuery = 64 | configuration.getOptional[Boolean](s"db.$dbName.migration.showInsertQuery").getOrElse(false) 65 | val schemaTable = flyways.schemaTable(dbName) 66 | Some( 67 | Ok(views.html.info(request, dbName, allMigrationInfo, scripts, showManualInsertQuery, schemaTable)) 68 | .as("text/html") 69 | ) 70 | case "/@flyway" => 71 | Some(Ok(views.html.index(flyways.allDatabaseNames)).as("text/html")) 72 | case _ => 73 | synchronized { 74 | if (!checkedAlready) { 75 | for (dbName <- flyways.allDatabaseNames) { 76 | if (environment.mode == Mode.Test || flyways.config(dbName).auto) { 77 | flyways.migrate(dbName) 78 | } else { 79 | flyways.checkState(dbName) 80 | } 81 | } 82 | checkedAlready = true 83 | } 84 | } 85 | None 86 | } 87 | } 88 | 89 | private def getRedirectUrlFromRequest(request: RequestHeader): String = { 90 | (for { 91 | urls <- request.queryString.get("redirect") 92 | url <- urls.headOption 93 | } yield url).getOrElse("/") 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /plugin/src/test/scala/org/flywaydb/play/FileUtilsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import java.io.{File, FileInputStream} 19 | 20 | import org.scalatest.funspec.AnyFunSpec 21 | import org.scalatest.matchers.should.Matchers 22 | 23 | class FileUtilsSpec extends AnyFunSpec with Matchers { 24 | 25 | describe("FileUtils") { 26 | 27 | it("should read a File to String") { 28 | val f = new File("plugin/src/test/resources/sample.sql") 29 | val s = FileUtils.readFileToString(f) 30 | s should be("""|create table person ( 31 | | id int not null, 32 | | name varchar(100) not null 33 | |); 34 | |""".stripMargin) 35 | } 36 | 37 | it("should read InputStream to String") { 38 | val f = new FileInputStream("plugin/src/test/resources/sample.sql") 39 | val s = FileUtils.readInputStreamToString(f) 40 | s should be("""|create table person ( 41 | | id int not null, 42 | | name varchar(100) not null 43 | |); 44 | |""".stripMargin) 45 | } 46 | 47 | it("should find files recursively") { 48 | val temp = File.createTempFile("flyway-play-", "-test") 49 | temp.delete() 50 | temp.mkdir() 51 | val sub1 = new File(temp, "sub1") 52 | sub1.mkdir() 53 | val sub2 = new File(sub1, "sub2") 54 | sub2.mkdir() 55 | val testfile1 = new File(sub2, "AAA.java") 56 | testfile1.createNewFile() 57 | val testfile2 = new File(sub2, "BBB.scala") 58 | testfile2.createNewFile() 59 | 60 | FileUtils.recursiveListFiles(temp) should contain theSameElementsAs Seq(sub1, sub2, testfile1, testfile2) 61 | 62 | testfile1.delete() 63 | testfile2.delete() 64 | sub2.delete() 65 | sub1.delete() 66 | temp.delete() 67 | } 68 | 69 | it("should find a file in file tree") { 70 | val temp = File.createTempFile("flyway-play-", "-test") 71 | temp.delete() 72 | temp.mkdir() 73 | val sub1 = new File(temp, "sub1") 74 | sub1.mkdir() 75 | val sub2 = new File(sub1, "sub2") 76 | sub2.mkdir() 77 | val testfile1 = new File(sub2, "AAA.java") 78 | testfile1.createNewFile() 79 | val testfile2 = new File(sub2, "BBB.scala") 80 | testfile2.createNewFile() 81 | 82 | FileUtils.findFile(temp, "AAA.java") should be(Some(testfile1)) 83 | 84 | testfile1.delete() 85 | testfile2.delete() 86 | sub2.delete() 87 | sub1.delete() 88 | temp.delete() 89 | } 90 | 91 | it("should find a java/scala file in file tree") { 92 | val temp = File.createTempFile("flyway-play-", "-test") 93 | temp.delete() 94 | temp.mkdir() 95 | val sub1 = new File(temp, "sub1") 96 | sub1.mkdir() 97 | val sub2 = new File(sub1, "sub2") 98 | sub2.mkdir() 99 | val testfile1 = new File(sub2, "AAA.java") 100 | testfile1.createNewFile() 101 | val testfile2 = new File(sub2, "BBB.scala") 102 | testfile2.createNewFile() 103 | 104 | FileUtils.findJdbcMigrationFile(temp, "org.flywaydb.flyway.AAA") should be(Some(testfile1)) 105 | FileUtils.findJdbcMigrationFile(temp, "org.flywaydb.flyway.BBB") should be(Some(testfile2)) 106 | 107 | testfile1.delete() 108 | testfile2.delete() 109 | sub2.delete() 110 | sub1.delete() 111 | temp.delete() 112 | } 113 | 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 9.1.0 4 | 5 | - Support Play 3.0.0 6 | - feat: load flyway configuration from env vars #389 by @slivkamiro 7 | 8 | ## 9.0.0 9 | 10 | - Support Play 3.0.0 11 | 12 | ## 8.0.1 13 | 14 | - Support Play 2.9.0 15 | 16 | ## 8.0.0 17 | 18 | - Support Play 2.9.0-M6 19 | 20 | ## 7.38.0 21 | 22 | - Support Flyway 9.16.0 23 | 24 | ## 7.37.0 25 | 26 | - Support Flyway 9.15.0 27 | 28 | ## 7.36.0 29 | 30 | - Support Flyway 9.14.0 31 | 32 | ## 7.35.0 33 | 34 | - Support Flyway 9.13.0 35 | 36 | ## 7.34.0 37 | 38 | - Support Flyway 9.12.0 39 | 40 | ## 7.33.0 41 | 42 | - Support Flyway 9.11.0 43 | 44 | ## 7.32.0 45 | 46 | - Support Flyway 9.10.0 47 | 48 | ## 7.31.0 49 | 50 | - Support Flyway 9.9.0 51 | 52 | ## 7.30.0 53 | 54 | - Support Flyway 9.8.0 55 | 56 | ## 7.29.0 57 | 58 | - Support Flyway 9.7.0 59 | 60 | ## 7.28.0 61 | 62 | - Support Flyway 9.6.0 63 | 64 | ## 7.27.0 65 | 66 | - Support Flyway 9.5.0 67 | 68 | ## 7.26.0 69 | 70 | - Support Flyway 9.4.0 71 | 72 | ## 7.25.0 73 | 74 | - Support Flyway 9.3.0 75 | 76 | ## 7.24.0 77 | 78 | - Support Flyway 9.2.0 79 | 80 | ## 7.23.0 81 | 82 | - Support Flyway 9.1.6 83 | 84 | ## 7.22.0 85 | 86 | - Support Flyway 9.0.0 87 | - Remove ignoreFutureMigrations setting 88 | - Remove ignoreMissingMigrations setting 89 | 90 | ## 7.21.0 91 | 92 | - Add ignoreMigrationPatterns setting 93 | 94 | ## 7.20.0 95 | 96 | - Support Flyway 8.5.0 97 | 98 | ## 7.19.0 99 | 100 | - Support Flyway 8.4.0 101 | 102 | ## 7.18.0 103 | 104 | - Support Flyway 8.3.0 105 | 106 | ## 7.17.0 107 | 108 | - Support Flyway 8.2.0 109 | 110 | ## 7.16.0 111 | 112 | - Support Flyway 8.1.0 113 | 114 | ## 7.15.0 115 | 116 | - Support Flyway 8.0.0 117 | 118 | ## 7.14.0 119 | 120 | - Support Flyway 7.14.0 121 | 122 | ## 7.13.0 123 | 124 | - Support Flyway 7.13.0 125 | 126 | ## 7.12.0 127 | 128 | - Support Flyway 7.12.0 129 | 130 | ## 7.11.0 131 | 132 | - Support Flyway 7.11.0 133 | 134 | ## 7.10.0 135 | 136 | - Support Flyway 7.10.0 137 | 138 | ## 7.9.0 139 | 140 | - Support Flyway 7.9.0 141 | 142 | ## 7.8.0 143 | 144 | - Support Flyway 7.8.0 145 | 146 | ## 7.7.0 147 | 148 | - Support Flyway 7.7.0 149 | 150 | ## 7.6.0 151 | 152 | - Support Flyway 7.6.0 153 | 154 | ## 7.5.0 155 | 156 | - Support Flyway 7.5.0 157 | 158 | ## 7.2.0 159 | 160 | - Support Flyway 7.2.1 161 | 162 | ## 6.5.0 163 | 164 | - Support Flyway 6.5.7 165 | 166 | ## 6.2.0 167 | 168 | - Support Flyway 6.2.4 169 | - Update dependencies 170 | 171 | ## 6.0.0 172 | 173 | - Support Play 2.8.0 174 | - Drop Scala 2.11 support 175 | 176 | ## 5.4.0 177 | 178 | - Support Flyway 6.0.1 179 | 180 | ## 5.3.3 181 | 182 | - Support Scala 2.13.0 183 | 184 | ## 5.3.2 185 | 186 | - Added 'mixed' configuration parameter 187 | - Fixed auto migration setting on production env 188 | 189 | ## 5.3.1 190 | 191 | - Some bug fixes 192 | 193 | ## 5.3.0 194 | 195 | - Support Play 2.7.0 196 | 197 | ## 5.2.0 198 | 199 | - Supported new configuration key, `db.default.migration.scriptDirectory`. 200 | - Support Flyway 5.2.4 201 | 202 | ## 5.1.0 203 | 204 | - Support Flyway 5.1.4 205 | 206 | ## 5.0.0 207 | 208 | - Support Flyway 5.0.7 209 | 210 | ## 4.0.0 211 | 212 | - Support Play 2.6.0 213 | 214 | ## 3.2.0 215 | 216 | - Added information for manual migration to admin page 217 | - Support Flyway 4.2.0 218 | 219 | ## 3.1.0 220 | 221 | - Support Flyway 4.1.2 222 | 223 | ## 3.0.2 224 | 225 | - Support more flyway configuration options 226 | - Some bug fixes 227 | 228 | ## 3.0.1 229 | 230 | - Fix problem with locating scripts when using locations 231 | 232 | ## 3.0.0 233 | 234 | - Support Play 2.5 235 | - Support compile-time DI 236 | - Refactored view code with twirl 237 | - Ignore non-flyway db.\* entry in application.conf 238 | 239 | ## 2.3.0 240 | 241 | - Flyway 4.0 242 | 243 | ## 2.2.1 244 | 245 | - Add support for Flyway sqlMigrationPrefix parameter. 246 | - Flyway 3.2.1 247 | 248 | ## 2.2.0 249 | 250 | - Removed dependency on play.api.Application 251 | 252 | ## 2.1.0 253 | 254 | - Support for specifying a list of schemas 255 | - Fixed classloader issue 256 | 257 | ## 2.0.1 258 | 259 | - Supported new configuration key, `db.default.username`. 260 | 261 | ## 2.0.0 262 | 263 | - Play 2.4 support 264 | -------------------------------------------------------------------------------- /playapp/test/com/github/tototoshi/play2/flyway/PlayModuleSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tototoshi.play2.flyway 17 | 18 | import org.scalatest._ 19 | import play.api.{Configuration, Environment, Mode} 20 | import play.api.inject.guice.GuiceApplicationBuilder 21 | import play.api.test.Helpers._ 22 | import scalikejdbc._ 23 | import scalikejdbc.config.DBs 24 | import org.scalatest.funspec.AnyFunSpec 25 | import org.scalatest.matchers.should.Matchers 26 | 27 | class PlayModuleSpec extends AnyFunSpec with Matchers { 28 | 29 | def test(): Boolean = { 30 | DB autoCommit { implicit session => 31 | val people = 32 | sql"SELECT * FROM person" 33 | .map(rs => rs.int("id") -> rs.string("name")) 34 | .list 35 | .apply() 36 | 37 | people.size should be(4) 38 | 39 | sql"DROP TABLE person".execute.apply() 40 | 41 | // Table created by flyway 42 | sql"""DROP TABLE "schema_version"""".execute.apply() 43 | } 44 | 45 | NamedDB("secondary") autoCommit { implicit session => 46 | val person = 47 | sql"SELECT * FROM job" 48 | .map(rs => rs.int("id") -> rs.string("name")) 49 | .list 50 | .apply() 51 | 52 | person.size should be(3) 53 | 54 | sql"DROP TABLE job".execute.apply() 55 | 56 | // Table created by flyway 57 | sql"""DROP TABLE "schema_version"""".execute.apply() 58 | } 59 | 60 | NamedDB("placeholders") autoCommit { implicit session => 61 | val wows = 62 | sql"SELECT * FROM wow" // This table name is substituted for a placeholder during migration 63 | .map(rs => rs.int("id") -> rs.string("name")) 64 | .list 65 | .apply() 66 | 67 | wows.size should be(1) 68 | wows.head should be((1, "Oh!")) 69 | 70 | sql"DROP TABLE wow".execute.apply() 71 | 72 | // Table created by flyway 73 | sql"""DROP TABLE "schema_version"""".execute.apply() 74 | } 75 | 76 | NamedDB("java") autoCommit { implicit session => 77 | val languages = 78 | sql"SELECT * FROM language" 79 | .map(rs => rs.int("id") -> rs.string("name")) 80 | .list 81 | .apply() 82 | 83 | languages.size should be(2) 84 | 85 | sql"DROP TABLE language".execute.apply() 86 | 87 | // Table created by flyway 88 | sql"""DROP TABLE "schema_version"""".execute.apply() 89 | } 90 | 91 | NamedDB("migration_prefix") autoCommit { implicit session => 92 | val projects = 93 | sql"SELECT * FROM project" 94 | .map(rs => rs.int("id") -> rs.string("name")) 95 | .list 96 | .apply() 97 | 98 | projects.size should be(2) 99 | 100 | sql"DROP TABLE project".execute.apply() 101 | 102 | // Table created by flyway 103 | sql"""DROP TABLE "schema_version"""".execute.apply() 104 | } 105 | 106 | } 107 | 108 | def withScalikejdbcPool[A](test: => A): A = { 109 | DBs.setupAll() 110 | try { 111 | test 112 | } finally { 113 | DBs.closeAll() 114 | } 115 | } 116 | 117 | describe("PlayModule") { 118 | 119 | it("should migrate automatically when testing") { 120 | val application = GuiceApplicationBuilder().build() 121 | running(application) { 122 | withScalikejdbcPool { 123 | test() 124 | } 125 | } 126 | } 127 | 128 | it("should migrate automatically when enabled in production mode") { 129 | val settings = Seq("default", "migration_prefix", "java", "placeholders", "secondary") 130 | .map(dbName => s"db.$dbName.migration.auto" -> true) 131 | val application = GuiceApplicationBuilder( 132 | environment = Environment.simple(mode = Mode.Prod), 133 | configuration = Configuration(settings: _*) 134 | ).build() 135 | running(application) { 136 | withScalikejdbcPool { 137 | test() 138 | } 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/ConfigReader.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api._ 19 | 20 | class ConfigReader(configuration: Configuration, environment: Environment) { 21 | 22 | case class JdbcConfig(driver: String, url: String, username: String, password: String) 23 | 24 | val urlParser = new UrlParser(environment: Environment) 25 | 26 | val logger = Logger(classOf[ConfigReader]) 27 | 28 | private def getAllDatabaseNames: Seq[String] = (for { 29 | config <- configuration.getOptional[Configuration]("db").toList 30 | dbName <- config.subKeys 31 | } yield { 32 | dbName 33 | }).distinct 34 | 35 | def getFlywayConfigurations: Map[String, FlywayConfiguration] = { 36 | (for { 37 | dbName <- getAllDatabaseNames 38 | database <- getDatabaseConfiguration(configuration, dbName) 39 | subConfig = configuration.getOptional[Configuration](s"db.$dbName.migration").getOrElse(Configuration.empty) 40 | } yield { 41 | val placeholders = { 42 | subConfig 43 | .getOptional[Configuration]("placeholders") 44 | .map { config => 45 | config.subKeys.map { key => key -> config.getOptional[String](key).getOrElse("") }.toMap 46 | } 47 | .getOrElse(Map.empty) 48 | } 49 | 50 | dbName -> FlywayConfiguration( 51 | database, 52 | subConfig.getOptional[Boolean]("validateOnStart").getOrElse(false), 53 | subConfig.getOptional[Boolean]("auto").getOrElse(false), 54 | subConfig.getOptional[Seq[String]]("locations").getOrElse(Seq.empty[String]), 55 | subConfig.getOptional[String]("encoding"), 56 | subConfig.getOptional[Seq[String]]("schemas").getOrElse(Seq.empty[String]), 57 | subConfig.getOptional[String]("table"), 58 | subConfig.getOptional[Boolean]("placeholderReplacement"), 59 | placeholders, 60 | subConfig.getOptional[String]("placeholderPrefix"), 61 | subConfig.getOptional[String]("placeholderSuffix"), 62 | subConfig.getOptional[String]("sqlMigrationPrefix"), 63 | subConfig.getOptional[String]("repeatableSqlMigrationPrefix"), 64 | subConfig.getOptional[String]("sqlMigrationSeparator"), 65 | subConfig.getOptional[String]("sqlMigrationSuffix"), 66 | subConfig.getOptional[Seq[String]]("sqlMigrationSuffixes").getOrElse(Seq.empty[String]), 67 | subConfig.getOptional[Seq[String]]("ignoreMigrationPatterns").getOrElse(Seq.empty[String]), 68 | subConfig.getOptional[Boolean]("validateOnMigrate"), 69 | subConfig.getOptional[Boolean]("cleanOnValidationError"), 70 | subConfig.getOptional[Boolean]("cleanDisabled"), 71 | subConfig.getOptional[Boolean]("initOnMigrate"), 72 | subConfig.getOptional[Boolean]("outOfOrder"), 73 | subConfig.getOptional[String]("scriptsDirectory"), 74 | subConfig.getOptional[Boolean]("mixed"), 75 | subConfig.getOptional[Boolean]("group") 76 | ) 77 | }).toMap 78 | } 79 | 80 | private def getDatabaseConfiguration(configuration: Configuration, dbName: String): Option[DatabaseConfiguration] = { 81 | val jdbcConfigOrError = for { 82 | jdbcUrl <- configuration.getOptional[String](s"db.$dbName.url").toRight(s"db.$dbName.url is not set") 83 | driver <- configuration.getOptional[String](s"db.$dbName.driver").toRight(s"db.$dbName.driver is not set") 84 | } yield { 85 | val (parsedUrl, parsedUser, parsedPass) = urlParser.parseUrl(jdbcUrl) 86 | val username = parsedUser 87 | .orElse(configuration.getOptional[String](s"db.$dbName.username")) 88 | .orElse(configuration.getOptional[String](s"db.$dbName.user")) 89 | .orNull 90 | val password = parsedPass 91 | .orElse(configuration.getOptional[String](s"db.$dbName.password")) 92 | .orElse(configuration.getOptional[String](s"db.$dbName.pass")) 93 | .orNull 94 | JdbcConfig(driver, parsedUrl, username, password) 95 | } 96 | 97 | jdbcConfigOrError match { 98 | case Left(message) => 99 | logger.warn(message) 100 | None 101 | case Right(jdbc) => 102 | Some(DatabaseConfiguration(jdbc.driver, jdbc.url, jdbc.username, jdbc.password)) 103 | } 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flyway-play 2 | 3 | ![Scala CI](https://github.com/flyway/flyway-play/workflows/Scala%20CI/badge.svg) 4 | 5 | Flyway module for Play 2.4 or later. It aims to be a substitute for play-evolutions. 6 | 7 | ## Features 8 | 9 | - Based on [Flyway](https://flywaydb.org/) 10 | - No 'Downs' part. 11 | - Independent of DBPlugin(play.api.db). 12 | 13 | ## Install 14 | 15 | | flyway-play version | play version | flyway version | 16 | | ------------------- | ------------ | -------------- | 17 | | 9.1.0 | 3.0.x | 9.16.0 | 18 | | 9.0.0 | 3.0.x | 9.16.0 | 19 | | 8.0.1 | 2.9.x | 9.16.0 | 20 | | 7.38.0 | 2.8.x | 9.16.0 | 21 | | 7.21.0 | 2.8.x | 8.5.0 | 22 | | 7.14.0 | 2.8.x | 7.14.0 | 23 | | 6.5.0 | 2.8.x | 6.5.7 | 24 | 25 | build.sbt 26 | 27 | ```scala 28 | libraryDependencies ++= Seq( 29 | "org.flywaydb" %% "flyway-play" % "9.1.0" 30 | ) 31 | ``` 32 | 33 | conf/application.conf 34 | 35 | ``` 36 | play.modules.enabled += "org.flywaydb.play.PlayModule" 37 | ``` 38 | 39 | ## Maintenance 40 | 41 | This repository is a community project and not officially maintained by the Flyway Team at Redgate. 42 | This project is looked after only by the open source community. Community Maintainers are people who have agreed to be contacted with queries for support and maintenance. 43 | Community Maintainers: 44 | 45 | - [@tototoshi](https://github.com/tototoshi) 46 | 47 | If you would like to be named as a Community Maintainer, let us know via Twitter: https://twitter.com/flywaydb. 48 | 49 | ## Getting Started 50 | 51 | ### Basic configuration 52 | 53 | Database settings can be set in the manner of Play2. 54 | 55 | ``` 56 | db.default.driver=org.h2.Driver 57 | db.default.url="jdbc:h2:mem:example2;db_CLOSE_DELAY=-1" 58 | db.default.username="sa" 59 | db.default.password="secret" 60 | 61 | # optional 62 | db.default.migration.schemas=["public", "other"] 63 | ``` 64 | 65 | ### Place migration scripts 66 | 67 | A migration script is just a simple SQL file. 68 | 69 | ```sql 70 | CREATE TABLE FOO (............. 71 | 72 | 73 | ``` 74 | 75 | By default place your migration scripts in `conf/db/migration/${dbName}` . 76 | 77 | If scriptsDirectory parameter is set, it will look for migrations scripts in `conf/db/migration/${scriptsDirectory}` . 78 | 79 | ``` 80 | playapp 81 | ├── app 82 | │ ├── controllers 83 | │ ├── models 84 | │ └── views 85 | ├── conf 86 | │ ├── application.conf 87 | │ ├── db 88 | │ │ └── migration 89 | │ │ ├── default 90 | │ │ │ ├── V1__Create_person_table.sql 91 | │ │ │ └── V2__Add_people.sql 92 | │ │ └── secondary 93 | │ │ ├── V1__create_job_table.sql 94 | │ │ └── V2__Add_job.sql 95 | │ ├── play.plugins 96 | │ └── routes 97 | ``` 98 | 99 | Alternatively, specify one or more locations per database and place your migrations in `conf/db/migration/${dbName}/${locations[1...N]}` . By varying the locations in each environment you are able to specify different scripts per RDBMS for each upgrade. These differences should be kept minimal. 100 | 101 | For example, in testing use the configuration: 102 | 103 | ``` 104 | db.default.migration.locations=["common","h2"] 105 | ``` 106 | 107 | And in production use the configuration: 108 | 109 | ``` 110 | db.default.migration.locations=["common","mysql"] 111 | ``` 112 | 113 | Then put your migrations in these folders. Note that the migrations for the `secondary` database remain in the default location. 114 | 115 | ``` 116 | playapp 117 | ├── app 118 | │ ├── controllers 119 | │ ├── models 120 | │ └── views 121 | ├── conf 122 | │ ├── application.conf 123 | │ ├── db 124 | │ │ └── migration 125 | │ │ ├── default 126 | │ │ │ ├── common 127 | │ │ │ │ └── V2__Add_people.sql 128 | │ │ │ ├── h2 129 | │ │ │ │ └── V1__Create_person_table.sql 130 | │ │ │ └── mysql 131 | │ │ │ └── V1__Create_person_table.sql 132 | │ │ └── secondary 133 | │ │ ├── V1__create_job_table.sql 134 | │ │ └── V2__Add_job.sql 135 | │ ├── play.plugins 136 | │ └── routes 137 | ``` 138 | 139 | Please see flyway's documents about the naming convention for migration scripts. 140 | 141 | https://flywaydb.org/documentation/migration/sql.html 142 | 143 | ### Placeholders 144 | 145 | Flyway can replace placeholders in Sql migrations. 146 | The default pattern is ${placeholder}. 147 | This can be configured using the placeholderPrefix and placeholderSuffix properties. 148 | 149 | The placeholder prefix, suffix, and key-value pairs can be specified in application.conf, e.g. 150 | 151 | ``` 152 | db.default.migration.placeholderPrefix="$flyway{{{" 153 | db.default.migration.placeholderSuffix="}}}" 154 | db.default.migration.placeholders.foo="bar" 155 | db.default.migration.placeholders.hoge="pupi" 156 | ``` 157 | 158 | This would cause 159 | 160 | ```sql 161 | INSERT INTO USERS ($flyway{{{foo}}}) VALUES ('$flyway{{{hoge}}}') 162 | ``` 163 | 164 | to be rewritten to 165 | 166 | ```sql 167 | INSERT INTO USERS (bar) VALUES ('pupi') 168 | ``` 169 | 170 | ### Enable/disable Validation 171 | 172 | From flyway 3.0, `validate` run before `migrate` by default. 173 | Set `validateOnMigrate` to false if you want to disable this. 174 | 175 | ``` 176 | db.${dbName}.migration.validateOnMigrate=false // true by default 177 | ``` 178 | 179 | ### Migration prefix 180 | 181 | Custom sql migration prefix key-value pair can be specified in application.conf: 182 | 183 | ``` 184 | db.${dbName}.migration.sqlMigrationPrefix="migration_" 185 | ``` 186 | 187 | ### Insert Query 188 | 189 | If you want to apply your migration not via the web interface, but manually on 190 | your production databases you also need the valid insert query for flyway. 191 | 192 | ``` 193 | db.${dbName}.migration.showInsertQuery=true 194 | ``` 195 | 196 | ### Dev 197 | 198 | ![screenshot](screenshot1.png) 199 | 200 | For existing schema, Flyway has a option called 'initOnMigrate'. This option is enabled when `-Ddb.${dbName}.migration.initOnMigrate=true`. 201 | For example, 202 | 203 | ``` 204 | $ play -Ddb.default.migration.initOnMigrate=true 205 | ``` 206 | 207 | Of course, You can write this in your `application.conf`. 208 | 209 | Manual migration is also supported. Click 'Other operations' or open `/@flyway/${dbName}` directly. 210 | 211 | ![screenshot](screenshot2.png) 212 | 213 | ### Test 214 | 215 | In Test mode, migration is done automatically. 216 | 217 | ### Prod 218 | 219 | In production mode, migration is done automatically if `db.${dbName}.migration.auto` is set to be true in application.conf. 220 | Otherwise, it failed to start when migration is needed. 221 | 222 | ``` 223 | $ play -Ddb.default.migration.auto=true start 224 | ``` 225 | 226 | ## Example application 227 | 228 | [seratch/devteam-app](https://github.com/scalikejdbc/devteam-app "seratch/devteam-app") is using play-flyway. Maybe this is a good example. 229 | 230 | ## compile-time DI support 231 | 232 | ```scala 233 | class MyComponents(context: Context) 234 | extends BuiltInComponentsFromContext(context) 235 | with FlywayPlayComponents 236 | ... 237 | { 238 | flywayPlayInitializer 239 | ... 240 | } 241 | ``` 242 | 243 | ## License 244 | 245 | - Apache 2.0 License 246 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/Flyways.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import java.io.FileNotFoundException 19 | 20 | import javax.inject.{Inject, Singleton} 21 | import org.flywaydb.core.Flyway 22 | import org.flywaydb.core.api.MigrationInfo 23 | import org.flywaydb.core.api.configuration.FluentConfiguration 24 | import org.flywaydb.core.internal.jdbc.DriverDataSource 25 | import play.api.{Configuration, Environment, Logger} 26 | 27 | import scala.jdk.CollectionConverters._ 28 | 29 | @Singleton 30 | class Flyways @Inject() (configuration: Configuration, environment: Environment) { 31 | 32 | val flywayPrefixToMigrationScript: String = "db/migration" 33 | 34 | private val flywayConfigurations = { 35 | val configReader = new ConfigReader(configuration, environment) 36 | configReader.getFlywayConfigurations 37 | } 38 | 39 | val allDatabaseNames: Seq[String] = flywayConfigurations.keys.toSeq 40 | 41 | private lazy val flyways: Map[String, Flyway] = { 42 | for { 43 | (dbName, configuration) <- flywayConfigurations 44 | migrationFilesLocation = s"$flywayPrefixToMigrationScript/${configuration.scriptsDirectory.getOrElse(dbName)}" 45 | if migrationFileDirectoryExists(migrationFilesLocation) 46 | } yield { 47 | val flyway = Flyway.configure(environment.classLoader) 48 | val database = configuration.database 49 | val dataSource = 50 | new DriverDataSource(getClass.getClassLoader, database.driver, database.url, database.user, database.password) 51 | flyway.dataSource(dataSource) 52 | if (configuration.locations.nonEmpty) { 53 | val locations = configuration.locations.map(location => s"$migrationFilesLocation/$location") 54 | flyway.locations(locations: _*) 55 | } else { 56 | flyway.locations(migrationFilesLocation) 57 | } 58 | configuration.encoding.foreach(flyway.encoding) 59 | flyway.schemas(configuration.schemas: _*) 60 | configuration.table.foreach(flyway.table) 61 | configuration.placeholderReplacement.foreach(flyway.placeholderReplacement) 62 | flyway.placeholders(configuration.placeholders.asJava) 63 | configuration.placeholderPrefix.foreach(flyway.placeholderPrefix) 64 | configuration.placeholderSuffix.foreach(flyway.placeholderSuffix) 65 | configuration.sqlMigrationPrefix.foreach(flyway.sqlMigrationPrefix) 66 | configuration.repeatableSqlMigrationPrefix.foreach(flyway.repeatableSqlMigrationPrefix) 67 | configuration.sqlMigrationSeparator.foreach(flyway.sqlMigrationSeparator) 68 | setSqlMigrationSuffixes(configuration, flyway) 69 | flyway.ignoreMigrationPatterns(configuration.ignoreMigrationPatterns: _*) 70 | configuration.validateOnMigrate.foreach(flyway.validateOnMigrate) 71 | configuration.cleanOnValidationError.foreach(flyway.cleanOnValidationError) 72 | configuration.cleanDisabled.foreach(flyway.cleanDisabled) 73 | configuration.initOnMigrate.foreach(flyway.baselineOnMigrate) 74 | configuration.outOfOrder.foreach(flyway.outOfOrder) 75 | configuration.mixed.foreach(flyway.mixed) 76 | configuration.group.foreach(flyway.group) 77 | flyway.envVars() 78 | 79 | dbName -> flyway.load() 80 | } 81 | } 82 | 83 | def config(dbName: String): FlywayConfiguration = 84 | flywayConfigurations.getOrElse(dbName, sys.error(s"database $dbName not found")) 85 | 86 | def allMigrationInfo(dbName: String): Seq[MigrationInfo] = 87 | flyways.get(dbName).toSeq.flatMap(_.info().all()) 88 | 89 | def schemaTable(dbName: String): String = { 90 | val flyway = flyways.getOrElse(dbName, sys.error(s"database $dbName not found")) 91 | flyway.getConfiguration.getTable 92 | } 93 | 94 | def migrate(dbName: String): Unit = { 95 | flyways.get(dbName).foreach(_.migrate()) 96 | } 97 | 98 | def clean(dbName: String): Unit = { 99 | flyways.get(dbName).foreach(_.clean) 100 | } 101 | 102 | def repair(dbName: String): Unit = { 103 | flyways.get(dbName).foreach(_.repair) 104 | } 105 | 106 | def baseline(dbName: String, version: String): Unit = { 107 | flyways.get(dbName).foreach { flyway => 108 | Flyway 109 | .configure() 110 | .configuration(flyway.getConfiguration) 111 | .baselineVersion(version) 112 | .load() 113 | .baseline() 114 | 115 | } 116 | } 117 | 118 | private def migrationFileDirectoryExists(path: String): Boolean = { 119 | environment.resource(path) match { 120 | case Some(_) => 121 | Logger("flyway").debug(s"Directory for migration files found. $path") 122 | true 123 | 124 | case None => 125 | Logger("flyway").warn(s"Directory for migration files not found. $path") 126 | false 127 | 128 | } 129 | } 130 | 131 | private def setSqlMigrationSuffixes(configuration: FlywayConfiguration, flyway: FluentConfiguration): Unit = { 132 | configuration.sqlMigrationSuffix.foreach(_ => 133 | Logger("flyway").warn( 134 | "sqlMigrationSuffix is deprecated in Flyway 5.0, and will be removed in a future version. Use sqlMigrationSuffixes instead." 135 | ) 136 | ) 137 | val suffixes: Seq[String] = configuration.sqlMigrationSuffixes ++ configuration.sqlMigrationSuffix 138 | if (suffixes.nonEmpty) flyway.sqlMigrationSuffixes(suffixes: _*) 139 | } 140 | 141 | private def migrationDescriptionToShow(dbName: String, migration: MigrationInfo): String = { 142 | val locations = flywayConfigurations(dbName).locations 143 | (if (locations.nonEmpty) 144 | locations 145 | .map(location => 146 | environment.resourceAsStream(s"$flywayPrefixToMigrationScript/${flywayConfigurations(dbName).scriptsDirectory 147 | .getOrElse(dbName)}/$location/${migration.getScript}") 148 | ) 149 | .find(resource => resource.nonEmpty) 150 | .flatten 151 | else { 152 | environment.resourceAsStream( 153 | s"$flywayPrefixToMigrationScript/${flywayConfigurations(dbName).scriptsDirectory.getOrElse(dbName)}/${migration.getScript}" 154 | ) 155 | }) 156 | .map { in => 157 | s"""|--- ${migration.getScript} --- 158 | |${FileUtils.readInputStreamToString(in)}""".stripMargin 159 | } 160 | .orElse { 161 | import scala.util.control.Exception._ 162 | val code = for { 163 | script <- FileUtils.findJdbcMigrationFile(environment.rootPath, migration.getScript) 164 | } yield FileUtils.readFileToString(script) 165 | allCatch opt { 166 | environment.classLoader.loadClass(migration.getScript) 167 | } map { _ => 168 | s"""|--- ${migration.getScript} --- 169 | |$code""".stripMargin 170 | } 171 | } 172 | .getOrElse(throw new FileNotFoundException(s"Migration file not found. ${migration.getScript}")) 173 | } 174 | 175 | def checkState(dbName: String): Unit = { 176 | flyways.get(dbName).foreach { flyway => 177 | val pendingMigrations = flyway.info().pending 178 | if (pendingMigrations.nonEmpty) { 179 | throw InvalidDatabaseRevision( 180 | dbName, 181 | pendingMigrations.map(migration => migrationDescriptionToShow(dbName, migration)).mkString("\n") 182 | ) 183 | } 184 | 185 | if (flywayConfigurations(dbName).validateOnStart) { 186 | flyway.validate() 187 | } 188 | } 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /plugin/src/test/scala/org/flywaydb/play/ConfigReaderSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api.{Configuration, Environment} 19 | import org.scalatest.funspec.AnyFunSpec 20 | import org.scalatest.matchers.should.Matchers 21 | 22 | class ConfigReaderSpec extends AnyFunSpec with Matchers { 23 | 24 | val defaultDB = Map( 25 | "db.default.driver" -> "org.h2.Driver", 26 | "db.default.url" -> "jdbc:h2:mem:example;DB_CLOSE_DELAY=-1", 27 | "db.default.username" -> "sa" 28 | ) 29 | 30 | val secondaryDB = Map( 31 | "db.secondary.driver" -> "org.h2.Driver", 32 | "db.secondary.url" -> "jdbc:h2:mem:example2;DB_CLOSE_DELAY=-1", 33 | "db.secondary.username" -> "sa", 34 | "db.secondary.password" -> "secret2" 35 | ) 36 | 37 | val thirdDB = Map( 38 | "db.third.driver" -> "org.h2.Driver", 39 | "db.third.url" -> "jdbc:h2:mem:example3;DB_CLOSE_DELAY=-1", 40 | "db.third.user" -> "sa", 41 | "db.third.pass" -> "secret3" 42 | ) 43 | 44 | def withDefaultDB[A](additionalConfiguration: Map[String, Object])(assertion: FlywayConfiguration => A): A = { 45 | val configuration = Configuration((defaultDB ++ additionalConfiguration).toSeq: _*) 46 | val environment = Environment.simple() 47 | val reader = new ConfigReader(configuration, environment) 48 | val configMap = reader.getFlywayConfigurations 49 | assertion(configMap("default")) 50 | } 51 | 52 | describe("ConfigReader") { 53 | 54 | it("should get database configurations") { 55 | val configuration = Configuration((defaultDB ++ secondaryDB ++ thirdDB).toSeq: _*) 56 | val environment = Environment.simple() 57 | val reader = new ConfigReader(configuration, environment) 58 | val configMap = reader.getFlywayConfigurations 59 | configMap("default").database should be( 60 | DatabaseConfiguration("org.h2.Driver", "jdbc:h2:mem:example;DB_CLOSE_DELAY=-1", "sa", null) 61 | ) 62 | configMap("secondary").database should be( 63 | DatabaseConfiguration("org.h2.Driver", "jdbc:h2:mem:example2;DB_CLOSE_DELAY=-1", "sa", "secret2") 64 | ) 65 | configMap("third").database should be( 66 | DatabaseConfiguration("org.h2.Driver", "jdbc:h2:mem:example3;DB_CLOSE_DELAY=-1", "sa", "secret3") 67 | ) 68 | } 69 | 70 | describe("auto") { 71 | it("should be parsed") { 72 | withDefaultDB(Map("db.default.migration.auto" -> "true")) { config => 73 | config.auto should be(true) 74 | } 75 | } 76 | it("should be false by default") { 77 | withDefaultDB(Map.empty) { config => 78 | config.auto should be(false) 79 | } 80 | } 81 | } 82 | 83 | describe("initOnMigrate") { 84 | it("should be parsed") { 85 | withDefaultDB(Map("db.default.migration.initOnMigrate" -> "true")) { config => 86 | config.initOnMigrate should be(Some(true)) 87 | } 88 | } 89 | it("should be None by default") { 90 | withDefaultDB(Map.empty) { config => 91 | config.initOnMigrate should be(None) 92 | } 93 | } 94 | } 95 | 96 | describe("validateOnMigrate") { 97 | it("should be parsed") { 98 | withDefaultDB(Map("db.default.migration.validateOnMigrate" -> "false")) { config => 99 | config.validateOnMigrate should be(Some(false)) 100 | } 101 | } 102 | it("should be None by default") { 103 | withDefaultDB(Map.empty) { config => 104 | config.validateOnMigrate should be(None) 105 | } 106 | } 107 | } 108 | 109 | describe("encoding") { 110 | it("should be parsed") { 111 | withDefaultDB(Map("db.default.migration.encoding" -> "EUC-JP")) { config => 112 | config.encoding should be(Some("EUC-JP")) 113 | } 114 | } 115 | it("should be None by default") { 116 | withDefaultDB(Map.empty) { config => 117 | config.encoding should be(None) 118 | } 119 | } 120 | } 121 | 122 | describe("placeholderPrefix") { 123 | it("should be parsed") { 124 | withDefaultDB(Map("db.default.migration.placeholderPrefix" -> "PREFIX_")) { config => 125 | config.placeholderPrefix should be(Some("PREFIX_")) 126 | } 127 | } 128 | it("should be None by default") { 129 | withDefaultDB(Map.empty) { config => 130 | config.placeholderPrefix should be(None) 131 | } 132 | } 133 | } 134 | 135 | describe("placeholderSuffix") { 136 | it("should be parsed") { 137 | withDefaultDB(Map("db.default.migration.placeholderSuffix" -> "SUFFIX_")) { config => 138 | config.placeholderSuffix should be(Some("SUFFIX_")) 139 | } 140 | } 141 | it("should be None by default") { 142 | withDefaultDB(Map.empty) { config => 143 | config.placeholderSuffix should be(None) 144 | } 145 | } 146 | } 147 | 148 | describe("placeholder") { 149 | it("should be parsed") { 150 | withDefaultDB( 151 | Map( 152 | "db.default.migration.placeholders.fleetwood" -> "mac", 153 | "db.default.migration.placeholders.buckingham" -> "nicks" 154 | ) 155 | ) { config => 156 | config.placeholders should be(Map("fleetwood" -> "mac", "buckingham" -> "nicks")) 157 | } 158 | } 159 | it("should be empty by default") { 160 | withDefaultDB(Map.empty) { config => 161 | config.placeholders should be(Symbol("empty")) 162 | } 163 | } 164 | } 165 | 166 | describe("outOfOrder") { 167 | it("should be parsed") { 168 | withDefaultDB(Map("db.default.migration.outOfOrder" -> "true")) { config => 169 | config.outOfOrder should be(Some(true)) 170 | } 171 | } 172 | it("should be None by default") { 173 | withDefaultDB(Map.empty) { config => 174 | config.outOfOrder should be(None) 175 | } 176 | } 177 | } 178 | 179 | describe("schemas") { 180 | it("should be parsed") { 181 | withDefaultDB(Map("db.default.migration.schemas" -> List("public", "other"))) { config => 182 | config.schemas should be(List("public", "other")) 183 | } 184 | } 185 | it("should be None by default") { 186 | withDefaultDB(Map.empty) { config => 187 | config.schemas should be(List.empty) 188 | } 189 | } 190 | } 191 | 192 | describe("locations") { 193 | it("should be parsed") { 194 | withDefaultDB(Map("db.default.migration.locations" -> List("h2", "common"))) { config => 195 | config.locations should be(List("h2", "common")) 196 | } 197 | } 198 | it("should be None by default") { 199 | withDefaultDB(Map.empty) { config => 200 | config.locations should be(List.empty) 201 | } 202 | } 203 | } 204 | 205 | describe("sqlMigrationPrefix") { 206 | it("should be parsed") { 207 | withDefaultDB(Map("db.default.migration.sqlMigrationPrefix" -> "migration_")) { config => 208 | config.sqlMigrationPrefix should be(Some("migration_")) 209 | } 210 | } 211 | it("should be None by default") { 212 | withDefaultDB(Map.empty) { config => 213 | config.sqlMigrationPrefix should be(None) 214 | } 215 | } 216 | } 217 | 218 | describe("table") { 219 | it("should be parsed") { 220 | withDefaultDB(Map("db.default.migration.table" -> "schema_revisions")) { config => 221 | config.table should be(Some("schema_revisions")) 222 | } 223 | } 224 | it("should be None by default") { 225 | withDefaultDB(Map.empty) { config => 226 | config.table should be(None) 227 | } 228 | } 229 | } 230 | 231 | describe("placeholderReplacement") { 232 | it("should be parsed") { 233 | withDefaultDB(Map("db.default.migration.placeholderReplacement" -> "false")) { config => 234 | config.placeholderReplacement should be(Some(false)) 235 | } 236 | } 237 | it("should be None by default") { 238 | withDefaultDB(Map.empty) { config => 239 | config.placeholderReplacement should be(None) 240 | } 241 | } 242 | } 243 | 244 | describe("repeatableSqlMigrationPrefix") { 245 | it("should be parsed") { 246 | withDefaultDB(Map("db.default.migration.repeatableSqlMigrationPrefix" -> "REP")) { config => 247 | config.repeatableSqlMigrationPrefix should be(Some("REP")) 248 | } 249 | } 250 | it("should be None by default") { 251 | withDefaultDB(Map.empty) { config => 252 | config.repeatableSqlMigrationPrefix should be(None) 253 | } 254 | } 255 | } 256 | 257 | describe("sqlMigrationSeparator") { 258 | it("should be parsed") { 259 | withDefaultDB(Map("db.default.migration.sqlMigrationSeparator" -> "$")) { config => 260 | config.sqlMigrationSeparator should be(Some("$")) 261 | } 262 | } 263 | it("should be None by default") { 264 | withDefaultDB(Map.empty) { config => 265 | config.sqlMigrationSeparator should be(None) 266 | } 267 | } 268 | } 269 | 270 | describe("sqlMigrationSuffix") { 271 | it("should be parsed") { 272 | withDefaultDB(Map("db.default.migration.sqlMigrationSuffix" -> ".psql")) { config => 273 | config.sqlMigrationSuffix should be(Some(".psql")) 274 | } 275 | } 276 | it("should be None by default") { 277 | withDefaultDB(Map.empty) { config => 278 | config.sqlMigrationSuffix should be(None) 279 | } 280 | } 281 | } 282 | 283 | describe("sqlMigrationSuffixes") { 284 | it("should be parsed") { 285 | withDefaultDB(Map("db.default.migration.sqlMigrationSuffixes" -> List(".psql", ".sql"))) { config => 286 | config.sqlMigrationSuffixes should be(List(".psql", ".sql")) 287 | } 288 | } 289 | it("should be Empty by default") { 290 | withDefaultDB(Map.empty) { config => 291 | config.sqlMigrationSuffixes should be(List()) 292 | } 293 | } 294 | } 295 | 296 | describe("ignoreMigrationPatterns") { 297 | it("should be parsed") { 298 | withDefaultDB(Map("db.default.migration.ignoreMigrationPatterns" -> Seq("repeatable:missing"))) { config => 299 | config.ignoreMigrationPatterns should be(Seq("repeatable:missing")) 300 | } 301 | } 302 | it("should be empty by default") { 303 | withDefaultDB(Map.empty) { config => 304 | config.ignoreMigrationPatterns should be(Seq.empty) 305 | } 306 | } 307 | } 308 | 309 | describe("cleanOnValidationError") { 310 | it("should be parsed") { 311 | withDefaultDB(Map("db.default.migration.cleanOnValidationError" -> "true")) { config => 312 | config.cleanOnValidationError should be(Some(true)) 313 | } 314 | } 315 | it("should be None by default") { 316 | withDefaultDB(Map.empty) { config => 317 | config.cleanOnValidationError should be(None) 318 | } 319 | } 320 | } 321 | 322 | describe("cleanDisabled") { 323 | it("should be parsed") { 324 | withDefaultDB(Map("db.default.migration.cleanDisabled" -> "true")) { config => 325 | config.cleanDisabled should be(Some(true)) 326 | } 327 | } 328 | it("should be None by default") { 329 | withDefaultDB(Map.empty) { config => 330 | config.cleanDisabled should be(None) 331 | } 332 | } 333 | } 334 | 335 | describe("mixed") { 336 | it("should be parsed") { 337 | withDefaultDB(Map("db.default.migration.mixed" -> "true")) { config => 338 | config.mixed should be(Some(true)) 339 | } 340 | } 341 | it("should be None by default") { 342 | withDefaultDB(Map.empty) { config => 343 | config.mixed should be(None) 344 | } 345 | } 346 | } 347 | 348 | describe("group") { 349 | it("should be parsed") { 350 | withDefaultDB(Map("db.default.migration.group" -> "true")) { config => 351 | config.group should be(Some(true)) 352 | } 353 | } 354 | it("should be None by default") { 355 | withDefaultDB(Map.empty) { config => 356 | config.group should be(None) 357 | } 358 | } 359 | } 360 | } 361 | } 362 | -------------------------------------------------------------------------------- /playapp/public/javascripts/jquery-1.9.0.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.9.0 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license */(function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function f(e,t,n){if(t=t||0,st.isFunction(t))return st.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return st.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=st.grep(e,function(e){return 1===e.nodeType});if(Wt.test(t))return st.filter(t,r,!n);t=st.filter(t,r)}return st.grep(e,function(e){return st.inArray(e,t)>=0===n})}function p(e){var t=zt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function d(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function h(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function g(e){var t=nn.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function m(e,t){for(var n,r=0;null!=(n=e[r]);r++)st._data(n,"globalEval",!t||st._data(t[r],"globalEval"))}function y(e,t){if(1===t.nodeType&&st.hasData(e)){var n,r,i,o=st._data(e),a=st._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)st.event.add(t,n,s[n][r])}a.data&&(a.data=st.extend({},a.data))}}function v(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!st.support.noCloneEvent&&t[st.expando]){r=st._data(t);for(i in r.events)st.removeEvent(t,i,r.handle);t.removeAttribute(st.expando)}"script"===n&&t.text!==e.text?(h(t).text=e.text,g(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),st.support.html5Clone&&e.innerHTML&&!st.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Zt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function b(e,n){var r,i,o=0,a=e.getElementsByTagName!==t?e.getElementsByTagName(n||"*"):e.querySelectorAll!==t?e.querySelectorAll(n||"*"):t;if(!a)for(a=[],r=e.childNodes||e;null!=(i=r[o]);o++)!n||st.nodeName(i,n)?a.push(i):st.merge(a,b(i,n));return n===t||n&&st.nodeName(e,n)?st.merge([e],a):a}function x(e){Zt.test(e.type)&&(e.defaultChecked=e.checked)}function T(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Nn.length;i--;)if(t=Nn[i]+n,t in e)return t;return r}function w(e,t){return e=t||e,"none"===st.css(e,"display")||!st.contains(e.ownerDocument,e)}function N(e,t){for(var n,r=[],i=0,o=e.length;o>i;i++)n=e[i],n.style&&(r[i]=st._data(n,"olddisplay"),t?(r[i]||"none"!==n.style.display||(n.style.display=""),""===n.style.display&&w(n)&&(r[i]=st._data(n,"olddisplay",S(n.nodeName)))):r[i]||w(n)||st._data(n,"olddisplay",st.css(n,"display")));for(i=0;o>i;i++)n=e[i],n.style&&(t&&"none"!==n.style.display&&""!==n.style.display||(n.style.display=t?r[i]||"":"none"));return e}function C(e,t,n){var r=mn.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function k(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=st.css(e,n+wn[o],!0,i)),r?("content"===n&&(a-=st.css(e,"padding"+wn[o],!0,i)),"margin"!==n&&(a-=st.css(e,"border"+wn[o]+"Width",!0,i))):(a+=st.css(e,"padding"+wn[o],!0,i),"padding"!==n&&(a+=st.css(e,"border"+wn[o]+"Width",!0,i)));return a}function E(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=ln(e),a=st.support.boxSizing&&"border-box"===st.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=un(e,t,o),(0>i||null==i)&&(i=e.style[t]),yn.test(i))return i;r=a&&(st.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+k(e,t,n||(a?"border":"content"),r,o)+"px"}function S(e){var t=V,n=bn[e];return n||(n=A(e,t),"none"!==n&&n||(cn=(cn||st("