Skip to content

Commit 7a9364d

Browse files
committed
controllers/krate/versions: Use semver_ord column to sort by semver in the database
1 parent 859488b commit 7a9364d

File tree

2 files changed

+69
-105
lines changed

2 files changed

+69
-105
lines changed

src/controllers/krate/versions.rs

Lines changed: 67 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::views::EncodableVersion;
1414
use axum::Json;
1515
use axum::extract::FromRequestParts;
1616
use axum_extra::extract::Query;
17+
use crates_io_diesel_helpers::semver_ord;
1718
use diesel::dsl::not;
1819
use diesel::prelude::*;
1920
use diesel_async::{AsyncPgConnection, RunQueryDsl};
@@ -234,9 +235,6 @@ async fn list_by_date(
234235

235236
/// Seek-based pagination of versions by semver
236237
///
237-
/// Unfortunately, Heroku Postgres has no support for the semver PG extension.
238-
/// Therefore, we need to perform both sorting and pagination manually on the server.
239-
///
240238
/// # Panics
241239
///
242240
/// This function will panic if `option` is built with `enable_pages` set to true.
@@ -249,131 +247,93 @@ async fn list_by_semver(
249247
) -> AppResult<PaginatedVersionsAndPublishers> {
250248
use seek::*;
251249

252-
let include = params.include()?;
253-
let mut query = versions::table
254-
.filter(versions::crate_id.eq(crate_id))
255-
.into_boxed();
256-
257-
if !params.nums.is_empty() {
258-
query = query.filter(versions::num.eq_any(params.nums.iter().map(|s| s.as_str())));
259-
}
260-
261-
let (data, total, release_tracks) = if let Some(options) = options {
262-
// Since versions will only increase in the future and both sorting and pagination need to
263-
// happen on the app server, implementing it with fetching only the data needed for sorting
264-
// and pagination, then making another query for the data to respond with, would minimize
265-
// payload and memory usage. This way, we can utilize the sorted map and enrich it later
266-
// without sorting twice.
267-
// Sorting by semver but opted for id as the seek key because num can be quite lengthy,
268-
// while id values are significantly smaller.
250+
let make_base_query = || {
251+
let mut query = versions::table
252+
.filter(versions::crate_id.eq(crate_id))
253+
.left_outer_join(users::table)
254+
.select(<(Version, Option<User>)>::as_select())
255+
.into_boxed();
269256

270-
let mut sorted_versions = IndexMap::new();
257+
if !params.nums.is_empty() {
258+
query = query.filter(versions::num.eq_any(params.nums.iter().map(|s| s.as_str())));
259+
}
271260
query
272-
.select((versions::id, versions::num, versions::yanked))
273-
.load_stream::<(i32, String, bool)>(conn)
274-
.await?
275-
.try_for_each(|(id, num, yanked)| {
276-
let semver = semver::Version::parse(&num).ok();
277-
sorted_versions.insert(id, (semver, yanked, None));
278-
future::ready(Ok(()))
279-
})
280-
.await?;
261+
};
281262

282-
sorted_versions
283-
.sort_unstable_by(|_, (semver_a, _, _), _, (semver_b, _, _)| semver_b.cmp(semver_a));
263+
let mut query = make_base_query();
284264

265+
if let Some(options) = options {
285266
assert!(
286267
!matches!(&options.page, Page::Numeric(_)),
287268
"?page= is not supported"
288269
);
289-
290-
let release_tracks = include.release_tracks.then(|| {
291-
ReleaseTracks::from_sorted_semver_iter(
292-
sorted_versions
293-
.values()
294-
.filter(|(_, yanked, _)| !yanked)
295-
.filter_map(|(semver, _, _)| semver.as_ref()),
270+
if let Some(SeekPayload::Semver(Semver { num, id })) = Seek::Semver.after(&options.page)? {
271+
query = query.filter(
272+
versions::semver_ord
273+
.eq(semver_ord(num.clone()))
274+
.and(versions::id.lt(id))
275+
.or(versions::semver_ord.lt(semver_ord(num))),
296276
)
297-
});
298-
299-
let mut idx = Some(0);
300-
if let Some(SeekPayload::Semver(Semver { id })) = Seek::Semver.after(&options.page)? {
301-
idx = sorted_versions
302-
.get_index_of(&id)
303-
.filter(|i| i + 1 < sorted_versions.len())
304-
.map(|i| i + 1);
305277
}
306-
if let Some(start) = idx {
307-
let end = (start + options.per_page as usize).min(sorted_versions.len());
308-
let ids = sorted_versions[start..end]
309-
.keys()
310-
.cloned()
311-
.collect::<Vec<_>>();
278+
query = query.limit(options.per_page);
279+
}
280+
281+
query = query.order((versions::semver_ord.desc(), versions::id.desc()));
282+
283+
let data: Vec<(Version, Option<User>)> = query.load(conn).await?;
284+
let mut next_page = None;
285+
if let Some(options) = options {
286+
next_page = next_seek_params(&data, options, |last| Seek::Semver.to_payload(last))?
287+
.map(|p| req.query_with_params(p));
288+
};
289+
290+
let release_tracks = if params.include()?.release_tracks {
291+
let mut sorted_versions = IndexSet::new();
292+
if options.is_some() {
312293
versions::table
313294
.filter(versions::crate_id.eq(crate_id))
314-
.left_outer_join(users::table)
315-
.select(<(Version, Option<User>)>::as_select())
316-
.filter(versions::id.eq_any(ids))
317-
.load_stream::<(Version, Option<User>)>(conn)
295+
.filter(not(versions::yanked))
296+
.select(versions::num)
297+
.load_stream::<String>(conn)
318298
.await?
319-
.try_for_each(|row| {
320-
// The versions are already sorted, and we only need to enrich the fetched rows into them.
321-
// Therefore, other values can now be safely ignored.
322-
sorted_versions
323-
.entry(row.0.id)
324-
.and_modify(|entry| *entry = (None, false, Some(row)));
325-
299+
.try_for_each(|num| {
300+
if let Ok(semver) = semver::Version::parse(&num) {
301+
sorted_versions.insert(semver);
302+
};
326303
future::ready(Ok(()))
327304
})
328305
.await?;
329-
330-
let len = sorted_versions.len();
331-
(
332-
sorted_versions
333-
.into_values()
334-
.filter_map(|(_, _, v)| v)
335-
.collect(),
336-
len,
337-
release_tracks,
338-
)
339306
} else {
340-
(vec![], 0, release_tracks)
307+
sorted_versions = data
308+
.iter()
309+
.flat_map(|(version, _)| {
310+
(!version.yanked)
311+
.then_some(version)
312+
.and_then(|v| semver::Version::parse(&v.num).ok())
313+
})
314+
.collect();
341315
}
316+
317+
sorted_versions.sort_unstable_by(|a, b| b.cmp(a));
318+
Some(ReleaseTracks::from_sorted_semver_iter(
319+
sorted_versions.iter(),
320+
))
342321
} else {
343-
let mut data = IndexMap::new();
344-
query
345-
.left_outer_join(users::table)
346-
.select(<(Version, Option<User>)>::as_select())
347-
.load_stream::<(Version, Option<User>)>(conn)
348-
.await?
349-
.try_for_each(|row| {
350-
if let Ok(semver) = semver::Version::parse(&row.0.num) {
351-
data.insert(semver, row);
352-
};
353-
future::ready(Ok(()))
354-
})
355-
.await?;
356-
data.sort_unstable_by(|a, _, b, _| b.cmp(a));
357-
let total = data.len();
358-
let release_tracks = include.release_tracks.then(|| {
359-
ReleaseTracks::from_sorted_semver_iter(
360-
data.iter()
361-
.flat_map(|(semver, (version, _))| (!version.yanked).then_some(semver)),
362-
)
363-
});
364-
(data.into_values().collect(), total, release_tracks)
322+
None
365323
};
366324

367-
let mut next_page = None;
368-
if let Some(options) = options {
369-
next_page = next_seek_params(&data, options, |last| Seek::Semver.to_payload(last))?
370-
.map(|p| req.query_with_params(p))
325+
// Since the total count is retrieved through an additional query, to maintain consistency
326+
// with other pagination methods, we only make a count query while data is not empty.
327+
let total = if !data.is_empty() {
328+
make_base_query().count().get_result(conn).await?
329+
} else {
330+
0
371331
};
372332

373333
Ok(PaginatedVersionsAndPublishers {
374334
data,
375335
meta: ResponseMeta {
376-
total: total as i64,
336+
total,
377337
next_page,
378338
release_tracks,
379339
},
@@ -392,6 +352,7 @@ mod seek {
392352
seek!(
393353
pub enum Seek {
394354
Semver {
355+
num: String,
395356
id: i32,
396357
},
397358
Date {
@@ -406,7 +367,10 @@ mod seek {
406367
pub(crate) fn to_payload(&self, record: &(Version, Option<User>)) -> SeekPayload {
407368
let (Version { id, created_at, .. }, _) = *record;
408369
match *self {
409-
Seek::Semver => SeekPayload::Semver(Semver { id }),
370+
Seek::Semver => SeekPayload::Semver(Semver {
371+
num: record.0.num.clone(),
372+
id,
373+
}),
410374
Seek::Date => SeekPayload::Date(Date { created_at, id }),
411375
}
412376
}

src/tests/routes/crates/versions/list.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,9 @@ async fn test_seek_based_pagination_semver_sorting() -> anyhow::Result<()> {
291291
assert_eq!(json.meta.total as usize, expects.len());
292292
assert_eq!(json.meta.release_tracks, None);
293293

294-
// A decodable seek value, MTAwCg (100), but doesn't actually exist
294+
// A decodable seek value, WyIwLjAuMCIsMTAwXQ (["0.0.0",100]), but doesn't actually exist
295295
let json: VersionList = anon
296-
.get_with_query(url, "per_page=10&sort=semver&seek=MTAwCg")
296+
.get_with_query(url, "per_page=10&sort=semver&seek=WyIwLjAuMCIsMTAwXQ")
297297
.await
298298
.good();
299299
assert_eq!(json.versions.len(), 0);

0 commit comments

Comments
 (0)