-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
Evolutions.scala
292 lines (257 loc) · 9.92 KB
/
Evolutions.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
/*
* Copyright (C) 2009-2016 Lightbend Inc. <https://www.lightbend.com>
*/
package play.api.db.evolutions
import java.io.File
import java.nio.charset.Charset
import java.nio.file._
import play.api.db.{ DBApi, Database }
import play.api.inject.DefaultApplicationLifecycle
import play.api.libs.Codecs.sha1
import play.api.{ Configuration, Environment, Logger, Mode, Play }
import play.core.DefaultWebCommands
import play.utils.PlayIO
/**
* An SQL evolution - database changes associated with a software version.
*
* An evolution includes ‘up’ changes, to upgrade to the next version, as well
* as ‘down’ changes, to downgrade the database to the previous version.
*
* @param revision revision number
* @param sql_up the SQL statements for UP application
* @param sql_down the SQL statements for DOWN application
*/
case class Evolution(revision: Int, sql_up: String = "", sql_down: String = "") {
/**
* Revision hash, automatically computed from the SQL content.
*/
val hash = sha1(sql_down.trim + sql_up.trim)
}
/**
* A Script to run on the database.
*/
trait Script {
/**
* Original evolution.
*/
def evolution: Evolution
/**
* The complete SQL to be run.
*/
def sql: String
/**
* The sql string separated into constituent ";"-delimited statements.
*
* Any ";;" found in the sql are escaped to ";".
*/
def statements: Seq[String] = {
// Regex matches on semicolons that neither precede nor follow other semicolons
sql.split("(?<!;);(?!;)").map(_.trim.replace(";;", ";")).filter(_ != "")
}
}
/**
* An UP Script to run on the database.
*
* @param evolution the original evolution
*/
case class UpScript(evolution: Evolution) extends Script {
def sql: String = evolution.sql_up
}
/**
* A DOWN Script to run on the database.
*
* @param evolution the original evolution
*/
case class DownScript(evolution: Evolution) extends Script {
def sql: String = evolution.sql_down
}
/**
* Defines database url patterns.
*/
private[evolutions] object DatabaseUrlPatterns {
lazy val SqlServerJdbcUrl = "^jdbc:sqlserver:.*".r
lazy val OracleJdbcUrl = "^jdbc:oracle:.*".r
lazy val MysqlJdbcUrl = "^(jdbc:)?mysql:.*".r
lazy val DerbyJdbcUrl = "^jdbc:derby:.*".r
}
/**
* Defines Evolutions utilities functions.
*/
object Evolutions {
/**
* Default evolutions directory location.
*/
def directoryName(db: String): String = s"conf/evolutions/${db}"
/**
* Default evolution file location.
*/
def fileName(db: String, revision: Int): String = s"${directoryName(db)}/${revision}.sql"
/**
* Default evolution resource name.
*/
def resourceName(db: String, revision: Int): String = s"evolutions/${db}/${revision}.sql"
/**
* Apply pending evolutions for the given DB.
*/
def applyFor(dbName: String, path: java.io.File = new java.io.File("."), autocommit: Boolean = true, schema: String = ""): Unit = {
val evolutions = Play.current.injector.instanceOf[EvolutionsApi]
val scripts = evolutions.scripts(dbName, new EnvironmentEvolutionsReader(Environment.simple(path = path)), schema)
evolutions.evolve(dbName, scripts, autocommit, schema)
}
/**
* Updates a local (file-based) evolution script.
*/
def updateEvolutionScript(db: String = "default", revision: Int = 1, comment: String = "Generated", ups: String, downs: String)(implicit environment: Environment) {
val evolutions = environment.getFile(fileName(db, revision))
Files.createDirectory(environment.getFile(directoryName(db)).toPath)
writeFileIfChanged(evolutions,
"""|# --- %s
|
|# --- !Ups
|%s
|
|# --- !Downs
|%s
|
|""".stripMargin.format(comment, ups, downs))
}
private def writeFileIfChanged(path: File, content: String): Unit = {
if (content != PlayIO.readFileAsString(path)) {
writeFile(path, content)
}
}
private def writeFile(destination: File, content: String): Unit = {
Files.write(destination.toPath, content.getBytes(utf8))
}
private lazy val utf8 = Charset.forName("UTF8")
/**
* Translates evolution scripts into something human-readable.
*
* @param scripts the evolution scripts
* @return a formatted script
*/
def toHumanReadableScript(scripts: Seq[Script]): String = {
val txt = scripts.map {
case UpScript(ev) => "# --- Rev:" + ev.revision + ",Ups - " + ev.hash.take(7) + "\n" + ev.sql_up + "\n"
case DownScript(ev) => "# --- Rev:" + ev.revision + ",Downs - " + ev.hash.take(7) + "\n" + ev.sql_down + "\n"
}.mkString("\n")
val hasDownWarning =
"# !!! WARNING! This script contains DOWNS evolutions that are likely destructive\n\n"
if (scripts.exists(_.isInstanceOf[DownScript])) hasDownWarning + txt else txt
}
/**
*
* Compare two evolution sequences.
*
* @param downs the seq of downs
* @param ups the seq of ups
* @return the downs and ups to run to have the db synced to the current stage
*/
def conflictings(downs: Seq[Evolution], ups: Seq[Evolution]): (Seq[Evolution], Seq[Evolution]) =
downs.zip(ups).reverse.dropWhile {
case (down, up) => down.hash == up.hash
}.reverse.unzip
/**
* Apply evolutions for the given database.
*
* @param database The database to apply the evolutions to.
* @param evolutionsReader The reader to read the evolutions.
* @param autocommit Whether to use autocommit or not, evolutions will be manually committed if false.
* @param schema The schema where all the play evolution tables are saved in
*/
def applyEvolutions(database: Database, evolutionsReader: EvolutionsReader = ThisClassLoaderEvolutionsReader,
autocommit: Boolean = true, schema: String = ""): Unit = {
val dbEvolutions = new DatabaseEvolutions(database, schema)
val evolutions = dbEvolutions.scripts(evolutionsReader)
dbEvolutions.evolve(evolutions, autocommit)
}
/**
* Cleanup evolutions for the given database.
*
* This will leave the database in the original state it was before evolutions were applied, by running the down
* scripts for all the evolutions that have been previously applied to the database.
*
* @param database The database to clean the evolutions for.
* @param autocommit Whether to use atocommit or not, evolutions will be manually committed if false.
* @param schema The schema where all the play evolution tables are saved in
*/
def cleanupEvolutions(database: Database, autocommit: Boolean = true, schema: String = ""): Unit = {
val dbEvolutions = new DatabaseEvolutions(database, schema)
val evolutions = dbEvolutions.resetScripts()
dbEvolutions.evolve(evolutions, autocommit)
}
/**
* Execute the following code block with the evolutions for the database, cleaning up afterwards by running the downs.
*
* @param database The database to execute the evolutions on
* @param evolutionsReader The evolutions reader to use. Defaults to reading evolutions from the evolution readers own classloader.
* @param autocommit Whether to use autocommit or not, evolutions will be manually committed if false.
* @param block The block to execute
* @param schema The schema where all the play evolution tables are saved in
*/
def withEvolutions[T](database: Database, evolutionsReader: EvolutionsReader = ThisClassLoaderEvolutionsReader,
autocommit: Boolean = true, schema: String = "")(block: => T): T = {
applyEvolutions(database, evolutionsReader, autocommit, schema)
try {
block
} finally {
try {
cleanupEvolutions(database, autocommit, schema)
} catch {
case e: Exception =>
Logger.warn("Error resetting evolutions", e)
}
}
}
}
/**
* Can be used to run off-line evolutions, i.e. outside a running application.
*/
object OfflineEvolutions {
private val logger = Logger(this.getClass)
private def isTest: Boolean = Play.privateMaybeApplication.exists(_.mode == Mode.Test)
private def getEvolutions(appPath: File, classloader: ClassLoader, dbApi: DBApi): EvolutionsComponents = {
val _dbApi = dbApi
new EvolutionsComponents {
lazy val environment = Environment(appPath, classloader, Mode.Dev)
lazy val configuration = Configuration.load(environment)
lazy val applicationLifecycle = new DefaultApplicationLifecycle
lazy val dbApi: DBApi = _dbApi
lazy val webCommands = new DefaultWebCommands
}
}
/**
* Computes and applies an evolutions script.
*
* @param appPath the application path
* @param classloader the classloader used to load the driver
* @param dbName the database name
* @param dbApi the database api for managing application databases
* @param schema The schema where all the play evolution tables are saved in
*/
def applyScript(appPath: File, classloader: ClassLoader, dbApi: DBApi, dbName: String, autocommit: Boolean = true, schema: String = ""): Unit = {
val evolutions = getEvolutions(appPath, classloader, dbApi)
val scripts = evolutions.evolutionsApi.scripts(dbName, evolutions.evolutionsReader, schema)
if (!isTest) {
logger.warn("Applying evolution scripts for database '" + dbName + "':\n\n" + Evolutions.toHumanReadableScript(scripts))
}
evolutions.evolutionsApi.evolve(dbName, scripts, autocommit, schema)
}
/**
* Resolve an inconsistent evolution.
*
* @param appPath the application path
* @param classloader the classloader used to load the driver
* @param dbApi the database api for managing application databases
* @param dbName the database name
* @param revision the revision
* @param schema The schema where all the play evolution tables are saved in
*/
def resolve(appPath: File, classloader: ClassLoader, dbApi: DBApi, dbName: String, revision: Int, schema: String = ""): Unit = {
val evolutions = getEvolutions(appPath, classloader, dbApi)
if (!isTest) {
logger.warn("Resolving evolution [" + revision + "] for database '" + dbName + "'")
}
evolutions.evolutionsApi.resolve(dbName, revision, schema)
}
}