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 */; }; FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */; };
FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.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 */; }; 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 */; }; 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 */; }; 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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; FD5C72FA284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+DataExtractionNotification.swift"; sourceTree = "<group>"; };
@ -3695,6 +3697,7 @@
FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */, FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */,
FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */, FDF2220E281B55E6000A4995 /* QueryInterfaceRequest+Utilities.swift */,
FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */, FD1A94FA2900D1C2000D73D3 /* PersistableRecord+Utilities.swift */,
FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */,
FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */, FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */,
); );
path = Utilities; path = Utilities;
@ -5681,6 +5684,7 @@
C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */,
C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */, C32C600F256E07F5003C73A2 /* NSUserDefaults+OWS.m in Sources */,
FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */, FDE658A329418E2F00A33BC1 /* KeyPair.swift in Sources */,
FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */,
FD37E9FF28A5F2CD003AE748 /* Configuration.swift in Sources */, FD37E9FF28A5F2CD003AE748 /* Configuration.swift in Sources */,
FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */, FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */,
C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */,

View File

@ -203,6 +203,11 @@ class GlobalSearchViewController: BaseVC, SessionUtilRespondingViewController, U
]) ])
} }
catch { 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) return .failure(error)
} }
} }

View File

@ -199,16 +199,18 @@ public class MediaGalleryViewModel {
} }
} }
public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable { public struct Item: FetchableRecordWithRowId, Decodable, Identifiable, Differentiable, Equatable, Hashable, ColumnExpressible {
fileprivate static let interactionIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionId.stringValue) public typealias Columns = CodingKeys
fileprivate static let interactionVariantKey: SQL = SQL(stringLiteral: CodingKeys.interactionVariant.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
fileprivate static let interactionAuthorIdKey: SQL = SQL(stringLiteral: CodingKeys.interactionAuthorId.stringValue) case interactionId
fileprivate static let interactionTimestampMsKey: SQL = SQL(stringLiteral: CodingKeys.interactionTimestampMs.stringValue) case interactionVariant
fileprivate static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) case interactionAuthorId
fileprivate static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) case interactionTimestampMs
fileprivate static let attachmentAlbumIndexKey: SQL = SQL(stringLiteral: CodingKeys.attachmentAlbumIndex.stringValue)
case rowId
fileprivate static let attachmentString: String = CodingKeys.attachment.stringValue case attachmentAlbumIndex
case attachment
}
public var id: String { attachment.id } public var id: String { attachment.id }
public var differenceIdentifier: String { attachment.id } public var differenceIdentifier: String { attachment.id }
@ -306,7 +308,7 @@ public class MediaGalleryViewModel {
let finalFilterSQL: SQL = { let finalFilterSQL: SQL = {
guard let customFilters: SQL = customFilters else { guard let customFilters: SQL = customFilters else {
return """ return """
WHERE \(attachment.alias[Column.rowID]) IN \(rowIds) WHERE \(attachment[.rowId]) IN \(rowIds)
""" """
} }
@ -318,14 +320,14 @@ public class MediaGalleryViewModel {
}() }()
let request: SQLRequest<Item> = """ let request: SQLRequest<Item> = """
SELECT SELECT
\(interaction[.id]) AS \(Item.interactionIdKey), \(interaction[.id]) AS \(Item.Columns.interactionId),
\(interaction[.variant]) AS \(Item.interactionVariantKey), \(interaction[.variant]) AS \(Item.Columns.interactionVariant),
\(interaction[.authorId]) AS \(Item.interactionAuthorIdKey), \(interaction[.authorId]) AS \(Item.Columns.interactionAuthorId),
\(interaction[.timestampMs]) AS \(Item.interactionTimestampMsKey), \(interaction[.timestampMs]) AS \(Item.Columns.interactionTimestampMs),
\(attachment.alias[Column.rowID]) AS \(Item.rowIdKey), \(attachment[.rowId]) AS \(Item.Columns.rowId),
\(interactionAttachment[.albumIndex]) AS \(Item.attachmentAlbumIndexKey), \(interactionAttachment[.albumIndex]) AS \(Item.Columns.attachmentAlbumIndex),
\(Item.attachmentKey).* \(attachment.allColumns)
FROM \(Attachment.self) FROM \(Attachment.self)
\(joinSQL) \(joinSQL)
\(finalFilterSQL) \(finalFilterSQL)
@ -338,8 +340,8 @@ public class MediaGalleryViewModel {
Attachment.numberOfSelectedColumns(db) Attachment.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(Item.self, [
Item.attachmentString: adapters[1] .attachment: adapters[1]
]) ])
} }
} }

View File

@ -258,11 +258,12 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
// MARK: - DataModel // MARK: - DataModel
public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { public struct DataModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) public typealias Columns = CodingKeys
public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case rowId
public static let profileString: String = CodingKeys.profile.stringValue case profile
}
public var differenceIdentifier: String { profile.id } public var differenceIdentifier: String { profile.id }
public var id: String { profile.id } public var id: String { profile.id }
@ -286,11 +287,11 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
let request: SQLRequest<DataModel> = """ let request: SQLRequest<DataModel> = """
SELECT SELECT
\(profile.alias[Column.rowID]) AS \(DataModel.rowIdKey), \(profile[.rowId]) AS \(DataModel.Columns.rowId),
\(DataModel.profileKey).* \(profile.allColumns)
FROM \(Profile.self) FROM \(Profile.self)
WHERE \(profile.alias[Column.rowID]) IN \(rowIds) WHERE \(profile[.rowId]) IN \(rowIds)
ORDER BY \(orderSQL) ORDER BY \(orderSQL)
""" """
@ -300,8 +301,8 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
Profile.numberOfSelectedColumns(db) Profile.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(DataModel.self, [
DataModel.profileString: adapters[1] .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 // MARK: - Convenience
public extension ClosedGroup { public extension ClosedGroup {

View File

@ -696,6 +696,17 @@ public extension Interaction {
// MARK: - Search Queries // MARK: - Search Queries
public extension Interaction { 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 { struct TimestampInfo: FetchableRecord, Codable {
public let id: Int64 public let id: Int64
public let timestampMs: Int64 public let timestampMs: Int64
@ -711,8 +722,7 @@ public extension Interaction {
static func idsForTermWithin(threadId: String, pattern: FTS5Pattern) -> SQLRequest<TimestampInfo> { static func idsForTermWithin(threadId: String, pattern: FTS5Pattern) -> SQLRequest<TimestampInfo> {
let interaction: TypedTableAlias<Interaction> = TypedTableAlias() let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
let interactionFullTextSearch: SQL = SQL(stringLiteral: Interaction.fullTextSearchTableName) let interactionFullTextSearch: TypedTableAlias<FullTextSearch> = TypedTableAlias(name: Interaction.fullTextSearchTableName)
let threadIdLiteral: SQL = SQL(stringLiteral: Interaction.Columns.threadId.name)
let request: SQLRequest<TimestampInfo> = """ let request: SQLRequest<TimestampInfo> = """
SELECT SELECT
@ -720,9 +730,9 @@ public extension Interaction {
\(interaction[.timestampMs]) \(interaction[.timestampMs])
FROM \(Interaction.self) FROM \(Interaction.self)
JOIN \(interactionFullTextSearch) ON ( JOIN \(interactionFullTextSearch) ON (
\(interactionFullTextSearch).rowid = \(interaction.alias[Column.rowID]) AND \(interactionFullTextSearch[.rowId]) = \(interaction[.rowId]) AND
\(SQL("\(interactionFullTextSearch).\(threadIdLiteral) = \(threadId)")) AND \(SQL("\(interactionFullTextSearch[.threadId]) = \(threadId)")) AND
\(interactionFullTextSearch).\(SQL(stringLiteral: Interaction.Columns.body.name)) MATCH \(pattern) \(interactionFullTextSearch[.body]) MATCH \(pattern)
) )
ORDER BY \(interaction[.timestampMs].desc) 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 // MARK: - Convenience
public extension OpenGroup { 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 // MARK: - Convenience
public extension Profile { public extension Profile {

View File

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

View File

@ -82,7 +82,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Interaction.self) DELETE FROM \(Interaction.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(interaction.alias[Column.rowID]) SELECT \(interaction[.rowId])
FROM \(Interaction.self) FROM \(Interaction.self)
JOIN \(SessionThread.self) ON ( JOIN \(SessionThread.self) ON (
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
@ -90,7 +90,7 @@ public enum GarbageCollectionJob: JobExecutor {
) )
JOIN ( JOIN (
SELECT SELECT
COUNT(\(interaction.alias[Column.rowID])) AS interactionCount, COUNT(\(interaction[.rowId])) AS interactionCount,
\(interaction[.threadId]) \(interaction[.threadId])
FROM \(Interaction.self) FROM \(Interaction.self)
GROUP BY \(interaction[.threadId]) GROUP BY \(interaction[.threadId])
@ -112,7 +112,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Job.self) DELETE FROM \(Job.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(job.alias[Column.rowID]) SELECT \(job[.rowId])
FROM \(Job.self) FROM \(Job.self)
LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(job[.threadId]) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(job[.threadId])
LEFT JOIN \(Interaction.self) ON \(interaction[.id]) = \(job[.interactionId]) LEFT JOIN \(Interaction.self) ON \(interaction[.id]) = \(job[.interactionId])
@ -139,7 +139,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(LinkPreview.self) DELETE FROM \(LinkPreview.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(linkPreview.alias[Column.rowID]) SELECT \(linkPreview[.rowId])
FROM \(LinkPreview.self) FROM \(LinkPreview.self)
LEFT JOIN \(Interaction.self) ON ( LEFT JOIN \(Interaction.self) ON (
\(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND \(interaction[.linkPreviewUrl]) = \(linkPreview[.url]) AND
@ -159,7 +159,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(OpenGroup.self) DELETE FROM \(OpenGroup.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(openGroup.alias[Column.rowID]) SELECT \(openGroup[.rowId])
FROM \(OpenGroup.self) FROM \(OpenGroup.self)
LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(openGroup[.threadId]) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(openGroup[.threadId])
WHERE ( WHERE (
@ -178,7 +178,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Capability.self) DELETE FROM \(Capability.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(capability.alias[Column.rowID]) SELECT \(capability[.rowId])
FROM \(Capability.self) FROM \(Capability.self)
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.server]) = \(capability[.openGroupServer]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.server]) = \(capability[.openGroupServer])
WHERE \(openGroup[.threadId]) IS NULL WHERE \(openGroup[.threadId]) IS NULL
@ -195,7 +195,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(BlindedIdLookup.self) DELETE FROM \(BlindedIdLookup.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(blindedIdLookup.alias[Column.rowID]) SELECT \(blindedIdLookup[.rowId])
FROM \(BlindedIdLookup.self) FROM \(BlindedIdLookup.self)
LEFT JOIN \(SessionThread.self) ON ( LEFT JOIN \(SessionThread.self) ON (
\(thread[.id]) = \(blindedIdLookup[.blindedId]) OR \(thread[.id]) = \(blindedIdLookup[.blindedId]) OR
@ -222,7 +222,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Contact.self) DELETE FROM \(Contact.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(contact.alias[Column.rowID]) SELECT \(contact[.rowId])
FROM \(Contact.self) FROM \(Contact.self)
LEFT JOIN \(BlindedIdLookup.self) ON ( LEFT JOIN \(BlindedIdLookup.self) ON (
\(blindedIdLookup[.blindedId]) = \(contact[.id]) AND \(blindedIdLookup[.blindedId]) = \(contact[.id]) AND
@ -243,7 +243,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Attachment.self) DELETE FROM \(Attachment.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(attachment.alias[Column.rowID]) SELECT \(attachment[.rowId])
FROM \(Attachment.self) FROM \(Attachment.self)
LEFT JOIN \(Quote.self) ON \(quote[.attachmentId]) = \(attachment[.id]) LEFT JOIN \(Quote.self) ON \(quote[.attachmentId]) = \(attachment[.id])
LEFT JOIN \(LinkPreview.self) ON \(linkPreview[.attachmentId]) = \(attachment[.id]) LEFT JOIN \(LinkPreview.self) ON \(linkPreview[.attachmentId]) = \(attachment[.id])
@ -269,7 +269,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(Profile.self) DELETE FROM \(Profile.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(profile.alias[Column.rowID]) SELECT \(profile[.rowId])
FROM \(Profile.self) FROM \(Profile.self)
LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(profile[.id]) LEFT JOIN \(SessionThread.self) ON \(thread[.id]) = \(profile[.id])
LEFT JOIN \(Interaction.self) ON \(interaction[.authorId]) = \(profile[.id]) LEFT JOIN \(Interaction.self) ON \(interaction[.authorId]) = \(profile[.id])
@ -310,7 +310,7 @@ public enum GarbageCollectionJob: JobExecutor {
try db.execute(literal: """ try db.execute(literal: """
DELETE FROM \(SessionThread.self) DELETE FROM \(SessionThread.self)
WHERE \(Column.rowID) IN ( WHERE \(Column.rowID) IN (
SELECT \(thread.alias[Column.rowID]) SELECT \(thread[.rowId])
FROM \(SessionThread.self) FROM \(SessionThread.self)
LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id]) LEFT JOIN \(Contact.self) ON \(contact[.id]) = \(thread[.id])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id])

View File

@ -3,12 +3,14 @@
import GRDB import GRDB
import SessionUtilitiesKit import SessionUtilitiesKit
public struct MentionInfo: FetchableRecord, Decodable { public struct MentionInfo: FetchableRecord, Decodable, ColumnExpressible {
fileprivate static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) public typealias Columns = CodingKeys
fileprivate static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
fileprivate static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) case profile
case threadVariant
fileprivate static let profileString: String = CodingKeys.profile.stringValue case openGroupServer
case openGroupRoomToken
}
public let profile: Profile public let profile: Profile
public let threadVariant: SessionThread.Variant public let threadVariant: SessionThread.Variant
@ -79,7 +81,7 @@ public extension MentionInfo {
return SQLRequest(""" return SQLRequest("""
SELECT SELECT
\(Profile.self).*, \(Profile.self).*,
\(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)"))
\(targetJoin) \(targetJoin)
\(targetWhere) AND \(SQL("\(profile[.id]) = \(threadId)")) \(targetWhere) AND \(SQL("\(profile[.id]) = \(threadId)"))
@ -89,7 +91,7 @@ public extension MentionInfo {
return SQLRequest(""" return SQLRequest("""
SELECT SELECT
\(Profile.self).*, \(Profile.self).*,
\(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")) \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)"))
\(targetJoin) \(targetJoin)
JOIN \(GroupMember.self) ON ( JOIN \(GroupMember.self) ON (
@ -107,9 +109,9 @@ public extension MentionInfo {
SELECT SELECT
\(Profile.self).*, \(Profile.self).*,
MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting)
\(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")), \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")),
\(openGroup[.server]) AS \(MentionInfo.openGroupServerKey), \(openGroup[.server]) AS \(MentionInfo.Columns.openGroupServer),
\(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey) \(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken)
\(targetJoin) \(targetJoin)
JOIN \(Interaction.self) ON ( JOIN \(Interaction.self) ON (
@ -130,8 +132,8 @@ public extension MentionInfo {
Profile.numberOfSelectedColumns(db) Profile.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(MentionInfo.self, [
MentionInfo.profileString: adapters[0] .profile: adapters[0]
]) ])
} }
} }

View File

@ -11,42 +11,66 @@ fileprivate typealias AttachmentInteractionInfo = MessageViewModel.AttachmentInt
fileprivate typealias ReactionInfo = MessageViewModel.ReactionInfo fileprivate typealias ReactionInfo = MessageViewModel.ReactionInfo
fileprivate typealias TypingIndicatorInfo = MessageViewModel.TypingIndicatorInfo fileprivate typealias TypingIndicatorInfo = MessageViewModel.TypingIndicatorInfo
public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable { public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable, Hashable, Identifiable, Differentiable, ColumnExpressible {
public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) public typealias Columns = CodingKeys
public static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
public static let threadIsTrustedKey: SQL = SQL(stringLiteral: CodingKeys.threadIsTrusted.stringValue) case threadId
public static let threadHasDisappearingMessagesEnabledKey: SQL = SQL(stringLiteral: CodingKeys.threadHasDisappearingMessagesEnabled.stringValue) case threadVariant
public static let threadOpenGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupServer.stringValue) case threadIsTrusted
public static let threadOpenGroupPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.threadOpenGroupPublicKey.stringValue) case threadHasDisappearingMessagesEnabled
public static let threadContactNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.threadContactNameInternal.stringValue) case threadOpenGroupServer
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) case threadOpenGroupPublicKey
public static let authorNameInternalKey: SQL = SQL(stringLiteral: CodingKeys.authorNameInternal.stringValue) case threadContactNameInternal
public static let stateKey: SQL = SQL(stringLiteral: CodingKeys.state.stringValue)
public static let hasAtLeastOneReadReceiptKey: SQL = SQL(stringLiteral: CodingKeys.hasAtLeastOneReadReceipt.stringValue) // Interaction Info
public static let mostRecentFailureTextKey: SQL = SQL(stringLiteral: CodingKeys.mostRecentFailureText.stringValue)
public static let isTypingIndicatorKey: SQL = SQL(stringLiteral: CodingKeys.isTypingIndicator.stringValue) case rowId
public static let isSenderOpenGroupModeratorKey: SQL = SQL(stringLiteral: CodingKeys.isSenderOpenGroupModerator.stringValue) case id
public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) case openGroupServerMessageId
public static let quoteKey: SQL = SQL(stringLiteral: CodingKeys.quote.stringValue) case variant
public static let quoteAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.quoteAttachment.stringValue) case timestampMs
public static let linkPreviewKey: SQL = SQL(stringLiteral: CodingKeys.linkPreview.stringValue) case receivedAtTimestampMs
public static let linkPreviewAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.linkPreviewAttachment.stringValue) case authorId
public static let currentUserPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.currentUserPublicKey.stringValue) case authorNameInternal
public static let cellTypeKey: SQL = SQL(stringLiteral: CodingKeys.cellType.stringValue) case body
public static let authorNameKey: SQL = SQL(stringLiteral: CodingKeys.authorName.stringValue) case rawBody
public static let canHaveProfileKey: SQL = SQL(stringLiteral: CodingKeys.canHaveProfile.stringValue) case expiresStartedAtMs
public static let shouldShowProfileKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowProfile.stringValue) case expiresInSeconds
public static let shouldShowDateHeaderKey: SQL = SQL(stringLiteral: CodingKeys.shouldShowDateHeader.stringValue)
public static let positionInClusterKey: SQL = SQL(stringLiteral: CodingKeys.positionInCluster.stringValue) case state
public static let isOnlyMessageInClusterKey: SQL = SQL(stringLiteral: CodingKeys.isOnlyMessageInCluster.stringValue) case hasAtLeastOneReadReceipt
public static let isLastKey: SQL = SQL(stringLiteral: CodingKeys.isLast.stringValue) case mostRecentFailureText
public static let isLastOutgoingKey: SQL = SQL(stringLiteral: CodingKeys.isLastOutgoing.stringValue) case isSenderOpenGroupModerator
case isTypingIndicator
public static let profileString: String = CodingKeys.profile.stringValue case profile
public static let quoteString: String = CodingKeys.quote.stringValue case quote
public static let quoteAttachmentString: String = CodingKeys.quoteAttachment.stringValue case quoteAttachment
public static let linkPreviewString: String = CodingKeys.linkPreview.stringValue case linkPreview
public static let linkPreviewAttachmentString: String = CodingKeys.linkPreviewAttachment.stringValue 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 { public enum CellType: Int, Decodable, Equatable, Hashable, DatabaseValueConvertible {
case textOnlyMessage case textOnlyMessage
@ -462,13 +486,13 @@ public struct MessageViewModel: FetchableRecordWithRowId, Decodable, Equatable,
// MARK: - AttachmentInteractionInfo // MARK: - AttachmentInteractionInfo
public extension MessageViewModel { public extension MessageViewModel {
struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable { struct AttachmentInteractionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, ColumnExpressible {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) public typealias Columns = CodingKeys
public static let attachmentKey: SQL = SQL(stringLiteral: CodingKeys.attachment.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
public static let interactionAttachmentKey: SQL = SQL(stringLiteral: CodingKeys.interactionAttachment.stringValue) case rowId
case attachment
public static let attachmentString: String = CodingKeys.attachment.stringValue case interactionAttachment
public static let interactionAttachmentString: String = CodingKeys.interactionAttachment.stringValue }
public let rowId: Int64 public let rowId: Int64
public let attachment: Attachment public let attachment: Attachment
@ -491,13 +515,13 @@ public extension MessageViewModel {
// MARK: - ReactionInfo // MARK: - ReactionInfo
public extension MessageViewModel { public extension MessageViewModel {
struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable { struct ReactionInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, Comparable, Hashable, Differentiable, ColumnExpressible {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) public typealias Columns = CodingKeys
public static let reactionKey: SQL = SQL(stringLiteral: CodingKeys.reaction.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
public static let profileKey: SQL = SQL(stringLiteral: CodingKeys.profile.stringValue) case rowId
case reaction
public static let reactionString: String = CodingKeys.reaction.stringValue case profile
public static let profileString: String = CodingKeys.profile.stringValue }
public let rowId: Int64 public let rowId: Int64
public let reaction: Reaction public let reaction: Reaction
@ -522,9 +546,12 @@ public extension MessageViewModel {
// MARK: - TypingIndicatorInfo // MARK: - TypingIndicatorInfo
public extension MessageViewModel { public extension MessageViewModel {
struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable { struct TypingIndicatorInfo: FetchableRecordWithRowId, Decodable, Identifiable, Equatable, ColumnExpressible {
public static let rowIdKey: SQL = SQL(stringLiteral: CodingKeys.rowId.stringValue) public typealias Columns = CodingKeys
public static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue) public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case rowId
case threadId
}
public let rowId: Int64 public let rowId: Int64
public let threadId: String public let threadId: String
@ -776,60 +803,48 @@ public extension MessageViewModel {
let contact: TypedTableAlias<Contact> = TypedTableAlias() let contact: TypedTableAlias<Contact> = TypedTableAlias()
let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias() let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias()
let profile: TypedTableAlias<Profile> = TypedTableAlias() let profile: TypedTableAlias<Profile> = TypedTableAlias()
let threadProfile: TypedTableAlias<Profile> = TypedTableAlias(name: "threadProfile")
let quote: TypedTableAlias<Quote> = TypedTableAlias() let quote: TypedTableAlias<Quote> = TypedTableAlias()
let quoteInteraction: TypedTableAlias<Interaction> = TypedTableAlias(name: "quoteInteraction") let quoteInteraction: TypedTableAlias<Interaction> = TypedTableAlias(name: "quoteInteraction")
let quoteInteractionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias(
name: "quoteInteractionAttachment"
)
let quoteLinkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias(name: "quoteLinkPreview") let quoteLinkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias(name: "quoteLinkPreview")
let quoteAttachment: TypedTableAlias<Attachment> = TypedTableAlias(name: ViewModel.CodingKeys.quoteAttachment.stringValue)
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias() let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
let linkPreviewAttachment: TypedTableAlias<Attachment> = TypedTableAlias(ViewModel.self, column: .linkPreviewAttachment)
let threadProfile: SQL = SQL(stringLiteral: "threadProfile") let readReceipt: TypedTableAlias<RecipientState> = TypedTableAlias(name: "readReceipt")
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 numColumnsBeforeLinkedRecords: Int = 22 let numColumnsBeforeLinkedRecords: Int = 22
let finalGroupSQL: SQL = (groupSQL ?? "") let finalGroupSQL: SQL = (groupSQL ?? "")
let request: SQLRequest<ViewModel> = """ let request: SQLRequest<ViewModel> = """
SELECT SELECT
\(thread[.id]) AS \(ViewModel.threadIdKey), \(thread[.id]) AS \(ViewModel.Columns.threadId),
\(thread[.variant]) AS \(ViewModel.threadVariantKey), \(thread[.variant]) AS \(ViewModel.Columns.threadVariant),
-- Default to 'true' for non-contact threads -- 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 -- Default to 'false' when no contact exists
IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.threadHasDisappearingMessagesEnabledKey), IFNULL(\(disappearingMessagesConfig[.isEnabled]), false) AS \(ViewModel.Columns.threadHasDisappearingMessagesEnabled),
\(openGroup[.server]) AS \(ViewModel.threadOpenGroupServerKey), \(openGroup[.server]) AS \(ViewModel.Columns.threadOpenGroupServer),
\(openGroup[.publicKey]) AS \(ViewModel.threadOpenGroupPublicKeyKey), \(openGroup[.publicKey]) AS \(ViewModel.Columns.threadOpenGroupPublicKey),
IFNULL(\(threadProfile).\(nicknameColumn), \(threadProfile).\(nameColumn)) AS \(ViewModel.threadContactNameInternalKey), 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[.id]),
\(interaction[.openGroupServerMessageId]), \(interaction[.openGroupServerMessageId]),
\(interaction[.variant]), \(interaction[.variant]),
\(interaction[.timestampMs]), \(interaction[.timestampMs]),
\(interaction[.receivedAtTimestampMs]), \(interaction[.receivedAtTimestampMs]),
\(interaction[.authorId]), \(interaction[.authorId]),
IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.authorNameInternalKey), IFNULL(\(profile[.nickname]), \(profile[.name])) AS \(ViewModel.Columns.authorNameInternal),
\(interaction[.body]), \(interaction[.body]),
\(interaction[.expiresStartedAtMs]), \(interaction[.expiresStartedAtMs]),
\(interaction[.expiresInSeconds]), \(interaction[.expiresInSeconds]),
-- Default to 'sending' assuming non-processed interaction when null -- Default to 'sending' assuming non-processed interaction when null
IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.stateKey), IFNULL(MIN(\(recipientState[.state])), \(SQL("\(RecipientState.State.sending)"))) AS \(ViewModel.Columns.state),
(\(readReceipt).\(readTimestampMsColumn) IS NOT NULL) AS \(ViewModel.hasAtLeastOneReadReceiptKey), (\(readReceipt[.readTimestampMs]) IS NOT NULL) AS \(ViewModel.Columns.hasAtLeastOneReadReceipt),
\(recipientState[.mostRecentFailureText]) AS \(ViewModel.mostRecentFailureTextKey), \(recipientState[.mostRecentFailureText]) AS \(ViewModel.Columns.mostRecentFailureText),
EXISTS ( EXISTS (
SELECT 1 SELECT 1
@ -840,36 +855,36 @@ public extension MessageViewModel {
\(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND \(SQL("\(thread[.variant]) = \(SessionThread.Variant.community)")) AND
\(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])")) \(SQL("\(groupMember[.role]) IN \([GroupMember.Role.moderator, GroupMember.Role.admin])"))
) )
) AS \(ViewModel.isSenderOpenGroupModeratorKey), ) AS \(ViewModel.Columns.isSenderOpenGroupModerator),
\(ViewModel.profileKey).*, \(profile.allColumns),
\(quote[.interactionId]), \(quote[.interactionId]),
\(quote[.authorId]), \(quote[.authorId]),
\(quote[.timestampMs]), \(quote[.timestampMs]),
\(quoteInteraction[.body]), \(quoteInteraction[.body]),
\(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) AS \(quoteAttachmentIdColumn), \(quoteInteractionAttachment[.attachmentId]),
\(ViewModel.quoteAttachmentKey).*, \(quoteAttachment.allColumns),
\(ViewModel.linkPreviewKey).*, \(linkPreview.allColumns),
\(ViewModel.linkPreviewAttachmentKey).*, \(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 -- 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 -- query from crashing when decoding we need to provide default values
\(CellType.textOnlyMessage) AS \(ViewModel.cellTypeKey), \(CellType.textOnlyMessage) AS \(ViewModel.Columns.cellType),
'' AS \(ViewModel.authorNameKey), '' AS \(ViewModel.Columns.authorName),
false AS \(ViewModel.canHaveProfileKey), false AS \(ViewModel.Columns.canHaveProfile),
false AS \(ViewModel.shouldShowProfileKey), false AS \(ViewModel.Columns.shouldShowProfile),
false AS \(ViewModel.shouldShowDateHeaderKey), false AS \(ViewModel.Columns.shouldShowDateHeader),
\(Position.middle) AS \(ViewModel.positionInClusterKey), \(Position.middle) AS \(ViewModel.Columns.positionInCluster),
false AS \(ViewModel.isOnlyMessageInClusterKey), false AS \(ViewModel.Columns.isOnlyMessageInCluster),
false AS \(ViewModel.isLastKey), false AS \(ViewModel.Columns.isLast),
false AS \(ViewModel.isLastOutgoingKey) false AS \(ViewModel.Columns.isLastOutgoing)
FROM \(Interaction.self) FROM \(Interaction.self)
JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId]) JOIN \(SessionThread.self) ON \(thread[.id]) = \(interaction[.threadId])
LEFT JOIN \(Contact.self) ON \(contact[.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 \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId])
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId])
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
@ -887,9 +902,9 @@ public extension MessageViewModel {
) )
) )
) )
LEFT JOIN \(InteractionAttachment.self) AS \(quoteInteractionAttachment) ON ( LEFT JOIN \(quoteInteractionAttachment) ON (
\(quoteInteractionAttachment).\(interactionAttachmentInteractionIdColumn) = \(quoteInteraction[.id]) AND \(quoteInteractionAttachment[.interactionId]) = \(quoteInteraction[.id]) AND
\(quoteInteractionAttachment).\(interactionAttachmentAlbumIndexColumn) = 0 \(quoteInteractionAttachment[.albumIndex]) = 0
) )
LEFT JOIN \(quoteLinkPreview) ON ( LEFT JOIN \(quoteLinkPreview) ON (
\(quoteLinkPreview[.url]) = \(quoteInteraction[.linkPreviewUrl]) AND \(quoteLinkPreview[.url]) = \(quoteInteraction[.linkPreviewUrl]) AND
@ -898,27 +913,27 @@ public extension MessageViewModel {
linkPreview: quoteLinkPreview linkPreview: quoteLinkPreview
)) ))
) )
LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON ( LEFT JOIN \(quoteAttachment) ON (
\(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteInteractionAttachment).\(interactionAttachmentAttachmentIdColumn) OR \(quoteAttachment[.id]) = \(quoteInteractionAttachment[.attachmentId]) OR
\(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quoteLinkPreview[.attachmentId]) OR \(quoteAttachment[.id]) = \(quoteLinkPreview[.attachmentId]) OR
\(ViewModel.quoteAttachmentKey).\(attachmentIdColumn) = \(quote[.attachmentId]) \(quoteAttachment[.id]) = \(quote[.attachmentId])
) )
LEFT JOIN \(LinkPreview.self) ON ( LEFT JOIN \(LinkPreview.self) ON (
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND \(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
\(Interaction.linkPreviewFilterLiteral()) \(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 ( LEFT JOIN \(RecipientState.self) ON (
-- Ignore 'skipped' states -- Ignore 'skipped' states
\(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND \(SQL("\(recipientState[.state]) != \(RecipientState.State.skipped)")) AND
\(recipientState[.interactionId]) = \(interaction[.id]) \(recipientState[.interactionId]) = \(interaction[.id])
) )
LEFT JOIN \(RecipientState.self) AS \(readReceipt) ON ( LEFT JOIN \(readReceipt) ON (
\(readReceipt).\(readTimestampMsColumn) IS NOT NULL AND \(readReceipt[.readTimestampMs]) IS NOT NULL AND
\(readReceipt).\(readReceiptInteractionIdColumn) = \(interaction[.id]) \(readReceipt[.interactionId]) = \(interaction[.id])
) )
WHERE \(interaction.alias[Column.rowID]) IN \(rowIds) WHERE \(interaction[.rowId]) IN \(rowIds)
\(finalGroupSQL) \(finalGroupSQL)
ORDER BY \(orderSQL) ORDER BY \(orderSQL)
""" """
@ -933,12 +948,12 @@ public extension MessageViewModel {
Attachment.numberOfSelectedColumns(db) Attachment.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(ViewModel.self, [
ViewModel.profileString: adapters[1], .profile: adapters[1],
ViewModel.quoteString: adapters[2], .quote: adapters[2],
ViewModel.quoteAttachmentString: adapters[3], .quoteAttachment: adapters[3],
ViewModel.linkPreviewString: adapters[4], .linkPreview: adapters[4],
ViewModel.linkPreviewAttachmentString: adapters[5] .linkPreviewAttachment: adapters[5]
]) ])
} }
} }
@ -965,9 +980,9 @@ public extension MessageViewModel.AttachmentInteractionInfo {
let numColumnsBeforeLinkedRecords: Int = 1 let numColumnsBeforeLinkedRecords: Int = 1
let request: SQLRequest<AttachmentInteractionInfo> = """ let request: SQLRequest<AttachmentInteractionInfo> = """
SELECT SELECT
\(attachment.alias[Column.rowID]) AS \(AttachmentInteractionInfo.rowIdKey), \(attachment[.rowId]) AS \(AttachmentInteractionInfo.Columns.rowId),
\(AttachmentInteractionInfo.attachmentKey).*, \(attachment.allColumns),
\(AttachmentInteractionInfo.interactionAttachmentKey).* \(interactionAttachment.allColumns)
FROM \(Attachment.self) FROM \(Attachment.self)
JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id]) JOIN \(InteractionAttachment.self) ON \(interactionAttachment[.attachmentId]) = \(attachment[.id])
\(finalFilterSQL) \(finalFilterSQL)
@ -980,9 +995,9 @@ public extension MessageViewModel.AttachmentInteractionInfo {
InteractionAttachment.numberOfSelectedColumns(db) InteractionAttachment.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(AttachmentInteractionInfo.self, [
AttachmentInteractionInfo.attachmentString: adapters[1], .attachment: adapters[1],
AttachmentInteractionInfo.interactionAttachmentString: adapters[2] .interactionAttachment: adapters[2]
]) ])
} }
} }
@ -1046,9 +1061,9 @@ public extension MessageViewModel.ReactionInfo {
let numColumnsBeforeLinkedRecords: Int = 1 let numColumnsBeforeLinkedRecords: Int = 1
let request: SQLRequest<ReactionInfo> = """ let request: SQLRequest<ReactionInfo> = """
SELECT SELECT
\(reaction.alias[Column.rowID]) AS \(ReactionInfo.rowIdKey), \(reaction[.rowId]) AS \(ReactionInfo.Columns.rowId),
\(ReactionInfo.reactionKey).*, \(reaction.allColumns),
\(ReactionInfo.profileKey).* \(profile.allColumns)
FROM \(Reaction.self) FROM \(Reaction.self)
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(reaction[.authorId]) LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(reaction[.authorId])
\(finalFilterSQL) \(finalFilterSQL)
@ -1061,9 +1076,9 @@ public extension MessageViewModel.ReactionInfo {
Profile.numberOfSelectedColumns(db) Profile.numberOfSelectedColumns(db)
]) ])
return ScopeAdapter([ return ScopeAdapter.with(ReactionInfo.self, [
ReactionInfo.reactionString: adapters[1], .reaction: adapters[1],
ReactionInfo.profileString: adapters[2] .profile: adapters[2]
]) ])
} }
} }
@ -1129,8 +1144,8 @@ public extension MessageViewModel.TypingIndicatorInfo {
}() }()
let request: SQLRequest<MessageViewModel.TypingIndicatorInfo> = """ let request: SQLRequest<MessageViewModel.TypingIndicatorInfo> = """
SELECT SELECT
\(threadTypingIndicator.alias[Column.rowID]) AS \(MessageViewModel.TypingIndicatorInfo.rowIdKey), \(threadTypingIndicator[.rowId]),
\(threadTypingIndicator[.threadId]) AS \(MessageViewModel.TypingIndicatorInfo.threadIdKey) \(threadTypingIndicator[.threadId])
FROM \(ThreadTypingIndicator.self) FROM \(ThreadTypingIndicator.self)
\(finalFilterSQL) \(finalFilterSQL)
""" """

View File

@ -1351,18 +1351,24 @@ public class AssociatedRecord<T, PagedType>: ErasedAssociatedRecord where T: Fet
// Fetch the inserted/updated rows // Fetch the inserted/updated rows
let additionalFilters: SQL = SQL(rowIds.contains(Column.rowID)) 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 do {
// preview) then trigger the update callback (if there were deletions) and stop here let updatedItems: [T] = try dataQuery(additionalFilters)
guard !updatedItems.isEmpty else { return hasOtherChanges } .fetchAll(db)
// Process the upserted data (assume at least one value changed) // If the inserted/updated rows we irrelevant (eg. associated to another thread, a quote or a link
dataCache.mutate { $0 = $0.upserting(items: updatedItems) } // preview) then trigger the update callback (if there were deletions) and stop here
guard !updatedItems.isEmpty else { return hasOtherChanges }
return true
// 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) { public func clearCache(_ db: Database) {

View File

@ -3,28 +3,65 @@
import Foundation import Foundation
import GRDB import GRDB
public class TypedTableAlias<T> where T: TableRecord, T: ColumnExpressible { public struct TypedTableAlias<T: ColumnExpressible> {
internal let name: String public enum RowIdColumn {
internal let tableName: String case rowId
public let alias: TableAlias }
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.name = name
self.tableName = T.databaseTableName self.tableName = T.databaseTableName
self.alias = TableAlias(name: name) 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 { public subscript(_ column: T.Columns) -> SQLExpression {
return alias[column.name] return alias[column.name]
} }
/// **Warning:** For this to work you **MUST** call the '.aliased()' method when joining or it will public subscript(_ column: RowIdColumn) -> SQLSelection {
/// throw when trying to decode return alias[Column.rowID]
public func allColumns() -> SQLSelection {
return alias[AllColumns().sqlSelection]
} }
} }
// MARK: - NeverJoiningTypedTableAlias
public struct NeverJoiningTypedTableAlias<T: ColumnExpressible> {
internal let alias: TypedTableAlias<T>
}
// MARK: - Extensions
extension QueryInterfaceRequest { extension QueryInterfaceRequest {
public func aliased<T>(_ typedAlias: TypedTableAlias<T>) -> Self { public func aliased<T>(_ typedAlias: TypedTableAlias<T>) -> Self {
return aliased(typedAlias.alias) return aliased(typedAlias.alias)
@ -38,7 +75,5 @@ extension Association {
} }
extension TableAlias { extension TableAlias {
public func allColumns() -> SQLSelection { public var allColumns: SQLSelection { self[AllColumns().sqlSelection] }
return self[AllColumns().sqlSelection]
}
} }

View File

@ -12,10 +12,58 @@ public extension SQLInterpolation {
@_disfavoredOverload @_disfavoredOverload
mutating func appendInterpolation<T>(_ typedTableAlias: TypedTableAlias<T>) { mutating func appendInterpolation<T>(_ typedTableAlias: TypedTableAlias<T>) {
let name: String = typedTableAlias.name 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) } guard name != tableName else { return appendLiteral(tableName.quotedDatabaseIdentifier) }
appendLiteral("\(tableName.quotedDatabaseIdentifier) AS \(name.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 })
}
}