-
-
Notifications
You must be signed in to change notification settings - Fork 899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Extracting pagination cursor utils into a trait. #5424
base: main
Are you sure you want to change the base?
Conversation
/// the pagination cursor to use to fetch the next page | ||
#[cfg_attr(feature = "full", ts(optional))] | ||
pub next_page: Option<PaginationCursor>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd forgotten to add next_page for all of these responses.
// parse pagination token | ||
let page_after = if let Some(pa) = &data.page_cursor { | ||
Some(pa.read(&mut context.pool()).await?) | ||
} else { | ||
None | ||
}; | ||
let page_back = data.page_back; | ||
let type_ = data.type_; | ||
|
||
let content = PersonContentCombinedQuery { | ||
creator_id: person_details_id, | ||
type_, | ||
page_after, | ||
page_back, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of doing a database read for the page cursor string before, I'm now passing the page_cursor string into the list queries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed my mind on this, as internal queries kept causing stack overflows.
} | ||
.list(&mut context.pool(), &local_user_view) | ||
.await?; | ||
|
||
Ok(Json(ListPersonContentResponse { content })) | ||
let next_page = content.last().map(PageCursorBuilder::cursor); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can use the trait function now.
query = match prefix { | ||
'A' => query.filter(modlog_combined::admin_allow_instance_id.eq(id)), | ||
'B' => query.filter(modlog_combined::admin_block_instance_id.eq(id)), | ||
'C' => query.filter(modlog_combined::admin_purge_comment_id.eq(id)), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the modlog, which has like 17 tables, I changed these from strings to chars.
crates/db_schema/src/newtypes.rs
Outdated
/// A pagination cursor | ||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] | ||
#[cfg_attr(feature = "full", derive(TS))] | ||
#[cfg_attr(feature = "full", ts(export))] | ||
pub struct PaginationCursor(pub String); | ||
|
||
impl PaginationCursor { | ||
pub fn create(prefix: char, id: i32) -> Self { | ||
// hex encoding to prevent ossification | ||
Self(format!("{prefix}{id:x}")) | ||
} | ||
|
||
pub fn prefix_and_id(&self) -> LemmyResult<(char, i32)> { | ||
let (prefix_str, id_str) = self | ||
.0 | ||
.split_at_checked(1) | ||
.ok_or(LemmyErrorType::CouldntParsePaginationToken)?; | ||
let prefix = prefix_str | ||
.chars() | ||
.next() | ||
.ok_or(LemmyErrorType::CouldntParsePaginationToken)?; | ||
let id = i32::from_str_radix(id_str, 16)?; | ||
|
||
Ok((prefix, id)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added some functions here to help create a cursor, and extract a prefix and id from a given one.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)] | ||
#[cfg_attr(feature = "full", derive(TS))] | ||
#[cfg_attr(feature = "full", ts(export))] | ||
pub struct PaginationCursor(pub String); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was about to make a whole detailed comment about how validation should be done at the time an instance of a struct is created and this this should have probably been pub struct PaginaitonCursor(char, i32)
and strings should be parsed into this type via something like impl TryFrom<String> for PaginationCursor
so that it's impossible to construct an invalid cursor, but then I noticed that this struct needs to be (de)serializable. Not sure how to use the "parse, don't validate" approach with this in mind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also ran into a case later, where the cursor id needs to be a string. The local_image table needs a cursor that uses the pictrs_alias
as its cursor id.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont see where this is used for local_image so it should be possible to make the String field private.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's part of a different pr I have in draft, where I remove the rest of page / limits.
let mut query = PaginatedQueryBuilder::new(query); | ||
|
||
if self.page_back.unwrap_or_default() { | ||
query = query.before(page_after).limit_and_offset_from_end(); | ||
query = query.before(self.cursor_data).limit_and_offset_from_end(); | ||
} else { | ||
query = query.after(page_after); | ||
query = query.after(self.cursor_data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dullbananas is this correct? Is no limit required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does need a limit. The fix should be in a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thx. I'm trying to add the rest of cursor pagination in #5429 , when its ready I'll give you a ping.
@Nutomic I'll wait on merging this, since we should probably merge your aggregates removal PR first. |
ModlogCombinedView::ModRemoveComment(v) => ('N', v.mod_remove_comment.id.0), | ||
ModlogCombinedView::ModRemoveCommunity(v) => ('O', v.mod_remove_community.id.0), | ||
ModlogCombinedView::ModRemovePost(v) => ('P', v.mod_remove_post.id.0), | ||
ModlogCombinedView::ModTransferCommunity(v) => ('Q', v.mod_transfer_community.id.0), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are currently defining the mapping of db type to letter twice for each view (in case of modlog its here and in crates/db_schema/src/impls/combined/modlog.rs). This way its easy to have a mistake where the definitions are different in both places, so it should only be defined once. But to be honest I dont see any way to do that.
Anyway I would definitely prefer to have both of these next to each other, then its harder to make a mistake by changing one conversion but not the other. So PaginationCursorReader
and PaginationCursorBuilder
should be merged into a single trait with two methods to_cursor()
and from_cursor()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also it looks like youre missing pagination for viewing all modlog types (or all person content, all inbox types etc).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I couldn't really figure out a way to have back and forth mappings in one function, because they work on different objects.
I originally tried to use a single trait, but removed it after I realized it didn't make sense. The from_cursor should be implemented by a single db table, while the to_cursor has to exist on the view.
I'll play around a bit more and see if there's a better way to do it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can have both impls on the view but return a single db table anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is done now. I couldn't really figure out what to do for the All
cases, since then no specific content row type ids are provided.
@dullbananas will have to give input on how to properly handle cursor pagination, because a lot of things are still missing, and I'm not sure I'm using the library properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For All
you need to add a separate letter, and use the combined table id. In fact it makes sense to use the combined table id also when a specific type is selected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I recommend not using the id from combined tables anywhere in the api, including in pagination cursor strings. If we use something other than combined tables in the future, then the combined id will stop working.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case I don't know what id we'd use for All. IMO these should all probably be fixed in a later PR anyway when you have time to help with cursor pagination. Even just having one correct example outside of post_view would help.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No change here is needed for handling All
. This handles variants of ModlogCombinedView
(which corresponds to a single item), not ListingType
.
let page_after = if let Some(pa) = &data.page_cursor { | ||
Some(pa.read(&mut context.pool()).await?) | ||
let cursor_data = if let Some(cursor) = &data.page_cursor { | ||
Some(InboxCombined::from_cursor(cursor, &mut context.pool()).await?) | ||
} else { | ||
None | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic belongs to the database, so change it here to pass the unchanged page_cursor as query paramter, and call from_cursor inside the query method. Same for all other types. Generally all the pagination logic for eg SearchCombinedView should be contained within search_combined_view.rs, not spread over various files.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried that initially, (and wanted to do it that way) but that can't be done unfortunately. If you try to run multiple sync queries inside of the list functions, you end up getting untraceable stack overflows.
So all of the pre-fetched data (like extracting cursor data from a cursor string to be able to paginate), has to be done outside of, or before, the list functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To get working stack traces you have to comment out this line. And I dont see why it would fail, there should be no difference calling a method outside of list or inside. Maybe you need to move the method call further to the top, before doing joins.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried putting it in both places, but it still failed. As long as the calls to convert from a pagination string to pagination data struct are pretty simple, then IMO its fine to have them outside of the list function, and have the list function take in the pagination data.
PaginationCursor
/PaginationCursorData
#5275Note: This does not handle this for post_view, which is a frustrating mess right now. I'm going to open up a new issue for that one and add it to the milestone.