Cleaned up the GRDB interface for complex queries

This commit is contained in:
Morgan Pretty 2023-08-17 16:39:47 +10:00
parent 42853a08c9
commit e6c26e7ff4
17 changed files with 877 additions and 675 deletions

View File

@ -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 = "<group>"; };
FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = "<group>"; };
FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = "<group>"; };
FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScopeAdapter+Utilities.swift"; sourceTree = "<group>"; };
FD5C72F6284F0E560029977D /* MessageReceiver+ReadReceipts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ReadReceipts.swift"; sourceTree = "<group>"; };
FD5C72F8284F0E880029977D /* MessageReceiver+TypingIndicators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+TypingIndicators.swift"; sourceTree = "<group>"; };
FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DataExtractionNotification.swift"; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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)
}
}

View File

@ -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<Item> = """
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]
])
}
}

View File

@ -258,11 +258,12 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
// MARK: - DataModel
public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue)
public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue)
public static let profileString: String = CodingKeys.profile.stringValue
public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible {
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case rowId
case profile
}
public var differenceIdentifier: String { profile.id }
public var id: String { profile.id }
@ -286,11 +287,11 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
let request: SQLRequest<DataModel> = """
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<NoNav, BlockedContactsView
Profile.numberOfSelectedColumns(db)
])
return ScopeAdapter([
DataModel.profileString: adapters[1]
return ScopeAdapter.with(DataModel.self, [
.profile: adapters[1]
])
}
}

View File

@ -95,6 +95,19 @@ public extension ClosedGroup {
}
}
// MARK: - Search Queries
public extension ClosedGroup {
struct FullTextSearch: Decodable, ColumnExpressible {
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case name
}
let name: String
}
}
// MARK: - Convenience
public extension ClosedGroup {

View File

@ -696,6 +696,17 @@ public extension Interaction {
// MARK: - Search Queries
public extension Interaction {
struct FullTextSearch: Decodable, ColumnExpressible {
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case threadId
case body
}
let threadId: String
let body: String
}
struct TimestampInfo: FetchableRecord, Codable {
public let id: Int64
public let timestampMs: Int64
@ -711,8 +722,7 @@ public extension Interaction {
static func idsForTermWithin(threadId: String, pattern: FTS5Pattern) -> SQLRequest<TimestampInfo> {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionFullTextSearch: SQL = SQL(stringLiteral: Interaction.fullTextSearchTableName)
let threadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name)
let interactionFullTextSearch: TypedTableAlias<FullTextSearch> = TypedTableAlias(name: Interaction.fullTextSearchTableName)
let request: SQLRequest<TimestampInfo> = """
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)

View File

@ -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 {

View File

@ -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 {

View File

@ -365,7 +365,7 @@ public extension SessionThread {
let contact: TypedTableAlias<Contact> = TypedTableAlias()
return """
SELECT \(thread.allColumns())
SELECT \(thread.allColumns)
FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
WHERE (

View File

@ -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])

View File

@ -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]
])
}
}

View File

@ -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<Contact> = TypedTableAlias()
let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias()
let threadProfile: TypedTableAlias<Profile> = TypedTableAlias(name: "threadProfile")
let quote: TypedTableAlias<Quote> = TypedTableAlias()
let quoteInteraction: TypedTableAlias<Interaction> = TypedTableAlias(name: "quoteInteraction")
let quoteInteractionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias(
name: "quoteInteractionAttachment"
)
let quoteLinkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias(name: "quoteLinkPreview")
let quoteAttachment: TypedTableAlias<Attachment> = TypedTableAlias(name: ViewModel.CodingKeys.quoteAttachment.stringValue)
let linkPreview: TypedTableAlias<LinkPreview> = 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<Attachment> = TypedTableAlias(ViewModel.self, column: .linkPreviewAttachment)
let readReceipt: TypedTableAlias<RecipientState> = TypedTableAlias(name: "readReceipt")
let numColumnsBeforeLinkedRecords: Int = 22
let finalGroupSQL: SQL = (groupSQL ?? "")
let request: SQLRequest<ViewModel> = """
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<AttachmentInteractionInfo> = """
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<ReactionInfo> = """
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<MessageViewModel.TypingIndicatorInfo> = """
SELECT
\(threadTypingIndicator.alias[Column.rowID]) AS \(MessageViewModel.TypingIndicatorInfo.rowIdKey),
\(threadTypingIndicator[.threadId]) AS \(MessageViewModel.TypingIndicatorInfo.threadIdKey)
\(threadTypingIndicator[.rowId]),
\(threadTypingIndicator[.threadId])
FROM \(ThreadTypingIndicator.self)
\(finalFilterSQL)
"""

View File

@ -1351,18 +1351,24 @@ public class AssociatedRecord<T, PagedType>: 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) {

View File

@ -3,28 +3,65 @@
import Foundation
import GRDB
public class TypedTableAlias<T> where T: TableRecord, T: ColumnExpressible {
internal let name: String
internal let tableName: String
public let alias: TableAlias
public struct TypedTableAlias<T: ColumnExpressible> {
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<T> { NeverJoiningTypedTableAlias<T>(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<VM: ColumnExpressible>(_ viewModel: VM.Type, column: VM.Columns, tableName: String?) {
self.name = column.name
self.tableName = tableName
self.alias = TableAlias(name: name)
}
public init<VM: ColumnExpressible>(_ 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<T: ColumnExpressible> {
internal let alias: TypedTableAlias<T>
}
// MARK: - Extensions
extension QueryInterfaceRequest {
public func aliased<T>(_ typedAlias: TypedTableAlias<T>) -> 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] }
}

View File

@ -12,10 +12,58 @@ public extension SQLInterpolation {
@_disfavoredOverload
mutating func appendInterpolation<T>(_ typedTableAlias: TypedTableAlias<T>) {
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<Player> = TypedTableAlias()
/// let testTeam: TypedTableAlias<Team> = TypedTableAlias(name: "testTeam")
/// let request: SQLRequest<Player> = "SELECT * FROM \(player) LEFT JOIN \(testTeam.never)
@_disfavoredOverload
mutating func appendInterpolation<T: ColumnExpressible>(_ neverJoiningAlias: NeverJoiningTypedTableAlias<T>) 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<Player> = TypedTableAlias()
/// let teamInfo: TypedTableAlias<TeamInfo> = TypedTableAlias(name: "teamInfo")
/// let request: SQLRequest<Player> = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never)
@_disfavoredOverload
mutating func appendInterpolation<T: ColumnExpressible>(_ neverJoiningAlias: NeverJoiningTypedTableAlias<T>) 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<Player> = TypedTableAlias()
/// let teamInfo: TypedTableAlias<TeamInfo> = TypedTableAlias(name: "teamInfo")
/// let request: SQLRequest<Player> = "SELECT * FROM \(player) LEFT JOIN \(teamInfo.never)
@_disfavoredOverload
mutating func appendInterpolation<T: ColumnExpressible>(_ neverJoiningAlias: NeverJoiningTypedTableAlias<T>) {
appendLiteral("(SELECT \(generateSelection(for: T.self))) AS \(neverJoiningAlias.alias.name.quotedDatabaseIdentifier) ON false")
}
private func generateSelection<T: ColumnExpressible>(for type: T.Type) -> String where T.Columns: CaseIterable {
return T.Columns.allCases
.map { "NULL AS \($0.name)" }
.joined(separator: ", ")
}
private func generateSelection<T: ColumnExpressible>(for type: T.Type) -> String {
return "SELECT 1"
}
}

View File

@ -0,0 +1,13 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
public extension ScopeAdapter {
static func with<VM: ColumnExpressible>(
_ viewModel: VM.Type,
_ scopes: [VM.Columns: RowAdapter]
) -> ScopeAdapter {
return ScopeAdapter(scopes.reduce(into: [:]) { result, next in result[next.key.name] = next.value })
}
}