Skip to content

Commit 830223e

Browse files
authored
Merge pull request #10819 from Turbo87/sort-by-semver
Use `semver_ord` column for sorting versions
2 parents b0e1ade + 9d65ca3 commit 830223e

File tree

4 files changed

+47
-172
lines changed

4 files changed

+47
-172
lines changed

crates/crates_io_diesel_helpers/src/fns.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ define_sql_function!(fn floor(x: Double) -> Integer);
1414
define_sql_function!(fn greatest<T: SingleValue>(x: T, y: T) -> T);
1515
define_sql_function!(fn least<T: SingleValue>(x: T, y: T) -> T);
1616
define_sql_function!(fn split_part(string: Text, delimiter: Text, n: Integer) -> Text);
17+
define_sql_function!(fn semver_ord(num: Text) -> Nullable<Jsonb>);

src/controllers/helpers/pagination.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ macro_rules! seek {
461461
impl From<[<$variant Helper>]> for $variant {
462462
fn from(helper: [<$variant Helper>]) -> Self {
463463
let [<$variant Helper>]($($field,)*) = helper;
464-
Self { $($field,)* }
464+
Self { $($field: $field.clone(),)* }
465465
}
466466
}
467467

@@ -470,7 +470,7 @@ macro_rules! seek {
470470
where
471471
S: serde::Serializer,
472472
{
473-
let helper = [<$variant Helper>]($(self.$field,)*);
473+
let helper = [<$variant Helper>]($(self.$field.clone(),)*);
474474
serde::Serialize::serialize(&helper, serializer)
475475
}
476476
}

src/controllers/krate/versions.rs

Lines changed: 42 additions & 168 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};
@@ -98,13 +99,8 @@ pub async fn list_versions(
9899
None => None,
99100
};
100101

101-
// Sort by semver by default
102-
let versions_and_publishers = match &params.sort.as_ref().map(|s| s.to_lowercase()).as_deref() {
103-
Some("date") => {
104-
list_by_date(crate_id, pagination.as_ref(), &params, &req, &mut conn).await?
105-
}
106-
_ => list_by_semver(crate_id, pagination.as_ref(), &params, &req, &mut conn).await?,
107-
};
102+
let versions_and_publishers =
103+
list(crate_id, pagination.as_ref(), &params, &req, &mut conn).await?;
108104

109105
let versions = versions_and_publishers
110106
.data
@@ -125,12 +121,12 @@ pub async fn list_versions(
125121
}))
126122
}
127123

128-
/// Seek-based pagination of versions by date
124+
/// Seek-based pagination of versions
129125
///
130126
/// # Panics
131127
///
132-
/// This function will panic if `option` is built with `enable_pages` set to true.
133-
async fn list_by_date(
128+
/// This function will panic if `options` is built with `enable_pages` set to true.
129+
async fn list(
134130
crate_id: i32,
135131
options: Option<&PaginationOptions>,
136132
params: &ListQueryParams,
@@ -139,6 +135,11 @@ async fn list_by_date(
139135
) -> AppResult<PaginatedVersionsAndPublishers> {
140136
use seek::*;
141137

138+
let seek = match &params.sort.as_ref().map(|s| s.to_lowercase()).as_deref() {
139+
Some("date") => Seek::Date,
140+
_ => Seek::Semver,
141+
};
142+
142143
let make_base_query = || {
143144
let mut query = versions::table
144145
.filter(versions::crate_id.eq(crate_id))
@@ -159,23 +160,40 @@ async fn list_by_date(
159160
!matches!(&options.page, Page::Numeric(_)),
160161
"?page= is not supported"
161162
);
162-
if let Some(SeekPayload::Date(Date { created_at, id })) = Seek::Date.after(&options.page)? {
163-
query = query.filter(
164-
versions::created_at
165-
.eq(created_at)
166-
.and(versions::id.lt(id))
167-
.or(versions::created_at.lt(created_at)),
168-
)
163+
164+
match seek.after(&options.page)? {
165+
Some(SeekPayload::Date(Date { created_at, id })) => {
166+
query = query.filter(
167+
versions::created_at
168+
.eq(created_at)
169+
.and(versions::id.lt(id))
170+
.or(versions::created_at.lt(created_at)),
171+
)
172+
}
173+
Some(SeekPayload::Semver(Semver { num, id })) => {
174+
query = query.filter(
175+
versions::semver_ord
176+
.eq(semver_ord(num.clone()))
177+
.and(versions::id.lt(id))
178+
.or(versions::semver_ord.lt(semver_ord(num))),
179+
)
180+
}
181+
None => {}
169182
}
183+
170184
query = query.limit(options.per_page);
171185
}
172186

173-
query = query.order((versions::created_at.desc(), versions::id.desc()));
187+
if seek == Seek::Date {
188+
query = query.order((versions::created_at.desc(), versions::id.desc()));
189+
} else {
190+
query = query.order((versions::semver_ord.desc(), versions::id.desc()));
191+
}
174192

175193
let data: Vec<(Version, Option<User>)> = query.load(conn).await?;
176194
let mut next_page = None;
177195
if let Some(options) = options {
178-
next_page = next_seek_params(&data, options, |last| Seek::Date.to_payload(last))?
196+
next_page = next_seek_params(&data, options, |last| seek.to_payload(last))?
179197
.map(|p| req.query_with_params(p));
180198
};
181199

@@ -232,154 +250,6 @@ async fn list_by_date(
232250
})
233251
}
234252

235-
/// Seek-based pagination of versions by semver
236-
///
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-
///
240-
/// # Panics
241-
///
242-
/// This function will panic if `option` is built with `enable_pages` set to true.
243-
async fn list_by_semver(
244-
crate_id: i32,
245-
options: Option<&PaginationOptions>,
246-
params: &ListQueryParams,
247-
req: &Parts,
248-
conn: &mut AsyncPgConnection,
249-
) -> AppResult<PaginatedVersionsAndPublishers> {
250-
use seek::*;
251-
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.
269-
270-
let mut sorted_versions = IndexMap::new();
271-
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?;
281-
282-
sorted_versions
283-
.sort_unstable_by(|_, (semver_a, _, _), _, (semver_b, _, _)| semver_b.cmp(semver_a));
284-
285-
assert!(
286-
!matches!(&options.page, Page::Numeric(_)),
287-
"?page= is not supported"
288-
);
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()),
296-
)
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);
305-
}
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<_>>();
312-
versions::table
313-
.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)
318-
.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-
326-
future::ready(Ok(()))
327-
})
328-
.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-
)
339-
} else {
340-
(vec![], 0, release_tracks)
341-
}
342-
} 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)
365-
};
366-
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))
371-
};
372-
373-
Ok(PaginatedVersionsAndPublishers {
374-
data,
375-
meta: ResponseMeta {
376-
total: total as i64,
377-
next_page,
378-
release_tracks,
379-
},
380-
})
381-
}
382-
383253
mod seek {
384254
use crate::controllers::helpers::pagination::seek;
385255
use crate::models::{User, Version};
@@ -392,6 +262,7 @@ mod seek {
392262
seek!(
393263
pub enum Seek {
394264
Semver {
265+
num: String,
395266
id: i32,
396267
},
397268
Date {
@@ -406,7 +277,10 @@ mod seek {
406277
pub(crate) fn to_payload(&self, record: &(Version, Option<User>)) -> SeekPayload {
407278
let (Version { id, created_at, .. }, _) = *record;
408279
match *self {
409-
Seek::Semver => SeekPayload::Semver(Semver { id }),
280+
Seek::Semver => SeekPayload::Semver(Semver {
281+
num: record.0.num.clone(),
282+
id,
283+
}),
410284
Seek::Date => SeekPayload::Date(Date { created_at, id }),
411285
}
412286
}

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)