0001    // MigrationManager.swift
0002    //
0003    // The MIT License (MIT)
0004    //
0005    // Copyright (c) 2015 Formbound
0006    //
0007    // Permission is hereby granted, free of charge, to any person obtaining a copy
0008    // of this software and associated documentation files (the "Software"), to deal
0009    // in the Software without restriction, including without limitation the rights
0010    // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
0011    // copies of the Software, and to permit persons to whom the Software is
0012    // furnished to do so, subject to the following conditions:
0013    //
0014    // The above copyright notice and this permission notice shall be included in all
0015    // copies or substantial portions of the Software.
0016    //
0017    // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
0018    // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
0019    // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
0020    // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
0021    // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
0022    // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
0023    // SOFTWARE.
0024    
0025    @_exported import File
0026    
0027    public struct MigrationError
MigrationManager.swift:49
            throw MigrationError(description: "up.sql not found at \(upPath)")
MigrationManager.swift:87
            throw MigrationError(description: "Unable to open find migrations directory at \(path)")
MigrationManager.swift:95
            throw MigrationError(
MigrationManager.swift:121
            throw MigrationError(description: "No migrations defined")
MigrationManager.swift:125
            throw MigrationError(description: "Target version out of range")
MigrationManager.swift:154
                    throw MigrationError(
MigrationManager.swift:165
                        throw MigrationError(
MigrationManager.swift:179
                    throw MigrationError(
MigrationManager.swift:185
                    throw MigrationError(
: ErrorType { 0028 public let description: String 0029 } 0030 0031 public struct Migration
MigrationManager.swift:70
    private(set) public var migrationsByNumber: [Int: Migration] = [:]
{ 0032 public let upStatement: String 0033 public let downStatement
MigrationManager.swift:60
            self.downStatement = nil
MigrationManager.swift:164
                    guard let downStatement = migration.downStatement else {
: String? 0034 0035 public init(path: String) throws { 0036 0037 var path = path 0038 0039 if path.characters.last == "/" { 0040 path = String(path.characters.dropLast()) 0041 } 0042 0043 let upPath = path + "/up.sql" 0044 let downPath = path + "/down.sql" 0045 0046 var (fileExists, isDirectory) = File.fileExistsAt(upPath) 0047 0048 guard fileExists && !isDirectory else { 0049 throw MigrationError(description: "up.sql not found at \(upPath)") 0050 } 0051 0052 0053 self.upStatement = try String(data: File(path: upPath).read()) 0054 0055 (fileExists, isDirectory) = File.fileExistsAt(downPath) 0056 0057 if fileExists && !isDirectory { 0058 self.downStatement = try String(data: File(path: downPath).read()) 0059 } else { 0060 self.downStatement = nil 0061 } 0062 } 0063 } 0064 0065 0066 public class MigrationManager<T
MigrationManager.swift:68
    public let connection: T
MigrationManager.swift:72
    public init(migrationsDirectory path: String, connection: T) throws {
: Connection> { 0067 0068 public let connection
MigrationManager.swift:74
        self.connection = connection
MigrationManager.swift:137
            try connection.transaction {
: T 0069 0070 private(set) public var migrationsByNumber
MigrationManager.swift:115
        return migrationsByNumber.keys.sort().last
MigrationManager.swift:153
                guard let migration = self.migrationsByNumber[migrationNumber] else {
: [Int: Migration] = [:] 0071 0072 public init(migrationsDirectory path: String, connection: T) throws { 0073 0074 self.connection = connection 0075 0076 try connection.execute("CREATE TABLE IF NOT EXISTS schema_migrations (timestamp TIMESTAMP(6) NOT NULL, from_version SMALLINT, to_version SMALLINT NOT NULL)") 0077 0078 var path = path 0079 0080 if path.characters.last == "/" { 0081 path = String(path.characters.dropLast()) 0082 } 0083 0084 let (fileExists, isDirectory) = File.fileExistsAt(path) 0085 0086 guard fileExists && isDirectory else { 0087 throw MigrationError(description: "Unable to open find migrations directory at \(path)") 0088 } 0089 0090 let directories = try File.contentsOfDirectoryAt(path).filter { path in 0091 path.split(".").last == "migration" 0092 }.sort() 0093 0094 guard !directories.isEmpty else { 0095 throw MigrationError( 0096 description: "No migrations found at \(path). Create folders named 'xx.migration' containing an 'up.sql' file, and optionally a 'down.sql' file \(path)" 0097 ) 0098 } 0099 0100 for (i, directoryPath) in directories.enumerate() { 0101 migrationsByNumber[i + 1] = try Migration(path: path + "/" + directoryPath) 0102 } 0103 0104 } 0105 0106 public var currentVersion
MigrationManager.swift:128
        let currentVersion = self.currentVersion
MigrationManager.swift:178
                guard let currentVersion = self.currentVersion else {
: Int? { 0107 guard let result = try? connection.execute("SELECT * FROM schema_migrations ORDER BY timestamp DESC LIMIT 1") else { 0108 return nil 0109 } 0110 0111 return (try? result.first?.value("to_version")) ?? nil 0112 } 0113 0114 public var latestVersion
MigrationManager.swift:120
        guard let latestVersion = latestVersion else {
: Int? { 0115 return migrationsByNumber.keys.sort().last 0116 } 0117 0118 public func migrate(to targetVersion: Int) throws { 0119 0120 guard let latestVersion = latestVersion else { 0121 throw MigrationError(description: "No migrations defined") 0122 } 0123 0124 guard targetVersion >= 0 && targetVersion <= latestVersion else { 0125 throw MigrationError(description: "Target version out of range") 0126 } 0127 0128 let currentVersion = self.currentVersion 0129 0130 var fromVersion = currentVersion ?? 0 0131 0132 if currentVersion == targetVersion { 0133 return 0134 } 0135 0136 while fromVersion != targetVersion { 0137 try connection.transaction { 0138 0139 // Are we migrating up or down? 0140 let upDirection = fromVersion < targetVersion 0141 0142 // The next predicted version 0143 let nextVersion = upDirection ? fromVersion + 1 : fromVersion - 1 0144 0145 /* 0146 The number of the migration we're either reading the up or down statement from 0147 0148 If the current version is *1*, and we're migrating up to *2*, we're executing the *up* statement of the migration with number *2* 0149 If the current version is *1*, and we're migrating down to 0 we're executing the *down* statement of the migration with number *1* 0150 */ 0151 let migrationNumber = upDirection ? nextVersion : nextVersion + 1 0152 0153 guard let migration = self.migrationsByNumber[migrationNumber] else { 0154 throw MigrationError( 0155 description: "Cannot migrate to version \(nextVersion). The migration with number \(migrationNumber) does not exist." 0156 ) 0157 } 0158 0159 if upDirection { 0160 0161 try self.connection.execute(Statement(migration.upStatement)) 0162 } 0163 else { 0164 guard let downStatement = migration.downStatement else { 0165 throw MigrationError( 0166 description: "Cannot migrate to version \(nextVersion). The migration with number \(migrationNumber) has no down statement." 0167 ) 0168 } 0169 0170 try self.connection.execute(Statement(downStatement)) 0171 } 0172 0173 try self.connection.execute( 0174 Statement("INSERT INTO schema_migrations (timestamp, from_version, to_version) VALUES(CURRENT_TIMESTAMP(6), $1, $2)", 0175 parameters: [fromVersion, nextVersion]) 0176 ) 0177 0178 guard let currentVersion = self.currentVersion else { 0179 throw MigrationError( 0180 description: "Failed to get current version of migration." 0181 ) 0182 } 0183 0184 guard nextVersion == currentVersion else { 0185 throw MigrationError( 0186 description: "The predicted next version(\(nextVersion)) does not match the current version (\(currentVersion)). Semething is wrong, possibly a bug!" 0187 ) 0188 } 0189 0190 fromVersion = currentVersion 0191 0192 } 0193 } 0194 } 0195 } 0196