use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ShardSpec {
pub id: String,
pub descriptor: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size_estimate: Option<u64>,
}
impl ShardSpec {
pub fn whole() -> Self {
Self {
id: "0".to_string(),
descriptor: serde_json::Value::Null,
size_estimate: None,
}
}
pub fn new(id: impl Into<String>, descriptor: serde_json::Value) -> Self {
Self {
id: id.into(),
descriptor,
size_estimate: None,
}
}
pub fn with_size(mut self, size: u64) -> Self {
self.size_estimate = Some(size);
self
}
pub fn is_whole(&self) -> bool {
self.id == "0" && self.descriptor.is_null()
}
}
pub fn plan_pk_shards(key: &str, min: i64, max: i64, target: usize) -> Vec<ShardSpec> {
let target = target.max(1);
let width = (max as i128 - min as i128 + 1).max(1) as u128;
let n = (target as u128).min(width) as usize; let step = width.div_ceil(n as u128);
let mut shards = Vec::with_capacity(n);
let mut lo = min as i128;
for i in 0..n {
let mut hi = lo + step as i128;
let is_first = i == 0;
let is_last = i == n - 1;
if is_last || hi > max as i128 {
hi = max as i128; }
let descriptor = serde_json::json!({
"key": key,
"lo": lo as i64,
"hi": hi as i64,
"lo_unbounded": is_first,
"hi_unbounded": is_last,
"include_null": is_last,
});
let size = (hi - lo).max(0) as u64 + if is_last { 1 } else { 0 };
shards.push(ShardSpec::new(i.to_string(), descriptor).with_size(size));
if is_last {
break;
}
lo = hi;
}
shards
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PkShardBounds {
pub key: String,
pub lo: i64,
pub hi: i64,
pub lo_unbounded: bool,
pub hi_unbounded: bool,
pub include_null: bool,
}
impl PkShardBounds {
pub fn from_spec(spec: &ShardSpec) -> Option<Self> {
let d = &spec.descriptor;
Some(Self {
key: d.get("key")?.as_str()?.to_string(),
lo: d.get("lo")?.as_i64()?,
hi: d.get("hi")?.as_i64()?,
lo_unbounded: d
.get("lo_unbounded")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
hi_unbounded: d
.get("hi_unbounded")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
include_null: d
.get("include_null")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false),
})
}
pub fn wrap(&self, inner: &str, quote_ident: impl Fn(&str) -> String) -> String {
let key = quote_ident(&self.key);
let mut parts: Vec<String> = Vec::with_capacity(2);
if !self.lo_unbounded {
parts.push(format!("{key} >= {lo}", lo = self.lo));
}
if !self.hi_unbounded {
parts.push(format!("{key} < {hi}", hi = self.hi));
}
let range = parts.join(" AND ");
let predicate = if self.include_null {
if range.is_empty() {
"TRUE".to_string()
} else {
format!("(({range}) OR {key} IS NULL)")
}
} else if range.is_empty() {
"TRUE".to_string()
} else {
range
};
format!("SELECT * FROM ({inner}) AS _faucet_shard WHERE {predicate}")
}
}
pub fn parse_pk_shard(
shard: &ShardSpec,
connector: &str,
) -> Result<Option<PkShardBounds>, crate::FaucetError> {
if shard.is_whole() {
return Ok(None);
}
PkShardBounds::from_spec(shard).map(Some).ok_or_else(|| {
crate::FaucetError::Source(format!(
"{connector}: invalid shard descriptor: {}",
shard.descriptor
))
})
}
pub fn pk_bounds_query(inner: &str, quoted_key: &str, int_cast: &str) -> String {
format!(
"SELECT CAST(MIN({quoted_key}) AS {int_cast}) AS lo, \
CAST(MAX({quoted_key}) AS {int_cast}) AS hi \
FROM ({inner}) AS _faucet_bounds"
)
}
pub fn pk_shards_from_bounds(
key: &str,
lo: Option<i64>,
hi: Option<i64>,
target: usize,
) -> Vec<ShardSpec> {
match (lo, hi) {
(Some(lo), Some(hi)) => plan_pk_shards(key, lo, hi, target),
_ => vec![ShardSpec::whole()],
}
}
pub fn shard_hash(key: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in key.as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
pub fn plan_hash_shards(target: usize) -> Vec<ShardSpec> {
if target <= 1 {
return vec![ShardSpec::whole()];
}
(0..target)
.map(|i| {
ShardSpec::new(
i.to_string(),
serde_json::json!({ "shards": target, "index": i }),
)
})
.collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HashShard {
pub shards: usize,
pub index: usize,
}
impl HashShard {
pub fn from_spec(spec: &ShardSpec) -> Option<Self> {
let d = &spec.descriptor;
Some(Self {
shards: d.get("shards")?.as_u64()? as usize,
index: d.get("index")?.as_u64()? as usize,
})
}
pub fn contains(&self, key: &str) -> bool {
self.shards <= 1 || (shard_hash(key) % self.shards as u64) == self.index as u64
}
}
pub fn parse_hash_shard(
shard: &ShardSpec,
connector: &str,
) -> Result<Option<HashShard>, crate::FaucetError> {
if shard.is_whole() {
return Ok(None);
}
HashShard::from_spec(shard).map(Some).ok_or_else(|| {
crate::FaucetError::Source(format!(
"{connector}: invalid shard descriptor: {}",
shard.descriptor
))
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn whole_is_the_no_op_shard() {
let w = ShardSpec::whole();
assert_eq!(w.id, "0");
assert!(w.descriptor.is_null());
assert_eq!(w.size_estimate, None);
assert!(w.is_whole());
}
#[test]
fn new_and_with_size() {
let s = ShardSpec::new("3", json!({ "partition": 3 })).with_size(42);
assert_eq!(s.id, "3");
assert_eq!(s.descriptor, json!({ "partition": 3 }));
assert_eq!(s.size_estimate, Some(42));
assert!(!s.is_whole(), "a real shard is not the whole-dataset shard");
}
#[test]
fn round_trips_through_json_with_size_omitted_when_none() {
let s = ShardSpec::new("a", json!({ "prefix": "dt=2026-06-11/" }));
let text = serde_json::to_string(&s).unwrap();
assert!(
!text.contains("size_estimate"),
"None size is skipped: {text}"
);
let back: ShardSpec = serde_json::from_str(&text).unwrap();
assert_eq!(back, s);
}
#[test]
fn round_trips_with_size_present() {
let s = ShardSpec::new("b", json!({ "pk_range": [0, 1000] })).with_size(1000);
let back: ShardSpec = serde_json::from_str(&serde_json::to_string(&s).unwrap()).unwrap();
assert_eq!(back, s);
assert_eq!(back.size_estimate, Some(1000));
}
#[test]
fn id_zero_with_descriptor_is_not_whole() {
let s = ShardSpec::new("0", json!({ "partition": 0 }));
assert!(!s.is_whole());
}
fn ansi_quote(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
#[test]
fn plan_pk_shards_covers_full_range_without_gaps_or_overlap() {
let shards = plan_pk_shards("id", 0, 99, 4);
assert_eq!(shards.len(), 4);
let mut expected_lo = 0i64;
for (i, s) in shards.iter().enumerate() {
let d = &s.descriptor;
assert_eq!(d["key"], "id");
assert_eq!(d["lo"].as_i64().unwrap(), expected_lo);
let hi = d["hi"].as_i64().unwrap();
let first = i == 0;
let last = i == shards.len() - 1;
assert_eq!(d["lo_unbounded"].as_bool().unwrap(), first);
assert_eq!(d["hi_unbounded"].as_bool().unwrap(), last);
expected_lo = hi; }
}
#[test]
fn plan_pk_shards_never_more_shards_than_values() {
let shards = plan_pk_shards("pk", 5, 7, 10);
assert!(shards.len() <= 3, "got {} shards", shards.len());
assert!(
shards[0].descriptor["lo_unbounded"].as_bool().unwrap(),
"first shard is unbounded below"
);
assert!(
shards.last().unwrap().descriptor["hi_unbounded"]
.as_bool()
.unwrap(),
"last shard is unbounded above"
);
}
#[test]
fn plan_pk_shards_single_value_one_shard() {
let shards = plan_pk_shards("id", 42, 42, 8);
assert_eq!(shards.len(), 1);
assert!(shards[0].descriptor["lo_unbounded"].as_bool().unwrap());
assert!(shards[0].descriptor["hi_unbounded"].as_bool().unwrap());
}
#[test]
fn plan_pk_shards_target_zero_treated_as_one() {
let shards = plan_pk_shards("id", 0, 9, 0);
assert_eq!(shards.len(), 1);
assert_eq!(shards[0].descriptor["hi"].as_i64().unwrap(), 9);
}
#[test]
fn plan_pk_shards_full_i64_range_does_not_overflow() {
let shards = plan_pk_shards("id", i64::MIN, i64::MAX, 4);
assert_eq!(shards.len(), 4);
assert!(shards[0].descriptor["lo_unbounded"].as_bool().unwrap());
assert!(
shards.last().unwrap().descriptor["hi_unbounded"]
.as_bool()
.unwrap()
);
}
#[test]
fn pk_bounds_wrap_builds_half_open_predicate() {
let spec = ShardSpec::new(
"1",
json!({"key": "id", "lo": 100, "hi": 200, "lo_unbounded": false, "hi_unbounded": false}),
);
let b = PkShardBounds::from_spec(&spec).unwrap();
let sql = b.wrap("SELECT * FROM t", ansi_quote);
assert!(sql.contains("(SELECT * FROM t) AS _faucet_shard"));
assert!(sql.contains(r#""id" >= 100"#), "got: {sql}");
assert!(
sql.contains(r#""id" < 200"#),
"half-open upper bound: {sql}"
);
}
#[test]
fn pk_bounds_wrap_first_shard_has_no_lower_bound() {
let spec = ShardSpec::new(
"0",
json!({"key": "id", "lo": 0, "hi": 100, "lo_unbounded": true, "hi_unbounded": false}),
);
let b = PkShardBounds::from_spec(&spec).unwrap();
let sql = b.wrap("SELECT * FROM t", ansi_quote);
assert!(sql.contains(r#""id" < 100"#), "upper bound present: {sql}");
assert!(!sql.contains(">="), "first shard has no lower floor: {sql}");
}
#[test]
fn pk_bounds_wrap_last_shard_has_no_upper_bound() {
let spec = ShardSpec::new(
"2",
json!({"key": "id", "lo": 200, "hi": 300, "lo_unbounded": false, "hi_unbounded": true}),
);
let b = PkShardBounds::from_spec(&spec).unwrap();
let sql = b.wrap("SELECT * FROM t", ansi_quote);
assert!(sql.contains(r#""id" >= 200"#), "lower bound present: {sql}");
assert!(
!sql.contains(" < ") && !sql.contains("<="),
"last shard has no upper bound: {sql}"
);
}
#[test]
fn pk_bounds_wrap_uses_the_supplied_dialect_quoting() {
let spec = ShardSpec::new(
"0",
json!({"key": "id", "lo": 0, "hi": 1, "lo_unbounded": false, "hi_unbounded": false}),
);
let b = PkShardBounds::from_spec(&spec).unwrap();
let backtick = b.wrap("SELECT 1", |k| format!("`{k}`"));
assert!(backtick.contains("`id` >= 0"), "got: {backtick}");
let bracket = b.wrap("SELECT 1", |k| format!("[{k}]"));
assert!(bracket.contains("[id] >= 0"), "got: {bracket}");
}
#[test]
fn pk_bounds_from_spec_rejects_malformed_descriptor() {
let spec = ShardSpec::new("0", json!({"key": "id"})); assert!(PkShardBounds::from_spec(&spec).is_none());
assert!(PkShardBounds::from_spec(&ShardSpec::whole()).is_none());
}
#[test]
fn exactly_one_shard_includes_null() {
let shards = plan_pk_shards("id", 0, 99, 5);
let null_owners: Vec<usize> = shards
.iter()
.enumerate()
.filter(|(_, s)| s.descriptor["include_null"].as_bool().unwrap_or(false))
.map(|(i, _)| i)
.collect();
assert_eq!(
null_owners,
vec![shards.len() - 1],
"exactly the last shard owns NULL keys"
);
}
#[test]
fn single_shard_plan_still_owns_null() {
let shards = plan_pk_shards("id", 7, 7, 4);
assert_eq!(shards.len(), 1);
assert!(shards[0].descriptor["include_null"].as_bool().unwrap());
}
#[test]
fn last_shard_wrap_emits_is_null_clause() {
let shards = plan_pk_shards("id", 0, 99, 3);
let last = PkShardBounds::from_spec(shards.last().unwrap()).unwrap();
let sql = last.wrap("SELECT * FROM t", ansi_quote);
assert!(
sql.contains(r#""id" IS NULL"#),
"last shard must match NULL keys: {sql}"
);
assert!(sql.contains(" OR "), "NULL clause OR'd with range: {sql}");
}
#[test]
fn non_last_shard_wrap_omits_is_null_clause() {
let shards = plan_pk_shards("id", 0, 99, 3);
let first = PkShardBounds::from_spec(&shards[0]).unwrap();
let sql = first.wrap("SELECT * FROM t", ansi_quote);
assert!(
!sql.contains("IS NULL"),
"non-last shard must not match NULL keys: {sql}"
);
}
#[test]
fn predicate_coverage_complete_and_non_overlapping() {
let (min, max, target) = (0i64, 19i64, 4usize);
let bounds: Vec<PkShardBounds> = plan_pk_shards("k", min, max, target)
.iter()
.map(|s| PkShardBounds::from_spec(s).unwrap())
.collect();
let matches_key = |b: &PkShardBounds, key: i64| -> bool {
let lower = b.lo_unbounded || key >= b.lo;
let upper = b.hi_unbounded || key < b.hi;
lower && upper
};
for key in (min - 50)..=(max + 50) {
let matches = bounds.iter().filter(|b| matches_key(b, key)).count();
assert_eq!(matches, 1, "key {key} matched {matches} shards (want 1)");
}
let null_matches = bounds.iter().filter(|b| b.include_null).count();
assert_eq!(null_matches, 1, "NULL keys must match exactly one shard");
}
#[test]
fn single_shard_wrap_selects_whole_dataset_including_null() {
let shards = plan_pk_shards("id", 7, 7, 1);
assert_eq!(shards.len(), 1);
let b = PkShardBounds::from_spec(&shards[0]).unwrap();
let sql = b.wrap("SELECT * FROM t", ansi_quote);
assert!(sql.contains("WHERE TRUE"), "whole-dataset predicate: {sql}");
assert!(!sql.contains(">="), "no bounds on a lone shard: {sql}");
}
#[test]
fn shard_hash_is_deterministic() {
assert_eq!(
shard_hash("data/part-001.jsonl"),
shard_hash("data/part-001.jsonl")
);
assert_ne!(shard_hash("a"), shard_hash("b"));
}
#[test]
fn plan_hash_shards_returns_target_disjoint_shards() {
let shards = plan_hash_shards(3);
assert_eq!(shards.len(), 3);
for (i, s) in shards.iter().enumerate() {
assert_eq!(s.descriptor["shards"], 3);
assert_eq!(s.descriptor["index"], i);
assert_eq!(s.id, i.to_string());
}
}
#[test]
fn plan_hash_shards_target_one_is_whole() {
let shards = plan_hash_shards(1);
assert_eq!(shards.len(), 1);
assert!(shards[0].is_whole());
let shards = plan_hash_shards(0);
assert_eq!(shards.len(), 1);
assert!(shards[0].is_whole());
}
#[test]
fn hash_shards_partition_keys_disjointly_and_completely() {
let keys: Vec<String> = (0..200).map(|i| format!("data/obj-{i}.jsonl")).collect();
let n = 4;
let members: Vec<HashShard> = plan_hash_shards(n)
.iter()
.map(|s| HashShard::from_spec(s).unwrap())
.collect();
for key in &keys {
let owners = members.iter().filter(|m| m.contains(key)).count();
assert_eq!(owners, 1, "key {key} owned by {owners} shards (want 1)");
}
}
#[test]
fn hash_shard_degenerate_single_shard_owns_everything() {
let m = HashShard {
shards: 1,
index: 0,
};
assert!(m.contains("anything"));
}
#[test]
fn hash_shard_from_spec_rejects_malformed_descriptor() {
assert!(HashShard::from_spec(&ShardSpec::new("0", json!({ "index": 0 }))).is_none());
assert!(HashShard::from_spec(&ShardSpec::new("0", json!({ "shards": 4 }))).is_none());
assert!(HashShard::from_spec(&ShardSpec::whole()).is_none());
}
#[test]
fn hash_shard_round_trips_plan_descriptors() {
for spec in plan_hash_shards(5) {
let m = HashShard::from_spec(&spec).unwrap();
assert_eq!(m.shards, 5);
assert_eq!(m.index, spec.id.parse::<usize>().unwrap());
}
}
#[test]
fn parse_pk_shard_whole_clears_and_real_parses() {
assert!(parse_pk_shard(&ShardSpec::whole(), "t").unwrap().is_none());
let spec = &plan_pk_shards("id", 0, 99, 3)[1];
let bounds = parse_pk_shard(spec, "t").unwrap().unwrap();
assert_eq!(bounds.key, "id");
}
#[test]
fn parse_pk_shard_malformed_names_connector() {
let bad = ShardSpec::new("0", json!({ "key": "id" }));
let err = parse_pk_shard(&bad, "mysql").unwrap_err();
assert!(err.to_string().contains("mysql"), "got: {err}");
}
#[test]
fn parse_hash_shard_whole_clears_and_real_parses() {
assert!(
parse_hash_shard(&ShardSpec::whole(), "t")
.unwrap()
.is_none()
);
let spec = &plan_hash_shards(3)[2];
let m = parse_hash_shard(spec, "t").unwrap().unwrap();
assert_eq!((m.shards, m.index), (3, 2));
}
#[test]
fn parse_hash_shard_malformed_names_connector() {
let bad = ShardSpec::new("0", json!({ "index": 0 }));
let err = parse_hash_shard(&bad, "gcs").unwrap_err();
assert!(err.to_string().contains("gcs"), "got: {err}");
}
#[test]
fn pk_bounds_query_shapes_the_probe() {
let sql = pk_bounds_query("SELECT * FROM t", "\"id\"", "BIGINT");
assert_eq!(
sql,
"SELECT CAST(MIN(\"id\") AS BIGINT) AS lo, CAST(MAX(\"id\") AS BIGINT) AS hi \
FROM (SELECT * FROM t) AS _faucet_bounds"
);
}
#[test]
fn pk_shards_from_bounds_plans_or_degrades() {
let shards = pk_shards_from_bounds("id", Some(0), Some(99), 4);
assert_eq!(shards.len(), 4);
for (lo, hi) in [(None, None), (Some(1), None), (None, Some(1))] {
let shards = pk_shards_from_bounds("id", lo, hi, 4);
assert_eq!(shards.len(), 1);
assert!(shards[0].is_whole());
}
}
}