use crate::{
db::{
cursor::{ContinuationSignature, CursorBoundary, token::TokenWireError},
query::plan::OrderDirection,
},
types::EntityTag,
};
use crate::db::cursor::token::{decode_scalar_token, encode_scalar_token};
pub(in crate::db::cursor) const MAX_SCALAR_CURSOR_ORDER_TERMS: usize = 32;
pub(in crate::db::cursor) const MAX_SCALAR_CURSOR_LOGICAL_BOUNDARY_BYTES: usize = 4 * 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) enum ScalarPageMode {
Live,
Exhaustive,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct ScalarOrderTermContract {
identity: String,
direction: OrderDirection,
}
impl ScalarOrderTermContract {
#[must_use]
pub(in crate::db) fn new(identity: impl Into<String>, direction: OrderDirection) -> Self {
Self {
identity: identity.into(),
direction,
}
}
#[must_use]
pub(in crate::db) const fn identity(&self) -> &str {
self.identity.as_str()
}
#[must_use]
pub(in crate::db) const fn direction(&self) -> OrderDirection {
self.direction
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) struct ScalarPageTokenAuthority {
database_incarnation: [u8; 16],
accepted_root_revision: u64,
accepted_root_fingerprint_method: u8,
accepted_root_fingerprint: [u8; 32],
accepted_entity_fingerprint: [u8; 16],
entity_tag: EntityTag,
}
impl ScalarPageTokenAuthority {
#[must_use]
pub(in crate::db) const fn new(
database_incarnation: [u8; 16],
accepted_root_revision: u64,
accepted_root_fingerprint_method: u8,
accepted_root_fingerprint: [u8; 32],
accepted_entity_fingerprint: [u8; 16],
entity_tag: EntityTag,
) -> Self {
Self {
database_incarnation,
accepted_root_revision,
accepted_root_fingerprint_method,
accepted_root_fingerprint,
accepted_entity_fingerprint,
entity_tag,
}
}
#[must_use]
pub(in crate::db) const fn database_incarnation(self) -> [u8; 16] {
self.database_incarnation
}
#[must_use]
pub(in crate::db) const fn accepted_root_revision(self) -> u64 {
self.accepted_root_revision
}
#[must_use]
pub(in crate::db) const fn accepted_root_fingerprint_method(self) -> u8 {
self.accepted_root_fingerprint_method
}
#[must_use]
pub(in crate::db) const fn accepted_root_fingerprint(self) -> [u8; 32] {
self.accepted_root_fingerprint
}
#[must_use]
pub(in crate::db) const fn accepted_entity_fingerprint(self) -> [u8; 16] {
self.accepted_entity_fingerprint
}
#[must_use]
pub(in crate::db) const fn entity_tag(self) -> EntityTag {
self.entity_tag
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::db) struct ScalarPageTokenWindow {
initial_offset: u32,
total_limit: Option<u32>,
envelope_identity: u64,
}
impl ScalarPageTokenWindow {
#[must_use]
pub(in crate::db) const fn new(
initial_offset: u32,
total_limit: Option<u32>,
envelope_identity: u64,
) -> Self {
Self {
initial_offset,
total_limit,
envelope_identity,
}
}
#[must_use]
pub(in crate::db) const fn initial_offset(self) -> u32 {
self.initial_offset
}
#[must_use]
pub(in crate::db) const fn total_limit(self) -> Option<u32> {
self.total_limit
}
#[must_use]
pub(in crate::db) const fn envelope_identity(self) -> u64 {
self.envelope_identity
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct ScalarPageTokenProgress {
last_emitted_logical: Option<CursorBoundary>,
last_consumed_physical: Option<Vec<u8>>,
unconsumed_lookahead: Option<Vec<u8>>,
matching_rows_skipped: u64,
rows_emitted: u64,
}
impl ScalarPageTokenProgress {
#[must_use]
pub(in crate::db) const fn new(
last_emitted_logical: Option<CursorBoundary>,
last_consumed_physical: Option<Vec<u8>>,
unconsumed_lookahead: Option<Vec<u8>>,
matching_rows_skipped: u64,
rows_emitted: u64,
) -> Self {
Self {
last_emitted_logical,
last_consumed_physical,
unconsumed_lookahead,
matching_rows_skipped,
rows_emitted,
}
}
#[must_use]
pub(in crate::db) const fn last_emitted_logical(&self) -> Option<&CursorBoundary> {
self.last_emitted_logical.as_ref()
}
#[must_use]
pub(in crate::db) fn last_consumed_physical(&self) -> Option<&[u8]> {
self.last_consumed_physical.as_deref()
}
#[must_use]
pub(in crate::db) fn unconsumed_lookahead(&self) -> Option<&[u8]> {
self.unconsumed_lookahead.as_deref()
}
#[must_use]
pub(in crate::db) const fn matching_rows_skipped(&self) -> u64 {
self.matching_rows_skipped
}
#[must_use]
pub(in crate::db) const fn rows_emitted(&self) -> u64 {
self.rows_emitted
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db) struct ScalarPageToken {
mode: ScalarPageMode,
signature: ContinuationSignature,
authority: ScalarPageTokenAuthority,
window: ScalarPageTokenWindow,
order_terms: Vec<ScalarOrderTermContract>,
progress: ScalarPageTokenProgress,
}
impl ScalarPageToken {
#[must_use]
pub(in crate::db) const fn new(
mode: ScalarPageMode,
signature: ContinuationSignature,
authority: ScalarPageTokenAuthority,
window: ScalarPageTokenWindow,
order_terms: Vec<ScalarOrderTermContract>,
progress: ScalarPageTokenProgress,
) -> Self {
Self {
mode,
signature,
authority,
window,
order_terms,
progress,
}
}
pub(in crate::db) fn encode(&self, mac_key: &[u8; 32]) -> Result<Vec<u8>, TokenWireError> {
encode_scalar_token(self, mac_key)
}
pub(in crate::db) fn decode(bytes: &[u8], mac_key: &[u8; 32]) -> Result<Self, TokenWireError> {
decode_scalar_token(bytes, mac_key)
}
#[must_use]
pub(in crate::db) const fn mode(&self) -> ScalarPageMode {
self.mode
}
#[must_use]
pub(in crate::db) const fn signature(&self) -> ContinuationSignature {
self.signature
}
#[must_use]
pub(in crate::db) const fn authority(&self) -> ScalarPageTokenAuthority {
self.authority
}
#[must_use]
pub(in crate::db) const fn window(&self) -> ScalarPageTokenWindow {
self.window
}
#[must_use]
pub(in crate::db) const fn order_terms(&self) -> &[ScalarOrderTermContract] {
self.order_terms.as_slice()
}
#[must_use]
pub(in crate::db) const fn progress(&self) -> &ScalarPageTokenProgress {
&self.progress
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
db::{cursor::CursorBoundarySlot, query::plan::OrderDirection},
value::Value,
};
fn token() -> ScalarPageToken {
ScalarPageToken::new(
ScalarPageMode::Live,
ContinuationSignature::from_bytes([0x11; 32]),
ScalarPageTokenAuthority::new(
[0x22; 16],
7,
1,
[0x33; 32],
[0x44; 16],
EntityTag::new(9),
),
ScalarPageTokenWindow::new(0, Some(2_048), 0x55),
vec![
ScalarOrderTermContract::new("score", OrderDirection::Desc),
ScalarOrderTermContract::new("nullable", OrderDirection::Asc),
ScalarOrderTermContract::new("id", OrderDirection::Desc),
],
ScalarPageTokenProgress::new(
Some(CursorBoundary {
slots: vec![
CursorBoundarySlot::Present(Value::Nat64(10)),
CursorBoundarySlot::Missing,
CursorBoundarySlot::Present(Value::Text("id-7".to_string())),
],
}),
Some(vec![1, 2, 3]),
Some(vec![4, 5, 6]),
3,
1_024,
),
)
}
#[test]
fn authenticated_scalar_token_round_trips_complete_mixed_order_contract() {
let key = [0x66; 32];
let token = token();
let encoded = token.encode(&key).expect("bounded token should encode");
let decoded = ScalarPageToken::decode(encoded.as_slice(), &key)
.expect("authenticated token should decode");
assert_eq!(decoded, token);
}
#[test]
fn authenticated_scalar_token_rejects_tampering_and_wrong_database_key() {
let key = [0x66; 32];
let mut encoded = token().encode(&key).expect("bounded token should encode");
let payload_index = 12;
encoded[payload_index] ^= 0x80;
assert!(ScalarPageToken::decode(encoded.as_slice(), &key).is_err());
assert!(
ScalarPageToken::decode(
token()
.encode(&key)
.expect("bounded token should encode")
.as_slice(),
&[0x77; 32],
)
.is_err()
);
}
#[test]
fn authenticated_scalar_token_rejects_oversized_order_contracts() {
let mut order_terms = Vec::new();
for index in 0..=MAX_SCALAR_CURSOR_ORDER_TERMS {
order_terms.push(ScalarOrderTermContract::new(
format!("field_{index}"),
OrderDirection::Asc,
));
}
let mut token = token();
token.order_terms = order_terms;
assert!(token.encode(&[0x66; 32]).is_err());
}
#[test]
fn authenticated_scalar_token_rejects_oversized_logical_boundaries() {
let mut token = token();
token.progress.last_emitted_logical = Some(CursorBoundary {
slots: vec![CursorBoundarySlot::Present(Value::Text(
"x".repeat(MAX_SCALAR_CURSOR_LOGICAL_BOUNDARY_BYTES + 1),
))],
});
assert!(token.encode(&[0x66; 32]).is_err());
}
}