use crate::models::effective_partition_key::EffectivePartitionKey;
use crate::models::range::EpkRange;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PartitionKeyRange {
#[serde(rename = "id")]
pub id: String,
#[serde(rename = "minInclusive")]
pub min_inclusive: EffectivePartitionKey,
#[serde(rename = "maxExclusive")]
pub max_exclusive: EffectivePartitionKey,
#[serde(rename = "status", default)]
pub(crate) status: PartitionKeyRangeStatus,
#[serde(rename = "throughputFraction", default)]
pub throughput_fraction: f64,
#[serde(rename = "parents", skip_serializing_if = "Option::is_none")]
pub parents: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub(crate) enum PartitionKeyRangeStatus {
#[default]
Online,
Splitting,
Offline,
Split,
}
impl PartitionKeyRange {
pub fn new(
id: String,
min_inclusive: impl Into<EffectivePartitionKey>,
max_exclusive: impl Into<EffectivePartitionKey>,
) -> Self {
Self {
id,
min_inclusive: min_inclusive.into(),
max_exclusive: max_exclusive.into(),
status: PartitionKeyRangeStatus::default(),
throughput_fraction: 0.0,
parents: None,
}
}
pub(crate) fn as_range(&self) -> EpkRange<&EffectivePartitionKey> {
EpkRange {
min: &self.min_inclusive,
max: &self.max_exclusive,
is_min_inclusive: true,
is_max_inclusive: false,
}
}
pub fn get_parent_ids(&self) -> HashSet<String> {
self.parents
.as_ref()
.map(|parents| parents.iter().cloned().collect())
.unwrap_or_default()
}
}
impl PartialEq for PartitionKeyRange {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.min_inclusive == other.min_inclusive
&& self.max_exclusive == other.max_exclusive
}
}
impl Eq for PartitionKeyRange {}
impl PartialOrd for PartitionKeyRange {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PartitionKeyRange {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.min_inclusive.cmp(&other.min_inclusive)
}
}
impl Hash for PartitionKeyRange {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
self.min_inclusive.hash(state);
self.max_exclusive.hash(state);
}
}
#[derive(Debug, Deserialize)]
pub(crate) struct PkRangesResponse {
#[serde(rename = "PartitionKeyRanges")]
pub partition_key_ranges: Vec<PartitionKeyRange>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn partition_key_range_creation() {
let pkr = PartitionKeyRange::new("1".to_string(), "", "FF");
assert_eq!(pkr.id, "1");
assert_eq!(pkr.min_inclusive.as_str(), "");
assert_eq!(pkr.max_exclusive.as_str(), "FF");
}
#[test]
fn as_range() {
let pkr = PartitionKeyRange::new("1".to_string(), "00", "FF");
let range = pkr.as_range();
assert_eq!(range.min.as_str(), "00");
assert_eq!(range.max.as_str(), "FF");
assert!(range.is_min_inclusive);
assert!(!range.is_max_inclusive);
}
#[test]
fn equality_check() {
let pkr1 = PartitionKeyRange::new("1".to_string(), "00", "FF");
let mut pkr2 = PartitionKeyRange::new("1".to_string(), "00", "FF");
assert_eq!(pkr1, pkr2);
pkr2.id = "2".to_string();
assert_ne!(pkr1, pkr2);
}
#[test]
fn deserialization_ignores_stripped_metadata_fields() {
let json = r#"{
"id": "1",
"_rid": "rid123",
"_self": "self/1",
"_etag": "\"etag\"",
"_ts": 1234567890,
"minInclusive": "",
"maxExclusive": "FF",
"ridPrefix": 42,
"throughputFraction": 0.5,
"targetThroughput": 1000.0,
"status": "online",
"_lsn": 100,
"parents": ["0"],
"ownedArchivalPKRangeIds": ["arch-0"]
}"#;
let pkr: PartitionKeyRange = serde_json::from_str(json).unwrap();
assert_eq!(pkr.id, "1");
assert_eq!(pkr.min_inclusive.as_str(), "");
assert_eq!(pkr.max_exclusive.as_str(), "FF");
assert_eq!(pkr.status, PartitionKeyRangeStatus::Online);
assert_eq!(pkr.throughput_fraction, 0.5);
assert_eq!(pkr.parents.as_deref(), Some(&["0".to_string()][..]));
}
#[test]
fn cached_size_stays_small() {
const MAX_SIZE: usize = 120;
let actual = std::mem::size_of::<PartitionKeyRange>();
assert!(
actual <= MAX_SIZE,
"PartitionKeyRange grew to {actual} bytes (cap {MAX_SIZE}). \
Cached pkrange entries should stay small — re-evaluate the field set."
);
#[cfg(target_pointer_width = "64")]
{
const EXPECTED_SIZE: usize = 112;
assert_eq!(
actual, EXPECTED_SIZE,
"PartitionKeyRange size shifted from {EXPECTED_SIZE} to {actual} bytes \
without a field change. A toolchain or stdlib layout assumption may \
have moved (`String`/`Vec` representation, `Option<Vec<T>>` niche, \
alignment). Re-confirm the per-cache-entry footprint expectation, \
then bump `EXPECTED_SIZE` if the new layout is acceptable."
);
}
}
#[test]
fn range_overlap() {
let range1: EpkRange<String> =
EpkRange::new("00".to_string(), "50".to_string(), true, false);
let range2 = EpkRange::new("40".to_string(), "80".to_string(), true, false);
let range3 = EpkRange::new("60".to_string(), "90".to_string(), true, false);
assert!(EpkRange::check_overlapping(&range1, &range2));
assert!(!EpkRange::check_overlapping(&range1, &range3));
}
}