diff --git a/Sources/ZcashLightClientKit/Block/DatabaseStorage/DatabaseMigrationManager.swift b/Sources/ZcashLightClientKit/Block/DatabaseStorage/DatabaseMigrationManager.swift index 5af74216..199d5eb1 100644 --- a/Sources/ZcashLightClientKit/Block/DatabaseStorage/DatabaseMigrationManager.swift +++ b/Sources/ZcashLightClientKit/Block/DatabaseStorage/DatabaseMigrationManager.swift @@ -9,17 +9,19 @@ import Foundation import SQLite class MigrationManager { - enum CacheDbMigration: Int32 { - case none = 0 - } - - enum PendingDbMigration: Int32 { + enum CacheDbMigration: Int32, CaseIterable { case none = 0 } - static let latestCacheDbMigrationVersion: Int32 = CacheDbMigration.none.rawValue - static let latestPendingDbMigrationVersion: Int32 = PendingDbMigration.none.rawValue - + enum PendingDbMigration: Int32, CaseIterable { + case none = 0 + case v1 = 1 + case v2 = 2 + } + + static let latestCacheDbMigration: CacheDbMigration = CacheDbMigration.none + static let latestPendingDbMigration: PendingDbMigration = PendingDbMigration.v1 + var cacheDb: ConnectionProvider var pendingDb: ConnectionProvider var network: NetworkType @@ -46,14 +48,96 @@ private extension MigrationManager { LoggerProxy.debug( "Attempting to perform migration for pending Db - currentVersion: \(currentPendingDbVersion)." + - "Latest version is: \(Self.latestPendingDbMigrationVersion)" + "Latest version is: \(Self.latestPendingDbMigration.rawValue - 1)" ) - if currentPendingDbVersion < Self.latestPendingDbMigrationVersion { - // perform no migration just adjust the version number - try self.cacheDb.connection().setUserVersion(PendingDbMigration.none.rawValue) - } else { - LoggerProxy.debug("PendingDb Db - no migration needed") + for v in (currentPendingDbVersion...Self.latestPendingDbMigration.rawValue) { + switch PendingDbMigration(rawValue: v) { + case .some(.none): + try migratePendingDbV1() + case .some(.v1): + try migratePendingDbV2() + case .some(.v2): + break + case nil: + throw StorageError.migrationFailedWithMessage(message: "Invalid migration version: \(v).") + } + } + } + + func migratePendingDbV1() throws { + let statement = PendingTransactionSQLDAO.table.create(ifNotExists: true) { createdTable in + createdTable.column(PendingTransactionSQLDAO.TableColumns.id, primaryKey: .autoincrement) + createdTable.column(PendingTransactionSQLDAO.TableColumns.toAddress) + createdTable.column(PendingTransactionSQLDAO.TableColumns.accountIndex) + createdTable.column(PendingTransactionSQLDAO.TableColumns.minedHeight) + createdTable.column(PendingTransactionSQLDAO.TableColumns.expiryHeight) + createdTable.column(PendingTransactionSQLDAO.TableColumns.cancelled) + createdTable.column(PendingTransactionSQLDAO.TableColumns.encodeAttempts, defaultValue: 0) + createdTable.column(PendingTransactionSQLDAO.TableColumns.errorMessage) + createdTable.column(PendingTransactionSQLDAO.TableColumns.errorCode) + createdTable.column(PendingTransactionSQLDAO.TableColumns.submitAttempts, defaultValue: 0) + createdTable.column(PendingTransactionSQLDAO.TableColumns.createTime) + createdTable.column(PendingTransactionSQLDAO.TableColumns.rawTransactionId) + createdTable.column(PendingTransactionSQLDAO.TableColumns.value) + createdTable.column(PendingTransactionSQLDAO.TableColumns.raw) + createdTable.column(PendingTransactionSQLDAO.TableColumns.memo) + } + + try pendingDb.connection().transaction { + try pendingDb.connection().run(statement); + try self.pendingDb.connection().setUserVersion(PendingDbMigration.v1.rawValue); + } + } + + func migratePendingDbV2() throws { + let statement = + """ + ALTER TABLE pending_transactions RENAME TO pending_transactions_old; + + CREATE TABLE pending_transactions( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + to_address TEXT, + to_internal INTEGER, + account_index INTEGER NOT NULL, + mined_height INTEGER, + expiry_height INTEGER, + cancelled INTEGER, + encode_attempts INTEGER DEFAULT (0), + error_message TEXT, + error_code INTEGER, + submit_attempts INTEGER DEFAULT (0), + create_time REAL, + txid BLOB, + value INTEGER NOT NULL, + raw BLOB, + memo BLOB + ); + + INSERT INTO pending_transactions + SELECT + id, + to_address, + NULL, + account_index, + mined_height, + expiry_height, + cancelled, + encode_attempts, + error_message, + error_code, + submit_attempts, + create_time, + txid, + value, + raw, + memo + FROM pending_transactions_old; + """ + + try pendingDb.connection().transaction { + try pendingDb.connection().run(statement); + try self.pendingDb.connection().setUserVersion(PendingDbMigration.v2.rawValue); } } @@ -62,10 +146,10 @@ private extension MigrationManager { LoggerProxy.debug( "Attempting to perform migration for cache Db - currentVersion: \(currentCacheDbVersion)." + - "Latest version is: \(Self.latestCacheDbMigrationVersion)" + "Latest version is: \(Self.latestCacheDbMigration.rawValue)" ) - if currentCacheDbVersion < Self.latestCacheDbMigrationVersion { + if currentCacheDbVersion < Self.latestCacheDbMigration.rawValue { // perform no migration just adjust the version number try self.cacheDb.connection().setUserVersion(CacheDbMigration.none.rawValue) } else { @@ -81,7 +165,7 @@ extension Connection { } return Int32(version) } - + func setUserVersion(_ version: Int32) throws { try run("PRAGMA user_version = \(version)") } diff --git a/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift b/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift index bb0cc7c3..beb2bf20 100644 --- a/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift +++ b/Sources/ZcashLightClientKit/DAO/PendingTransactionDao.swift @@ -191,8 +191,8 @@ extension PendingTransaction { class PendingTransactionSQLDAO: PendingTransactionRepository { enum TableColumns { - static var toAddress = Expression("to_address") - static var toInternalAccount = Expression("to_internal") + static var toAddress = Expression("to_address") + static var toInternalAccount = Expression("to_internal") static var accountIndex = Expression("account_index") static var minedHeight = Expression("mined_height") static var expiryHeight = Expression("expiry_height") @@ -209,7 +209,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository { static var rawTransactionId = Expression("txid") } - let table = Table("pending_transactions") + static let table = Table("pending_transactions") var dbProvider: ConnectionProvider @@ -217,33 +217,10 @@ class PendingTransactionSQLDAO: PendingTransactionRepository { self.dbProvider = dbProvider } - func createrTableIfNeeded() throws { - let statement = table.create(ifNotExists: true) { createdTable in - createdTable.column(TableColumns.id, primaryKey: .autoincrement) - createdTable.column(TableColumns.toAddress) - createdTable.column(TableColumns.toInternalAccount) - createdTable.column(TableColumns.accountIndex) - createdTable.column(TableColumns.minedHeight) - createdTable.column(TableColumns.expiryHeight) - createdTable.column(TableColumns.cancelled) - createdTable.column(TableColumns.encodeAttempts, defaultValue: 0) - createdTable.column(TableColumns.errorMessage) - createdTable.column(TableColumns.errorCode) - createdTable.column(TableColumns.submitAttempts, defaultValue: 0) - createdTable.column(TableColumns.createTime) - createdTable.column(TableColumns.rawTransactionId) - createdTable.column(TableColumns.value) - createdTable.column(TableColumns.raw) - createdTable.column(TableColumns.memo) - } - - try dbProvider.connection().run(statement) - } - func create(_ transaction: PendingTransactionEntity) throws -> Int { let pendingTx = transaction as? PendingTransaction ?? PendingTransaction.from(entity: transaction) - return try Int(dbProvider.connection().run(table.insert(pendingTx))) + return try Int(dbProvider.connection().run(Self.table.insert(pendingTx))) } func update(_ transaction: PendingTransactionEntity) throws { @@ -252,7 +229,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository { throw StorageError.malformedEntity(fields: ["id"]) } - let updatedRows = try dbProvider.connection().run(table.filter(TableColumns.id == id).update(pendingTx)) + let updatedRows = try dbProvider.connection().run(Self.table.filter(TableColumns.id == id).update(pendingTx)) if updatedRows == 0 { LoggerProxy.error("attempted to update pending transactions but no rows were updated") } @@ -264,7 +241,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository { } do { - try dbProvider.connection().run(table.filter(TableColumns.id == id).delete()) + try dbProvider.connection().run(Self.table.filter(TableColumns.id == id).delete()) } catch { throw StorageError.updateFailed } @@ -277,11 +254,11 @@ class PendingTransactionSQLDAO: PendingTransactionRepository { throw StorageError.malformedEntity(fields: ["id"]) } - try dbProvider.connection().run(table.filter(TableColumns.id == txId).update(pendingTx)) + try dbProvider.connection().run(Self.table.filter(TableColumns.id == txId).update(pendingTx)) } func find(by id: Int) throws -> PendingTransactionEntity? { - guard let row = try dbProvider.connection().pluck(table.filter(TableColumns.id == id).limit(1)) else { + guard let row = try dbProvider.connection().pluck(Self.table.filter(TableColumns.id == id).limit(1)) else { return nil } @@ -295,7 +272,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository { } func getAll() throws -> [PendingTransactionEntity] { - let allTxs: [PendingTransaction] = try dbProvider.connection().prepare(table).map { row in + let allTxs: [PendingTransaction] = try dbProvider.connection().prepare(Self.table).map { row in try row.decode() } @@ -303,7 +280,7 @@ class PendingTransactionSQLDAO: PendingTransactionRepository { } func applyMinedHeight(_ height: BlockHeight, id: Int) throws { - let transaction = table.filter(TableColumns.id == id) + let transaction = Self.table.filter(TableColumns.id == id) let updatedRows = try dbProvider.connection() .run(transaction.update([TableColumns.minedHeight <- height])) diff --git a/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift b/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift index a6495e4f..5343e19c 100644 --- a/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift +++ b/Sources/ZcashLightClientKit/Transaction/PersistentTransactionManager.swift @@ -269,9 +269,7 @@ enum OutboundTransactionManagerBuilder { enum PendingTransactionRepositoryBuilder { static func build(initializer: Initializer) throws -> PendingTransactionRepository { - let dao = PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.pendingDbURL.path, readonly: false)) - try dao.createrTableIfNeeded() - return dao + PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: initializer.pendingDbURL.path, readonly: false)) } } diff --git a/Tests/OfflineTests/PendingTransactionRepositoryTests.swift b/Tests/OfflineTests/PendingTransactionRepositoryTests.swift index 0403110b..6f937cbe 100644 --- a/Tests/OfflineTests/PendingTransactionRepositoryTests.swift +++ b/Tests/OfflineTests/PendingTransactionRepositoryTests.swift @@ -19,8 +19,10 @@ class PendingTransactionRepositoryTests: XCTestCase { override func setUp() { super.setUp() cleanUpDb() - let dao = PendingTransactionSQLDAO(dbProvider: SimpleConnectionProvider(path: try! TestDbBuilder.pendingTransactionsDbURL().absoluteString)) - try! dao.createrTableIfNeeded() + let pendingDbProvider = SimpleConnectionProvider(path: try! TestDbBuilder.pendingTransactionsDbURL().absoluteString) + let dao = PendingTransactionSQLDAO(dbProvider: pendingDbProvider) + let migrations = try! MigrationManager(cacheDbConnection: InMemoryDbProvider(), pendingDbConnection: pendingDbProvider, networkType: .testnet) + try! migrations.performMigration(ufvks: []) pendingRepository = dao }