mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Added a method to allow safer database inserts
Fixed an issue where the app settings were updating immediately making them seem to Updated GRDB to version 6.1 and SQLCipher to 4.5.2 Added a method which allows for inserting into the database while omitting columns which exist in the object but not in the database (so allow for old migrations to run with less issues) Updated all the migrations to use the migration safe insert method Removed some ObjC support extension functions
This commit is contained in:
parent
6c58e08b4c
commit
89df1261e3
39 changed files with 1125 additions and 238 deletions
2
Podfile
2
Podfile
|
@ -11,7 +11,7 @@ abstract_target 'GlobalDependencies' do
|
||||||
# FIXME: If https://github.com/jedisct1/swift-sodium/pull/249 gets resolved then revert this back to the standard pod
|
# FIXME: If https://github.com/jedisct1/swift-sodium/pull/249 gets resolved then revert this back to the standard pod
|
||||||
pod 'Sodium', :git => 'https://github.com/oxen-io/session-ios-swift-sodium.git', branch: 'session-build'
|
pod 'Sodium', :git => 'https://github.com/oxen-io/session-ios-swift-sodium.git', branch: 'session-build'
|
||||||
pod 'GRDB.swift/SQLCipher'
|
pod 'GRDB.swift/SQLCipher'
|
||||||
pod 'SQLCipher', '~> 4.0'
|
pod 'SQLCipher', '~> 4.5.0' # FIXME: Version 4.5.2 is crashing when access DB settings
|
||||||
|
|
||||||
# FIXME: We want to remove this once it's been long enough since the migration to GRDB
|
# FIXME: We want to remove this once it's been long enough since the migration to GRDB
|
||||||
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/oxen-io/session-ios-yap-database.git', branch: 'signal-release'
|
pod 'YapDatabase/SQLCipher', :git => 'https://github.com/oxen-io/session-ios-yap-database.git', branch: 'signal-release'
|
||||||
|
|
10
Podfile.lock
10
Podfile.lock
|
@ -27,8 +27,8 @@ PODS:
|
||||||
- DifferenceKit/Core (1.2.0)
|
- DifferenceKit/Core (1.2.0)
|
||||||
- DifferenceKit/UIKitExtension (1.2.0):
|
- DifferenceKit/UIKitExtension (1.2.0):
|
||||||
- DifferenceKit/Core
|
- DifferenceKit/Core
|
||||||
- GRDB.swift/SQLCipher (5.26.0):
|
- GRDB.swift/SQLCipher (6.1.0):
|
||||||
- SQLCipher (>= 3.4.0)
|
- SQLCipher (>= 3.4.2)
|
||||||
- libwebp (1.2.1):
|
- libwebp (1.2.1):
|
||||||
- libwebp/demux (= 1.2.1)
|
- libwebp/demux (= 1.2.1)
|
||||||
- libwebp/mux (= 1.2.1)
|
- libwebp/mux (= 1.2.1)
|
||||||
|
@ -154,7 +154,7 @@ DEPENDENCIES:
|
||||||
- SignalCoreKit (from `https://github.com/oxen-io/session-ios-core-kit`, branch `session-version`)
|
- SignalCoreKit (from `https://github.com/oxen-io/session-ios-core-kit`, branch `session-version`)
|
||||||
- SocketRocket (~> 0.5.1)
|
- SocketRocket (~> 0.5.1)
|
||||||
- Sodium (from `https://github.com/oxen-io/session-ios-swift-sodium.git`, branch `session-build`)
|
- Sodium (from `https://github.com/oxen-io/session-ios-swift-sodium.git`, branch `session-build`)
|
||||||
- SQLCipher (~> 4.0)
|
- SQLCipher (~> 4.5.0)
|
||||||
- SwiftProtobuf (~> 1.5.0)
|
- SwiftProtobuf (~> 1.5.0)
|
||||||
- WebRTC-lib
|
- WebRTC-lib
|
||||||
- YapDatabase/SQLCipher (from `https://github.com/oxen-io/session-ios-yap-database.git`, branch `signal-release`)
|
- YapDatabase/SQLCipher (from `https://github.com/oxen-io/session-ios-yap-database.git`, branch `signal-release`)
|
||||||
|
@ -222,7 +222,7 @@ SPEC CHECKSUMS:
|
||||||
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
|
CryptoSwift: a532e74ed010f8c95f611d00b8bbae42e9fe7c17
|
||||||
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
|
Curve25519Kit: e63f9859ede02438ae3defc5e1a87e09d1ec7ee6
|
||||||
DifferenceKit: 5659c430bb7fe45876fa32ce5cba5d6167f0c805
|
DifferenceKit: 5659c430bb7fe45876fa32ce5cba5d6167f0c805
|
||||||
GRDB.swift: 1395cb3556df6b16ed69dfc74c3886abc75d2825
|
GRDB.swift: 611778a5e113385373baeb3e2ce474887d1aadb7
|
||||||
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
|
libwebp: 98a37e597e40bfdb4c911fc98f2c53d0b12d05fc
|
||||||
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
|
Nimble: 5316ef81a170ce87baf72dd961f22f89a602ff84
|
||||||
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
|
NVActivityIndicatorView: 1f6c5687f1171810aa27a3296814dc2d7dec3667
|
||||||
|
@ -242,6 +242,6 @@ SPEC CHECKSUMS:
|
||||||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||||
|
|
||||||
PODFILE CHECKSUM: 430e3b57d986dc8890415294fc6cf5e4eabfce3e
|
PODFILE CHECKSUM: 402850f74d70b3b57fc81eff82d0fc86d695b392
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.11.3
|
||||||
|
|
|
@ -570,6 +570,8 @@
|
||||||
FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E427F6A09900122BE0 /* Identity.swift */; };
|
FD17D7E527F6A09900122BE0 /* Identity.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E427F6A09900122BE0 /* Identity.swift */; };
|
||||||
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; };
|
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */; };
|
||||||
FD17D7EA27F6A1C600122BE0 /* SUKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */; };
|
FD17D7EA27F6A1C600122BE0 /* SUKLegacy.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */; };
|
||||||
|
FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */; };
|
||||||
|
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */; };
|
||||||
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; };
|
FD1C98E4282E3C5B00B76F9E /* UINavigationBar+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */; };
|
||||||
FD23EA5C28ED00F80058676E /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; };
|
FD23EA5C28ED00F80058676E /* Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC290A527D860CE005DAE71 /* Mock.swift */; };
|
||||||
FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; };
|
FD23EA5D28ED00FA0058676E /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; };
|
||||||
|
@ -1683,6 +1685,8 @@
|
||||||
FD17D7E427F6A09900122BE0 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = "<group>"; };
|
FD17D7E427F6A09900122BE0 /* Identity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Identity.swift; sourceTree = "<group>"; };
|
||||||
FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
|
FD17D7E627F6A16700122BE0 /* _003_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _003_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
|
||||||
FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUKLegacy.swift; sourceTree = "<group>"; };
|
FD17D7E927F6A1C600122BE0 /* SUKLegacy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SUKLegacy.swift; sourceTree = "<group>"; };
|
||||||
|
FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PersistableRecord+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
|
FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistableRecordUtilitiesSpec.swift; sourceTree = "<group>"; };
|
||||||
FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = "<group>"; };
|
FD1C98E3282E3C5B00B76F9E /* UINavigationBar+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationBar+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = "<group>"; };
|
FD23EA6028ED0B260058676E /* CombineExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineExtensions.swift; sourceTree = "<group>"; };
|
||||||
FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
FD245C612850664300B966DD /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||||
|
@ -3619,6 +3623,7 @@
|
||||||
FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */,
|
FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */,
|
||||||
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */,
|
FDF22210281B5E0B000A4995 /* TableRecord+Utilities.swift */,
|
||||||
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */,
|
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */,
|
||||||
|
FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */,
|
||||||
);
|
);
|
||||||
path = Utilities;
|
path = Utilities;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3652,6 +3657,14 @@
|
||||||
path = LegacyDatabase;
|
path = LegacyDatabase;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
FD1A94FC2900D2DB000D73D3 /* Utilities */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
FD1A94FD2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift */,
|
||||||
|
);
|
||||||
|
path = Utilities;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
FD37E9C428A1C701003AE748 /* Themes */ = {
|
FD37E9C428A1C701003AE748 /* Themes */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -3698,6 +3711,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
FD37EA1328AB42C1003AE748 /* Models */,
|
FD37EA1328AB42C1003AE748 /* Models */,
|
||||||
|
FD1A94FC2900D2DB000D73D3 /* Utilities */,
|
||||||
);
|
);
|
||||||
path = Database;
|
path = Database;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -5336,6 +5350,7 @@
|
||||||
7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */,
|
7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */,
|
||||||
B8BC00C0257D90E30032E807 /* General.swift in Sources */,
|
B8BC00C0257D90E30032E807 /* General.swift in Sources */,
|
||||||
FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */,
|
FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */,
|
||||||
|
FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */,
|
||||||
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */,
|
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */,
|
||||||
C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */,
|
C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */,
|
||||||
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
|
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
|
||||||
|
@ -5776,6 +5791,7 @@
|
||||||
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,
|
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,
|
||||||
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
|
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
|
||||||
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
|
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
|
||||||
|
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */,
|
||||||
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,
|
FDC290AA27D9B6FD005DAE71 /* Mock.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -69,8 +69,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
minEstimatedTotalTime: minEstimatedTotalTime
|
minEstimatedTotalTime: minEstimatedTotalTime
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
migrationsCompletion: { [weak self] error, needsConfigSync in
|
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||||
guard error == nil else {
|
if case .failure(let error) = result {
|
||||||
self?.showFailedMigrationAlert(error: error)
|
self?.showFailedMigrationAlert(error: error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -322,8 +322,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||||
minEstimatedTotalTime: minEstimatedTotalTime
|
minEstimatedTotalTime: minEstimatedTotalTime
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
migrationsCompletion: { [weak self] error, needsConfigSync in
|
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||||
guard error == nil else {
|
if case .failure(let error) = result {
|
||||||
self?.showFailedMigrationAlert(error: error)
|
self?.showFailedMigrationAlert(error: error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -597,7 +597,7 @@ class NotificationActionHandler {
|
||||||
interactionId: try thread.interactions
|
interactionId: try thread.interactions
|
||||||
.select(.id)
|
.select(.id)
|
||||||
.order(Interaction.Columns.timestampMs.desc)
|
.order(Interaction.Columns.timestampMs.desc)
|
||||||
.asRequest(of: Int64?.self)
|
.asRequest(of: Int64.self)
|
||||||
.fetchOne(db),
|
.fetchOne(db),
|
||||||
threadId: thread.id,
|
threadId: thread.id,
|
||||||
includingOlder: true,
|
includingOlder: true,
|
||||||
|
|
|
@ -61,7 +61,7 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
|
||||||
.settingBool(key: .trimOpenGroupMessagesOlderThanSixMonths)
|
.settingBool(key: .trimOpenGroupMessagesOlderThanSixMonths)
|
||||||
),
|
),
|
||||||
onTap: {
|
onTap: {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.write { db in
|
||||||
db[.trimOpenGroupMessagesOlderThanSixMonths] = !db[.trimOpenGroupMessagesOlderThanSixMonths]
|
db[.trimOpenGroupMessagesOlderThanSixMonths] = !db[.trimOpenGroupMessagesOlderThanSixMonths]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@ class ConversationSettingsViewModel: SessionTableViewModel<NoNav, ConversationSe
|
||||||
.settingBool(key: .shouldAutoPlayConsecutiveAudioMessages)
|
.settingBool(key: .shouldAutoPlayConsecutiveAudioMessages)
|
||||||
),
|
),
|
||||||
onTap: {
|
onTap: {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.write { db in
|
||||||
db[.shouldAutoPlayConsecutiveAudioMessages] = !db[.shouldAutoPlayConsecutiveAudioMessages]
|
db[.shouldAutoPlayConsecutiveAudioMessages] = !db[.shouldAutoPlayConsecutiveAudioMessages]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ class NotificationSettingsViewModel: SessionTableViewModel<NoNav, NotificationSe
|
||||||
title: "NOTIFICATIONS_STYLE_SOUND_WHEN_OPEN_TITLE".localized(),
|
title: "NOTIFICATIONS_STYLE_SOUND_WHEN_OPEN_TITLE".localized(),
|
||||||
rightAccessory: .toggle(.settingBool(key: .playNotificationSoundInForeground)),
|
rightAccessory: .toggle(.settingBool(key: .playNotificationSoundInForeground)),
|
||||||
onTap: {
|
onTap: {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.write { db in
|
||||||
db[.playNotificationSoundInForeground] = !db[.playNotificationSoundInForeground]
|
db[.playNotificationSoundInForeground] = !db[.playNotificationSoundInForeground]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,7 +100,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
|
||||||
subtitle: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION".localized(),
|
subtitle: "PRIVACY_SCREEN_SECURITY_LOCK_SESSION_DESCRIPTION".localized(),
|
||||||
rightAccessory: .toggle(.settingBool(key: .isScreenLockEnabled)),
|
rightAccessory: .toggle(.settingBool(key: .isScreenLockEnabled)),
|
||||||
onTap: {
|
onTap: {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.write { db in
|
||||||
db[.isScreenLockEnabled] = !db[.isScreenLockEnabled]
|
db[.isScreenLockEnabled] = !db[.isScreenLockEnabled]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +116,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
|
||||||
subtitle: "PRIVACY_READ_RECEIPTS_DESCRIPTION".localized(),
|
subtitle: "PRIVACY_READ_RECEIPTS_DESCRIPTION".localized(),
|
||||||
rightAccessory: .toggle(.settingBool(key: .areReadReceiptsEnabled)),
|
rightAccessory: .toggle(.settingBool(key: .areReadReceiptsEnabled)),
|
||||||
onTap: {
|
onTap: {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.write { db in
|
||||||
db[.areReadReceiptsEnabled] = !db[.areReadReceiptsEnabled]
|
db[.areReadReceiptsEnabled] = !db[.areReadReceiptsEnabled]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -158,7 +158,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
|
||||||
},
|
},
|
||||||
rightAccessory: .toggle(.settingBool(key: .typingIndicatorsEnabled)),
|
rightAccessory: .toggle(.settingBool(key: .typingIndicatorsEnabled)),
|
||||||
onTap: {
|
onTap: {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.write { db in
|
||||||
db[.typingIndicatorsEnabled] = !db[.typingIndicatorsEnabled]
|
db[.typingIndicatorsEnabled] = !db[.typingIndicatorsEnabled]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
|
||||||
subtitle: "PRIVACY_LINK_PREVIEWS_DESCRIPTION".localized(),
|
subtitle: "PRIVACY_LINK_PREVIEWS_DESCRIPTION".localized(),
|
||||||
rightAccessory: .toggle(.settingBool(key: .areLinkPreviewsEnabled)),
|
rightAccessory: .toggle(.settingBool(key: .areLinkPreviewsEnabled)),
|
||||||
onTap: {
|
onTap: {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.write { db in
|
||||||
db[.areLinkPreviewsEnabled] = !db[.areLinkPreviewsEnabled]
|
db[.areLinkPreviewsEnabled] = !db[.areLinkPreviewsEnabled]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -198,7 +198,7 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
|
||||||
onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() }
|
onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() }
|
||||||
),
|
),
|
||||||
onTap: {
|
onTap: {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.write { db in
|
||||||
db[.areCallsEnabled] = !db[.areCallsEnabled]
|
db[.areCallsEnabled] = !db[.areCallsEnabled]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -446,7 +446,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
||||||
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete()
|
||||||
|
|
||||||
// Wait for the database transaction to complete before updating the UI
|
// Wait for the database transaction to complete before updating the UI
|
||||||
db.afterNextTransactionCommit { _ in
|
db.afterNextTransaction { _ in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
modalActivityIndicator.dismiss(completion: {})
|
modalActivityIndicator.dismiss(completion: {})
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ enum _001_InitialSetupMigration: Migration {
|
||||||
.notNull()
|
.notNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
try db.create(table: _006_FixHiddenModAdminSupport.PreMigrationGroupMember.self) { t in
|
try db.create(table: GroupMember.self) { t in
|
||||||
// Note: Since we don't know whether this will be stored against a 'ClosedGroup' or
|
// Note: Since we don't know whether this will be stored against a 'ClosedGroup' or
|
||||||
// an 'OpenGroup' we add the foreign key constraint against the thread itself (which
|
// an 'OpenGroup' we add the foreign key constraint against the thread itself (which
|
||||||
// shares the same 'id' as the 'groupId') so we can cascade delete automatically
|
// shares the same 'id' as the 'groupId') so we can cascade delete automatically
|
||||||
|
|
|
@ -22,34 +22,34 @@ enum _002_SetupStandardJobs: Migration {
|
||||||
variant: .disappearingMessages,
|
variant: .disappearingMessages,
|
||||||
behaviour: .recurringOnLaunch,
|
behaviour: .recurringOnLaunch,
|
||||||
shouldBlock: true
|
shouldBlock: true
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
|
|
||||||
_ = try Job(
|
_ = try Job(
|
||||||
variant: .failedMessageSends,
|
variant: .failedMessageSends,
|
||||||
behaviour: .recurringOnLaunch,
|
behaviour: .recurringOnLaunch,
|
||||||
shouldBlock: true
|
shouldBlock: true
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
|
|
||||||
_ = try Job(
|
_ = try Job(
|
||||||
variant: .failedAttachmentDownloads,
|
variant: .failedAttachmentDownloads,
|
||||||
behaviour: .recurringOnLaunch,
|
behaviour: .recurringOnLaunch,
|
||||||
shouldBlock: true
|
shouldBlock: true
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
|
|
||||||
_ = try Job(
|
_ = try Job(
|
||||||
variant: .updateProfilePicture,
|
variant: .updateProfilePicture,
|
||||||
behaviour: .recurringOnActive
|
behaviour: .recurringOnActive
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
|
|
||||||
_ = try Job(
|
_ = try Job(
|
||||||
variant: .retrieveDefaultOpenGroupRooms,
|
variant: .retrieveDefaultOpenGroupRooms,
|
||||||
behaviour: .recurringOnActive
|
behaviour: .recurringOnActive
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
|
|
||||||
_ = try Job(
|
_ = try Job(
|
||||||
variant: .garbageCollection,
|
variant: .garbageCollection,
|
||||||
behaviour: .recurringOnActive
|
behaviour: .recurringOnActive
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||||
|
|
|
@ -422,7 +422,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
profilePictureUrl: legacyContact.profilePictureURL,
|
profilePictureUrl: legacyContact.profilePictureURL,
|
||||||
profilePictureFileName: legacyContact.profilePictureFileName,
|
profilePictureFileName: legacyContact.profilePictureFileName,
|
||||||
profileEncryptionKey: legacyContact.profileEncryptionKey
|
profileEncryptionKey: legacyContact.profileEncryptionKey
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
/// **Note:** The blow "shouldForce" flags are here to allow us to avoid having to run legacy migrations they
|
/// **Note:** The blow "shouldForce" flags are here to allow us to avoid having to run legacy migrations they
|
||||||
/// replicate the behaviour of a number of the migrations and perform the changes if the migrations had never run
|
/// replicate the behaviour of a number of the migrations and perform the changes if the migrations had never run
|
||||||
|
@ -490,7 +490,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
shouldForceDidApproveMe
|
shouldForceDidApproveMe
|
||||||
),
|
),
|
||||||
hasBeenBlocked: (!isCurrentUser && (legacyContact.hasBeenBlocked || legacyContact.isBlocked))
|
hasBeenBlocked: (!isCurrentUser && (legacyContact.hasBeenBlocked || legacyContact.isBlocked))
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment the progress for each contact
|
// Increment the progress for each contact
|
||||||
|
@ -587,7 +587,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
),
|
),
|
||||||
mutedUntilTimestamp: legacyThread.mutedUntilDate?.timeIntervalSince1970,
|
mutedUntilTimestamp: legacyThread.mutedUntilDate?.timeIntervalSince1970,
|
||||||
onlyNotifyForMentions: onlyNotifyForMentions
|
onlyNotifyForMentions: onlyNotifyForMentions
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
// Disappearing Messages Configuration
|
// Disappearing Messages Configuration
|
||||||
if let config: SMKLegacy._DisappearingMessagesConfiguration = disappearingMessagesConfiguration[threadId] {
|
if let config: SMKLegacy._DisappearingMessagesConfiguration = disappearingMessagesConfiguration[threadId] {
|
||||||
|
@ -595,12 +595,12 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
isEnabled: config.isEnabled,
|
isEnabled: config.isEnabled,
|
||||||
durationSeconds: TimeInterval(config.durationSeconds)
|
durationSeconds: TimeInterval(config.durationSeconds)
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
try DisappearingMessagesConfiguration
|
try DisappearingMessagesConfiguration
|
||||||
.defaultWith(threadId)
|
.defaultWith(threadId)
|
||||||
.insert(db)
|
.migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closed Groups
|
// Closed Groups
|
||||||
|
@ -618,7 +618,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
name: name,
|
name: name,
|
||||||
formationTimestamp: TimeInterval(formationTimestamp)
|
formationTimestamp: TimeInterval(formationTimestamp)
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
// Note: If a user has left a closed group then they won't actually have any keys
|
// Note: If a user has left a closed group then they won't actually have any keys
|
||||||
// but they should still be able to browse the old messages so we do want to allow
|
// but they should still be able to browse the old messages so we do want to allow
|
||||||
|
@ -629,7 +629,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
publicKey: legacyKeys.publicKey,
|
publicKey: legacyKeys.publicKey,
|
||||||
secretKey: legacyKeys.privateKey,
|
secretKey: legacyKeys.privateKey,
|
||||||
receivedTimestamp: timestamp
|
receivedTimestamp: timestamp
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the 'GroupMember' models for the group (even if the current user is no longer
|
// Create the 'GroupMember' models for the group (even if the current user is no longer
|
||||||
|
@ -643,15 +643,16 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
try? Profile(
|
try? Profile(
|
||||||
id: profileId,
|
id: profileId,
|
||||||
name: profileId
|
name: profileId
|
||||||
).save(db)
|
).migrationSafeSave(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
try groupModel.groupMemberIds.forEach { memberId in
|
try groupModel.groupMemberIds.forEach { memberId in
|
||||||
try _006_FixHiddenModAdminSupport.PreMigrationGroupMember(
|
try GroupMember(
|
||||||
groupId: threadId,
|
groupId: threadId,
|
||||||
profileId: memberId,
|
profileId: memberId,
|
||||||
role: .standard
|
role: .standard,
|
||||||
).insert(db)
|
isHidden: false // Ignored: Didn't exist at time of migration
|
||||||
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
if !validProfileIds.contains(memberId) {
|
if !validProfileIds.contains(memberId) {
|
||||||
createDummyProfile(profileId: memberId)
|
createDummyProfile(profileId: memberId)
|
||||||
|
@ -659,11 +660,12 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
}
|
}
|
||||||
|
|
||||||
try groupModel.groupAdminIds.forEach { adminId in
|
try groupModel.groupAdminIds.forEach { adminId in
|
||||||
try _006_FixHiddenModAdminSupport.PreMigrationGroupMember(
|
try GroupMember(
|
||||||
groupId: threadId,
|
groupId: threadId,
|
||||||
profileId: adminId,
|
profileId: adminId,
|
||||||
role: .admin
|
role: .admin,
|
||||||
).insert(db)
|
isHidden: false // Ignored: Didn't exist at time of migration
|
||||||
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
if !validProfileIds.contains(adminId) {
|
if !validProfileIds.contains(adminId) {
|
||||||
createDummyProfile(profileId: adminId)
|
createDummyProfile(profileId: adminId)
|
||||||
|
@ -671,11 +673,12 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
}
|
}
|
||||||
|
|
||||||
try (closedGroupZombieMemberIds[legacyThread.uniqueId] ?? []).forEach { zombieId in
|
try (closedGroupZombieMemberIds[legacyThread.uniqueId] ?? []).forEach { zombieId in
|
||||||
try _006_FixHiddenModAdminSupport.PreMigrationGroupMember(
|
try GroupMember(
|
||||||
groupId: threadId,
|
groupId: threadId,
|
||||||
profileId: zombieId,
|
profileId: zombieId,
|
||||||
role: .zombie
|
role: .zombie,
|
||||||
).insert(db)
|
isHidden: false // Ignored: Didn't exist at time of migration
|
||||||
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
if !validProfileIds.contains(zombieId) {
|
if !validProfileIds.contains(zombieId) {
|
||||||
createDummyProfile(profileId: zombieId)
|
createDummyProfile(profileId: zombieId)
|
||||||
|
@ -707,7 +710,8 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
sequenceNumber: 0,
|
sequenceNumber: 0,
|
||||||
inboxLatestMessageId: 0,
|
inboxLatestMessageId: 0,
|
||||||
outboxLatestMessageId: 0
|
outboxLatestMessageId: 0
|
||||||
).insert(db)
|
)
|
||||||
|
.migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -930,7 +934,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
openGroupServerMessageId: openGroupServerMessageId,
|
openGroupServerMessageId: openGroupServerMessageId,
|
||||||
openGroupWhisperMods: false,
|
openGroupWhisperMods: false,
|
||||||
openGroupWhisperTo: nil
|
openGroupWhisperTo: nil
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
switch error {
|
switch error {
|
||||||
|
@ -950,7 +954,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
variant: variant,
|
variant: variant,
|
||||||
timestampMs: Int64.zeroingOverflow(legacyInteraction.timestamp)
|
timestampMs: Int64.zeroingOverflow(legacyInteraction.timestamp)
|
||||||
)?.insert(db)
|
)?.migrationSafeInsert(db)
|
||||||
|
|
||||||
// Remove timestamps we created records for (they will be protected by unique
|
// Remove timestamps we created records for (they will be protected by unique
|
||||||
// constraints so don't need legacy process records)
|
// constraints so don't need legacy process records)
|
||||||
|
@ -1012,7 +1016,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
mostRecentFailureText :
|
mostRecentFailureText :
|
||||||
nil
|
nil
|
||||||
)
|
)
|
||||||
).save(db)
|
).migrationSafeSave(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle any quote
|
// Handle any quote
|
||||||
|
@ -1045,7 +1049,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
try Profile(
|
try Profile(
|
||||||
id: quotedMessage.authorId,
|
id: quotedMessage.authorId,
|
||||||
name: quotedMessage.authorId
|
name: quotedMessage.authorId
|
||||||
).save(db)
|
).migrationSafeSave(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: It looks like there is a way for a quote to not have it's
|
// Note: It looks like there is a way for a quote to not have it's
|
||||||
|
@ -1093,7 +1097,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
timestampMs: Int64.zeroingOverflow(quotedMessage.timestamp),
|
timestampMs: Int64.zeroingOverflow(quotedMessage.timestamp),
|
||||||
body: quotedMessage.body,
|
body: quotedMessage.body,
|
||||||
attachmentId: attachmentId
|
attachmentId: attachmentId
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle any LinkPreview
|
// Handle any LinkPreview
|
||||||
|
@ -1120,7 +1124,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
variant: linkPreviewVariant,
|
variant: linkPreviewVariant,
|
||||||
title: linkPreview.title,
|
title: linkPreview.title,
|
||||||
attachmentId: attachmentId
|
attachmentId: attachmentId
|
||||||
).save(db)
|
).migrationSafeSave(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle any attachments
|
// Handle any attachments
|
||||||
|
@ -1156,7 +1160,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
albumIndex: index,
|
albumIndex: index,
|
||||||
interactionId: interactionId,
|
interactionId: interactionId,
|
||||||
attachmentId: attachmentId
|
attachmentId: attachmentId
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment the progress for each contact
|
// Increment the progress for each contact
|
||||||
|
@ -1225,7 +1229,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
timestampMs: legacyJob.message.timestamp
|
timestampMs: legacyJob.message.timestamp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)?.inserted(db)
|
)?.migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1256,7 +1260,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
messages: [processedMessage.messageInfo],
|
messages: [processedMessage.messageInfo],
|
||||||
calledFromBackgroundPoller: legacyJob.isBackgroundPoll
|
calledFromBackgroundPoller: legacyJob.isBackgroundPoll
|
||||||
)
|
)
|
||||||
)?.inserted(db)
|
)?.migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1346,7 +1350,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
destination: legacyJob.destination,
|
destination: legacyJob.destination,
|
||||||
message: legacyJob.message.toNonLegacy()
|
message: legacyJob.message.toNonLegacy()
|
||||||
)
|
)
|
||||||
)?.inserted(db)
|
)?.migrationSafeInserted(db)
|
||||||
|
|
||||||
if let oldId: String = legacyJob.id {
|
if let oldId: String = legacyJob.id {
|
||||||
messageSendJobLegacyMap[oldId] = job
|
messageSendJobLegacyMap[oldId] = job
|
||||||
|
@ -1373,7 +1377,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
messageSendJobId: sendJobId,
|
messageSendJobId: sendJobId,
|
||||||
attachmentId: legacyJob.attachmentID
|
attachmentId: legacyJob.attachmentID
|
||||||
)
|
)
|
||||||
)?.inserted(db)
|
)?.migrationSafeInserted(db)
|
||||||
|
|
||||||
// Add the dependency to the relevant MessageSendJob
|
// Add the dependency to the relevant MessageSendJob
|
||||||
guard let uploadJobId: Int64 = uploadJob?.id else {
|
guard let uploadJobId: Int64 = uploadJob?.id else {
|
||||||
|
@ -1384,7 +1388,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
try JobDependencies(
|
try JobDependencies(
|
||||||
jobId: sendJobId,
|
jobId: sendJobId,
|
||||||
dependantId: uploadJobId
|
dependantId: uploadJobId
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1413,7 +1417,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
details: AttachmentDownloadJob.Details(
|
details: AttachmentDownloadJob.Details(
|
||||||
attachmentId: legacyJob.attachmentID
|
attachmentId: legacyJob.attachmentID
|
||||||
)
|
)
|
||||||
)?.inserted(db)
|
)?.migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1429,7 +1433,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
destination: .contact(publicKey: threadId),
|
destination: .contact(publicKey: threadId),
|
||||||
timestampMsValues: timestampsMs
|
timestampMsValues: timestampsMs
|
||||||
)
|
)
|
||||||
)?.inserted(db)
|
)?.migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Storage.update(progress: 0.99, for: self, in: target)
|
Storage.update(progress: 0.99, for: self, in: target)
|
||||||
|
@ -1625,7 +1629,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
}
|
}
|
||||||
}(),
|
}(),
|
||||||
caption: legacyAttachment.caption
|
caption: legacyAttachment.caption
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
|
|
||||||
processedAttachmentIds.insert(legacyAttachmentId)
|
processedAttachmentIds.insert(legacyAttachmentId)
|
||||||
|
|
||||||
|
@ -1664,7 +1668,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
encryptionKey: nil,
|
encryptionKey: nil,
|
||||||
digest: nil,
|
digest: nil,
|
||||||
caption: nil
|
caption: nil
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
|
|
||||||
processedAttachmentIds.insert(legacyAttachmentId)
|
processedAttachmentIds.insert(legacyAttachmentId)
|
||||||
|
|
||||||
|
|
|
@ -28,41 +28,3 @@ enum _006_FixHiddenModAdminSupport: Migration {
|
||||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Pre-Migration Types
|
|
||||||
|
|
||||||
extension _006_FixHiddenModAdminSupport {
|
|
||||||
internal struct PreMigrationGroupMember: Codable, PersistableRecord, TableRecord, ColumnExpressible {
|
|
||||||
public static var databaseTableName: String { "groupMember" }
|
|
||||||
|
|
||||||
public typealias Columns = CodingKeys
|
|
||||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
|
||||||
case groupId
|
|
||||||
case profileId
|
|
||||||
case role
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Role: Int, Codable, DatabaseValueConvertible {
|
|
||||||
case standard
|
|
||||||
case zombie
|
|
||||||
case moderator
|
|
||||||
case admin
|
|
||||||
}
|
|
||||||
|
|
||||||
public let groupId: String
|
|
||||||
public let profileId: String
|
|
||||||
public let role: Role
|
|
||||||
|
|
||||||
// MARK: - Initialization
|
|
||||||
|
|
||||||
public init(
|
|
||||||
groupId: String,
|
|
||||||
profileId: String,
|
|
||||||
role: Role
|
|
||||||
) {
|
|
||||||
self.groupId = groupId
|
|
||||||
self.profileId = profileId
|
|
||||||
self.role = role
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -113,7 +113,9 @@ public struct ControlMessageProcessRecord: Codable, FetchableRecord, Persistable
|
||||||
self.serverExpirationTimestamp = serverExpirationTimestamp
|
self.serverExpirationTimestamp = serverExpirationTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
public func insert(_ db: Database) throws {
|
// MARK: - Custom Database Interaction
|
||||||
|
|
||||||
|
public func willInsert(_ db: Database) throws {
|
||||||
// If this isn't a legacy entry then check if there is a single entry and, if so,
|
// If this isn't a legacy entry then check if there is a single entry and, if so,
|
||||||
// try to create a "legacy entry" version of this record to see if a unique constraint
|
// try to create a "legacy entry" version of this record to see if a unique constraint
|
||||||
// conflict occurs
|
// conflict occurs
|
||||||
|
@ -132,8 +134,6 @@ public struct ControlMessageProcessRecord: Codable, FetchableRecord, Persistable
|
||||||
).insert(db)
|
).insert(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try performInsert(db)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -316,21 +316,22 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
|
|
||||||
// MARK: - Custom Database Interaction
|
// MARK: - Custom Database Interaction
|
||||||
|
|
||||||
public mutating func insert(_ db: Database) throws {
|
public mutating func willInsert(_ db: Database) throws {
|
||||||
// Automatically mark interactions which can't be unread as read so the unread count
|
// Automatically mark interactions which can't be unread as read so the unread count
|
||||||
// isn't impacted
|
// isn't impacted
|
||||||
self.wasRead = (self.wasRead || !self.variant.canBeUnread)
|
self.wasRead = (self.wasRead || !self.variant.canBeUnread)
|
||||||
|
}
|
||||||
|
|
||||||
try performInsert(db)
|
public func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws {
|
||||||
|
let success: InsertionSuccess = try insert()
|
||||||
|
|
||||||
// Since we need to do additional logic upon insert we can just set the 'id' value
|
guard
|
||||||
// here directly instead of in the 'didInsert' method (if you look at the docs the
|
let threadVariant: SessionThread.Variant = try? SessionThread
|
||||||
// 'db.lastInsertedRowID' value is the row id of the newly inserted row which the
|
.filter(id: threadId)
|
||||||
// interaction uses as it's id)
|
.select(.variant)
|
||||||
let interactionId: Int64 = db.lastInsertedRowID
|
.asRequest(of: SessionThread.Variant.self)
|
||||||
self.id = interactionId
|
.fetchOne(db)
|
||||||
|
else {
|
||||||
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: threadId) else {
|
|
||||||
SNLog("Inserted an interaction but couldn't find it's associated thead")
|
SNLog("Inserted an interaction but couldn't find it's associated thead")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -339,10 +340,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
case .standardOutgoing:
|
case .standardOutgoing:
|
||||||
// New outgoing messages should immediately determine their recipient list
|
// New outgoing messages should immediately determine their recipient list
|
||||||
// from current thread state
|
// from current thread state
|
||||||
switch thread.variant {
|
switch threadVariant {
|
||||||
case .contact:
|
case .contact:
|
||||||
try RecipientState(
|
try RecipientState(
|
||||||
interactionId: interactionId,
|
interactionId: success.rowID,
|
||||||
recipientId: threadId, // Will be the contact id
|
recipientId: threadId, // Will be the contact id
|
||||||
state: .sending
|
state: .sending
|
||||||
).insert(db)
|
).insert(db)
|
||||||
|
@ -350,7 +351,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
case .closedGroup:
|
case .closedGroup:
|
||||||
let closedGroupMemberIds: Set<String> = (try? GroupMember
|
let closedGroupMemberIds: Set<String> = (try? GroupMember
|
||||||
.select(.profileId)
|
.select(.profileId)
|
||||||
.filter(GroupMember.Columns.groupId == thread.id)
|
.filter(GroupMember.Columns.groupId == threadId)
|
||||||
.asRequest(of: String.self)
|
.asRequest(of: String.self)
|
||||||
.fetchSet(db))
|
.fetchSet(db))
|
||||||
.defaulting(to: [])
|
.defaulting(to: [])
|
||||||
|
@ -367,7 +368,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
.filter { memberId -> Bool in memberId != userPublicKey }
|
.filter { memberId -> Bool in memberId != userPublicKey }
|
||||||
.forEach { memberId in
|
.forEach { memberId in
|
||||||
try RecipientState(
|
try RecipientState(
|
||||||
interactionId: interactionId,
|
interactionId: success.rowID,
|
||||||
recipientId: memberId,
|
recipientId: memberId,
|
||||||
state: .sending
|
state: .sending
|
||||||
).insert(db)
|
).insert(db)
|
||||||
|
@ -378,7 +379,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
// we need to ensure we have a state for all threads; so for open groups
|
// we need to ensure we have a state for all threads; so for open groups
|
||||||
// we just use the open group id as the 'recipientId' value
|
// we just use the open group id as the 'recipientId' value
|
||||||
try RecipientState(
|
try RecipientState(
|
||||||
interactionId: interactionId,
|
interactionId: success.rowID,
|
||||||
recipientId: threadId, // Will be the open group id
|
recipientId: threadId, // Will be the open group id
|
||||||
state: .sending
|
state: .sending
|
||||||
).insert(db)
|
).insert(db)
|
||||||
|
@ -387,6 +388,10 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||||
|
self.id = inserted.rowID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Mutation
|
// MARK: - Mutation
|
||||||
|
|
|
@ -125,9 +125,7 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
|
||||||
|
|
||||||
// MARK: - Custom Database Interaction
|
// MARK: - Custom Database Interaction
|
||||||
|
|
||||||
public func insert(_ db: Database) throws {
|
public func willInsert(_ db: Database) throws {
|
||||||
try performInsert(db)
|
|
||||||
|
|
||||||
db[.hasSavedThread] = true
|
db[.hasSavedThread] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -350,73 +348,3 @@ public extension SessionThread {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Objective-C Support
|
|
||||||
|
|
||||||
// FIXME: Remove when possible
|
|
||||||
|
|
||||||
@objc(SMKThread)
|
|
||||||
public class SMKThread: NSObject {
|
|
||||||
@objc(deleteAll)
|
|
||||||
public static func deleteAll() {
|
|
||||||
Storage.shared.writeAsync { db in
|
|
||||||
_ = try SessionThread.deleteAll(db)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc(isThreadMuted:)
|
|
||||||
public static func isThreadMuted(_ threadId: String) -> Bool {
|
|
||||||
return Storage.shared.read { db in
|
|
||||||
let mutedUntilTimestamp: TimeInterval? = try SessionThread
|
|
||||||
.select(SessionThread.Columns.mutedUntilTimestamp)
|
|
||||||
.filter(id: threadId)
|
|
||||||
.asRequest(of: TimeInterval?.self)
|
|
||||||
.fetchOne(db)
|
|
||||||
|
|
||||||
return (mutedUntilTimestamp != nil)
|
|
||||||
}
|
|
||||||
.defaulting(to: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc(isOnlyNotifyingForMentions:)
|
|
||||||
public static func isOnlyNotifyingForMentions(_ threadId: String) -> Bool {
|
|
||||||
return Storage.shared.read { db in
|
|
||||||
return try SessionThread
|
|
||||||
.select(SessionThread.Columns.onlyNotifyForMentions)
|
|
||||||
.filter(id: threadId)
|
|
||||||
.asRequest(of: Bool.self)
|
|
||||||
.fetchOne(db)
|
|
||||||
}
|
|
||||||
.defaulting(to: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc(setIsOnlyNotifyingForMentions:to:)
|
|
||||||
public static func isOnlyNotifyingForMentions(_ threadId: String, isEnabled: Bool) {
|
|
||||||
Storage.shared.write { db in
|
|
||||||
try SessionThread
|
|
||||||
.filter(id: threadId)
|
|
||||||
.updateAll(db, SessionThread.Columns.onlyNotifyForMentions.set(to: isEnabled))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc(mutedUntilDateFor:)
|
|
||||||
public static func mutedUntilDateFor(_ threadId: String) -> Date? {
|
|
||||||
return Storage.shared.read { db in
|
|
||||||
return try SessionThread
|
|
||||||
.select(SessionThread.Columns.mutedUntilTimestamp)
|
|
||||||
.filter(id: threadId)
|
|
||||||
.asRequest(of: TimeInterval.self)
|
|
||||||
.fetchOne(db)
|
|
||||||
}
|
|
||||||
.map { Date(timeIntervalSince1970: $0) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc(updateWithMutedUntilDateTo:forThreadId:)
|
|
||||||
public static func updateWithMutedUntilDate(to date: Date?, threadId: String) {
|
|
||||||
Storage.shared.write { db in
|
|
||||||
try SessionThread
|
|
||||||
.filter(id: threadId)
|
|
||||||
.updateAll(db, SessionThread.Columns.mutedUntilTimestamp.set(to: date?.timeIntervalSince1970))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -469,7 +469,7 @@ public final class OpenGroupManager: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db.afterNextTransactionCommit { db in
|
db.afterNextTransaction { db in
|
||||||
// Start the poller if needed
|
// Start the poller if needed
|
||||||
if dependencies.cache.pollers[server.lowercased()] == nil {
|
if dependencies.cache.pollers[server.lowercased()] == nil {
|
||||||
dependencies.mutableCache.mutate {
|
dependencies.mutableCache.mutate {
|
||||||
|
|
|
@ -376,7 +376,7 @@ public enum MessageReceiver {
|
||||||
|
|
||||||
// Download the profile picture if needed
|
// Download the profile picture if needed
|
||||||
if updatedProfile.profilePictureUrl != profile.profilePictureUrl || updatedProfile.profileEncryptionKey != profile.profileEncryptionKey {
|
if updatedProfile.profilePictureUrl != profile.profilePictureUrl || updatedProfile.profileEncryptionKey != profile.profileEncryptionKey {
|
||||||
db.afterNextTransactionCommit { _ in
|
db.afterNextTransaction { _ in
|
||||||
ProfileManager.downloadAvatar(for: updatedProfile)
|
ProfileManager.downloadAvatar(for: updatedProfile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,8 @@ public extension MentionInfo {
|
||||||
|
|
||||||
let request: SQLRequest<MentionInfo> = {
|
let request: SQLRequest<MentionInfo> = {
|
||||||
guard let pattern: FTS5Pattern = pattern else {
|
guard let pattern: FTS5Pattern = pattern else {
|
||||||
|
let finalLimitSQL: SQL = (limitSQL ?? "")
|
||||||
|
|
||||||
return """
|
return """
|
||||||
SELECT
|
SELECT
|
||||||
\(Profile.self).*,
|
\(Profile.self).*,
|
||||||
|
@ -61,12 +63,13 @@ public extension MentionInfo {
|
||||||
)
|
)
|
||||||
GROUP BY \(profile[.id])
|
GROUP BY \(profile[.id])
|
||||||
ORDER BY \(interaction[.timestampMs].desc)
|
ORDER BY \(interaction[.timestampMs].desc)
|
||||||
\(limitSQL ?? "")
|
\(finalLimitSQL)
|
||||||
"""
|
"""
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we do have a search patern then use FTS
|
// If we do have a search patern then use FTS
|
||||||
let matchLiteral: SQL = SQL(stringLiteral: "\(Profile.Columns.nickname.name):\(pattern.rawPattern) OR \(Profile.Columns.name.name):\(pattern.rawPattern)")
|
let matchLiteral: SQL = SQL(stringLiteral: "\(Profile.Columns.nickname.name):\(pattern.rawPattern) OR \(Profile.Columns.name.name):\(pattern.rawPattern)")
|
||||||
|
let finalLimitSQL: SQL = (limitSQL ?? "")
|
||||||
|
|
||||||
return """
|
return """
|
||||||
SELECT
|
SELECT
|
||||||
|
@ -93,7 +96,7 @@ public extension MentionInfo {
|
||||||
WHERE \(profileFullTextSearch) MATCH '\(matchLiteral)'
|
WHERE \(profileFullTextSearch) MATCH '\(matchLiteral)'
|
||||||
GROUP BY \(profile[.id])
|
GROUP BY \(profile[.id])
|
||||||
ORDER BY \(interaction[.timestampMs].desc)
|
ORDER BY \(interaction[.timestampMs].desc)
|
||||||
\(limitSQL ?? "")
|
\(finalLimitSQL)
|
||||||
"""
|
"""
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
|
@ -650,6 +650,7 @@ public extension MessageViewModel {
|
||||||
let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name)
|
let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name)
|
||||||
|
|
||||||
let numColumnsBeforeLinkedRecords: Int = 20
|
let numColumnsBeforeLinkedRecords: Int = 20
|
||||||
|
let finalGroupSQL: SQL = (groupSQL ?? "")
|
||||||
let request: SQLRequest<ViewModel> = """
|
let request: SQLRequest<ViewModel> = """
|
||||||
SELECT
|
SELECT
|
||||||
\(thread[.id]) AS \(ViewModel.threadIdKey),
|
\(thread[.id]) AS \(ViewModel.threadIdKey),
|
||||||
|
@ -736,7 +737,7 @@ public extension MessageViewModel {
|
||||||
\(SQL("\(groupMemberAdminTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)"))
|
\(SQL("\(groupMemberAdminTableLiteral).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)"))
|
||||||
)
|
)
|
||||||
WHERE \(interaction.alias[Column.rowID]) IN \(rowIds)
|
WHERE \(interaction.alias[Column.rowID]) IN \(rowIds)
|
||||||
\(groupSQL ?? "")
|
\(finalGroupSQL)
|
||||||
ORDER BY \(orderSQL)
|
ORDER BY \(orderSQL)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ class OpenGroupAPISpec: QuickSpec {
|
||||||
|
|
||||||
beforeEach {
|
beforeEach {
|
||||||
mockStorage = Storage(
|
mockStorage = Storage(
|
||||||
customWriter: DatabaseQueue(),
|
customWriter: try! DatabaseQueue(),
|
||||||
customMigrations: [
|
customMigrations: [
|
||||||
SNUtilitiesKit.migrations(),
|
SNUtilitiesKit.migrations(),
|
||||||
SNMessagingKit.migrations()
|
SNMessagingKit.migrations()
|
||||||
|
|
|
@ -100,7 +100,7 @@ class OpenGroupManagerSpec: QuickSpec {
|
||||||
mockOGMCache = MockOGMCache()
|
mockOGMCache = MockOGMCache()
|
||||||
mockGeneralCache = MockGeneralCache()
|
mockGeneralCache = MockGeneralCache()
|
||||||
mockStorage = Storage(
|
mockStorage = Storage(
|
||||||
customWriter: DatabaseQueue(),
|
customWriter: try! DatabaseQueue(),
|
||||||
customMigrations: [
|
customMigrations: [
|
||||||
SNUtilitiesKit.migrations(),
|
SNUtilitiesKit.migrations(),
|
||||||
SNMessagingKit.migrations()
|
SNMessagingKit.migrations()
|
||||||
|
|
|
@ -26,7 +26,7 @@ class MessageReceiverDecryptionSpec: QuickSpec {
|
||||||
describe("a MessageReceiver") {
|
describe("a MessageReceiver") {
|
||||||
beforeEach {
|
beforeEach {
|
||||||
mockStorage = Storage(
|
mockStorage = Storage(
|
||||||
customWriter: DatabaseQueue(),
|
customWriter: try! DatabaseQueue(),
|
||||||
customMigrations: [
|
customMigrations: [
|
||||||
SNUtilitiesKit.migrations(),
|
SNUtilitiesKit.migrations(),
|
||||||
SNMessagingKit.migrations()
|
SNMessagingKit.migrations()
|
||||||
|
|
|
@ -23,7 +23,7 @@ class MessageSenderEncryptionSpec: QuickSpec {
|
||||||
describe("a MessageSender") {
|
describe("a MessageSender") {
|
||||||
beforeEach {
|
beforeEach {
|
||||||
mockStorage = Storage(
|
mockStorage = Storage(
|
||||||
customWriter: DatabaseQueue(),
|
customWriter: try! DatabaseQueue(),
|
||||||
customMigrations: [
|
customMigrations: [
|
||||||
SNUtilitiesKit.migrations(),
|
SNUtilitiesKit.migrations(),
|
||||||
SNMessagingKit.migrations()
|
SNMessagingKit.migrations()
|
||||||
|
|
|
@ -18,7 +18,7 @@ enum _002_SetupStandardJobs: Migration {
|
||||||
variant: .getSnodePool,
|
variant: .getSnodePool,
|
||||||
behaviour: .recurringOnLaunch,
|
behaviour: .recurringOnLaunch,
|
||||||
shouldBlock: true
|
shouldBlock: true
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
|
|
||||||
// Note: We also want this job to run both onLaunch and onActive as we want it to block
|
// Note: We also want this job to run both onLaunch and onActive as we want it to block
|
||||||
// 'onLaunch' and 'onActive' doesn't support blocking jobs
|
// 'onLaunch' and 'onActive' doesn't support blocking jobs
|
||||||
|
@ -26,7 +26,7 @@ enum _002_SetupStandardJobs: Migration {
|
||||||
variant: .getSnodePool,
|
variant: .getSnodePool,
|
||||||
behaviour: .recurringOnActive,
|
behaviour: .recurringOnActive,
|
||||||
shouldSkipLaunchBecomeActive: true
|
shouldSkipLaunchBecomeActive: true
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||||
|
|
|
@ -159,7 +159,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
port: legacySnode.port,
|
port: legacySnode.port,
|
||||||
ed25519PublicKey: legacySnode.publicKeySet.ed25519Key,
|
ed25519PublicKey: legacySnode.publicKeySet.ed25519Key,
|
||||||
x25519PublicKey: legacySnode.publicKeySet.x25519Key
|
x25519PublicKey: legacySnode.publicKeySet.x25519Key
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
Storage.update(progress: 0.96, for: self, in: target)
|
Storage.update(progress: 0.96, for: self, in: target)
|
||||||
|
|
||||||
|
@ -173,7 +173,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
nodeIndex: nodeIndex,
|
nodeIndex: nodeIndex,
|
||||||
address: legacySnode.address,
|
address: legacySnode.address,
|
||||||
port: legacySnode.port
|
port: legacySnode.port
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Storage.update(progress: 0.98, for: self, in: target)
|
Storage.update(progress: 0.98, for: self, in: target)
|
||||||
|
@ -188,7 +188,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
key: key,
|
key: key,
|
||||||
hash: hash,
|
hash: hash,
|
||||||
expirationDateMs: SnodeReceivedMessage.defaultExpirationSeconds
|
expirationDateMs: SnodeReceivedMessage.defaultExpirationSeconds
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Storage.update(progress: 0.99, for: self, in: target)
|
Storage.update(progress: 0.99, for: self, in: target)
|
||||||
|
@ -205,7 +205,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
expirationDateMs :
|
expirationDateMs :
|
||||||
SnodeReceivedMessage.defaultExpirationSeconds
|
SnodeReceivedMessage.defaultExpirationSeconds
|
||||||
)
|
)
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
|
||||||
|
|
||||||
beforeEach {
|
beforeEach {
|
||||||
mockStorage = Storage(
|
mockStorage = Storage(
|
||||||
customWriter: DatabaseQueue(),
|
customWriter: try! DatabaseQueue(),
|
||||||
customMigrations: [
|
customMigrations: [
|
||||||
SNUtilitiesKit.migrations(),
|
SNUtilitiesKit.migrations(),
|
||||||
SNSnodeKit.migrations(),
|
SNSnodeKit.migrations(),
|
||||||
|
|
|
@ -25,7 +25,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
|
||||||
|
|
||||||
beforeEach {
|
beforeEach {
|
||||||
mockStorage = SynchronousStorage(
|
mockStorage = SynchronousStorage(
|
||||||
customWriter: DatabaseQueue(),
|
customWriter: try! DatabaseQueue(),
|
||||||
customMigrations: [
|
customMigrations: [
|
||||||
SNUtilitiesKit.migrations(),
|
SNUtilitiesKit.migrations(),
|
||||||
SNSnodeKit.migrations(),
|
SNSnodeKit.migrations(),
|
||||||
|
|
|
@ -21,7 +21,7 @@ class NotificationContentViewModelSpec: QuickSpec {
|
||||||
|
|
||||||
beforeEach {
|
beforeEach {
|
||||||
mockStorage = Storage(
|
mockStorage = Storage(
|
||||||
customWriter: DatabaseQueue(),
|
customWriter: try! DatabaseQueue(),
|
||||||
customMigrations: [
|
customMigrations: [
|
||||||
SNUtilitiesKit.migrations(),
|
SNUtilitiesKit.migrations(),
|
||||||
SNSnodeKit.migrations(),
|
SNSnodeKit.migrations(),
|
||||||
|
|
|
@ -18,7 +18,7 @@ enum _002_SetupStandardJobs: Migration {
|
||||||
_ = try Job(
|
_ = try Job(
|
||||||
variant: .syncPushTokens,
|
variant: .syncPushTokens,
|
||||||
behaviour: .recurringOnLaunch
|
behaviour: .recurringOnLaunch
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
|
|
||||||
// Note: We actually need this job to run both onLaunch and onActive as the logic differs
|
// Note: We actually need this job to run both onLaunch and onActive as the logic differs
|
||||||
// slightly and there are cases where a user might not be registered in 'onLaunch' but is
|
// slightly and there are cases where a user might not be registered in 'onLaunch' but is
|
||||||
|
@ -27,7 +27,7 @@ enum _002_SetupStandardJobs: Migration {
|
||||||
variant: .syncPushTokens,
|
variant: .syncPushTokens,
|
||||||
behaviour: .recurringOnActive,
|
behaviour: .recurringOnActive,
|
||||||
shouldSkipLaunchBecomeActive: true
|
shouldSkipLaunchBecomeActive: true
|
||||||
).inserted(db)
|
).migrationSafeInserted(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||||
|
|
|
@ -91,27 +91,27 @@ enum _003_YDBToGRDBMigration: Migration {
|
||||||
try Identity(
|
try Identity(
|
||||||
variant: .seed,
|
variant: .seed,
|
||||||
data: Data(hex: seedHexString)
|
data: Data(hex: seedHexString)
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
try Identity(
|
try Identity(
|
||||||
variant: .ed25519SecretKey,
|
variant: .ed25519SecretKey,
|
||||||
data: Data(hex: userEd25519SecretKeyHexString)
|
data: Data(hex: userEd25519SecretKeyHexString)
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
try Identity(
|
try Identity(
|
||||||
variant: .ed25519PublicKey,
|
variant: .ed25519PublicKey,
|
||||||
data: Data(hex: userEd25519PublicKeyHexString)
|
data: Data(hex: userEd25519PublicKeyHexString)
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
try Identity(
|
try Identity(
|
||||||
variant: .x25519PrivateKey,
|
variant: .x25519PrivateKey,
|
||||||
data: userX25519KeyPair.privateKey
|
data: userX25519KeyPair.privateKey
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
|
|
||||||
try Identity(
|
try Identity(
|
||||||
variant: .x25519PublicKey,
|
variant: .x25519PublicKey,
|
||||||
data: userX25519KeyPair.publicKey
|
data: userX25519KeyPair.publicKey
|
||||||
).insert(db)
|
).migrationSafeInsert(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||||
|
|
|
@ -103,7 +103,7 @@ open class Storage {
|
||||||
migrations: [TargetMigrations],
|
migrations: [TargetMigrations],
|
||||||
async: Bool = true,
|
async: Bool = true,
|
||||||
onProgressUpdate: ((CGFloat, TimeInterval) -> ())?,
|
onProgressUpdate: ((CGFloat, TimeInterval) -> ())?,
|
||||||
onComplete: @escaping (Error?, Bool) -> ()
|
onComplete: @escaping (Swift.Result<Database, Error>, Bool) -> ()
|
||||||
) {
|
) {
|
||||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return }
|
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return }
|
||||||
|
|
||||||
|
@ -179,27 +179,31 @@ open class Storage {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the logic to run when the migration completes
|
// Store the logic to run when the migration completes
|
||||||
let migrationCompleted: (Database, Error?) -> () = { [weak self] db, error in
|
let migrationCompleted: (Swift.Result<Database, Error>) -> () = { [weak self] result in
|
||||||
self?.hasCompletedMigrations = true
|
self?.hasCompletedMigrations = true
|
||||||
self?.migrationProgressUpdater = nil
|
self?.migrationProgressUpdater = nil
|
||||||
SUKLegacy.clearLegacyDatabaseInstance()
|
SUKLegacy.clearLegacyDatabaseInstance()
|
||||||
|
|
||||||
if let error = error {
|
if case .failure(let error) = result {
|
||||||
SNLog("[Migration Error] Migration failed with error: \(error)")
|
SNLog("[Migration Error] Migration failed with error: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
onComplete(error, needsConfigSync)
|
onComplete(result, needsConfigSync)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: The non-async migration should only be used for unit tests
|
// Note: The non-async migration should only be used for unit tests
|
||||||
guard async else {
|
guard async else {
|
||||||
do { try self.migrator?.migrate(dbWriter) }
|
do { try self.migrator?.migrate(dbWriter) }
|
||||||
catch { try? dbWriter.read { db in migrationCompleted(db, error) } }
|
catch {
|
||||||
|
try? dbWriter.read { db in
|
||||||
|
migrationCompleted(Swift.Result<Database, Error>.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.migrator?.asyncMigrate(dbWriter) { db, error in
|
self.migrator?.asyncMigrate(dbWriter) { result in
|
||||||
migrationCompleted(db, error)
|
migrationCompleted(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,7 +438,7 @@ public extension ValueObservation {
|
||||||
func publisher(
|
func publisher(
|
||||||
in storage: Storage,
|
in storage: Storage,
|
||||||
scheduling scheduler: ValueObservationScheduler = Storage.defaultPublisherScheduler
|
scheduling scheduler: ValueObservationScheduler = Storage.defaultPublisherScheduler
|
||||||
) -> AnyPublisher<Reducer.Value, Error> {
|
) -> AnyPublisher<Reducer.Value, Error> where Reducer: ValueReducer {
|
||||||
guard storage.isValid, let dbWriter: DatabaseWriter = storage.dbWriter else {
|
guard storage.isValid, let dbWriter: DatabaseWriter = storage.dbWriter else {
|
||||||
return Fail(error: StorageError.databaseInvalid).eraseToAnyPublisher()
|
return Fail(error: StorageError.databaseInvalid).eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1005,10 +1005,11 @@ public enum PagedData {
|
||||||
filterSQL: SQL
|
filterSQL: SQL
|
||||||
) -> Int {
|
) -> Int {
|
||||||
let tableNameLiteral: SQL = SQL(stringLiteral: tableName)
|
let tableNameLiteral: SQL = SQL(stringLiteral: tableName)
|
||||||
|
let finalJoinSQL: SQL = (requiredJoinSQL ?? "")
|
||||||
let request: SQLRequest<Int> = """
|
let request: SQLRequest<Int> = """
|
||||||
SELECT \(tableNameLiteral).rowId
|
SELECT \(tableNameLiteral).rowId
|
||||||
FROM \(tableNameLiteral)
|
FROM \(tableNameLiteral)
|
||||||
\(requiredJoinSQL ?? "")
|
\(finalJoinSQL)
|
||||||
WHERE \(filterSQL)
|
WHERE \(filterSQL)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -1027,12 +1028,14 @@ public enum PagedData {
|
||||||
offset: Int
|
offset: Int
|
||||||
) -> [Int64] {
|
) -> [Int64] {
|
||||||
let tableNameLiteral: SQL = SQL(stringLiteral: tableName)
|
let tableNameLiteral: SQL = SQL(stringLiteral: tableName)
|
||||||
|
let finalJoinSQL: SQL = (requiredJoinSQL ?? "")
|
||||||
|
let finalGroupSQL: SQL = (groupSQL ?? "")
|
||||||
let request: SQLRequest<Int64> = """
|
let request: SQLRequest<Int64> = """
|
||||||
SELECT \(tableNameLiteral).rowId
|
SELECT \(tableNameLiteral).rowId
|
||||||
FROM \(tableNameLiteral)
|
FROM \(tableNameLiteral)
|
||||||
\(requiredJoinSQL ?? "")
|
\(finalJoinSQL)
|
||||||
WHERE \(filterSQL)
|
WHERE \(filterSQL)
|
||||||
\(groupSQL ?? "")
|
\(finalGroupSQL)
|
||||||
ORDER BY \(orderSQL)
|
ORDER BY \(orderSQL)
|
||||||
LIMIT \(limit) OFFSET \(offset)
|
LIMIT \(limit) OFFSET \(offset)
|
||||||
"""
|
"""
|
||||||
|
@ -1052,6 +1055,7 @@ public enum PagedData {
|
||||||
) -> Int? {
|
) -> Int? {
|
||||||
let tableNameLiteral: SQL = SQL(stringLiteral: tableName)
|
let tableNameLiteral: SQL = SQL(stringLiteral: tableName)
|
||||||
let idColumnLiteral: SQL = SQL(stringLiteral: idColumn)
|
let idColumnLiteral: SQL = SQL(stringLiteral: idColumn)
|
||||||
|
let finalJoinSQL: SQL = (requiredJoinSQL ?? "")
|
||||||
let request: SQLRequest<Int> = """
|
let request: SQLRequest<Int> = """
|
||||||
SELECT
|
SELECT
|
||||||
(data.rowIndex - 1) AS rowIndex -- Converting from 1-Indexed to 0-indexed
|
(data.rowIndex - 1) AS rowIndex -- Converting from 1-Indexed to 0-indexed
|
||||||
|
@ -1060,7 +1064,7 @@ public enum PagedData {
|
||||||
\(tableNameLiteral).\(idColumnLiteral) AS \(idColumnLiteral),
|
\(tableNameLiteral).\(idColumnLiteral) AS \(idColumnLiteral),
|
||||||
ROW_NUMBER() OVER (ORDER BY \(orderSQL)) AS rowIndex
|
ROW_NUMBER() OVER (ORDER BY \(orderSQL)) AS rowIndex
|
||||||
FROM \(tableNameLiteral)
|
FROM \(tableNameLiteral)
|
||||||
\(requiredJoinSQL ?? "")
|
\(finalJoinSQL)
|
||||||
WHERE \(filterSQL)
|
WHERE \(filterSQL)
|
||||||
) AS data
|
) AS data
|
||||||
WHERE \(SQL("data.\(idColumnLiteral) = \(id)"))
|
WHERE \(SQL("data.\(idColumnLiteral) = \(id)"))
|
||||||
|
@ -1083,6 +1087,7 @@ public enum PagedData {
|
||||||
guard !rowIds.isEmpty else { return [] }
|
guard !rowIds.isEmpty else { return [] }
|
||||||
|
|
||||||
let tableNameLiteral: SQL = SQL(stringLiteral: tableName)
|
let tableNameLiteral: SQL = SQL(stringLiteral: tableName)
|
||||||
|
let finalJoinSQL: SQL = (requiredJoinSQL ?? "")
|
||||||
let request: SQLRequest<RowIndexInfo> = """
|
let request: SQLRequest<RowIndexInfo> = """
|
||||||
SELECT
|
SELECT
|
||||||
data.rowId AS rowId,
|
data.rowId AS rowId,
|
||||||
|
@ -1092,7 +1097,7 @@ public enum PagedData {
|
||||||
\(tableNameLiteral).rowid AS rowid,
|
\(tableNameLiteral).rowid AS rowid,
|
||||||
ROW_NUMBER() OVER (ORDER BY \(orderSQL)) AS rowIndex
|
ROW_NUMBER() OVER (ORDER BY \(orderSQL)) AS rowIndex
|
||||||
FROM \(tableNameLiteral)
|
FROM \(tableNameLiteral)
|
||||||
\(requiredJoinSQL ?? "")
|
\(finalJoinSQL)
|
||||||
WHERE \(filterSQL)
|
WHERE \(filterSQL)
|
||||||
) AS data
|
) AS data
|
||||||
WHERE \(SQL("data.rowid IN \(rowIds)"))
|
WHERE \(SQL("data.rowid IN \(rowIds)"))
|
||||||
|
|
|
@ -20,6 +20,7 @@ public struct TargetMigrations: Comparable {
|
||||||
case snodeKit
|
case snodeKit
|
||||||
case messagingKit
|
case messagingKit
|
||||||
case uiKit
|
case uiKit
|
||||||
|
case test
|
||||||
|
|
||||||
public static func < (lhs: Self, rhs: Self) -> Bool {
|
public static func < (lhs: Self, rhs: Self) -> Bool {
|
||||||
let lhsIndex: Int = (Identifier.allCases.firstIndex(of: lhs) ?? Identifier.allCases.count)
|
let lhsIndex: Int = (Identifier.allCases.firstIndex(of: lhs) ?? Identifier.allCases.count)
|
||||||
|
|
|
@ -0,0 +1,277 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
// MARK: - Migration Safe Functions
|
||||||
|
|
||||||
|
public extension MutablePersistableRecord where Self: TableRecord & EncodableRecord & Codable {
|
||||||
|
func migrationSafeInsert(
|
||||||
|
_ db: Database,
|
||||||
|
onConflict conflictResolution: Database.ConflictResolution? = nil
|
||||||
|
) throws {
|
||||||
|
var record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||||
|
try record.insert(db, onConflict: conflictResolution)
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrationSafeInserted(
|
||||||
|
_ db: Database,
|
||||||
|
onConflict conflictResolution: Database.ConflictResolution? = nil
|
||||||
|
) throws -> Self {
|
||||||
|
let record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||||
|
let updatedRecord = try record.inserted(db, onConflict: conflictResolution)
|
||||||
|
return updatedRecord.originalRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrationSafeSave(
|
||||||
|
_ db: Database,
|
||||||
|
onConflict conflictResolution: Database.ConflictResolution? = nil
|
||||||
|
) throws {
|
||||||
|
var record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||||
|
try record.save(db, onConflict: conflictResolution)
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrationSafeSaved(
|
||||||
|
_ db: Database,
|
||||||
|
onConflict conflictResolution: Database.ConflictResolution? = nil
|
||||||
|
) throws -> Self {
|
||||||
|
let record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||||
|
let updatedRecord = try record.saved(db, onConflict: conflictResolution)
|
||||||
|
return updatedRecord.originalRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrationSafeUpsert(_ db: Database) throws {
|
||||||
|
var record = try MigrationSafeMutableRecord(db, originalRecord: self)
|
||||||
|
try record.upsert(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - MigrationSafeMutableRecord
|
||||||
|
|
||||||
|
private class MigrationSafeRecord<T: PersistableRecord & Encodable>: MigrationSafeMutableRecord<T> {}
|
||||||
|
|
||||||
|
private class MigrationSafeMutableRecord<T: MutablePersistableRecord & Encodable>: MutablePersistableRecord & Encodable {
|
||||||
|
public static var databaseTableName: String { T.databaseTableName }
|
||||||
|
|
||||||
|
fileprivate var originalRecord: T
|
||||||
|
private let availableColumnNames: [String]
|
||||||
|
|
||||||
|
init(_ db: Database, originalRecord: T) throws {
|
||||||
|
// Check the current columns in the database and filter out any properties on the object which
|
||||||
|
// don't exist in the dictionary
|
||||||
|
self.originalRecord = originalRecord
|
||||||
|
self.availableColumnNames = try db.columns(in: Self.databaseTableName).map(\.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: Encoder) throws {
|
||||||
|
let filteredEncoder: FilteredEncoder = FilteredEncoder(
|
||||||
|
originalEncoder: encoder,
|
||||||
|
availableKeys: availableColumnNames
|
||||||
|
)
|
||||||
|
try originalRecord.encode(to: filteredEncoder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Persistence Callbacks
|
||||||
|
|
||||||
|
func willInsert(_ db: Database) throws {
|
||||||
|
try originalRecord.willInsert(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func aroundInsert(_ db: Database, insert: () throws -> InsertionSuccess) throws {
|
||||||
|
try originalRecord.aroundInsert(db, insert: insert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didInsert(_ inserted: InsertionSuccess) {
|
||||||
|
originalRecord.didInsert(inserted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func willUpdate(_ db: Database, columns: Set<String>) throws {
|
||||||
|
try originalRecord.willUpdate(db, columns: columns)
|
||||||
|
}
|
||||||
|
|
||||||
|
func aroundUpdate(_ db: Database, columns: Set<String>, update: () throws -> PersistenceSuccess) throws {
|
||||||
|
try originalRecord.aroundUpdate(db, columns: columns, update: update)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didUpdate(_ updated: PersistenceSuccess) {
|
||||||
|
originalRecord.didUpdate(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func willSave(_ db: Database) throws {
|
||||||
|
try originalRecord.willSave(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func aroundSave(_ db: Database, save: () throws -> PersistenceSuccess) throws {
|
||||||
|
try originalRecord.aroundSave(db, save: save)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didSave(_ saved: PersistenceSuccess) {
|
||||||
|
originalRecord.didSave(saved)
|
||||||
|
}
|
||||||
|
|
||||||
|
func willDelete(_ db: Database) throws {
|
||||||
|
try originalRecord.willDelete(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func aroundDelete(_ db: Database, delete: () throws -> Bool) throws {
|
||||||
|
try originalRecord.aroundDelete(db, delete: delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func didDelete(deleted: Bool) {
|
||||||
|
originalRecord.didDelete(deleted: deleted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - FilteredEncoder
|
||||||
|
|
||||||
|
private class FilteredEncoder: Encoder {
|
||||||
|
let originalEncoder: Encoder
|
||||||
|
let availableKeys: [String]
|
||||||
|
|
||||||
|
init(originalEncoder: Encoder, availableKeys: [String]) {
|
||||||
|
self.originalEncoder = originalEncoder
|
||||||
|
self.availableKeys = availableKeys
|
||||||
|
}
|
||||||
|
|
||||||
|
var codingPath: [CodingKey] { originalEncoder.codingPath }
|
||||||
|
var userInfo: [CodingUserInfoKey: Any] { originalEncoder.userInfo }
|
||||||
|
|
||||||
|
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey {
|
||||||
|
let container = originalEncoder.container(keyedBy: type)
|
||||||
|
let filteredContainer = FilteredKeyedEncodingContainer(
|
||||||
|
availableKeys: availableKeys,
|
||||||
|
originalContainer: container
|
||||||
|
)
|
||||||
|
|
||||||
|
return KeyedEncodingContainer(filteredContainer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> UnkeyedEncodingContainer { originalEncoder.unkeyedContainer() }
|
||||||
|
func singleValueContainer() -> SingleValueEncodingContainer { originalEncoder.singleValueContainer() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - FilteredKeyedEncodingContainer
|
||||||
|
|
||||||
|
private class FilteredKeyedEncodingContainer<Key: CodingKey>: KeyedEncodingContainerProtocol {
|
||||||
|
let codingPath: [CodingKey]
|
||||||
|
let availableKeys: [String]
|
||||||
|
var originalContainer: KeyedEncodingContainer<Key>
|
||||||
|
|
||||||
|
init(availableKeys: [String], originalContainer: KeyedEncodingContainer<Key>) {
|
||||||
|
self.availableKeys = availableKeys
|
||||||
|
self.codingPath = originalContainer.codingPath
|
||||||
|
self.originalContainer = originalContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNil(forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encodeNil(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Bool, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: String, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Double, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Float, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Int, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Int8, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Int16, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Int32, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: Int64, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: UInt, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: UInt8, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: UInt16, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: UInt32, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ value: UInt64, forKey key: Key) throws {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable {
|
||||||
|
guard availableKeys.contains(key.stringValue) else { return }
|
||||||
|
|
||||||
|
try originalContainer.encode(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey {
|
||||||
|
return originalContainer.nestedContainer(keyedBy: keyType, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
|
||||||
|
return originalContainer.nestedUnkeyedContainer(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func superEncoder() -> Encoder {
|
||||||
|
return originalContainer.superEncoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func superEncoder(forKey key: Key) -> Encoder {
|
||||||
|
return originalContainer.superEncoder(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
|
@ -138,7 +138,7 @@ public final class JobRunner {
|
||||||
guard canStartJob else { return }
|
guard canStartJob else { return }
|
||||||
|
|
||||||
// Start the job runner if needed
|
// Start the job runner if needed
|
||||||
db.afterNextTransactionCommit { _ in
|
db.afterNextTransaction { _ in
|
||||||
queues.wrappedValue[updatedJob.variant]?.start()
|
queues.wrappedValue[updatedJob.variant]?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@ public final class JobRunner {
|
||||||
queues.wrappedValue[job.variant]?.upsert(job, canStartJob: canStartJob)
|
queues.wrappedValue[job.variant]?.upsert(job, canStartJob: canStartJob)
|
||||||
|
|
||||||
// Start the job runner if needed
|
// Start the job runner if needed
|
||||||
db.afterNextTransactionCommit { _ in
|
db.afterNextTransaction { _ in
|
||||||
queues.wrappedValue[job.variant]?.start()
|
queues.wrappedValue[job.variant]?.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,7 @@ public final class JobRunner {
|
||||||
queues.wrappedValue[updatedJob.variant]?.insert(updatedJob, before: otherJob)
|
queues.wrappedValue[updatedJob.variant]?.insert(updatedJob, before: otherJob)
|
||||||
|
|
||||||
// Start the job runner if needed
|
// Start the job runner if needed
|
||||||
db.afterNextTransactionCommit { _ in
|
db.afterNextTransaction { _ in
|
||||||
queues.wrappedValue[updatedJob.variant]?.start()
|
queues.wrappedValue[updatedJob.variant]?.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ class IdentitySpec: QuickSpec {
|
||||||
describe("an Identity") {
|
describe("an Identity") {
|
||||||
beforeEach {
|
beforeEach {
|
||||||
mockStorage = Storage(
|
mockStorage = Storage(
|
||||||
customWriter: DatabaseQueue(),
|
customWriter: try! DatabaseQueue(),
|
||||||
customMigrations: [
|
customMigrations: [
|
||||||
SNUtilitiesKit.migrations()
|
SNUtilitiesKit.migrations()
|
||||||
]
|
]
|
||||||
|
|
|
@ -0,0 +1,681 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
import Quick
|
||||||
|
import Nimble
|
||||||
|
|
||||||
|
@testable import SessionUtilitiesKit
|
||||||
|
|
||||||
|
class PersistableRecordUtilitiesSpec: QuickSpec {
|
||||||
|
static var customWriter: DatabaseQueue!
|
||||||
|
|
||||||
|
struct TestType: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||||
|
public static var databaseTableName: String { "TestType" }
|
||||||
|
|
||||||
|
public typealias Columns = CodingKeys
|
||||||
|
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||||
|
case columnA
|
||||||
|
case columnB
|
||||||
|
}
|
||||||
|
|
||||||
|
public let columnA: String
|
||||||
|
public let columnB: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MutableTestType: Codable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
|
||||||
|
public static var databaseTableName: String { "MutableTestType" }
|
||||||
|
|
||||||
|
public typealias Columns = CodingKeys
|
||||||
|
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||||
|
case id
|
||||||
|
case columnA
|
||||||
|
case columnB
|
||||||
|
}
|
||||||
|
|
||||||
|
public var id: Int64?
|
||||||
|
public let columnA: String
|
||||||
|
public let columnB: String?
|
||||||
|
|
||||||
|
init(id: Int64? = nil, columnA: String, columnB: String?) {
|
||||||
|
self.id = id
|
||||||
|
self.columnA = columnA
|
||||||
|
self.columnB = columnB
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func didInsert(_ inserted: InsertionSuccess) {
|
||||||
|
self.id = inserted.rowID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestInsertTestTypeMigration: Migration {
|
||||||
|
static let target: TargetMigrations.Identifier = .test
|
||||||
|
static let identifier: String = "TestInsertTestType"
|
||||||
|
static let needsConfigSync: Bool = false
|
||||||
|
static let minExpectedRunDuration: TimeInterval = 0
|
||||||
|
|
||||||
|
static func migrate(_ db: Database) throws {
|
||||||
|
try db.create(table: TestType.self) { t in
|
||||||
|
t.column(.columnA, .text).primaryKey()
|
||||||
|
}
|
||||||
|
|
||||||
|
try db.create(table: MutableTestType.self) { t in
|
||||||
|
t.column(.id, .integer).primaryKey(autoincrement: true)
|
||||||
|
t.column(.columnA, .text).unique()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestAddColumnMigration: Migration {
|
||||||
|
static let target: TargetMigrations.Identifier = .test
|
||||||
|
static let identifier: String = "TestAddColumn"
|
||||||
|
static let needsConfigSync: Bool = false
|
||||||
|
static let minExpectedRunDuration: TimeInterval = 0
|
||||||
|
|
||||||
|
static func migrate(_ db: Database) throws {
|
||||||
|
try db.alter(table: TestType.self) { t in
|
||||||
|
t.add(.columnB, .text)
|
||||||
|
}
|
||||||
|
|
||||||
|
try db.alter(table: MutableTestType.self) { t in
|
||||||
|
t.add(.columnB, .text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Spec
|
||||||
|
|
||||||
|
override func spec() {
|
||||||
|
var customWriter: DatabaseQueue!
|
||||||
|
var mockStorage: Storage!
|
||||||
|
|
||||||
|
describe("a PersistableRecord") {
|
||||||
|
beforeEach {
|
||||||
|
customWriter = try! DatabaseQueue()
|
||||||
|
PersistableRecordUtilitiesSpec.customWriter = customWriter
|
||||||
|
mockStorage = Storage(
|
||||||
|
customWriter: customWriter,
|
||||||
|
customMigrations: [
|
||||||
|
TargetMigrations(
|
||||||
|
identifier: .test,
|
||||||
|
migrations: (0..<100)
|
||||||
|
.map { _ in [] }
|
||||||
|
.appending([TestInsertTestTypeMigration.self])
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach {
|
||||||
|
customWriter = nil
|
||||||
|
mockStorage = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
context("before running the add column migration") {
|
||||||
|
it("fails when using the standard insert") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test1", columnB: "Test1B").insert(db)
|
||||||
|
}
|
||||||
|
.to(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("fails when using the standard inserted") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test2", columnB: "Test2B").inserted(db)
|
||||||
|
}
|
||||||
|
.to(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("fails when using the standard save and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test3", columnB: "Test3B").save(db)
|
||||||
|
}
|
||||||
|
.to(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("fails when using the standard saved and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test4", columnB: "Test4B").saved(db)
|
||||||
|
}
|
||||||
|
.to(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("fails when using the standard upsert and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test5", columnB: "Test5B").upsert(db)
|
||||||
|
}
|
||||||
|
.to(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("fails when using the standard mutable upsert and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
var result = MutableTestType(columnA: "Test6", columnB: "Test6B")
|
||||||
|
try result.upsert(db)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
.to(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("fails when using the standard upsert and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO TestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test19"])
|
||||||
|
)
|
||||||
|
try TestType(columnA: "Test19", columnB: "Test19B").upsert(db)
|
||||||
|
}
|
||||||
|
.to(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("fails when using the standard mutable upsert and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test20"])
|
||||||
|
)
|
||||||
|
var result = MutableTestType(id: 1, columnA: "Test20", columnB: "Test20B")
|
||||||
|
try result.upsert(db)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
.to(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe insert") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test7", columnB: "Test7B").migrationSafeInsert(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
expect(try TestType.fetchAll(db))
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe inserted") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test8", columnB: "Test8B").migrationSafeInserted(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test9", columnB: "Test9B")
|
||||||
|
.migrationSafeInserted(db)
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
expect(try MutableTestType.fetchAll(db))
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe save and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test10", columnB: "Test10B").migrationSafeSave(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe saved and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test11", columnB: "Test11B").migrationSafeSaved(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test12", columnB: "Test12B")
|
||||||
|
.migrationSafeSaved(db)
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
expect(try MutableTestType.fetchAll(db))
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe upsert and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test13", columnB: "Test13B").migrationSafeUpsert(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe mutable upsert and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
var result = MutableTestType(columnA: "Test14", columnB: "Test14B")
|
||||||
|
try result.migrationSafeUpsert(db)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
expect(try MutableTestType.fetchAll(db))
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The built-in 'update' method only updates existing columns so this shouldn't fail
|
||||||
|
it("succeeds when using the standard save and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO TestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test16"])
|
||||||
|
)
|
||||||
|
try TestType(columnA: "Test16", columnB: "Test16B").save(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The built-in 'update' method only updates existing columns so this won't fail
|
||||||
|
// due to the structure discrepancy but won't update the id as that only happens on
|
||||||
|
// insert
|
||||||
|
it("succeeds when using the standard saved and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test17"])
|
||||||
|
)
|
||||||
|
_ = try MutableTestType(id: 1, columnA: "Test17", columnB: "Test17B").saved(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test18"])
|
||||||
|
)
|
||||||
|
return try MutableTestType(id: 2, columnA: "Test18", columnB: "Test18B")
|
||||||
|
.saved(db)
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
let types: [MutableTestType]? = try MutableTestType.fetchAll(db)
|
||||||
|
|
||||||
|
expect(types).toNot(beNil())
|
||||||
|
expect(types?.compactMap { $0.id }.count).to(equal(types?.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("after running the add column migration") {
|
||||||
|
beforeEach {
|
||||||
|
var migrator: DatabaseMigrator = DatabaseMigrator()
|
||||||
|
migrator.registerMigration(
|
||||||
|
TestAddColumnMigration.target,
|
||||||
|
migration: TestAddColumnMigration.self
|
||||||
|
)
|
||||||
|
|
||||||
|
expect { try migrator.migrate(customWriter) }
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the standard insert") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test1", columnB: "Test1B").insert(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
expect(try TestType.fetchAll(db))
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the standard inserted") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test2", columnB: "Test2B").inserted(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
expect(try MutableTestType.fetchAll(db))
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the standard save and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test3", columnB: "Test3B").save(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the standard saved and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test3", columnB: "Test3B").saved(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the standard save and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO TestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test4"])
|
||||||
|
)
|
||||||
|
try TestType(columnA: "Test4", columnB: "Test4B").save(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The built-in 'update' method won't update the id as that only happens on
|
||||||
|
// insert
|
||||||
|
it("succeeds when using the standard saved and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test5"])
|
||||||
|
)
|
||||||
|
_ = try MutableTestType(id: 1, columnA: "Test5", columnB: "Test5B").saved(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test6"])
|
||||||
|
)
|
||||||
|
return try MutableTestType(id: 2, columnA: "Test6", columnB: "Test6B")
|
||||||
|
.saved(db)
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
let types: [MutableTestType]? = try MutableTestType.fetchAll(db)
|
||||||
|
|
||||||
|
expect(types).toNot(beNil())
|
||||||
|
expect(types?.compactMap { $0.id }.count).to(equal(types?.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the standard upsert and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test7", columnB: "Test7B").upsert(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the standard mutable upsert and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
var result = MutableTestType(columnA: "Test8", columnB: "Test8B")
|
||||||
|
try result.upsert(db)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the standard upsert and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO TestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test9"])
|
||||||
|
)
|
||||||
|
try TestType(columnA: "Test9", columnB: "Test9B").upsert(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The built-in 'update' method won't update the id as that only happens on
|
||||||
|
// insert
|
||||||
|
it("succeeds when using the standard mutable upsert and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test10"])
|
||||||
|
)
|
||||||
|
var result = MutableTestType(id: 1, columnA: "Test10", columnB: "Test10B")
|
||||||
|
try result.upsert(db)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test11"])
|
||||||
|
)
|
||||||
|
var result = MutableTestType(id: 2, columnA: "Test11", columnB: "Test11B")
|
||||||
|
try result.upsert(db)
|
||||||
|
return result.id
|
||||||
|
}
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
let types: [MutableTestType]? = try MutableTestType.fetchAll(db)
|
||||||
|
|
||||||
|
expect(types).toNot(beNil())
|
||||||
|
expect(types?.compactMap { $0.id }.count).to(equal(types?.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe insert") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test12", columnB: "Test12B").migrationSafeInsert(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
expect(try TestType.fetchAll(db))
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe inserted") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test13", columnB: "Test13B").migrationSafeInserted(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test14", columnB: "Test14B")
|
||||||
|
.migrationSafeInserted(db)
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
expect(try MutableTestType.fetchAll(db))
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe save and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test15", columnB: "Test15B").migrationSafeSave(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe saved and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try MutableTestType(columnA: "Test16", columnB: "Test16B").migrationSafeSaved(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe save and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO TestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test17"])
|
||||||
|
)
|
||||||
|
try TestType(columnA: "Test17", columnB: "Test17B").migrationSafeSave(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The built-in 'update' method won't update the id as that only happens on
|
||||||
|
// insert
|
||||||
|
it("succeeds when using the migration safe saved and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test18"])
|
||||||
|
)
|
||||||
|
_ = try MutableTestType(id: 1, columnA: "Test18", columnB: "Test18B")
|
||||||
|
.migrationSafeSaved(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test19"])
|
||||||
|
)
|
||||||
|
return try MutableTestType(id: 2, columnA: "Test19", columnB: "Test19B")
|
||||||
|
.migrationSafeSaved(db)
|
||||||
|
.id
|
||||||
|
}
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
let types: [MutableTestType]? = try MutableTestType.fetchAll(db)
|
||||||
|
|
||||||
|
expect(types).toNot(beNil())
|
||||||
|
expect(types?.compactMap { $0.id }.count).to(equal(types?.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe upsert and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try TestType(columnA: "Test20", columnB: "Test20B").migrationSafeUpsert(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe mutable upsert and the item does not already exist") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
var result = MutableTestType(columnA: "Test21", columnB: "Test21B")
|
||||||
|
try result.migrationSafeUpsert(db)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it("succeeds when using the migration safe upsert and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO TestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test22"])
|
||||||
|
)
|
||||||
|
try TestType(columnA: "Test22", columnB: "Test22B").migrationSafeUpsert(db)
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The built-in 'update' method won't update the id as that only happens on
|
||||||
|
// insert
|
||||||
|
it("succeeds when using the migration safe mutable upsert and the item already exists") {
|
||||||
|
mockStorage.write { db in
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test23"])
|
||||||
|
)
|
||||||
|
var result = MutableTestType(id: 1, columnA: "Test23", columnB: "Test23B")
|
||||||
|
try result.migrationSafeUpsert(db)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
.toNot(throwError())
|
||||||
|
|
||||||
|
expect {
|
||||||
|
try db.execute(
|
||||||
|
sql: "INSERT INTO MutableTestType (columnA) VALUES (?)",
|
||||||
|
arguments: StatementArguments(["Test24"])
|
||||||
|
)
|
||||||
|
var result = MutableTestType(id: 2, columnA: "Test24", columnB: "Test24B")
|
||||||
|
try result.migrationSafeUpsert(db)
|
||||||
|
return result.id
|
||||||
|
}
|
||||||
|
.toNot(beNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
mockStorage.read { db in
|
||||||
|
let types: [MutableTestType]? = try MutableTestType.fetchAll(db)
|
||||||
|
|
||||||
|
expect(types).toNot(beNil())
|
||||||
|
expect(types?.compactMap { $0.id }.count).to(equal(types?.count))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import GRDB
|
||||||
import SessionMessagingKit
|
import SessionMessagingKit
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
import UIKit
|
|
||||||
import SessionUIKit
|
import SessionUIKit
|
||||||
|
|
||||||
public enum AppSetup {
|
public enum AppSetup {
|
||||||
|
@ -12,7 +12,7 @@ public enum AppSetup {
|
||||||
public static func setupEnvironment(
|
public static func setupEnvironment(
|
||||||
appSpecificBlock: @escaping () -> (),
|
appSpecificBlock: @escaping () -> (),
|
||||||
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
||||||
migrationsCompletion: @escaping (Error?, Bool) -> ()
|
migrationsCompletion: @escaping (Result<Database, Error>, Bool) -> ()
|
||||||
) {
|
) {
|
||||||
guard !AppSetup.hasRun else { return }
|
guard !AppSetup.hasRun else { return }
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ public enum AppSetup {
|
||||||
public static func runPostSetupMigrations(
|
public static func runPostSetupMigrations(
|
||||||
backgroundTask: OWSBackgroundTask? = nil,
|
backgroundTask: OWSBackgroundTask? = nil,
|
||||||
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
||||||
migrationsCompletion: @escaping (Error?, Bool) -> ()
|
migrationsCompletion: @escaping (Result<Database, Error>, Bool) -> ()
|
||||||
) {
|
) {
|
||||||
var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function))
|
var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function))
|
||||||
|
|
||||||
|
@ -73,9 +73,9 @@ public enum AppSetup {
|
||||||
SNUIKit.migrations()
|
SNUIKit.migrations()
|
||||||
],
|
],
|
||||||
onProgressUpdate: migrationProgressChanged,
|
onProgressUpdate: migrationProgressChanged,
|
||||||
onComplete: { error, needsConfigSync in
|
onComplete: { result, needsConfigSync in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
migrationsCompletion(error, needsConfigSync)
|
migrationsCompletion(result, needsConfigSync)
|
||||||
|
|
||||||
// The 'if' is only there to prevent the "variable never read" warning from showing
|
// The 'if' is only there to prevent the "variable never read" warning from showing
|
||||||
if backgroundTask != nil { backgroundTask = nil }
|
if backgroundTask != nil { backgroundTask = nil }
|
||||||
|
|
Loading…
Reference in a new issue