Skip to content
Snippets Groups Projects
Commit 0c7f8f22 authored by Dariusz Rybicki's avatar Dariusz Rybicki
Browse files

Merge branch 'feature/chats-query-improvements' into 'main'

Chats query improvements

See merge request elixxir/client-ios-db!24
parents bd3a8c33 9baa910b
Branches
Tags v1.0.4
1 merge request!24Chats query improvements
......@@ -18,20 +18,26 @@ extension Fetch where Model == ChatInfo, Query == ChatInfo.Query {
let fetchGroups: Group.Fetch =
.grdb(writer, queue, Group.request(_:))
let contactChatsQuery = ContactChatInfo.Query(userId: query.userId)
let contactChats = try fetchContactChats(contactChatsQuery)
.map(ChatInfo.contactChat)
let contactChats: [ChatInfo]
if let query = query.contactChatInfoQuery {
contactChats = try fetchContactChats(query).map(ChatInfo.contactChat)
} else {
contactChats = []
}
let groupChatsQuery = GroupChatInfo.Query()
let groupChats = try fetchGroupChats(groupChatsQuery)
.map(ChatInfo.groupChat)
let groupChats: [ChatInfo]
if let query = query.groupChatInfoQuery {
groupChats = try fetchGroupChats(query).map(ChatInfo.groupChat)
} else {
groupChats = []
}
let groupsQuery = Group.Query(
withMessages: false,
sortBy: .createdAt(desc: true)
)
let groups = try fetchGroups(groupsQuery)
.map(ChatInfo.group)
let groups: [ChatInfo]
if let query = query.groupQuery {
groups = try fetchGroups(query).map(ChatInfo.group)
} else {
groups = []
}
let chats = (contactChats + groupChats + groups)
.sorted(by: { $0.date > $1.date })
......@@ -47,25 +53,41 @@ extension FetchPublisher where Model == ChatInfo, Query == ChatInfo.Query {
_ queue: DispatchQueue
) -> FetchPublisher<ChatInfo, ChatInfo.Query> {
FetchPublisher<ChatInfo, ChatInfo.Query> { query in
let fetchContactChats: ContactChatInfo.FetchPublisher =
let contactChats: AnyPublisher<[ChatInfo], Error>
if let query = query.contactChatInfoQuery {
contactChats = ContactChatInfo.FetchPublisher
.grdb(writer, queue, ContactChatInfo.request(_:))
.run(query)
.map { $0.map(ChatInfo.contactChat) }
.eraseToAnyPublisher()
} else {
contactChats = Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
}
let fetchGroupChats: GroupChatInfo.FetchPublisher =
let groupChats: AnyPublisher<[ChatInfo], Error>
if let query = query.groupChatInfoQuery {
groupChats = GroupChatInfo.FetchPublisher
.grdb(writer, queue, GroupChatInfo.request(_:))
.run(query)
.map { $0.map(ChatInfo.groupChat) }
.eraseToAnyPublisher()
} else {
groupChats = Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
}
let fetchGroups: Group.FetchPublisher =
let groups: AnyPublisher<[ChatInfo], Error>
if let query = query.groupQuery {
groups = Group.FetchPublisher
.grdb(writer, queue, Group.request(_:))
let contactChatsQuery = ContactChatInfo.Query(userId: query.userId)
let groupChatsQuery = GroupChatInfo.Query()
let groupsQuery = Group.Query(withMessages: false, sortBy: .createdAt(desc: true))
.run(query)
.map { $0.map(ChatInfo.group) }
.eraseToAnyPublisher()
} else {
groups = Just([]).setFailureType(to: Error.self).eraseToAnyPublisher()
}
return Publishers
.CombineLatest3(
fetchContactChats(contactChatsQuery).map { $0.map(ChatInfo.contactChat) },
fetchGroupChats(groupChatsQuery).map { $0.map(ChatInfo.groupChat) },
fetchGroups(groupsQuery).map { $0.map(ChatInfo.group) }
)
.CombineLatest3(contactChats, groupChats, groups)
.map { (contactChats: [ChatInfo],
groupChats: [ChatInfo],
groups: [ChatInfo]) -> [ChatInfo] in
......
......@@ -9,8 +9,15 @@ extension ContactChatInfo: FetchableRecord {
}
static func request(_ query: Query) -> AdaptedFetchRequest<SQLRequest<ContactChatInfo>> {
SQLRequest(
sql: """
var sqlWhere: [String] = ["c1.id = :userId"]
var sqlArguments: StatementArguments = ["userId": query.userId]
if let authStatus = query.authStatus {
sqlWhere.append("AND \(sqlWhereAuthStatus(count: authStatus.count))")
_ = sqlArguments.append(contentsOf: sqlArgumentsAuthStatus(authStatus))
}
let sql = """
SELECT
-- All contact columns:
c2.*,
......@@ -28,15 +35,16 @@ extension ContactChatInfo: FetchableRecord {
ON c2.id IN (m.senderId, m.recipientId)
AND c1.id <> c2.id
WHERE
c1.id = :userId
\(sqlWhere.joined(separator: "\n "))
GROUP BY
c2.id
ORDER BY
date DESC;
""",
arguments: [
"userId": query.userId
]
"""
return SQLRequest(
sql: sql,
arguments: sqlArguments
)
.adapted { db in
let adapters = try splittingRowAdapters(columnCounts: [
......@@ -50,4 +58,17 @@ extension ContactChatInfo: FetchableRecord {
])
}
}
private static func sqlWhereAuthStatus(count: Int) -> String {
let arguments = (0..<count).map { ":authStatus\($0)" }.joined(separator: ", ")
return "c2.authStatus IN (\(arguments))"
}
private static func sqlArgumentsAuthStatus(
_ statuses: Set<Contact.AuthStatus>
) -> StatementArguments {
StatementArguments(statuses.enumerated().reduce(into: [:]) {
$0["authStatus\($1.offset)"] = $1.element.rawValue
})
}
}
......@@ -62,13 +62,38 @@ extension ChatInfo {
public struct Query: Equatable {
/// Instantiate chat info query
///
/// Results are sorted by `ChatInfo.date` in descending order.
///
/// - Parameters:
/// - userId: Current user's contact ID
public init(userId: Contact.ID) {
self.userId = userId
/// - contactChatInfoQuery: Direct chat infos query.
/// If `nil`, exclude direct chats from results.
/// - groupChatInfoQuery: Group chat infos query.
/// If `nil`, exclude group chats from results.
/// - groupQuery: Groups query.
/// If `nil`, exclude groups results.
public init(
contactChatInfoQuery: ContactChatInfo.Query?,
groupChatInfoQuery: GroupChatInfo.Query?,
groupQuery: Group.Query?
) {
self.contactChatInfoQuery = contactChatInfoQuery
self.groupChatInfoQuery = groupChatInfoQuery
self.groupQuery = groupQuery
}
/// Current user's contact ID
public var userId: Contact.ID
/// Direct chats query
///
/// If `nil`, exclude direct chats from results.
public var contactChatInfoQuery: ContactChatInfo.Query?
/// Group chats query
///
/// If `nil`, exclude group chats from results.
public var groupChatInfoQuery: GroupChatInfo.Query?
/// Groups query
///
/// If `nil`, exclude groups results.
public var groupQuery: Group.Query?
}
}
......@@ -46,12 +46,27 @@ extension ContactChatInfo {
/// Instantiate query
///
/// - Parameters:
/// - userId: Current user's contact ID
public init(userId: Contact.ID) {
/// - userId: Current user's contact ID.
/// - authStatus: Filter by other contact auth status.
/// If set, only chats with contacts that have any of the provided
/// auth statuses will be included.
/// If `nil` (default), the filter is not used.
public init(
userId: Contact.ID,
authStatus: Set<Contact.AuthStatus>? = nil
) {
self.userId = userId
self.authStatus = authStatus
}
/// Current user's contact ID
public var userId: Contact.ID
/// Filter by other contact auth status
///
/// If set, only chats with contacts that have any of the provided
/// auth statuses will be included.
/// If `nil`, the filter is not used.
public var authStatus: Set<Contact.AuthStatus>?
}
}
......@@ -177,8 +177,14 @@ final class ChatInfoGRDBTests: XCTestCase {
)),
]
let query = ChatInfo.Query(
contactChatInfoQuery: .init(userId: contactA.id),
groupChatInfoQuery: .init(),
groupQuery: .init(withMessages: false)
)
XCTAssertNoDifference(
try db.fetchChatInfos(ChatInfo.Query(userId: contactA.id)),
try db.fetchChatInfos(query),
expectedFetchResults
)
......@@ -186,10 +192,36 @@ final class ChatInfoGRDBTests: XCTestCase {
let fetchAssertion = PublisherAssertion<[ChatInfo], Error>()
fetchAssertion.expectValue()
fetchAssertion.subscribe(to: db.fetchChatInfosPublisher(ChatInfo.Query(userId: contactA.id)))
fetchAssertion.subscribe(to: db.fetchChatInfosPublisher(query))
fetchAssertion.waitForValues()
XCTAssertNoDifference(fetchAssertion.receivedValues(), [expectedFetchResults])
XCTAssertNil(fetchAssertion.receivedCompletion())
}
func testFetchingExcludingSubqueries() throws {
let query = ChatInfo.Query(
contactChatInfoQuery: nil,
groupChatInfoQuery: nil,
groupQuery: nil
)
// Fetch excluding subqueries:
XCTAssertNoDifference(try db.fetchChatInfos(query), [])
// Subscribe to publisher:
let fetchAssertion = PublisherAssertion<[ChatInfo], Error>()
fetchAssertion.expectValue()
fetchAssertion.expectCompletion()
fetchAssertion.subscribe(to: db.fetchChatInfosPublisher(query))
fetchAssertion.waitForValues()
XCTAssertNoDifference(fetchAssertion.receivedValues(), [[]])
fetchAssertion.waitForCompletion()
XCTAssert(fetchAssertion.receivedCompletion()?.isFinished == true)
}
}
......@@ -148,4 +148,115 @@ final class ContactChatInfoGRDBTests: XCTestCase {
]
)
}
func testFetchingByContactAuthStatus() throws {
// Mock up contacts:
let contactA = try db.saveContact(.stub("A"))
let contactB = try db.saveContact(.stub("B", authStatus: .friend))
let contactC = try db.saveContact(.stub("C", authStatus: .hidden))
let contactD = try db.saveContact(.stub("D", authStatus: .stranger))
// Mock up conversation between contact A and B:
try db.saveMessage(.stub(
from: contactA,
to: contactB,
at: 1,
isUnread: false
))
try db.saveMessage(.stub(
from: contactB,
to: contactA,
at: 2,
isUnread: true
))
let lastMessage_betweenAandB_at3 = try db.saveMessage(.stub(
from: contactA,
to: contactB,
at: 3,
isUnread: true
))
// Mock up conversation between contact A and C:
try db.saveMessage(.stub(
from: contactA,
to: contactC,
at: 4,
isUnread: false
))
let lastMessage_betweenAandC_at5 = try db.saveMessage(.stub(
from: contactC,
to: contactA,
at: 5,
isUnread: true
))
// Mock up conversation between contact A and D:
try db.saveMessage(.stub(
from: contactA,
to: contactD,
at: 6,
isUnread: false
))
let lastMessage_betweenAandD_at7 = try db.saveMessage(.stub(
from: contactD,
to: contactA,
at: 7,
isUnread: false
))
// Fetch contact chat infos for contact A, filtered by other contact auth status:
XCTAssertNoDifference(
try db.fetchContactChatInfos(ContactChatInfo.Query(
userId: contactA.id,
authStatus: [.friend, .stranger]
)),
[
ContactChatInfo(
contact: contactD,
lastMessage: lastMessage_betweenAandD_at7,
unreadCount: 0
),
ContactChatInfo(
contact: contactB,
lastMessage: lastMessage_betweenAandB_at3,
unreadCount: 2
),
]
)
// Fetch contact chat infos for contact A, regardless other contact auth status:
XCTAssertNoDifference(
try db.fetchContactChatInfos(ContactChatInfo.Query(
userId: contactA.id,
authStatus: nil
)),
[
ContactChatInfo(
contact: contactD,
lastMessage: lastMessage_betweenAandD_at7,
unreadCount: 0
),
ContactChatInfo(
contact: contactC,
lastMessage: lastMessage_betweenAandC_at5,
unreadCount: 1
),
ContactChatInfo(
contact: contactB,
lastMessage: lastMessage_betweenAandB_at3,
unreadCount: 2
),
]
)
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment