diff --git a/server/src/crate_server/follow_index.gleam b/server/src/crate_server/follow_index.gleam index 686383a..ea71727 100644 --- a/server/src/crate_server/follow_index.gleam +++ b/server/src/crate_server/follow_index.gleam @@ -291,13 +291,9 @@ fn page_after( let remaining = case after { option.None -> values option.Some(marker) -> - case list.contains(values, marker) { - True -> - list.drop_while(values, fn(value) { - string.compare(value, marker) != order.Gt - }) - False -> values - } + list.drop_while(values, fn(value) { + string.compare(value, marker) != order.Gt + }) } list.take(remaining, limit) } diff --git a/server/src/crate_server/follow_index_postgres.gleam b/server/src/crate_server/follow_index_postgres.gleam index 1758b3c..f742b70 100644 --- a/server/src/crate_server/follow_index_postgres.gleam +++ b/server/src/crate_server/follow_index_postgres.gleam @@ -275,17 +275,10 @@ fn connection_page( use value <- decode.field("value", decode.string) decode.success(value) } - pog.query("with marker as ( - select $2::text as value - where $2::text is null or exists ( - select 1 from follow_edges - where " <> scope_column <> " = $1 and " <> value_column <> " = $2 - ) - ) - select distinct " <> value_column <> " as value - from follow_edges cross join marker + pog.query("select distinct " <> value_column <> " as value + from follow_edges where " <> scope_column <> " = $1 - and (marker.value is null or " <> value_column <> " > marker.value) + and ($2::text is null or " <> value_column <> " > $2::text) order by value limit $3") |> pog.parameter(pog.text(scope)) |> pog.parameter(pog.nullable(pog.text, after)) diff --git a/server/test/follow_index_postgres_test.gleam b/server/test/follow_index_postgres_test.gleam index 8c35810..4eca9db 100644 --- a/server/test/follow_index_postgres_test.gleam +++ b/server/test/follow_index_postgres_test.gleam @@ -70,6 +70,35 @@ pub fn follow_round_trip_test() { assert store.edges.following(did) == [] } +pub fn connection_pages_resume_past_a_deleted_anchor_test() { + use store <- with_store() + let did = "did:plc:follow-anchor" + let follower = "did:plc:follow-anchor-er" + store.edges.delete_for_did(did) + store.edges.delete_for_did(follower) + store.edges.upsert(follow(did, "did:plc:anchor-a", "fa1")) + store.edges.upsert(follow(did, "did:plc:anchor-b", "fa2")) + store.edges.upsert(follow(did, "did:plc:anchor-c", "fa3")) + store.edges.upsert(follow(follower, did, "fa4")) + + assert store.edges.following_page(did, Some("did:plc:anchor-a"), 2) + == ["did:plc:anchor-b", "did:plc:anchor-c"] + + // The anchor is unfollowed between pages: the next page must resume past it + // rather than restart from the top or collapse to nothing. + store.edges.delete( + "at://" <> did <> "/dev.mokkenstorm.crate.graph.follow/fa2", + ) + assert store.edges.following_page(did, Some("did:plc:anchor-b"), 2) + == ["did:plc:anchor-c"] + assert store.edges.following_page(did, Some("did:plc:zzz"), 2) == [] + assert store.edges.followers_page(did, Some("did:plc:absent"), 2) + == [follower] + + store.edges.delete_for_did(did) + store.edges.delete_for_did(follower) +} + pub fn follow_seen_round_trip_test() { use store <- with_store() let did = "did:plc:follow-seen" diff --git a/server/test/follow_index_test.gleam b/server/test/follow_index_test.gleam index bcacba6..14f6c41 100644 --- a/server/test/follow_index_test.gleam +++ b/server/test/follow_index_test.gleam @@ -92,9 +92,13 @@ pub fn connection_pages_are_distinct_bounded_and_resume_after_a_subject_test() { assert store.edges.following_page("did:a", None, 2) == ["did:b", "did:c"] assert store.edges.following_page("did:a", Some("did:c"), 2) == ["did:d"] - assert store.edges.following_page("did:a", Some("did:missing"), 2) - == ["did:b", "did:c"] assert store.edges.followers_page("did:a", None, 1) == ["did:y"] + + // A cursor whose anchor was unfollowed between pages resumes at the next + // surviving subject rather than restarting, so page 2 never re-serves page 1. + store.edges.delete("at://did:a/dev.mokkenstorm.crate.graph.follow/f3") + assert store.edges.following_page("did:a", Some("did:c"), 2) == ["did:d"] + assert store.edges.following_page("did:a", Some("did:zzz"), 2) == [] } pub fn relationship_intersections_only_return_matching_candidates_test() {