1 //  Copyright © 2021 650 Industries. All rights reserved.
2 
3 import Foundation
4 import sqlite3
5 
6 internal enum UpdatesDatabaseMigrationError: Error {
7   case foreignKeysError
8   case transactionError
9   case migrationSQLError
10 }
11 
12 internal final class TransactionExecutor {
13   let db: OpaquePointer
14 
15   init(db: OpaquePointer) {
16     self.db = db
17   }
18 
19   func safeExecOrRollback(sql: String) throws {
20     guard sqlite3_exec(db, String(sql.utf8), nil, nil, nil) == SQLITE_OK else {
21       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
22       throw UpdatesDatabaseMigrationError.migrationSQLError
23     }
24   }
25 
26   func safeExecOrRollback(sql: String, args: [Any?]) throws {
27     do {
28       _ = try UpdatesDatabaseUtils.execute(sql: sql, withArgs: args, onDatabase: db)
29     } catch {
30       sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
31       throw UpdatesDatabaseMigrationError.migrationSQLError
32     }
33   }
34 }
35 
36 internal extension OpaquePointer {
37   func withForeignKeysOff<R>(_ body: () throws -> R) throws -> R {
38     // https://www.sqlite.org/lang_altertable.html#otheralter
39     guard sqlite3_exec(self, "PRAGMA foreign_keys=OFF;", nil, nil, nil) == SQLITE_OK else {
40       throw UpdatesDatabaseMigrationError.foreignKeysError
41     }
42     defer {
43       sqlite3_exec(self, "PRAGMA foreign_keys=ON;", nil, nil, nil)
44     }
45 
46     return try body()
47   }
48 
49   func withTransaction<R>(_ body: (TransactionExecutor) throws -> R) throws -> R {
50     guard sqlite3_exec(self, "BEGIN;", nil, nil, nil) == SQLITE_OK else {
51       throw UpdatesDatabaseMigrationError.transactionError
52     }
53 
54     let result = try body(TransactionExecutor(db: self))
55 
56     guard sqlite3_exec(self, "COMMIT;", nil, nil, nil) == SQLITE_OK else {
57       throw UpdatesDatabaseMigrationError.transactionError
58     }
59 
60     return result
61   }
62 }
63 
64 internal protocol UpdatesDatabaseMigration {
65   var filename: String { get }
66   func runMigration(onDatabase db: OpaquePointer) throws
67 }
68