Skip to content
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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from

Conversation

dessalines
Copy link
Member

Note: 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.

Comment on lines +277 to +279
/// the pagination cursor to use to fetch the next page
#[cfg_attr(feature = "full", ts(optional))]
pub next_page: Option<PaginationCursor>,
Copy link
Member Author

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.

Comment on lines 32 to 45
// 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,
Copy link
Member Author

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.

Copy link
Member Author

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);
Copy link
Member Author

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.

Comment on lines 21 to 24
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)),
Copy link
Member Author

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.

Comment on lines 425 to 448
/// 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))
Copy link
Member Author

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.

@dessalines dessalines marked this pull request as ready for review February 14, 2025 15:55
dessalines added a commit that referenced this pull request Feb 14, 2025
#[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);
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Comment on lines +381 to +386
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);
Copy link
Member Author

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?

Copy link
Collaborator

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.

Copy link
Member Author

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.

@dessalines
Copy link
Member Author

@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),
Copy link
Member

@Nutomic Nutomic Feb 17, 2025

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().

Copy link
Member

@Nutomic Nutomic Feb 17, 2025

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

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Collaborator

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.

Copy link
Member Author

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.

Copy link
Collaborator

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
};
Copy link
Member

@Nutomic Nutomic Feb 17, 2025

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Make a trait out of PaginationCursor / PaginationCursorData
4 participants