Add cursor pagination to report_combined view (#5244)

* add pagination cursor

* store timestamp instead of id in cursor (partial)

* Revert "store timestamp instead of id in cursor (partial)"

This reverts commit 89359dde4b.

* use paginated query builder
This commit is contained in:
dullbananas 2024-12-02 10:56:14 -07:00 committed by GitHub
parent d8dda44010
commit 588e1f6c0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 100 additions and 19 deletions

View file

@ -89,19 +89,19 @@ pub struct PersonMentionId(i32);
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The comment report id.
pub struct CommentReportId(i32);
pub struct CommentReportId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The post report id.
pub struct PostReportId(i32);
pub struct PostReportId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The private message report id.
pub struct PrivateMessageReportId(i32);
pub struct PrivateMessageReportId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]

View file

@ -2,6 +2,8 @@ use crate::newtypes::{CommentReportId, PostReportId, PrivateMessageReportId, Rep
#[cfg(feature = "full")]
use crate::schema::report_combined;
use chrono::{DateTime, Utc};
#[cfg(feature = "full")]
use i_love_jesus::CursorKeysModule;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
@ -9,10 +11,14 @@ use ts_rs::TS;
#[skip_serializing_none]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "full", derive(Identifiable, Queryable, Selectable, TS))]
#[cfg_attr(
feature = "full",
derive(Identifiable, Queryable, Selectable, TS, CursorKeysModule)
)]
#[cfg_attr(feature = "full", diesel(table_name = report_combined))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
#[cfg_attr(feature = "full", cursor_keys_module(name = report_combined_keys))]
/// A combined reports table.
pub struct ReportCombined {
pub id: ReportCombinedId,

View file

@ -3,6 +3,7 @@ use crate::structs::{
LocalUserView,
PostReportView,
PrivateMessageReportView,
ReportCombinedPaginationCursor,
ReportCombinedView,
ReportCombinedViewInternal,
};
@ -13,8 +14,10 @@ use diesel::{
JoinOnDsl,
NullableExpressionMethods,
QueryDsl,
SelectableHelper,
};
use diesel_async::RunQueryDsl;
use i_love_jesus::PaginatedQueryBuilder;
use lemmy_db_schema::{
aliases::{self, creator_community_actions},
newtypes::CommunityId,
@ -36,8 +39,19 @@ use lemmy_db_schema::{
private_message_report,
report_combined,
},
source::community::CommunityFollower,
utils::{actions, actions_alias, functions::coalesce, get_conn, limit_and_offset, DbPool},
source::{
combined::report::{report_combined_keys as key, ReportCombined},
community::CommunityFollower,
},
utils::{
actions,
actions_alias,
functions::coalesce,
get_conn,
limit_and_offset,
DbPool,
ReverseTimestampKey,
},
};
use lemmy_utils::error::LemmyResult;
@ -94,12 +108,47 @@ impl ReportCombinedViewInternal {
}
}
impl ReportCombinedPaginationCursor {
// get cursor for page that starts immediately after the given post
pub fn after_post(view: &ReportCombinedView) -> ReportCombinedPaginationCursor {
let (prefix, id) = match view {
ReportCombinedView::Comment(v) => ('C', v.comment_report.id.0),
ReportCombinedView::Post(v) => ('P', v.post_report.id.0),
ReportCombinedView::PrivateMessage(v) => ('M', v.private_message_report.id.0),
};
// hex encoding to prevent ossification
ReportCombinedPaginationCursor(format!("{prefix}{id:x}"))
}
pub async fn read(&self, pool: &mut DbPool<'_>) -> Result<PaginationCursorData, Error> {
let err_msg = || Error::QueryBuilderError("Could not parse pagination token".into());
let mut query = report_combined::table
.select(ReportCombined::as_select())
.into_boxed();
let (prefix, id_str) = self.0.split_at_checked(1).ok_or_else(err_msg)?;
let id = i32::from_str_radix(id_str, 16).map_err(|_err| err_msg())?;
query = match prefix {
"C" => query.filter(report_combined::comment_report_id.eq(id)),
"P" => query.filter(report_combined::post_report_id.eq(id)),
"M" => query.filter(report_combined::private_message_report_id.eq(id)),
_ => return Err(err_msg()),
};
let token = query.first(&mut get_conn(pool).await?).await?;
Ok(PaginationCursorData(token))
}
}
#[derive(Clone)]
pub struct PaginationCursorData(ReportCombined);
#[derive(Default)]
pub struct ReportCombinedQuery {
pub community_id: Option<CommunityId>,
pub page: Option<i64>,
pub limit: Option<i64>,
pub unresolved_only: bool,
pub page_after: Option<PaginationCursorData>,
pub page_back: bool,
}
impl ReportCombinedQuery {
@ -237,18 +286,6 @@ impl ReportCombinedQuery {
query = query.filter(community::id.eq(community_id));
}
// If viewing all reports, order by newest, but if viewing unresolved only, show the oldest
// first (FIFO)
if options.unresolved_only {
query = query
.filter(post_report::resolved.eq(false))
.or_filter(comment_report::resolved.eq(false))
.or_filter(private_message_report::resolved.eq(false))
.order_by(report_combined::published.asc());
} else {
query = query.order_by(report_combined::published.desc());
}
// If its not an admin, get only the ones you mod
if !user.local_user.admin {
query = query.filter(community_actions::became_moderator.is_not_null());
@ -258,6 +295,36 @@ impl ReportCombinedQuery {
query = query.limit(limit).offset(offset);
let mut query = PaginatedQueryBuilder::new(query);
let page_after = options.page_after.map(|c| c.0);
if options.page_back {
query = query.before(page_after).limit_and_offset_from_end();
} else {
query = query.after(page_after);
}
// If viewing all reports, order by newest, but if viewing unresolved only, show the oldest
// first (FIFO)
if options.unresolved_only {
query = query
.filter(
post_report::resolved
.eq(false)
.or(comment_report::resolved.eq(false))
.or(private_message_report::resolved.eq(false)),
)
// TODO: when a `then_asc` method is added, use it here, make the id sort direction match,
// and remove the separate index; unless additional columns are added to this sort
.then_desc(ReverseTimestampKey(key::published));
} else {
query = query.then_desc(key::published);
}
// Tie breaker
query = query.then_desc(key::id);
let res = query.load::<ReportCombinedViewInternal>(conn).await?;
// Map the query results to the enum

View file

@ -126,6 +126,12 @@ pub struct PostReportView {
#[cfg_attr(feature = "full", ts(export))]
pub struct PaginationCursor(pub String);
/// like PaginationCursor but for the report_combined table
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(ts_rs::TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ReportCombinedPaginationCursor(pub String);
#[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]

View file

@ -17,7 +17,9 @@ CREATE TABLE report_combined (
UNIQUE (post_report_id, comment_report_id, private_message_report_id)
);
CREATE INDEX idx_report_combined_published ON report_combined (published DESC);
CREATE INDEX idx_report_combined_published ON report_combined (published DESC, id DESC);
CREATE INDEX idx_report_combined_published_asc ON report_combined (reverse_timestamp_sort (published) DESC, id DESC);
-- Updating the history
INSERT INTO report_combined (published, post_report_id)