Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions packages/stream_chat_persistence/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## Upcoming Changes

🚀 Performance

- Read only the messages matching the `PaginationParams` from DB when calling `PinnedMessageDao.getMessagesByCid` instead of reading all pinned messages for the channel and applying pagination in memory.

🐞 Fixed

- `PinnedMessageDao.getMessagesByCid` now honours `PaginationParams.lessThanOrEqual` and `PaginationParams.greaterThanOrEqual` (inclusive of the cursor message), in addition to the existing strict `lessThan`/`greaterThan`.
- `PinnedMessageDao.getMessagesByCid` with a forward cursor (`greaterThan`/`greaterThanOrEqual`) and a `limit` now returns the messages immediately AFTER the pivot, instead of the channel tail.

## 9.25.0

🚀 Performance
Expand Down
114 changes: 88 additions & 26 deletions packages/stream_chat_persistence/lib/src/dao/pinned_message_dao.dart
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,38 @@ class PinnedMessageDao extends DatabaseAccessor<DriftChatDatabase>
bool fetchDraft = true,
PaginationParams? messagePagination,
}) async {
final (
lessThanCursor,
lessThanOrEqualCursor,
greaterThanCursor,
greaterThanOrEqualCursor,
) = await (
_lookupCursor(messagePagination?.lessThan),
_lookupCursor(messagePagination?.lessThanOrEqual),
_lookupCursor(messagePagination?.greaterThan),
_lookupCursor(messagePagination?.greaterThanOrEqual),
).wait;

// When the caller is paginating forward (greaterThan / greaterThanOrEqual
// only), order ASC so the SQL `LIMIT` retains the N messages immediately
// AFTER the cursor. Otherwise order DESC so `LIMIT` retains the N most
// recent (closest to a `lessThan` cursor, or the channel tail when no
// cursor is set). The final result is always reshaped to ASC for display.
final isForwardPagination =
(greaterThanCursor != null || greaterThanOrEqualCursor != null) &&
lessThanCursor == null &&
lessThanOrEqualCursor == null;

final orderBy = isForwardPagination
? [
OrderingTerm.asc(pinnedMessages.createdAt),
OrderingTerm.asc(pinnedMessages.id),
]
: [
OrderingTerm.desc(pinnedMessages.createdAt),
OrderingTerm.desc(pinnedMessages.id),
];

final query = select(pinnedMessages).join([
leftOuterJoin(_users, pinnedMessages.userId.equalsExp(_users.id)),
leftOuterJoin(
Expand All @@ -253,35 +285,65 @@ class PinnedMessageDao extends DatabaseAccessor<DriftChatDatabase>
..where(pinnedMessages.channelCid.equals(cid))
..where(pinnedMessages.parentId.isNull() |
pinnedMessages.showInChannel.equals(true))
..orderBy([OrderingTerm.asc(pinnedMessages.createdAt)]);
..orderBy(orderBy);

final rows = await query.get();
final msgList = await _messagesFromJoinRows(rows, fetchDraft: fetchDraft);
// Cursor predicates compare the full `(createdAt, id)` tuple — the same
// key used in ORDER BY — so messages sharing a `createdAt` with the cursor
// fall on the correct side of the boundary. Filtering on `createdAt` alone
// would skip or repeat those siblings across pages.
if (lessThanCursor case final c?) {
query.where(
pinnedMessages.createdAt.isSmallerThanValue(c.createdAt) |
(pinnedMessages.createdAt.equals(c.createdAt) &
pinnedMessages.id.isSmallerThanValue(c.id)),
);
}
if (lessThanOrEqualCursor case final c?) {
query.where(
pinnedMessages.createdAt.isSmallerThanValue(c.createdAt) |
(pinnedMessages.createdAt.equals(c.createdAt) &
pinnedMessages.id.isSmallerOrEqualValue(c.id)),
);
}
if (greaterThanCursor case final c?) {
query.where(
pinnedMessages.createdAt.isBiggerThanValue(c.createdAt) |
(pinnedMessages.createdAt.equals(c.createdAt) &
pinnedMessages.id.isBiggerThanValue(c.id)),
);
}
if (greaterThanOrEqualCursor case final c?) {
query.where(
pinnedMessages.createdAt.isBiggerThanValue(c.createdAt) |
(pinnedMessages.createdAt.equals(c.createdAt) &
pinnedMessages.id.isBiggerOrEqualValue(c.id)),
);
}

if (msgList.isNotEmpty) {
final mutable = msgList.toList();
if (messagePagination?.lessThan != null) {
final lessThanIndex = mutable.indexWhere(
(m) => m.id == messagePagination!.lessThan,
);
if (lessThanIndex != -1) {
mutable.removeRange(lessThanIndex, mutable.length);
}
}
if (messagePagination?.greaterThan != null) {
final greaterThanIndex = mutable.indexWhere(
(m) => m.id == messagePagination!.greaterThan,
);
if (greaterThanIndex != -1) {
mutable.removeRange(0, greaterThanIndex);
}
}
if (messagePagination?.limit != null) {
return mutable.take(messagePagination!.limit).toList();
}
return mutable;
if (messagePagination != null) {
query.limit(messagePagination.limit);
}
return msgList;

final rows = await query.get();
final orderedRows = isForwardPagination ? rows : rows.reversed.toList();
return _messagesFromJoinRows(orderedRows, fetchDraft: fetchDraft);
}

/// Returns the `(createdAt, id)` cursor for the pinned message with [id] in
/// the local cache, or `null` if [id] is null, the message isn't cached, or
/// isn't visible in the channel (i.e. a thread reply with
/// `showInChannel = false`).
Future<({DateTime createdAt, String id})?> _lookupCursor(String? id) async {
if (id == null) return null;
final createdAt = await (selectOnly(pinnedMessages)
..addColumns([pinnedMessages.createdAt])
..where(pinnedMessages.id.equals(id))
..where(pinnedMessages.parentId.isNull() |
pinnedMessages.showInChannel.equals(true)))
.map((row) => row.read(pinnedMessages.createdAt))
.getSingleOrNull();
if (createdAt == null) return null;
return (createdAt: createdAt, id: id);
}

/// Updates the message data of a particular channel with
Expand Down
Loading
Loading