diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index a8954a3b9..d720d5f32 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -636,6 +636,7 @@ FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */; }; FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */; }; FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */; }; + FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */; }; FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */; }; FD5C72F9284F0E880029977D /* MessageReceiver+TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */; }; FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */; }; @@ -1757,6 +1758,7 @@ FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = ""; }; FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = ""; }; + FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScopeAdapter+Utilities.swift"; sourceTree = ""; }; FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ReadReceipts.swift"; sourceTree = ""; }; FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+TypingIndicators.swift"; sourceTree = ""; }; FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DataExtractionNotification.swift"; sourceTree = ""; }; @@ -3695,6 +3697,7 @@ FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */, + FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */, FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */, ); path = Utilities; @@ -5681,6 +5684,7 @@ C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */, + FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */, FD37E9FF28A5F2CD003AE748 /* Configuration.swift in Sources */, FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, diff --git a/Session/Home/GlobalSearch/GlobalSearchViewController.swift b/Session/Home/GlobalSearch/GlobalSearchViewController.swift index 7d4103a85..fdb8ca753 100644 --- a/Session/Home/GlobalSearch/GlobalSearchViewController.swift +++ b/Session/Home/GlobalSearch/GlobalSearchViewController.swift @@ -203,6 +203,11 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U ]) } catch { + // Don't log the 'interrupt' error as that's just the user typing too fast + if (error as? DatabaseError)?.resultCode != DatabaseError.SQLITE_INTERRUPT { + SNLog("[GlobalSearch] Failed to find results due to error: \(error)") + } + return .failure(error) } } diff --git a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift index 10eff35e3..0ff182537 100644 --- a/Session/Media Viewing & Editing/MediaGalleryViewModel.swift +++ b/Session/Media Viewing & Editing/MediaGalleryViewModel.swift @@ -199,16 +199,18 @@ public class MediaGalleryViewModel { } } - public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable { - fileprivate static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) - fileprivate static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) - fileprivate static let interactionAuthorIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionAuthorId.stringValue) - fileprivate static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) - fileprivate static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - fileprivate static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) - fileprivate static let attachmentAlbumIndexKey: SQL = SQL(stringLiteral: CodingKeys.attachmentAlbumIndex.stringValue) - - fileprivate static let attachmentString: String = CodingKeys.attachment.stringValue + public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case interactionId + case interactionVariant + case interactionAuthorId + case interactionTimestampMs + + case rowId + case attachmentAlbumIndex + case attachment + } public var id: String { attachment.id } public var differenceIdentifier: String { attachment.id } @@ -306,7 +308,7 @@ public class MediaGalleryViewModel { let finalFilterSQL: SQL = { guard let customFilters: SQL = customFilters else { return """ - WHERE \(attachment.alias[Column.rowID]) IN \(rowIds) + WHERE \(attachment[.rowId]) IN \(rowIds) """ } @@ -318,14 +320,14 @@ public class MediaGalleryViewModel { }() let request: SQLRequest = """ SELECT - \(interaction[.id]) AS \(Item.interactionIdKey), - \(interaction[.variant]) AS \(Item.interactionVariantKey), - \(interaction[.authorId]) AS \(Item.interactionAuthorIdKey), - \(interaction[.timestampMs]) AS \(Item.interactionTimestampMsKey), + \(interaction[.id]) AS \(Item.Columns.interactionId), + \(interaction[.variant]) AS \(Item.Columns.interactionVariant), + \(interaction[.authorId]) AS \(Item.Columns.interactionAuthorId), + \(interaction[.timestampMs]) AS \(Item.Columns.interactionTimestampMs), - \(attachment.alias[Column.rowID]) AS \(Item.rowIdKey), - \(interactionAttachment[.albumIndex]) AS \(Item.attachmentAlbumIndexKey), - \(Item.attachmentKey).* + \(attachment[.rowId]) AS \(Item.Columns.rowId), + \(interactionAttachment[.albumIndex]) AS \(Item.Columns.attachmentAlbumIndex), + \(attachment.allColumns) FROM \(Attachment.self) \(joinSQL) \(finalFilterSQL) @@ -338,8 +340,8 @@ public class MediaGalleryViewModel { Attachment.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - Item.attachmentString: adapters[1] + return ScopeAdapter.with(Item.self, [ + .attachment: adapters[1] ]) } } diff --git a/Session/Settings/BlockedContactsViewModel.swift b/Session/Settings/BlockedContactsViewModel.swift index bf4a46bea..0a612d0b0 100644 --- a/Session/Settings/BlockedContactsViewModel.swift +++ b/Session/Settings/BlockedContactsViewModel.swift @@ -258,11 +258,12 @@ class BlockedContactsViewModel: SessionTableViewModel = """ SELECT - \(profile.alias[Column.rowID]) AS \(DataModel.rowIdKey), - \(DataModel.profileKey).* + \(profile[.rowId]) AS \(DataModel.Columns.rowId), + \(profile.allColumns) FROM \(Profile.self) - WHERE \(profile.alias[Column.rowID]) IN \(rowIds) + WHERE \(profile[.rowId]) IN \(rowIds) ORDER BY \(orderSQL) """ @@ -300,8 +301,8 @@ class BlockedContactsViewModel: SessionTableViewModel SQLRequest { let interaction: TypedTableAlias = TypedTableAlias() - let interactionFullTextSearch: SQL = SQL(stringLiteral: Interaction.fullTextSearchTableName) - let threadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name) + let interactionFullTextSearch: TypedTableAlias = TypedTableAlias(name: Interaction.fullTextSearchTableName) let request: SQLRequest = """ SELECT @@ -720,9 +730,9 @@ public extension Interaction { \(interaction[.timestampMs]) FROM \(Interaction.self) JOIN \(interactionFullTextSearch) ON ( - \(interactionFullTextSearch).rowid = \(interaction.alias[Column.rowID]) AND - \(SQL("\(interactionFullTextSearch).\(threadIdLiteral) = \(threadId)")) AND - \(interactionFullTextSearch).\(SQL(stringLiteral: Interaction.Columns.body.name)) MATCH \(pattern) + \(interactionFullTextSearch[.rowId]) = \(interaction[.rowId]) AND + \(SQL("\(interactionFullTextSearch[.threadId]) = \(threadId)")) AND + \(interactionFullTextSearch[.body]) MATCH \(pattern) ) ORDER BY \(interaction[.timestampMs].desc) diff --git a/SessionMessagingKit/Database/Models/OpenGroup.swift b/SessionMessagingKit/Database/Models/OpenGroup.swift index 55da87c1d..d4b27a35c 100644 --- a/SessionMessagingKit/Database/Models/OpenGroup.swift +++ b/SessionMessagingKit/Database/Models/OpenGroup.swift @@ -215,6 +215,19 @@ public extension OpenGroup { } } +// MARK: - Search Queries + +public extension OpenGroup { + struct FullTextSearch: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case name + } + + let name: String + } +} + // MARK: - Convenience public extension OpenGroup { diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index a9a6bd8af..7b8695929 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -298,6 +298,21 @@ public extension Profile { } } +// MARK: - Search Queries + +public extension Profile { + struct FullTextSearch: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case nickname + case name + } + + let nickname: String? + let name: String + } +} + // MARK: - Convenience public extension Profile { diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 163d38038..cb8f84fc3 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -365,7 +365,7 @@ public extension SessionThread { let contact: TypedTableAlias = TypedTableAlias() return """ - SELECT \(thread.allColumns()) + SELECT \(thread.allColumns) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) WHERE ( diff --git a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift index 96c28cf99..633588a3d 100644 --- a/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift +++ b/SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift @@ -82,7 +82,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Interaction.self) WHERE \(Column.rowID) IN ( - SELECT \(interaction.alias[Column.rowID]) + SELECT \(interaction[.rowId]) FROM \(Interaction.self) JOIN \(SessionThread.self) ON ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND @@ -90,7 +90,7 @@ public enum GarbageCollectionJob: JobExecutor { ) JOIN ( SELECT - COUNT(\(interaction.alias[Column.rowID])) AS interactionCount, + COUNT(\(interaction[.rowId])) AS interactionCount, \(interaction[.threadId]) FROM \(Interaction.self) GROUP BY \(interaction[.threadId]) @@ -112,7 +112,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Job.self) WHERE \(Column.rowID) IN ( - SELECT \(job.alias[Column.rowID]) + SELECT \(job[.rowId]) FROM \(Job.self) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(job[.threadId]) LEFT JOIN \(Interaction.self) ON \(interaction[.id]) = \(job[.interactionId]) @@ -139,7 +139,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(LinkPreview.self) WHERE \(Column.rowID) IN ( - SELECT \(linkPreview.alias[Column.rowID]) + SELECT \(linkPreview[.rowId]) FROM \(LinkPreview.self) LEFT JOIN \(Interaction.self) ON ( \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND @@ -159,7 +159,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(OpenGroup.self) WHERE \(Column.rowID) IN ( - SELECT \(openGroup.alias[Column.rowID]) + SELECT \(openGroup[.rowId]) FROM \(OpenGroup.self) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(openGroup[.threadId]) WHERE ( @@ -178,7 +178,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Capability.self) WHERE \(Column.rowID) IN ( - SELECT \(capability.alias[Column.rowID]) + SELECT \(capability[.rowId]) FROM \(Capability.self) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.server]) = \(capability[.openGroupServer]) WHERE \(openGroup[.threadId]) IS NULL @@ -195,7 +195,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(BlindedIdLookup.self) WHERE \(Column.rowID) IN ( - SELECT \(blindedIdLookup.alias[Column.rowID]) + SELECT \(blindedIdLookup[.rowId]) FROM \(BlindedIdLookup.self) LEFT JOIN \(SessionThread.self) ON ( \(thread[.id]) = \(blindedIdLookup[.blindedId]) OR @@ -222,7 +222,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Contact.self) WHERE \(Column.rowID) IN ( - SELECT \(contact.alias[Column.rowID]) + SELECT \(contact[.rowId]) FROM \(Contact.self) LEFT JOIN \(BlindedIdLookup.self) ON ( \(blindedIdLookup[.blindedId]) = \(contact[.id]) AND @@ -243,7 +243,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Attachment.self) WHERE \(Column.rowID) IN ( - SELECT \(attachment.alias[Column.rowID]) + SELECT \(attachment[.rowId]) FROM \(Attachment.self) LEFT JOIN \(Quote.self) ON \(quote[.attachmentId]) = \(attachment[.id]) LEFT JOIN \(LinkPreview.self) ON \(linkPreview[.attachmentId]) = \(attachment[.id]) @@ -269,7 +269,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(Profile.self) WHERE \(Column.rowID) IN ( - SELECT \(profile.alias[Column.rowID]) + SELECT \(profile[.rowId]) FROM \(Profile.self) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(profile[.id]) LEFT JOIN \(Interaction.self) ON \(interaction[.authorId]) = \(profile[.id]) @@ -310,7 +310,7 @@ public enum GarbageCollectionJob: JobExecutor { try db.execute(literal: """ DELETE FROM \(SessionThread.self) WHERE \(Column.rowID) IN ( - SELECT \(thread.alias[Column.rowID]) + SELECT \(thread[.rowId]) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) diff --git a/SessionMessagingKit/Shared Models/MentionInfo.swift b/SessionMessagingKit/Shared Models/MentionInfo.swift index 984ddf63d..f4bb049cb 100644 --- a/SessionMessagingKit/Shared Models/MentionInfo.swift +++ b/SessionMessagingKit/Shared Models/MentionInfo.swift @@ -3,12 +3,14 @@ import GRDB import SessionUtilitiesKit -public struct MentionInfo: FetchableRecord, Decodable { - fileprivate static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) - fileprivate static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) - fileprivate static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) - - fileprivate static let profileString: String = CodingKeys.profile.stringValue +public struct MentionInfo: FetchableRecord, Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case profile + case threadVariant + case openGroupServer + case openGroupRoomToken + } public let profile: Profile public let threadVariant: SessionThread.Variant @@ -79,7 +81,7 @@ public extension MentionInfo { return SQLRequest(""" SELECT \(Profile.self).*, - \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) + \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")) \(targetJoin) \(targetWhere) AND \(SQL("\(profile[.id]) = \(threadId)")) @@ -89,7 +91,7 @@ public extension MentionInfo { return SQLRequest(""" SELECT \(Profile.self).*, - \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) + \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")) \(targetJoin) JOIN \(GroupMember.self) ON ( @@ -107,9 +109,9 @@ public extension MentionInfo { SELECT \(Profile.self).*, MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) - \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")), - \(openGroup[.server]) AS \(MentionInfo.openGroupServerKey), - \(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey) + \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")), + \(openGroup[.server]) AS \(MentionInfo.Columns.openGroupServer), + \(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken) \(targetJoin) JOIN \(Interaction.self) ON ( @@ -130,8 +132,8 @@ public extension MentionInfo { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - MentionInfo.profileString: adapters[0] + return ScopeAdapter.with(MentionInfo.self, [ + .profile: adapters[0] ]) } } diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index c4aeef7a6..3eaada1b1 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -11,42 +11,66 @@ fileprivate typealias AttachmentInteractionInfo = MessageViewModel.AttachmentInt fileprivate typealias ReactionInfo = MessageViewModel.ReactionInfo fileprivate typealias TypingIndicatorInfo = MessageViewModel.TypingIndicatorInfo -public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { - public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) - public static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) - public static let threadIsTrustedKey: SQL = SQL(stringLiteral: CodingKeys.threadIsTrusted.stringValue) - public static let threadHasDisappearingMessagesEnabledKey: SQL = SQL(stringLiteral: CodingKeys.threadHasDisappearingMessagesEnabled.stringValue) - public static let threadOpenGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupServer.stringValue) - public static let threadOpenGroupPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupPublicKey.stringValue) - public static let threadContactNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.threadContactNameInternal.stringValue) - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let authorNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.authorNameInternal.stringValue) - public static let stateKey: SQL = SQL(stringLiteral: CodingKeys.state.stringValue) - public static let hasAtLeastOneReadReceiptKey: SQL = SQL(stringLiteral: CodingKeys.hasAtLeastOneReadReceipt.stringValue) - public static let mostRecentFailureTextKey: SQL = SQL(stringLiteral: CodingKeys.mostRecentFailureText.stringValue) - public static let isTypingIndicatorKey: SQL = SQL(stringLiteral: CodingKeys.isTypingIndicator.stringValue) - public static let isSenderOpenGroupModeratorKey: SQL = SQL(stringLiteral: CodingKeys.isSenderOpenGroupModerator.stringValue) - public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) - public static let quoteKey: SQL = SQL(stringLiteral: CodingKeys.quote.stringValue) - public static let quoteAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.quoteAttachment.stringValue) - public static let linkPreviewKey: SQL = SQL(stringLiteral: CodingKeys.linkPreview.stringValue) - public static let linkPreviewAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.linkPreviewAttachment.stringValue) - public static let currentUserPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.currentUserPublicKey.stringValue) - public static let cellTypeKey: SQL = SQL(stringLiteral: CodingKeys.cellType.stringValue) - public static let authorNameKey: SQL = SQL(stringLiteral: CodingKeys.authorName.stringValue) - public static let canHaveProfileKey: SQL = SQL(stringLiteral: CodingKeys.canHaveProfile.stringValue) - public static let shouldShowProfileKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowProfile.stringValue) - public static let shouldShowDateHeaderKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowDateHeader.stringValue) - public static let positionInClusterKey: SQL = SQL(stringLiteral: CodingKeys.positionInCluster.stringValue) - public static let isOnlyMessageInClusterKey: SQL = SQL(stringLiteral: CodingKeys.isOnlyMessageInCluster.stringValue) - public static let isLastKey: SQL = SQL(stringLiteral: CodingKeys.isLast.stringValue) - public static let isLastOutgoingKey: SQL = SQL(stringLiteral: CodingKeys.isLastOutgoing.stringValue) - - public static let profileString: String = CodingKeys.profile.stringValue - public static let quoteString: String = CodingKeys.quote.stringValue - public static let quoteAttachmentString: String = CodingKeys.quoteAttachment.stringValue - public static let linkPreviewString: String = CodingKeys.linkPreview.stringValue - public static let linkPreviewAttachmentString: String = CodingKeys.linkPreviewAttachment.stringValue +public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case threadId + case threadVariant + case threadIsTrusted + case threadHasDisappearingMessagesEnabled + case threadOpenGroupServer + case threadOpenGroupPublicKey + case threadContactNameInternal + + // Interaction Info + + case rowId + case id + case openGroupServerMessageId + case variant + case timestampMs + case receivedAtTimestampMs + case authorId + case authorNameInternal + case body + case rawBody + case expiresStartedAtMs + case expiresInSeconds + + case state + case hasAtLeastOneReadReceipt + case mostRecentFailureText + case isSenderOpenGroupModerator + case isTypingIndicator + case profile + case quote + case quoteAttachment + case linkPreview + case linkPreviewAttachment + + case currentUserPublicKey + + // Post-Query Processing Data + + case attachments + case reactionInfo + case cellType + case authorName + case senderName + case canHaveProfile + case shouldShowProfile + case shouldShowDateHeader + case containsOnlyEmoji + case glyphCount + case previousVariant + case positionInCluster + case isOnlyMessageInCluster + case isLast + case isLastOutgoing + case currentUserBlinded15PublicKey + case currentUserBlinded25PublicKey + case optimisticMessageId + } public enum CellType: Int, Decodable, Equatable, Hashable, DatabaseValueConvertible { case textOnlyMessage @@ -462,13 +486,13 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, // MARK: - AttachmentInteractionInfo public extension MessageViewModel { - struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) - public static let interactionAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachment.stringValue) - - public static let attachmentString: String = CodingKeys.attachment.stringValue - public static let interactionAttachmentString: String = CodingKeys.interactionAttachment.stringValue + struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case attachment + case interactionAttachment + } public let rowId: Int64 public let attachment: Attachment @@ -491,13 +515,13 @@ public extension MessageViewModel { // MARK: - ReactionInfo public extension MessageViewModel { - struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let reactionKey: SQL = SQL(stringLiteral: CodingKeys.reaction.stringValue) - public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) - - public static let reactionString: String = CodingKeys.reaction.stringValue - public static let profileString: String = CodingKeys.profile.stringValue + struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case reaction + case profile + } public let rowId: Int64 public let reaction: Reaction @@ -522,9 +546,12 @@ public extension MessageViewModel { // MARK: - TypingIndicatorInfo public extension MessageViewModel { - struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) + struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case threadId + } public let rowId: Int64 public let threadId: String @@ -776,60 +803,48 @@ public extension MessageViewModel { let contact: TypedTableAlias = TypedTableAlias() let disappearingMessagesConfig: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() + let threadProfile: TypedTableAlias = TypedTableAlias(name: "threadProfile") let quote: TypedTableAlias = TypedTableAlias() let quoteInteraction: TypedTableAlias = TypedTableAlias(name: "quoteInteraction") + let quoteInteractionAttachment: TypedTableAlias = TypedTableAlias( + name: "quoteInteractionAttachment" + ) let quoteLinkPreview: TypedTableAlias = TypedTableAlias(name: "quoteLinkPreview") + let quoteAttachment: TypedTableAlias = TypedTableAlias(name: ViewModel.CodingKeys.quoteAttachment.stringValue) let linkPreview: TypedTableAlias = TypedTableAlias() - - let threadProfile: SQL = SQL(stringLiteral: "threadProfile") - let quoteInteractionAttachment: SQL = SQL(stringLiteral: "quoteInteractionAttachment") - let readReceipt: SQL = SQL(stringLiteral: "readReceipt") - let idColumn: SQL = SQL(stringLiteral: Interaction.Columns.id.name) - let interactionBodyColumn: SQL = SQL(stringLiteral: Interaction.Columns.body.name) - let profileIdColumn: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let nicknameColumn: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let nameColumn: SQL = SQL(stringLiteral: Profile.Columns.name.name) - let quoteBodyColumn: SQL = SQL(stringLiteral: Quote.Columns.body.name) - let quoteAttachmentIdColumn: SQL = SQL(stringLiteral: Quote.Columns.attachmentId.name) - let readReceiptInteractionIdColumn: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) - let readTimestampMsColumn: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) - let timestampMsColumn: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) - let authorIdColumn: SQL = SQL(stringLiteral: Interaction.Columns.authorId.name) - let attachmentIdColumn: SQL = SQL(stringLiteral: Attachment.Columns.id.name) - let interactionAttachmentInteractionIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) - let interactionAttachmentAttachmentIdColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) - let interactionAttachmentAlbumIndexColumn: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) + let linkPreviewAttachment: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .linkPreviewAttachment) + let readReceipt: TypedTableAlias = TypedTableAlias(name: "readReceipt") let numColumnsBeforeLinkedRecords: Int = 22 let finalGroupSQL: SQL = (groupSQL ?? "") let request: SQLRequest = """ SELECT - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), -- Default to 'true' for non-contact threads - IFNULL(\(contact[.isTrusted]), true) AS \(ViewModel.threadIsTrustedKey), + IFNULL(\(contact[.isTrusted]), true) AS \(ViewModel.Columns.threadIsTrusted), -- Default to 'false' when no contact exists - IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey), - \(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey), - \(openGroup[.publicKey]) AS \(ViewModel.threadOpenGroupPublicKeyKey), - IFNULL(\(threadProfile).\(nicknameColumn), \(threadProfile).\(nameColumn)) AS \(ViewModel.threadContactNameInternalKey), + IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.Columns.threadHasDisappearingMessagesEnabled), + \(openGroup[.server]) AS \(ViewModel.Columns.threadOpenGroupServer), + \(openGroup[.publicKey]) AS \(ViewModel.Columns.threadOpenGroupPublicKey), + IFNULL(\(threadProfile[.nickname]), \(threadProfile[.name])) AS \(ViewModel.Columns.threadContactNameInternal), - \(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey), + \(interaction[.rowId]) AS \(ViewModel.Columns.rowId), \(interaction[.id]), \(interaction[.openGroupServerMessageId]), \(interaction[.variant]), \(interaction[.timestampMs]), \(interaction[.receivedAtTimestampMs]), \(interaction[.authorId]), - IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), + IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal), \(interaction[.body]), \(interaction[.expiresStartedAtMs]), \(interaction[.expiresInSeconds]), -- Default to 'sending' assuming non-processed interaction when null - IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey), - (\(readReceipt).\(readTimestampMsColumn) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey), - \(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey), + IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.Columns.state), + (\(readReceipt[.readTimestampMs]) IS NOT NULL) AS \(ViewModel.Columns.hasAtLeastOneReadReceipt), + \(recipientState[.mostRecentFailureText]) AS \(ViewModel.Columns.mostRecentFailureText), EXISTS ( SELECT 1 @@ -840,36 +855,36 @@ public extension MessageViewModel { \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND \(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])")) ) - ) AS \(ViewModel.isSenderOpenGroupModeratorKey), + ) AS \(ViewModel.Columns.isSenderOpenGroupModerator), - \(ViewModel.profileKey).*, + \(profile.allColumns), \(quote[.interactionId]), \(quote[.authorId]), \(quote[.timestampMs]), \(quoteInteraction[.body]), - \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn), - \(ViewModel.quoteAttachmentKey).*, - \(ViewModel.linkPreviewKey).*, - \(ViewModel.linkPreviewAttachmentKey).*, + \(quoteInteractionAttachment[.attachmentId]), + \(quoteAttachment.allColumns), + \(linkPreview.allColumns), + \(linkPreviewAttachment.allColumns), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey), + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey), -- All of the below properties are set in post-query processing but to prevent the -- query from crashing when decoding we need to provide default values - \(CellType.textOnlyMessage) AS \(ViewModel.cellTypeKey), - '' AS \(ViewModel.authorNameKey), - false AS \(ViewModel.canHaveProfileKey), - false AS \(ViewModel.shouldShowProfileKey), - false AS \(ViewModel.shouldShowDateHeaderKey), - \(Position.middle) AS \(ViewModel.positionInClusterKey), - false AS \(ViewModel.isOnlyMessageInClusterKey), - false AS \(ViewModel.isLastKey), - false AS \(ViewModel.isLastOutgoingKey) + \(CellType.textOnlyMessage) AS \(ViewModel.Columns.cellType), + '' AS \(ViewModel.Columns.authorName), + false AS \(ViewModel.Columns.canHaveProfile), + false AS \(ViewModel.Columns.shouldShowProfile), + false AS \(ViewModel.Columns.shouldShowDateHeader), + \(Position.middle) AS \(ViewModel.Columns.positionInCluster), + false AS \(ViewModel.Columns.isOnlyMessageInCluster), + false AS \(ViewModel.Columns.isLast), + false AS \(ViewModel.Columns.isLastOutgoing) FROM \(Interaction.self) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(interaction[.threadId]) - LEFT JOIN \(Profile.self) AS \(threadProfile) ON \(threadProfile).\(profileIdColumn) = \(interaction[.threadId]) + LEFT JOIN \(threadProfile) ON \(threadProfile[.id]) = \(interaction[.threadId]) LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) @@ -887,9 +902,9 @@ public extension MessageViewModel { ) ) ) - LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON ( - \(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction[.id]) AND - \(quoteInteractionAttachment).\(interactionAttachmentAlbumIndexColumn) = 0 + LEFT JOIN \(quoteInteractionAttachment) ON ( + \(quoteInteractionAttachment[.interactionId]) = \(quoteInteraction[.id]) AND + \(quoteInteractionAttachment[.albumIndex]) = 0 ) LEFT JOIN \(quoteLinkPreview) ON ( \(quoteLinkPreview[.url]) = \(quoteInteraction[.linkPreviewUrl]) AND @@ -898,27 +913,27 @@ public extension MessageViewModel { linkPreview: quoteLinkPreview )) ) - LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON ( - \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) OR - \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteLinkPreview[.attachmentId]) OR - \(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quote[.attachmentId]) + LEFT JOIN \(quoteAttachment) ON ( + \(quoteAttachment[.id]) = \(quoteInteractionAttachment[.attachmentId]) OR + \(quoteAttachment[.id]) = \(quoteLinkPreview[.attachmentId]) OR + \(quoteAttachment[.id]) = \(quote[.attachmentId]) ) LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND \(Interaction.linkPreviewFilterLiteral()) ) - LEFT JOIN \(Attachment.self) AS \(ViewModel.linkPreviewAttachmentKey) ON \(ViewModel.linkPreviewAttachmentKey).\(attachmentIdColumn) = \(linkPreview[.attachmentId]) + LEFT JOIN \(linkPreviewAttachment) ON \(linkPreviewAttachment[.id]) = \(linkPreview[.attachmentId]) LEFT JOIN \(RecipientState.self) ON ( -- Ignore 'skipped' states \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND \(recipientState[.interactionId]) = \(interaction[.id]) ) - LEFT JOIN \(RecipientState.self) AS \(readReceipt) ON ( - \(readReceipt).\(readTimestampMsColumn) IS NOT NULL AND - \(readReceipt).\(readReceiptInteractionIdColumn) = \(interaction[.id]) + LEFT JOIN \(readReceipt) ON ( + \(readReceipt[.readTimestampMs]) IS NOT NULL AND + \(readReceipt[.interactionId]) = \(interaction[.id]) ) - WHERE \(interaction.alias[Column.rowID]) IN \(rowIds) + WHERE \(interaction[.rowId]) IN \(rowIds) \(finalGroupSQL) ORDER BY \(orderSQL) """ @@ -933,12 +948,12 @@ public extension MessageViewModel { Attachment.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.profileString: adapters[1], - ViewModel.quoteString: adapters[2], - ViewModel.quoteAttachmentString: adapters[3], - ViewModel.linkPreviewString: adapters[4], - ViewModel.linkPreviewAttachmentString: adapters[5] + return ScopeAdapter.with(ViewModel.self, [ + .profile: adapters[1], + .quote: adapters[2], + .quoteAttachment: adapters[3], + .linkPreview: adapters[4], + .linkPreviewAttachment: adapters[5] ]) } } @@ -965,9 +980,9 @@ public extension MessageViewModel.AttachmentInteractionInfo { let numColumnsBeforeLinkedRecords: Int = 1 let request: SQLRequest = """ SELECT - \(attachment.alias[Column.rowID]) AS \(AttachmentInteractionInfo.rowIdKey), - \(AttachmentInteractionInfo.attachmentKey).*, - \(AttachmentInteractionInfo.interactionAttachmentKey).* + \(attachment[.rowId]) AS \(AttachmentInteractionInfo.Columns.rowId), + \(attachment.allColumns), + \(interactionAttachment.allColumns) FROM \(Attachment.self) JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id]) \(finalFilterSQL) @@ -980,9 +995,9 @@ public extension MessageViewModel.AttachmentInteractionInfo { InteractionAttachment.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - AttachmentInteractionInfo.attachmentString: adapters[1], - AttachmentInteractionInfo.interactionAttachmentString: adapters[2] + return ScopeAdapter.with(AttachmentInteractionInfo.self, [ + .attachment: adapters[1], + .interactionAttachment: adapters[2] ]) } } @@ -1046,9 +1061,9 @@ public extension MessageViewModel.ReactionInfo { let numColumnsBeforeLinkedRecords: Int = 1 let request: SQLRequest = """ SELECT - \(reaction.alias[Column.rowID]) AS \(ReactionInfo.rowIdKey), - \(ReactionInfo.reactionKey).*, - \(ReactionInfo.profileKey).* + \(reaction[.rowId]) AS \(ReactionInfo.Columns.rowId), + \(reaction.allColumns), + \(profile.allColumns) FROM \(Reaction.self) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(reaction[.authorId]) \(finalFilterSQL) @@ -1061,9 +1076,9 @@ public extension MessageViewModel.ReactionInfo { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ReactionInfo.reactionString: adapters[1], - ReactionInfo.profileString: adapters[2] + return ScopeAdapter.with(ReactionInfo.self, [ + .reaction: adapters[1], + .profile: adapters[2] ]) } } @@ -1129,8 +1144,8 @@ public extension MessageViewModel.TypingIndicatorInfo { }() let request: SQLRequest = """ SELECT - \(threadTypingIndicator.alias[Column.rowID]) AS \(MessageViewModel.TypingIndicatorInfo.rowIdKey), - \(threadTypingIndicator[.threadId]) AS \(MessageViewModel.TypingIndicatorInfo.threadIdKey) + \(threadTypingIndicator[.rowId]), + \(threadTypingIndicator[.threadId]) FROM \(ThreadTypingIndicator.self) \(finalFilterSQL) """ diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index de2548745..87a09f337 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -14,65 +14,70 @@ fileprivate typealias ViewModel = SessionThreadViewModel /// /// **Note:** When updating the UI make sure to check the actual queries being run as some fields will have incorrect default values /// in order to optimise their queries to only include the required data -public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { - public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) - public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) - public static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) - public static let threadCreationDateTimestampKey: SQL = SQL(stringLiteral: CodingKeys.threadCreationDateTimestamp.stringValue) - public static let threadMemberNamesKey: SQL = SQL(stringLiteral: CodingKeys.threadMemberNames.stringValue) - public static let threadIsNoteToSelfKey: SQL = SQL(stringLiteral: CodingKeys.threadIsNoteToSelf.stringValue) - public static let threadIsMessageRequestKey: SQL = SQL(stringLiteral: CodingKeys.threadIsMessageRequest.stringValue) - public static let threadRequiresApprovalKey: SQL = SQL(stringLiteral: CodingKeys.threadRequiresApproval.stringValue) - public static let threadShouldBeVisibleKey: SQL = SQL(stringLiteral: CodingKeys.threadShouldBeVisible.stringValue) - public static let threadPinnedPriorityKey: SQL = SQL(stringLiteral: CodingKeys.threadPinnedPriority.stringValue) - public static let threadIsBlockedKey: SQL = SQL(stringLiteral: CodingKeys.threadIsBlocked.stringValue) - public static let threadMutedUntilTimestampKey: SQL = SQL(stringLiteral: CodingKeys.threadMutedUntilTimestamp.stringValue) - public static let threadOnlyNotifyForMentionsKey: SQL = SQL(stringLiteral: CodingKeys.threadOnlyNotifyForMentions.stringValue) - public static let threadMessageDraftKey: SQL = SQL(stringLiteral: CodingKeys.threadMessageDraft.stringValue) - public static let threadContactIsTypingKey: SQL = SQL(stringLiteral: CodingKeys.threadContactIsTyping.stringValue) - public static let threadWasMarkedUnreadKey: SQL = SQL(stringLiteral: CodingKeys.threadWasMarkedUnread.stringValue) - public static let threadUnreadCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadCount.stringValue) - public static let threadUnreadMentionCountKey: SQL = SQL(stringLiteral: CodingKeys.threadUnreadMentionCount.stringValue) - public static let disappearingMessagesConfigurationKey: SQL = SQL(stringLiteral: CodingKeys.disappearingMessagesConfiguration.stringValue) - public static let contactProfileKey: SQL = SQL(stringLiteral: CodingKeys.contactProfile.stringValue) - public static let closedGroupNameKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupName.stringValue) - public static let closedGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupUserCount.stringValue) - public static let currentUserIsClosedGroupMemberKey: SQL = SQL(stringLiteral: CodingKeys.currentUserIsClosedGroupMember.stringValue) - public static let currentUserIsClosedGroupAdminKey: SQL = SQL(stringLiteral: CodingKeys.currentUserIsClosedGroupAdmin.stringValue) - public static let closedGroupProfileFrontKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupProfileFront.stringValue) - public static let closedGroupProfileBackKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupProfileBack.stringValue) - public static let closedGroupProfileBackFallbackKey: SQL = SQL(stringLiteral: CodingKeys.closedGroupProfileBackFallback.stringValue) - public static let openGroupNameKey: SQL = SQL(stringLiteral: CodingKeys.openGroupName.stringValue) - public static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) - public static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) - public static let openGroupPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.openGroupPublicKey.stringValue) - public static let openGroupProfilePictureDataKey: SQL = SQL(stringLiteral: CodingKeys.openGroupProfilePictureData.stringValue) - public static let openGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.openGroupUserCount.stringValue) - public static let openGroupPermissionsKey: SQL = SQL(stringLiteral: CodingKeys.openGroupPermissions.stringValue) - public static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) - public static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) - public static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) - public static let interactionBodyKey: SQL = SQL(stringLiteral: CodingKeys.interactionBody.stringValue) - public static let interactionStateKey: SQL = SQL(stringLiteral: CodingKeys.interactionState.stringValue) - public static let interactionHasAtLeastOneReadReceiptKey: SQL = SQL(stringLiteral: CodingKeys.interactionHasAtLeastOneReadReceipt.stringValue) - public static let interactionIsOpenGroupInvitationKey: SQL = SQL(stringLiteral: CodingKeys.interactionIsOpenGroupInvitation.stringValue) - public static let interactionAttachmentDescriptionInfoKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachmentDescriptionInfo.stringValue) - public static let interactionAttachmentCountKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachmentCount.stringValue) - public static let threadContactNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.threadContactNameInternal.stringValue) - public static let authorNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.authorNameInternal.stringValue) - public static let currentUserPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.currentUserPublicKey.stringValue) - - public static let threadWasMarkedUnreadString: String = CodingKeys.threadWasMarkedUnread.stringValue - public static let threadUnreadCountString: String = CodingKeys.threadUnreadCount.stringValue - public static let threadUnreadMentionCountString: String = CodingKeys.threadUnreadMentionCount.stringValue - public static let closedGroupUserCountString: String = CodingKeys.closedGroupUserCount.stringValue - public static let openGroupUserCountString: String = CodingKeys.openGroupUserCount.stringValue - public static let disappearingMessagesConfigurationString: String = CodingKeys.disappearingMessagesConfiguration.stringValue - public static let contactProfileString: String = CodingKeys.contactProfile.stringValue - public static let closedGroupProfileFrontString: String = CodingKeys.closedGroupProfileFront.stringValue - public static let closedGroupProfileBackString: String = CodingKeys.closedGroupProfileBack.stringValue - public static let closedGroupProfileBackFallbackString: String = CodingKeys.closedGroupProfileBackFallback.stringValue - public static let interactionAttachmentDescriptionInfoString: String = CodingKeys.interactionAttachmentDescriptionInfo.stringValue +public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case rowId + case threadId + case threadVariant + case threadCreationDateTimestamp + case threadMemberNames + + case threadIsNoteToSelf + case threadIsMessageRequest + case threadRequiresApproval + case threadShouldBeVisible + case threadPinnedPriority + case threadIsBlocked + case threadMutedUntilTimestamp + case threadOnlyNotifyForMentions + case threadMessageDraft + + case threadContactIsTyping + case threadWasMarkedUnread + case threadUnreadCount + case threadUnreadMentionCount + + // Thread display info + + case disappearingMessagesConfiguration + + case contactProfile + case closedGroupProfileFront + case closedGroupProfileBack + case closedGroupProfileBackFallback + case closedGroupName + case closedGroupUserCount + case currentUserIsClosedGroupMember + case currentUserIsClosedGroupAdmin + case openGroupName + case openGroupServer + case openGroupRoomToken + case openGroupPublicKey + case openGroupProfilePictureData + case openGroupUserCount + case openGroupPermissions + + // Interaction display info + + case interactionId + case interactionVariant + case interactionTimestampMs + case interactionBody + case interactionState + case interactionHasAtLeastOneReadReceipt + case interactionIsOpenGroupInvitation + case interactionAttachmentDescriptionInfo + case interactionAttachmentCount + + case authorId + case threadContactNameInternal + case authorNameInternal + case currentUserPublicKey + case currentUserBlinded15PublicKey + case currentUserBlinded25PublicKey + case recentReactionEmoji + } public var differenceIdentifier: String { threadId } public var id: String { threadId } @@ -554,6 +559,51 @@ public extension SessionThreadViewModel { } } +// MARK: - AggregateInteraction + +private struct AggregateInteraction: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case interactionId + case threadId + case interactionTimestampMs + case threadUnreadCount + case threadUnreadMentionCount + } + + let interactionId: Int64 + let threadId: String + let interactionTimestampMs: Int64 + let threadUnreadCount: UInt? + let threadUnreadMentionCount: UInt? +} + +// MARK: - ClosedGroupUserCount + +private struct ClosedGroupUserCount: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case groupId + case closedGroupUserCount + } + + let groupId: String + let closedGroupUserCount: Int +} + +// MARK: - GroupMemberInfo + +private struct GroupMemberInfo: Decodable, ColumnExpressible { + public typealias Columns = CodingKeys + public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { + case groupId + case threadMemberNames + } + + let groupId: String + let threadMemberNames: String +} + // MARK: - HomeVC & MessageRequestsViewController // MARK: --SessionThreadViewModel @@ -570,65 +620,57 @@ public extension SessionThreadViewModel { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() let typingIndicator: TypedTableAlias = TypedTableAlias() - let closedGroup: TypedTableAlias = TypedTableAlias() - let groupMember: TypedTableAlias = TypedTableAlias() - let openGroup: TypedTableAlias = TypedTableAlias() + let aggregateInteraction: TypedTableAlias = TypedTableAlias(name: "aggregateInteraction") let interaction: TypedTableAlias = TypedTableAlias() let recipientState: TypedTableAlias = TypedTableAlias() + let readReceipt: TypedTableAlias = TypedTableAlias(name: "readReceipt") let linkPreview: TypedTableAlias = TypedTableAlias() + let firstInteractionAttachment: TypedTableAlias = TypedTableAlias(name: "firstInteractionAttachment") let attachment: TypedTableAlias = TypedTableAlias() let interactionAttachment: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() - - let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") - let timestampMsColumnLiteral: SQL = SQL(stringLiteral: Interaction.Columns.timestampMs.name) - let interactionStateInteractionIdColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.interactionId.name) - let readReceiptTableLiteral: SQL = SQL(stringLiteral: "readReceipt") - let readReceiptReadTimestampMsColumnLiteral: SQL = SQL(stringLiteral: RecipientState.Columns.readTimestampMs.name) - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) - let firstInteractionAttachmentLiteral: SQL = SQL(stringLiteral: "firstInteractionAttachment") - let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) - let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) - let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) - + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) + let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) + let groupMember: TypedTableAlias = TypedTableAlias() + let openGroup: TypedTableAlias = TypedTableAlias() /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 14 let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), - \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), - \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), + \(thread[.mutedUntilTimestamp]) AS \(ViewModel.Columns.threadMutedUntilTimestamp), + \(thread[.onlyNotifyForMentions]) AS \(ViewModel.Columns.threadOnlyNotifyForMentions), ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND \(SQL("\(thread[.id]) != \(userPublicKey)")) AND IFNULL(\(contact[.isApproved]), false) = false - ) AS \(ViewModel.threadIsMessageRequestKey), + ) AS \(ViewModel.Columns.threadIsMessageRequest), - (\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.threadContactIsTypingKey), - \(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey), - \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey), - \(aggregateInteractionLiteral).\(ViewModel.threadUnreadMentionCountKey), + (\(typingIndicator[.threadId]) IS NOT NULL) AS \(ViewModel.Columns.threadContactIsTyping), + \(thread[.markedAsUnread]) AS \(ViewModel.Columns.threadWasMarkedUnread), + \(aggregateInteraction[.threadUnreadCount]), + \(aggregateInteraction[.threadUnreadMentionCount]), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), EXISTS ( SELECT 1 @@ -638,7 +680,7 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), EXISTS ( SELECT 1 @@ -648,15 +690,15 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupAdmin), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.variant]) AS \(ViewModel.interactionVariantKey), - \(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey), - \(interaction[.body]) AS \(ViewModel.interactionBodyKey), + \(interaction[.id]) AS \(ViewModel.Columns.interactionId), + \(interaction[.variant]) AS \(ViewModel.Columns.interactionVariant), + \(interaction[.timestampMs]) AS \(ViewModel.Columns.interactionTimestampMs), + \(interaction[.body]) AS \(ViewModel.Columns.interactionBody), -- Default to 'sending' assuming non-processed interaction when null IFNULL(( @@ -668,22 +710,22 @@ public extension SessionThreadViewModel { \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) ) LIMIT 1 - ), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.interactionStateKey), + ), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.Columns.interactionState), - (\(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL) AS \(ViewModel.interactionHasAtLeastOneReadReceiptKey), - (\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.interactionIsOpenGroupInvitationKey), + (\(readReceipt[.readTimestampMs]) IS NOT NULL) AS \(ViewModel.Columns.interactionHasAtLeastOneReadReceipt), + (\(linkPreview[.url]) IS NOT NULL) AS \(ViewModel.Columns.interactionIsOpenGroupInvitation), -- These 4 properties will be combined into 'Attachment.DescriptionInfo' \(attachment[.id]), \(attachment[.variant]), \(attachment[.contentType]), \(attachment[.sourceFilename]), - COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.interactionAttachmentCountKey), + COUNT(\(interactionAttachment[.interactionId])) AS \(ViewModel.Columns.interactionAttachmentCount), \(interaction[.authorId]), - IFNULL(\(ViewModel.contactProfileKey).\(profileNicknameColumnLiteral), \(ViewModel.contactProfileKey).\(profileNameColumnLiteral)) AS \(ViewModel.threadContactNameInternalKey), - IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + IFNULL(\(contactProfile[.nickname]), \(contactProfile[.name])) AS \(ViewModel.Columns.threadContactNameInternal), + IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal), + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) @@ -691,46 +733,46 @@ public extension SessionThreadViewModel { LEFT JOIN ( SELECT - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]) AS \(ViewModel.threadIdKey), - MAX(\(interaction[.timestampMs])) AS \(timestampMsColumnLiteral), - SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey), - SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(ViewModel.threadUnreadMentionCountKey) + \(interaction[.id]) AS \(AggregateInteraction.Columns.interactionId), + \(interaction[.threadId]) AS \(AggregateInteraction.Columns.threadId), + MAX(\(interaction[.timestampMs])) AS \(AggregateInteraction.Columns.interactionTimestampMs), + SUM(\(interaction[.wasRead]) = false) AS \(AggregateInteraction.Columns.threadUnreadCount), + SUM(\(interaction[.wasRead]) = false AND \(interaction[.hasMention]) = true) AS \(AggregateInteraction.Columns.threadUnreadMentionCount) FROM \(Interaction.self) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) GROUP BY \(interaction[.threadId]) - ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + ) AS \(aggregateInteraction) ON \(aggregateInteraction[.threadId]) = \(thread[.id]) LEFT JOIN \(Interaction.self) ON ( \(interaction[.threadId]) = \(thread[.id]) AND - \(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey) + \(interaction[.id]) = \(aggregateInteraction[.interactionId]) ) - LEFT JOIN \(RecipientState.self) AS \(readReceiptTableLiteral) ON ( - \(interaction[.id]) = \(readReceiptTableLiteral).\(interactionStateInteractionIdColumnLiteral) AND - \(readReceiptTableLiteral).\(readReceiptReadTimestampMsColumnLiteral) IS NOT NULL + LEFT JOIN \(readReceipt) ON ( + \(interaction[.id]) = \(readReceipt[.interactionId]) AND + \(readReceipt[.readTimestampMs]) IS NOT NULL ) LEFT JOIN \(LinkPreview.self) ON ( \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND \(Interaction.linkPreviewFilterLiteral()) AND \(SQL("\(linkPreview[.variant]) = \(LinkPreview.Variant.openGroupInvitation)")) ) - LEFT JOIN \(InteractionAttachment.self) AS \(firstInteractionAttachmentLiteral) ON ( - \(firstInteractionAttachmentLiteral).\(interactionAttachmentInteractionIdColumnLiteral) = \(interaction[.id]) AND - \(firstInteractionAttachmentLiteral).\(interactionAttachmentAlbumIndexColumnLiteral) = 0 + LEFT JOIN \(firstInteractionAttachment) ON ( + \(firstInteractionAttachment[.interactionId]) = \(interaction[.id]) AND + \(firstInteractionAttachment[.albumIndex]) = 0 ) - LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachmentLiteral).\(interactionAttachmentAttachmentIdColumnLiteral) + LEFT JOIN \(Attachment.self) ON \(attachment[.id]) = \(firstInteractionAttachment[.attachmentId]) LEFT JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.interactionId]) = \(interaction[.id]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) -- Thread naming & avatar content - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -741,9 +783,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -754,13 +796,13 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(SQL("\(userPublicKey)")) ) - WHERE \(thread.alias[Column.rowID]) IN \(rowIds) + WHERE \(thread[.rowId]) IN \(rowIds) \(groupSQL) ORDER BY \(orderSQL) """ @@ -776,12 +818,12 @@ public extension SessionThreadViewModel { Attachment.DescriptionInfo.numberOfSelectedColumns() ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4], - ViewModel.interactionAttachmentDescriptionInfoString: adapters[6] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4], + .interactionAttachmentDescriptionInfo: adapters[6] ]) } } @@ -868,55 +910,52 @@ public extension SessionThreadViewModel { let thread: TypedTableAlias = TypedTableAlias() let disappearingMessagesConfiguration: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() + let aggregateInteraction: TypedTableAlias = TypedTableAlias(name: "aggregateInteraction") let interaction: TypedTableAlias = TypedTableAlias() - - let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") - let closedGroupUserCountTableLiteral: SQL = SQL(stringLiteral: "\(ViewModel.closedGroupUserCountString)_table") - let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) + let closedGroupUserCount: TypedTableAlias = TypedTableAlias(name: "closedGroupUserCount") /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `disappearingMessageSConfiguration` entry below otherwise the query will fail to parse and might throw /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 15 let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND \(SQL("\(thread[.id]) != \(userPublicKey)")) AND IFNULL(\(contact[.isApproved]), false) = false - ) AS \(ViewModel.threadIsMessageRequestKey), + ) AS \(ViewModel.Columns.threadIsMessageRequest), ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND IFNULL(\(contact[.didApproveMe]), false) = false - ) AS \(ViewModel.threadRequiresApprovalKey), - \(thread[.shouldBeVisible]) AS \(ViewModel.threadShouldBeVisibleKey), + ) AS \(ViewModel.Columns.threadRequiresApproval), + \(thread[.shouldBeVisible]) AS \(ViewModel.Columns.threadShouldBeVisible), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), - \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), - \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), - \(thread[.messageDraft]) AS \(ViewModel.threadMessageDraftKey), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), + \(thread[.mutedUntilTimestamp]) AS \(ViewModel.Columns.threadMutedUntilTimestamp), + \(thread[.onlyNotifyForMentions]) AS \(ViewModel.Columns.threadOnlyNotifyForMentions), + \(thread[.messageDraft]) AS \(ViewModel.Columns.threadMessageDraft), - \(thread[.markedAsUnread]) AS \(ViewModel.threadWasMarkedUnreadKey), - \(aggregateInteractionLiteral).\(ViewModel.threadUnreadCountKey), + \(thread[.markedAsUnread]) AS \(ViewModel.Columns.threadWasMarkedUnread), + \(aggregateInteraction[.threadUnreadCount]), - \(ViewModel.disappearingMessagesConfigurationKey).*, + \(disappearingMessagesConfiguration.allColumns), - \(ViewModel.contactProfileKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(closedGroupUserCountTableLiteral).\(ViewModel.closedGroupUserCountKey) AS \(ViewModel.closedGroupUserCountKey), + \(contactProfile.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(closedGroupUserCount[.closedGroupUserCount]), EXISTS ( SELECT 1 @@ -926,49 +965,50 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.server]) AS \(ViewModel.openGroupServerKey), - \(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey), - \(openGroup[.publicKey]) AS \(ViewModel.openGroupPublicKeyKey), - \(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey), - \(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.server]) AS \(ViewModel.Columns.openGroupServer), + \(openGroup[.roomToken]) AS \(ViewModel.Columns.openGroupRoomToken), + \(openGroup[.publicKey]) AS \(ViewModel.Columns.openGroupPublicKey), + \(openGroup[.userCount]) AS \(ViewModel.Columns.openGroupUserCount), + \(openGroup[.permissions]) AS \(ViewModel.Columns.openGroupPermissions), - \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey), - \(aggregateInteractionLiteral).\(ViewModel.interactionTimestampMsKey), + \(aggregateInteraction[.interactionId]), + \(aggregateInteraction[.interactionTimestampMs]), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfiguration[.threadId]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( SELECT - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]) AS \(ViewModel.threadIdKey), - MAX(\(interaction[.timestampMs])) AS \(ViewModel.interactionTimestampMsKey), - SUM(\(interaction[.wasRead]) = false) AS \(ViewModel.threadUnreadCountKey) + \(interaction[.id]) AS \(AggregateInteraction.Columns.interactionId), + \(interaction[.threadId]) AS \(AggregateInteraction.Columns.threadId), + MAX(\(interaction[.timestampMs])) AS \(AggregateInteraction.Columns.interactionTimestampMs), + SUM(\(interaction[.wasRead]) = false) AS \(AggregateInteraction.Columns.threadUnreadCount), + 0 AS \(AggregateInteraction.Columns.threadUnreadMentionCount) FROM \(Interaction.self) WHERE ( \(SQL("\(interaction[.threadId]) = \(threadId)")) AND \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) ) - ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + ) AS \(aggregateInteraction) ON \(aggregateInteraction[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) LEFT JOIN ( SELECT \(groupMember[.groupId]), - COUNT(\(groupMember.alias[Column.rowID])) AS \(ViewModel.closedGroupUserCountKey) + COUNT(\(groupMember[.rowId])) AS \(ClosedGroupUserCount.Columns.closedGroupUserCount) FROM \(GroupMember.self) WHERE ( \(SQL("\(groupMember[.groupId]) = \(threadId)")) AND \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) ) - ) AS \(closedGroupUserCountTableLiteral) ON \(SQL("\(closedGroupUserCountTableLiteral).\(groupMemberGroupIdColumnLiteral) = \(threadId)")) + ) AS \(closedGroupUserCount) ON \(SQL("\(closedGroupUserCount[.groupId]) = \(threadId)")) WHERE \(SQL("\(thread[.id]) = \(threadId)")) """ @@ -980,9 +1020,9 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.disappearingMessagesConfigurationString: adapters[1], - ViewModel.contactProfileString: adapters[2] + return ScopeAdapter.with(ViewModel.self, [ + .disappearingMessagesConfiguration: adapters[1], + .contactProfile: adapters[2] ]) } } @@ -990,39 +1030,40 @@ public extension SessionThreadViewModel { static func conversationSettingsQuery(threadId: String, userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 9 let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), - \(thread[.mutedUntilTimestamp]) AS \(ViewModel.threadMutedUntilTimestampKey), - \(thread[.onlyNotifyForMentions]) AS \(ViewModel.threadOnlyNotifyForMentionsKey), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), + \(thread[.mutedUntilTimestamp]) AS \(ViewModel.Columns.threadMutedUntilTimestamp), + \(thread[.onlyNotifyForMentions]) AS \(ViewModel.Columns.threadOnlyNotifyForMentions), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), EXISTS ( SELECT 1 @@ -1032,7 +1073,7 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupMember), EXISTS ( SELECT 1 @@ -1042,24 +1083,24 @@ public extension SessionThreadViewModel { \(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) ) - ) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + ) AS \(ViewModel.Columns.currentUserIsClosedGroupAdmin), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.server]) AS \(ViewModel.openGroupServerKey), - \(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey), - \(openGroup[.publicKey]) AS \(ViewModel.openGroupPublicKeyKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.server]) AS \(ViewModel.Columns.openGroupServer), + \(openGroup[.roomToken]) AS \(ViewModel.Columns.openGroupRoomToken), + \(openGroup[.publicKey]) AS \(ViewModel.Columns.openGroupPublicKey), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1070,9 +1111,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1083,10 +1124,10 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(SQL("\(userPublicKey)")) ) WHERE \(SQL("\(thread[.id]) = \(threadId)")) @@ -1101,11 +1142,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } @@ -1192,13 +1233,15 @@ public extension SessionThreadViewModel { static func messagesQuery(userPublicKey: String, pattern: FTS5Pattern) -> AdaptedFetchRequest> { let interaction: TypedTableAlias = TypedTableAlias() let thread: TypedTableAlias = TypedTableAlias() + let profile: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() - let profile: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let interactionLiteral: SQL = SQL(stringLiteral: Interaction.databaseTableName) - let interactionFullTextSearch: SQL = SQL(stringLiteral: Interaction.fullTextSearchTableName) + let interactionFullTextSearch: TypedTableAlias = TypedTableAlias(name: Interaction.fullTextSearchTableName) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to @@ -1208,44 +1251,44 @@ public extension SessionThreadViewModel { let numColumnsBeforeProfiles: Int = 6 let request: SQLRequest = """ SELECT - \(interaction.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(interaction[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.variant]) AS \(ViewModel.interactionVariantKey), - \(interaction[.timestampMs]) AS \(ViewModel.interactionTimestampMsKey), - snippet(\(interactionFullTextSearch), -1, '', '', '...', 6) AS \(ViewModel.interactionBodyKey), + \(interaction[.id]) AS \(ViewModel.Columns.interactionId), + \(interaction[.variant]) AS \(ViewModel.Columns.interactionVariant), + \(interaction[.timestampMs]) AS \(ViewModel.Columns.interactionTimestampMs), + snippet(\(interactionFullTextSearch), -1, '', '', '...', 6) AS \(ViewModel.Columns.interactionBody), \(interaction[.authorId]), - IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal), + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(Interaction.self) JOIN \(interactionFullTextSearch) ON ( - \(interactionFullTextSearch).rowid = \(interactionLiteral).rowid AND - \(interactionFullTextSearch).\(SQL(stringLiteral: Interaction.Columns.body.name)) MATCH \(pattern) + \(interactionFullTextSearch[.rowId]) = \(interaction[.rowId]) AND + \(interactionFullTextSearch[.body]) MATCH \(pattern) ) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(interaction[.threadId]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(interaction[.threadId]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1256,9 +1299,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1269,10 +1312,10 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(userPublicKey) ) ORDER BY \(Column.rank), \(interaction[.timestampMs].desc) @@ -1288,11 +1331,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } @@ -1315,31 +1358,26 @@ public extension SessionThreadViewModel { /// returned results will always be `-1` for those results static func contactsAndGroupsQuery(userPublicKey: String, pattern: FTS5Pattern, searchTerm: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() + let groupMemberProfile: TypedTableAlias = TypedTableAlias(name: "groupMemberProfile") let openGroup: TypedTableAlias = TypedTableAlias() + let groupMemberInfo: TypedTableAlias = TypedTableAlias(name: "groupMemberInfo") let profile: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - let profileNicknameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.nickname.name) - let profileNameColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.name.name) + let profileFullTextSearch: TypedTableAlias = TypedTableAlias(name: Profile.fullTextSearchTableName) + let closedGroupFullTextSearch: TypedTableAlias = TypedTableAlias(name: ClosedGroup.fullTextSearchTableName) + let openGroupFullTextSearch: TypedTableAlias = TypedTableAlias(name: OpenGroup.fullTextSearchTableName) - let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName) - let closedGroupNameColumnLiteral: SQL = SQL(stringLiteral: ClosedGroup.Columns.name.name) - let closedGroupLiteral: SQL = SQL(stringLiteral: ClosedGroup.databaseTableName) - let closedGroupFullTextSearch: SQL = SQL(stringLiteral: ClosedGroup.fullTextSearchTableName) - let openGroupNameColumnLiteral: SQL = SQL(stringLiteral: OpenGroup.Columns.name.name) - let openGroupLiteral: SQL = SQL(stringLiteral: OpenGroup.databaseTableName) - let openGroupFullTextSearch: SQL = SQL(stringLiteral: OpenGroup.fullTextSearchTableName) - let groupMemberInfoLiteral: SQL = SQL(stringLiteral: "groupMemberInfo") - let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) - let groupMemberProfileLiteral: SQL = SQL(stringLiteral: "groupMemberProfile") let noteToSelfLiteral: SQL = SQL(stringLiteral: "NOTE_TO_SELF".localized().lowercased()) let searchTermLiteral: SQL = SQL(stringLiteral: searchTerm.lowercased()) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw /// /// We use `IFNULL(rank, 100)` because the custom `Note to Self` like comparison will get a null /// `rank` value which ends up as the first result, by defaulting to `100` it will always be ranked last compared @@ -1350,24 +1388,24 @@ public extension SessionThreadViewModel { SELECT IFNULL(\(Column.rank), 100) AS \(Column.rank), - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), - \(groupMemberInfoLiteral).\(ViewModel.threadMemberNamesKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), + \(groupMemberInfo[.threadMemberNames]), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) @@ -1375,18 +1413,13 @@ public extension SessionThreadViewModel { // MARK: --Contact Threads let contactQueryCommonJoinFilterGroup: SQL = """ - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN \(OpenGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(openGroup.never) + LEFT JOIN \(groupMemberInfo.never) WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.contact)")) AND @@ -1398,8 +1431,8 @@ public extension SessionThreadViewModel { sqlQuery += selectQuery sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += contactQueryCommonJoinFilterGroup @@ -1413,8 +1446,8 @@ public extension SessionThreadViewModel { sqlQuery += selectQuery sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += contactQueryCommonJoinFilterGroup @@ -1429,14 +1462,14 @@ public extension SessionThreadViewModel { LEFT JOIN ( SELECT \(groupMember[.groupId]), - GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(ViewModel.threadMemberNamesKey) + GROUP_CONCAT(IFNULL(\(profile[.nickname]), \(profile[.name])), ', ') AS \(GroupMemberInfo.Columns.threadMemberNames) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) WHERE \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) GROUP BY \(groupMember[.groupId]) - ) AS \(groupMemberInfoLiteral) ON \(groupMemberInfoLiteral).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + ) AS \(groupMemberInfo) ON \(groupMemberInfo[.groupId]) = \(closedGroup[.threadId]) + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1447,9 +1480,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1460,13 +1493,13 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(userPublicKey) + LEFT JOIN \(closedGroupProfileBackFallback) ON ( + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(userPublicKey) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON false - LEFT JOIN \(OpenGroup.self) ON false + LEFT JOIN \(contactProfile.never) + LEFT JOIN \(openGroup.never) WHERE ( \(SQL("\(thread[.variant]) = \(SessionThread.Variant.legacyGroup)")) OR @@ -1484,8 +1517,8 @@ public extension SessionThreadViewModel { sqlQuery += selectQuery sqlQuery += """ JOIN \(closedGroupFullTextSearch) ON ( - \(closedGroupFullTextSearch).rowid = \(closedGroupLiteral).rowid AND - \(closedGroupFullTextSearch).\(closedGroupNameColumnLiteral) MATCH \(pattern) + \(closedGroupFullTextSearch[.rowId]) = \(closedGroup[.rowId]) AND + \(closedGroupFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += closedGroupQueryCommonJoinFilterGroup @@ -1498,10 +1531,10 @@ public extension SessionThreadViewModel { """ sqlQuery += selectQuery sqlQuery += """ - JOIN \(Profile.self) AS \(groupMemberProfileLiteral) ON \(groupMemberProfileLiteral).\(profileIdColumnLiteral) = \(groupMember[.profileId]) + JOIN \(groupMemberProfile) ON \(groupMemberProfile[.id]) = \(groupMember[.profileId]) JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(groupMemberProfileLiteral).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(groupMemberProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += closedGroupQueryCommonJoinFilterGroup @@ -1514,10 +1547,10 @@ public extension SessionThreadViewModel { """ sqlQuery += selectQuery sqlQuery += """ - JOIN \(Profile.self) AS \(groupMemberProfileLiteral) ON \(groupMemberProfileLiteral).\(profileIdColumnLiteral) = \(groupMember[.profileId]) + JOIN \(groupMemberProfile) ON \(groupMemberProfile[.id]) = \(groupMember[.profileId]) JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(groupMemberProfileLiteral).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(groupMemberProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += closedGroupQueryCommonJoinFilterGroup @@ -1533,20 +1566,15 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) JOIN \(openGroupFullTextSearch) ON ( - \(openGroupFullTextSearch).rowid = \(openGroupLiteral).rowid AND - \(openGroupFullTextSearch).\(openGroupNameColumnLiteral) MATCH \(pattern) + \(openGroupFullTextSearch[.rowId]) = \(openGroup[.rowId]) AND + \(openGroupFullTextSearch[.name]) MATCH \(pattern) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + LEFT JOIN \(contactProfile.never) + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(groupMemberInfo.never) WHERE \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND @@ -1556,18 +1584,13 @@ public extension SessionThreadViewModel { // MARK: --Note to Self Thread let noteToSelfQueryCommonJoins: SQL = """ - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false - LEFT JOIN \(OpenGroup.self) ON false - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(openGroup.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(groupMemberInfo.never) """ // Note to self thread searching for 'Note to Self' (need to join an FTS table to @@ -1600,8 +1623,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += noteToSelfQueryCommonJoins @@ -1620,8 +1643,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += noteToSelfQueryCommonJoins @@ -1635,41 +1658,36 @@ public extension SessionThreadViewModel { SELECT IFNULL(\(Column.rank), 100) AS \(Column.rank), - -1 AS \(ViewModel.rowIdKey), - \(contact[.id]) AS \(ViewModel.threadIdKey), - \(SQL("\(SessionThread.Variant.contact)")) AS \(ViewModel.threadVariantKey), - 0 AS \(ViewModel.threadCreationDateTimestampKey), - \(groupMemberInfoLiteral).\(ViewModel.threadMemberNamesKey), + -1 AS \(ViewModel.Columns.rowId), + \(contact[.id]) AS \(ViewModel.Columns.threadId), + \(SQL("\(SessionThread.Variant.contact)")) AS \(ViewModel.Columns.threadVariant), + 0 AS \(ViewModel.Columns.threadCreationDateTimestamp), + \(groupMemberInfo[.threadMemberNames]), - false AS \(ViewModel.threadIsNoteToSelfKey), - -1 AS \(ViewModel.threadPinnedPriorityKey), + false AS \(ViewModel.Columns.threadIsNoteToSelf), + -1 AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(Contact.self) """ let hiddenContactQueryCommonJoins: SQL = """ - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(contact[.id]) + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(contact[.id]) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(contact[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON false - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON false - LEFT JOIN \(ClosedGroup.self) ON false - LEFT JOIN \(OpenGroup.self) ON false - LEFT JOIN ( - SELECT - \(groupMember[.groupId]), - '' AS \(ViewModel.threadMemberNamesKey) - FROM \(GroupMember.self) - ) AS \(groupMemberInfoLiteral) ON false + LEFT JOIN \(closedGroupProfileFront.never) + LEFT JOIN \(closedGroupProfileBack.never) + LEFT JOIN \(closedGroupProfileBackFallback.never) + LEFT JOIN \(closedGroup.never) + LEFT JOIN \(openGroup.never) + LEFT JOIN \(groupMemberInfo.never) WHERE \(thread[.id]) IS NULL GROUP BY \(contact[.id]) @@ -1685,8 +1703,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNicknameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.nickname]) MATCH \(pattern) ) """ sqlQuery += hiddenContactQueryCommonJoins @@ -1701,8 +1719,8 @@ public extension SessionThreadViewModel { sqlQuery += """ JOIN \(profileFullTextSearch) ON ( - \(profileFullTextSearch).rowid = \(ViewModel.contactProfileKey).rowid AND - \(profileFullTextSearch).\(profileNameColumnLiteral) MATCH \(pattern) + \(profileFullTextSearch[.rowId]) = \(contactProfile[.rowId]) AND + \(profileFullTextSearch[.name]) MATCH \(pattern) ) """ sqlQuery += hiddenContactQueryCommonJoins @@ -1717,13 +1735,13 @@ public extension SessionThreadViewModel { \(sqlQuery) ) - GROUP BY \(ViewModel.threadIdKey) + GROUP BY \(ViewModel.Columns.threadId) ORDER BY \(Column.rank), - \(ViewModel.threadIsNoteToSelfKey), - \(ViewModel.closedGroupNameKey), - \(ViewModel.openGroupNameKey), - \(ViewModel.threadIdKey) + \(ViewModel.Columns.threadIsNoteToSelf), + \(ViewModel.Columns.closedGroupName), + \(ViewModel.Columns.openGroupName), + \(ViewModel.Columns.threadId) LIMIT \(SQL("\(SessionThreadViewModel.searchResultsLimit)")) """ @@ -1752,11 +1770,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } @@ -1764,31 +1782,30 @@ public extension SessionThreadViewModel { /// This method returns only the 'Note to Self' thread in the structure of a search result conversation static func noteToSelfOnlyQuery(userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw let numColumnsBeforeProfiles: Int = 8 let request: SQLRequest = """ SELECT 100 AS \(Column.rank), - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), - '' AS \(ViewModel.threadMemberNamesKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), + '' AS \(ViewModel.Columns.threadMemberNames), - true AS \(ViewModel.threadIsNoteToSelfKey), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), + true AS \(ViewModel.Columns.threadIsNoteToSelf), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), - \(ViewModel.contactProfileKey).*, + \(contactProfile.allColumns), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) - JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) WHERE \(SQL("\(thread[.id]) = \(userPublicKey)")) """ @@ -1801,8 +1818,8 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1] ]) } } @@ -1814,67 +1831,70 @@ public extension SessionThreadViewModel { static func shareQuery(userPublicKey: String) -> AdaptedFetchRequest> { let thread: TypedTableAlias = TypedTableAlias() let contact: TypedTableAlias = TypedTableAlias() + let contactProfile: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .contactProfile) let closedGroup: TypedTableAlias = TypedTableAlias() + let closedGroupProfileFront: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileFront) + let closedGroupProfileBack: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBack) + let closedGroupProfileBackFallback: TypedTableAlias = TypedTableAlias(ViewModel.self, column: .closedGroupProfileBackFallback) let groupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() let profile: TypedTableAlias = TypedTableAlias() + let aggregateInteraction: TypedTableAlias = TypedTableAlias(name: "aggregateInteraction") let interaction: TypedTableAlias = TypedTableAlias() - let aggregateInteractionLiteral: SQL = SQL(stringLiteral: "aggregateInteraction") - let profileIdColumnLiteral: SQL = SQL(stringLiteral: Profile.Columns.id.name) - /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before - /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to - /// parse and might throw + /// the `contactProfile` entry below otherwise the query will fail to parse and might throw /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 7 let request: SQLRequest = """ SELECT - \(thread.alias[Column.rowID]) AS \(ViewModel.rowIdKey), - \(thread[.id]) AS \(ViewModel.threadIdKey), - \(thread[.variant]) AS \(ViewModel.threadVariantKey), - \(thread[.creationDateTimestamp]) AS \(ViewModel.threadCreationDateTimestampKey), + \(thread[.rowId]) AS \(ViewModel.Columns.rowId), + \(thread[.id]) AS \(ViewModel.Columns.threadId), + \(thread[.variant]) AS \(ViewModel.Columns.threadVariant), + \(thread[.creationDateTimestamp]) AS \(ViewModel.Columns.threadCreationDateTimestamp), - (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.threadIsNoteToSelfKey), + (\(SQL("\(thread[.id]) = \(userPublicKey)"))) AS \(ViewModel.Columns.threadIsNoteToSelf), - IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.threadPinnedPriorityKey), - \(contact[.isBlocked]) AS \(ViewModel.threadIsBlockedKey), + IFNULL(\(thread[.pinnedPriority]), 0) AS \(ViewModel.Columns.threadPinnedPriority), + \(contact[.isBlocked]) AS \(ViewModel.Columns.threadIsBlocked), - \(ViewModel.contactProfileKey).*, - \(ViewModel.closedGroupProfileFrontKey).*, - \(ViewModel.closedGroupProfileBackKey).*, - \(ViewModel.closedGroupProfileBackFallbackKey).*, - \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), - \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), + \(contactProfile.allColumns), + \(closedGroupProfileFront.allColumns), + \(closedGroupProfileBack.allColumns), + \(closedGroupProfileBackFallback.allColumns), + \(closedGroup[.name]) AS \(ViewModel.Columns.closedGroupName), + \(openGroup[.name]) AS \(ViewModel.Columns.openGroupName), + \(openGroup[.imageData]) AS \(ViewModel.Columns.openGroupProfilePictureData), - \(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey) + \(SQL("\(userPublicKey)")) AS \(ViewModel.Columns.currentUserPublicKey) FROM \(SessionThread.self) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN ( SELECT - \(interaction[.id]) AS \(ViewModel.interactionIdKey), - \(interaction[.threadId]) AS \(ViewModel.threadIdKey), - MAX(\(interaction[.timestampMs])) + \(interaction[.id]) AS \(AggregateInteraction.Columns.interactionId), + \(interaction[.threadId]) AS \(AggregateInteraction.Columns.threadId), + MAX(\(interaction[.timestampMs])) AS \(AggregateInteraction.Columns.interactionTimestampMs), + 0 AS \(AggregateInteraction.Columns.threadUnreadCount), + 0 AS \(AggregateInteraction.Columns.threadUnreadMentionCount) FROM \(Interaction.self) WHERE \(SQL("\(interaction[.variant]) != \(Interaction.Variant.standardIncomingDeleted)")) GROUP BY \(interaction[.threadId]) - ) AS \(aggregateInteractionLiteral) ON \(aggregateInteractionLiteral).\(ViewModel.threadIdKey) = \(thread[.id]) + ) AS \(aggregateInteraction) ON \(aggregateInteraction[.threadId]) = \(thread[.id]) LEFT JOIN \(Interaction.self) ON ( \(interaction[.threadId]) = \(thread[.id]) AND - \(interaction[.id]) = \(aggregateInteractionLiteral).\(ViewModel.interactionIdKey) + \(interaction[.id]) = \(aggregateInteraction[.interactionId]) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) + LEFT JOIN \(contactProfile) ON \(contactProfile[.id]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON ( - \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileFront) ON ( + \(closedGroupProfileFront[.id]) = ( SELECT MIN(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1885,9 +1905,9 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackKey) ON ( - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) != \(ViewModel.closedGroupProfileFrontKey).\(profileIdColumnLiteral) AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) = ( + LEFT JOIN \(closedGroupProfileBack) ON ( + \(closedGroupProfileBack[.id]) != \(closedGroupProfileFront[.id]) AND + \(closedGroupProfileBack[.id]) = ( SELECT MAX(\(groupMember[.profileId])) FROM \(GroupMember.self) JOIN \(Profile.self) ON \(profile[.id]) = \(groupMember[.profileId]) @@ -1898,10 +1918,10 @@ public extension SessionThreadViewModel { ) ) ) - LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileBackFallbackKey) ON ( + LEFT JOIN \(closedGroupProfileBackFallback) ON ( \(closedGroup[.threadId]) IS NOT NULL AND - \(ViewModel.closedGroupProfileBackKey).\(profileIdColumnLiteral) IS NULL AND - \(ViewModel.closedGroupProfileBackFallbackKey).\(profileIdColumnLiteral) = \(SQL("\(userPublicKey)")) + \(closedGroupProfileBack[.id]) IS NULL AND + \(closedGroupProfileBackFallback[.id]) = \(SQL("\(userPublicKey)")) ) WHERE ( @@ -1929,11 +1949,11 @@ public extension SessionThreadViewModel { Profile.numberOfSelectedColumns(db) ]) - return ScopeAdapter([ - ViewModel.contactProfileString: adapters[1], - ViewModel.closedGroupProfileFrontString: adapters[2], - ViewModel.closedGroupProfileBackString: adapters[3], - ViewModel.closedGroupProfileBackFallbackString: adapters[4] + return ScopeAdapter.with(ViewModel.self, [ + .contactProfile: adapters[1], + .closedGroupProfileFront: adapters[2], + .closedGroupProfileBack: adapters[3], + .closedGroupProfileBackFallback: adapters[4] ]) } } diff --git a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift index ab6ae915f..2c41643c2 100644 --- a/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift +++ b/SessionUtilitiesKit/Database/Types/PagedDatabaseObserver.swift @@ -1351,18 +1351,24 @@ public class AssociatedRecord: ErasedAssociatedRecord where T: Fet // Fetch the inserted/updated rows let additionalFilters: SQL = SQL(rowIds.contains(Column.rowID)) - let updatedItems: [T] = (try? dataQuery(additionalFilters) - .fetchAll(db)) - .defaulting(to: []) - // If the inserted/updated rows we irrelevant (eg. associated to another thread, a quote or a link - // preview) then trigger the update callback (if there were deletions) and stop here - guard !updatedItems.isEmpty else { return hasOtherChanges } - - // Process the upserted data (assume at least one value changed) - dataCache.mutate { $0 = $0.upserting(items: updatedItems) } - - return true + do { + let updatedItems: [T] = try dataQuery(additionalFilters) + .fetchAll(db) + + // If the inserted/updated rows we irrelevant (eg. associated to another thread, a quote or a link + // preview) then trigger the update callback (if there were deletions) and stop here + guard !updatedItems.isEmpty else { return hasOtherChanges } + + // Process the upserted data (assume at least one value changed) + dataCache.mutate { $0 = $0.upserting(items: updatedItems) } + + return true + } + catch { + SNLog("[PagedDatabaseObserver] Error loading associated data: \(error)") + return hasOtherChanges + } } public func clearCache(_ db: Database) { diff --git a/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift index fa91388c4..ed42d63e0 100644 --- a/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift +++ b/SessionUtilitiesKit/Database/Types/TypedTableAlias.swift @@ -3,28 +3,65 @@ import Foundation import GRDB -public class TypedTableAlias where T: TableRecord, T: ColumnExpressible { - internal let name: String - internal let tableName: String - public let alias: TableAlias +public struct TypedTableAlias { + public enum RowIdColumn { + case rowId + } - public init(name: String = T.databaseTableName) { + internal let name: String + internal let tableName: String? + internal let alias: TableAlias + + public var allColumns: SQLSelection { alias[AllColumns().sqlSelection] } + public var never: NeverJoiningTypedTableAlias { NeverJoiningTypedTableAlias(alias: self) } + + // MARK: - Initialization + + public init(name: String, tableName: String? = nil) { + self.name = name + self.tableName = tableName + self.alias = TableAlias(name: name) + } + + public init(name: String) where T: TableRecord { self.name = name self.tableName = T.databaseTableName self.alias = TableAlias(name: name) } + public init() where T: TableRecord { + self = TypedTableAlias(name: T.databaseTableName) + } + + public init(_ viewModel: VM.Type, column: VM.Columns, tableName: String?) { + self.name = column.name + self.tableName = tableName + self.alias = TableAlias(name: name) + } + + public init(_ viewModel: VM.Type, column: VM.Columns) where T: TableRecord { + self = TypedTableAlias(viewModel, column: column, tableName: T.databaseTableName) + } + + // MARK: - Functions + public subscript(_ column: T.Columns) -> SQLExpression { return alias[column.name] } - /// **Warning:** For this to work you **MUST** call the '.aliased()' method when joining or it will - /// throw when trying to decode - public func allColumns() -> SQLSelection { - return alias[AllColumns().sqlSelection] + public subscript(_ column: RowIdColumn) -> SQLSelection { + return alias[Column.rowID] } } +// MARK: - NeverJoiningTypedTableAlias + +public struct NeverJoiningTypedTableAlias { + internal let alias: TypedTableAlias +} + +// MARK: - Extensions + extension QueryInterfaceRequest { public func aliased(_ typedAlias: TypedTableAlias) -> Self { return aliased(typedAlias.alias) @@ -38,7 +75,5 @@ extension Association { } extension TableAlias { - public func allColumns() -> SQLSelection { - return self[AllColumns().sqlSelection] - } + public var allColumns: SQLSelection { self[AllColumns().sqlSelection] } } diff --git a/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift index d8918ef17..01d0c64bb 100644 --- a/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift +++ b/SessionUtilitiesKit/Database/Utilities/SQLInterpolation+Utilities.swift @@ -12,10 +12,58 @@ public extension SQLInterpolation { @_disfavoredOverload mutating func appendInterpolation(_ typedTableAlias: TypedTableAlias) { let name: String = typedTableAlias.name - let tableName: String = typedTableAlias.tableName + guard let tableName: String = typedTableAlias.tableName else { return appendLiteral(name.quotedDatabaseIdentifier) } guard name != tableName else { return appendLiteral(tableName.quotedDatabaseIdentifier) } appendLiteral("\(tableName.quotedDatabaseIdentifier) AS \(name.quotedDatabaseIdentifier)") } + + /// Appends a simple SQL query for use when we want a `LEFT JOIN` that will always fail + /// + /// // SELECT * FROM player LEFT JOIN team AS testTeam ON false + /// let player: TypedTableAlias = TypedTableAlias() + /// let testTeam: TypedTableAlias = TypedTableAlias(name: "testTeam") + /// let request: SQLRequest = "SELECT * FROM \(player) LEFT JOIN \(testTeam.never) + @_disfavoredOverload + mutating func appendInterpolation(_ neverJoiningAlias: NeverJoiningTypedTableAlias) where T: TableRecord { + guard let tableName: String = neverJoiningAlias.alias.tableName else { + appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + return + } + + appendLiteral("\(tableName.quotedDatabaseIdentifier) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + } + + /// Appends a simple SQL query for use when we want a `LEFT JOIN` that will always fail + /// + /// // SELECT * FROM player LEFT JOIN (SELECT 0 AS teamInfo.Column.A, 0 AS teamInfo.Column.B) AS teamInfo ON false + /// let player: TypedTableAlias = TypedTableAlias() + /// let teamInfo: TypedTableAlias = TypedTableAlias(name: "teamInfo") + /// let request: SQLRequest = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never) + @_disfavoredOverload + mutating func appendInterpolation(_ neverJoiningAlias: NeverJoiningTypedTableAlias) where T.Columns: CaseIterable { + appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + } + + /// Appends a simple SQL query for use when we want a `LEFT JOIN` that will always fail + /// + /// // SELECT * FROM player LEFT JOIN (SELECT 0 AS teamInfo.Column.A, 0 AS teamInfo.Column.B) AS teamInfo ON false + /// let player: TypedTableAlias = TypedTableAlias() + /// let teamInfo: TypedTableAlias = TypedTableAlias(name: "teamInfo") + /// let request: SQLRequest = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never) + @_disfavoredOverload + mutating func appendInterpolation(_ neverJoiningAlias: NeverJoiningTypedTableAlias) { + appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false") + } + + private func generateSelection(for type: T.Type) -> String where T.Columns: CaseIterable { + return T.Columns.allCases + .map { "NULL AS \($0.name)" } + .joined(separator: ", ") + } + + private func generateSelection(for type: T.Type) -> String { + return "SELECT 1" + } } diff --git a/SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift new file mode 100644 index 000000000..500131972 --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/ScopeAdapter+Utilities.swift @@ -0,0 +1,13 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public extension ScopeAdapter { + static func with( + _ viewModel: VM.Type, + _ scopes: [VM.Columns: RowAdapter] + ) -> ScopeAdapter { + return ScopeAdapter(scopes.reduce(into: [:]) { result, next in result[next.key.name] = next.value }) + } +}