Skip to content

Commit df35ee9

Browse files
authored
PaginatedByTimeAndId (#7842)
Extracted from #7339 for use in #7277. This PR does not use the pagination helper in any endpoints. There are proper integration tests like `test_audit_log_list` in #7339 demonstrating the ordering and cursor work as expected.
1 parent a009f5e commit df35ee9

File tree

3 files changed

+244
-0
lines changed

3 files changed

+244
-0
lines changed

common/src/api/external/http_pagination.rs

+171
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ use crate::api::external::Name;
4545
use crate::api::external::NameOrId;
4646
use crate::api::external::ObjectIdentity;
4747
use crate::api::external::PaginationOrder;
48+
use chrono::DateTime;
49+
use chrono::Utc;
4850
use dropshot::HttpError;
4951
use dropshot::PaginationParams;
5052
use dropshot::RequestContext;
@@ -421,6 +423,57 @@ impl<T: Clone + Debug + DeserializeOwned + JsonSchema + PartialEq + Serialize>
421423
}
422424
}
423425

426+
/// Query parameters for pagination by timestamp and ID
427+
pub type PaginatedByTimeAndId<Selector = ()> = PaginationParams<
428+
ScanByTimeAndId<Selector>,
429+
PageSelectorByTimeAndId<Selector>,
430+
>;
431+
/// Page selector for pagination by timestamp and ID
432+
pub type PageSelectorByTimeAndId<Selector = ()> =
433+
PageSelector<ScanByTimeAndId<Selector>, (DateTime<Utc>, Uuid)>;
434+
435+
/// Scan parameters for resources that support scanning by (timestamp, id)
436+
#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
437+
pub struct ScanByTimeAndId<Selector = ()> {
438+
#[serde(default = "default_ts_id_sort_mode")]
439+
sort_by: TimeAndIdSortMode,
440+
441+
#[serde(flatten)]
442+
pub selector: Selector,
443+
}
444+
445+
/// Supported set of sort modes for scanning by timestamp and ID
446+
#[derive(Copy, Clone, Debug, Deserialize, JsonSchema, PartialEq, Serialize)]
447+
#[serde(rename_all = "snake_case")]
448+
pub enum TimeAndIdSortMode {
449+
/// sort in increasing order of timestamp and ID, i.e., earliest first
450+
Ascending,
451+
/// sort in increasing order of timestamp and ID, i.e., most recent first
452+
Descending,
453+
}
454+
455+
fn default_ts_id_sort_mode() -> TimeAndIdSortMode {
456+
TimeAndIdSortMode::Ascending
457+
}
458+
459+
impl<T: Clone + Debug + DeserializeOwned + JsonSchema + PartialEq + Serialize>
460+
ScanParams for ScanByTimeAndId<T>
461+
{
462+
type MarkerValue = (DateTime<Utc>, Uuid);
463+
fn direction(&self) -> PaginationOrder {
464+
match self.sort_by {
465+
TimeAndIdSortMode::Ascending => PaginationOrder::Ascending,
466+
TimeAndIdSortMode::Descending => PaginationOrder::Descending,
467+
}
468+
}
469+
fn from_query(p: &PaginatedByTimeAndId<T>) -> Result<&Self, HttpError> {
470+
Ok(match p.page {
471+
WhichPage::First(ref scan_params) => scan_params,
472+
WhichPage::Next(PageSelector { ref scan, .. }) => scan,
473+
})
474+
}
475+
}
476+
424477
#[cfg(test)]
425478
mod test {
426479
use super::IdSortMode;
@@ -432,14 +485,18 @@ mod test {
432485
use super::PageSelectorById;
433486
use super::PageSelectorByName;
434487
use super::PageSelectorByNameOrId;
488+
use super::PageSelectorByTimeAndId;
435489
use super::PaginatedBy;
436490
use super::PaginatedById;
437491
use super::PaginatedByName;
438492
use super::PaginatedByNameOrId;
493+
use super::PaginatedByTimeAndId;
439494
use super::ScanById;
440495
use super::ScanByName;
441496
use super::ScanByNameOrId;
497+
use super::ScanByTimeAndId;
442498
use super::ScanParams;
499+
use super::TimeAndIdSortMode;
443500
use super::data_page_params_with_limit;
444501
use super::marker_for_id;
445502
use super::marker_for_name;
@@ -448,6 +505,8 @@ mod test {
448505
use crate::api::external::IdentityMetadata;
449506
use crate::api::external::ObjectIdentity;
450507
use crate::api::external::http_pagination::name_or_id_pagination;
508+
use chrono::DateTime;
509+
use chrono::TimeZone;
451510
use chrono::Utc;
452511
use dropshot::PaginationOrder;
453512
use dropshot::PaginationParams;
@@ -486,6 +545,10 @@ mod test {
486545
"page selector, scan by name or id",
487546
schema_for!(PageSelectorByNameOrId),
488547
),
548+
(
549+
"page selector, scan by time and id",
550+
schema_for!(PageSelectorByTimeAndId),
551+
),
489552
];
490553

491554
let mut found_output = String::new();
@@ -515,8 +578,14 @@ mod test {
515578
sort_by: NameOrIdSortMode::IdAscending,
516579
selector: (),
517580
};
581+
let scan_by_time_and_id = ScanByTimeAndId::<()> {
582+
sort_by: TimeAndIdSortMode::Ascending,
583+
selector: (),
584+
};
518585
let id: Uuid = "61a78113-d3c6-4b35-a410-23e9eae64328".parse().unwrap();
519586
let name: Name = "bort".parse().unwrap();
587+
let time: DateTime<Utc> =
588+
Utc.with_ymd_and_hms(2025, 3, 20, 10, 30, 45).unwrap();
520589
let examples = vec![
521590
// scan parameters only
522591
("scan by id ascending", to_string_pretty(&scan_by_id).unwrap()),
@@ -532,6 +601,14 @@ mod test {
532601
"scan by name or id, using name ascending",
533602
to_string_pretty(&scan_by_nameid_name).unwrap(),
534603
),
604+
(
605+
"scan by name or id, using name ascending",
606+
to_string_pretty(&scan_by_nameid_name).unwrap(),
607+
),
608+
(
609+
"scan by time and id, ascending",
610+
to_string_pretty(&scan_by_time_and_id).unwrap(),
611+
),
535612
// page selectors
536613
(
537614
"page selector: by id ascending",
@@ -565,6 +642,14 @@ mod test {
565642
})
566643
.unwrap(),
567644
),
645+
(
646+
"page selector: by time and id, ascending",
647+
to_string_pretty(&PageSelectorByTimeAndId {
648+
scan: scan_by_time_and_id,
649+
last_seen: (time, id),
650+
})
651+
.unwrap(),
652+
),
568653
];
569654

570655
let mut found_output = String::new();
@@ -834,6 +919,7 @@ mod test {
834919
let thing0_marker = NameOrId::Id(list[0].identity.id);
835920
let thinglast_id = list[list.len() - 1].identity.id;
836921
let thinglast_marker = NameOrId::Id(list[list.len() - 1].identity.id);
922+
837923
let (p0, p1) = test_scan_param_common(
838924
&list,
839925
&scan,
@@ -871,4 +957,89 @@ mod test {
871957
assert_eq!(data_page.direction, PaginationOrder::Ascending);
872958
assert_eq!(data_page.limit, limit);
873959
}
960+
961+
#[test]
962+
fn test_scan_by_time_and_id() {
963+
let scan = ScanByTimeAndId {
964+
sort_by: TimeAndIdSortMode::Ascending,
965+
selector: (),
966+
};
967+
968+
let list = list_of_things();
969+
let item0_time = list[0].identity.time_created;
970+
let item0_id = list[0].identity.id;
971+
let item0_marker = (item0_time, item0_id);
972+
973+
let last_idx = list.len() - 1;
974+
let item_last_time = list[last_idx].identity.time_created;
975+
let item_last_id = list[last_idx].identity.id;
976+
let item_last_marker = (item_last_time, item_last_id);
977+
978+
let marker_fn =
979+
|_: &ScanByTimeAndId, item: &MyThing| -> (DateTime<Utc>, Uuid) {
980+
(item.identity.time_created, item.identity.id)
981+
};
982+
let (p0, p1) = test_scan_param_common(
983+
&list,
984+
&scan,
985+
"sort_by=ascending",
986+
&item0_marker,
987+
&item_last_marker,
988+
&scan,
989+
&marker_fn,
990+
);
991+
992+
assert_eq!(scan.direction(), PaginationOrder::Ascending);
993+
994+
// Verify data pages based on the query params.
995+
let limit = NonZeroU32::new(123).unwrap();
996+
let data_page = data_page_params_with_limit(limit, &p0).unwrap();
997+
assert_eq!(data_page.marker, None);
998+
assert_eq!(data_page.direction, PaginationOrder::Ascending);
999+
assert_eq!(data_page.limit, limit);
1000+
1001+
let data_page = data_page_params_with_limit(limit, &p1).unwrap();
1002+
assert_eq!(data_page.marker, Some(&item_last_marker));
1003+
assert_eq!(data_page.direction, PaginationOrder::Ascending);
1004+
assert_eq!(data_page.limit, limit);
1005+
1006+
// test descending too, why not (it caught a mistake!)
1007+
let scan_desc = ScanByTimeAndId {
1008+
sort_by: TimeAndIdSortMode::Descending,
1009+
selector: (),
1010+
};
1011+
let (p0, p1) = test_scan_param_common(
1012+
&list,
1013+
&scan_desc,
1014+
"sort_by=descending",
1015+
&item0_marker,
1016+
&item_last_marker,
1017+
&scan,
1018+
&marker_fn,
1019+
);
1020+
assert_eq!(scan_desc.direction(), PaginationOrder::Descending);
1021+
1022+
// Verify data pages based on the query params.
1023+
let limit = NonZeroU32::new(123).unwrap();
1024+
let data_page = data_page_params_with_limit(limit, &p0).unwrap();
1025+
assert_eq!(data_page.marker, None);
1026+
assert_eq!(data_page.direction, PaginationOrder::Descending);
1027+
assert_eq!(data_page.limit, limit);
1028+
1029+
let data_page = data_page_params_with_limit(limit, &p1).unwrap();
1030+
assert_eq!(data_page.marker, Some(&item_last_marker));
1031+
assert_eq!(data_page.direction, PaginationOrder::Descending);
1032+
assert_eq!(data_page.limit, limit);
1033+
1034+
// Test error case
1035+
let error = serde_urlencoded::from_str::<PaginatedByTimeAndId>(
1036+
"sort_by=nothing",
1037+
)
1038+
.unwrap_err();
1039+
1040+
assert_eq!(
1041+
error.to_string(),
1042+
"unknown variant `nothing`, expected `ascending` or `descending`"
1043+
);
1044+
}
8741045
}

common/tests/output/pagination-examples.txt

+16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ example pagination parameters: scan by name or id, using name ascending
1414
{
1515
"sort_by": "name_ascending"
1616
}
17+
example pagination parameters: scan by name or id, using name ascending
18+
{
19+
"sort_by": "name_ascending"
20+
}
21+
example pagination parameters: scan by time and id, ascending
22+
{
23+
"sort_by": "ascending"
24+
}
1725
example pagination parameters: page selector: by id ascending
1826
{
1927
"sort_by": "id_ascending",
@@ -34,3 +42,11 @@ example pagination parameters: page selector: by name or id, using id ascending
3442
"sort_by": "name_ascending",
3543
"last_seen": "bort"
3644
}
45+
example pagination parameters: page selector: by time and id, ascending
46+
{
47+
"sort_by": "ascending",
48+
"last_seen": [
49+
"2025-03-20T10:30:45Z",
50+
"61a78113-d3c6-4b35-a410-23e9eae64328"
51+
]
52+
}

common/tests/output/pagination-schema.txt

+57
Original file line numberDiff line numberDiff line change
@@ -279,3 +279,60 @@ schema for pagination parameters: page selector, scan by name or id
279279
}
280280
}
281281
}
282+
schema for pagination parameters: page selector, scan by time and id
283+
{
284+
"$schema": "http://json-schema.org/draft-07/schema#",
285+
"title": "PageSelector_for_ScanByTimeAndId_for_Null_and_Tuple_of_DateTime_and_Uuid",
286+
"description": "Specifies which page of results we're on\n\nThis type is generic over the different scan modes that we support.",
287+
"type": "object",
288+
"required": [
289+
"last_seen"
290+
],
291+
"properties": {
292+
"last_seen": {
293+
"description": "value of the marker field last seen by the client",
294+
"type": "array",
295+
"items": [
296+
{
297+
"type": "string",
298+
"format": "date-time"
299+
},
300+
{
301+
"type": "string",
302+
"format": "uuid"
303+
}
304+
],
305+
"maxItems": 2,
306+
"minItems": 2
307+
},
308+
"sort_by": {
309+
"default": "ascending",
310+
"allOf": [
311+
{
312+
"$ref": "#/definitions/TimeAndIdSortMode"
313+
}
314+
]
315+
}
316+
},
317+
"definitions": {
318+
"TimeAndIdSortMode": {
319+
"description": "Supported set of sort modes for scanning by timestamp and ID",
320+
"oneOf": [
321+
{
322+
"description": "sort in increasing order of timestamp and ID, i.e., earliest first",
323+
"type": "string",
324+
"enum": [
325+
"ascending"
326+
]
327+
},
328+
{
329+
"description": "sort in increasing order of timestamp and ID, i.e., most recent first",
330+
"type": "string",
331+
"enum": [
332+
"descending"
333+
]
334+
}
335+
]
336+
}
337+
}
338+
}

0 commit comments

Comments
 (0)