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)
fileprivate static let attachmentString: String = CodingKeys.attachment.stringValue case rowId
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)
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 // Interaction Info
public static let quoteString: String = CodingKeys.quote.stringValue
public static let quoteAttachmentString: String = CodingKeys.quoteAttachment.stringValue case rowId
public static let linkPreviewString: String = CodingKeys.linkPreview.stringValue case id
public static let linkPreviewAttachmentString: String = CodingKeys.linkPreviewAttachment.stringValue 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 { 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,9 +1351,10 @@ 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)) do {
.defaulting(to: []) 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 // 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 // preview) then trigger the update callback (if there were deletions) and stop here
@ -1364,6 +1365,11 @@ public class AssociatedRecord<T, PagedType>: ErasedAssociatedRecord where T: Fet
return true return true
} }
catch {
SNLog("[PagedDatabaseObserver] Error loading associated data: \(error)")
return hasOtherChanges
}
}
public func clearCache(_ db: Database) { public func clearCache(_ db: Database) {
dataCache.mutate { $0 = DataCache() } dataCache.mutate { $0 = DataCache() }

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