Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pgdog-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pgdog-vector = { path = "../pgdog-vector" }
once_cell = "*"
schemars.workspace = true
indexmap.workspace = true
derive_more = { version = "2", features = ["from_str"] }
derive_more = { version = "2", features = ["display", "from_str"] }

[dev-dependencies]
tempfile = "3.23.0"
38 changes: 35 additions & 3 deletions pgdog-config/src/sharding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,21 @@ pub enum Hasher {
/// Data type of the sharding column.
///
/// <https://docs.pgdog.dev/configuration/pgdog.toml/sharded_tables/#data_type>
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default, Copy, Eq, Hash, JsonSchema)]
#[derive(
Serialize,
Deserialize,
PartialEq,
Debug,
Clone,
Default,
Copy,
Eq,
Hash,
JsonSchema,
derive_more::Display,
)]
#[serde(rename_all = "snake_case")]
#[display(rename_all = "snake_case")]
pub enum DataType {
/// 64-bit integer (default).
#[default]
Expand Down Expand Up @@ -253,8 +266,24 @@ pub struct ShardedMappingList {
}

/// A range rule: routes values in `[start, end)` to `shard` (`PARTITION BY RANGE`).
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Default, Hash, Eq, JsonSchema)]
#[derive(
Serialize,
Deserialize,
PartialEq,
Debug,
Clone,
Default,
Hash,
Eq,
JsonSchema,
derive_more::Display,
)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
#[display(
"[{}, {}) -> shard={shard}",
match start { Some(v) => v.to_string(), None => "-inf".to_string() },
match end { Some(v) => v.to_string(), None => "+inf".to_string() }
)]
pub struct ShardedMappingRange {
/// Target shard number for matched queries.
pub shard: usize,
Expand All @@ -265,7 +294,9 @@ pub struct ShardedMappingRange {
}

/// A sharding key value that can be an integer, UUID, or string.
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone, Eq, Hash, JsonSchema)]
#[derive(
Serialize, Deserialize, PartialEq, Debug, Clone, Eq, Hash, JsonSchema, derive_more::Display,
)]
#[serde(untagged)]
pub enum FlexibleType {
/// 64-bit signed integer.
Expand All @@ -274,6 +305,7 @@ pub enum FlexibleType {
#[schemars(with = "String")]
Uuid(Uuid),
/// Text string.
#[display("'{_0}'")]
String(String),
}

Expand Down
104 changes: 61 additions & 43 deletions pgdog/src/backend/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,32 @@ use crate::frontend::router::sharding::mapping::compare_flexible_type;
#[derive(Debug, Display)]
pub enum ValidationError {
/// A shard number in a mapping entry exceeds the available shard count.
#[display("shard {shard} is out of range (num_shards={num_shards})")]
#[display("shard={shard} exceeds the configured shard count ({num_shards})")]
ShardOutOfRange { shard: usize, num_shards: usize },

/// A range entry has neither `start` nor `end` defined.
#[display("range for shard {shard} has neither start nor end defined")]
#[display("shard={shard} range must define a start, an end, or both")]
RangeNoBounds { shard: usize },

/// A range entry's `start` bound is greater than its `end` bound.
#[display("range for shard {shard} is inverted: start {start:?} > end {end:?}")]
#[display(
"shard={shard} range is invalid: start ({start}) must not be greater than end ({end})"
)]
RangeInverted {
shard: usize,
start: FlexibleType,
end: FlexibleType,
},

/// Two or more range entries overlap.
#[display("ranges: {range1:?} & {range2:?} overlap")]
#[display("shard ranges overlap: {range1} and {range2}")]
RangesOverlap {
range1: ShardedMappingRange,
range2: ShardedMappingRange,
},

/// A value in a list entry or range bound does not match the column's `data_type`.
#[display("value {value:?} is incompatible with data_type {data_type:?}")]
#[display("value {value} is not a valid {data_type} value")]
IncompatibleType {
value: FlexibleType,
data_type: DataType,
Expand Down Expand Up @@ -181,7 +183,6 @@ fn type_compatible(value: &FlexibleType, data_type: DataType) -> bool {
mod tests {
use super::*;
use pgdog_config::{FlexibleType, ShardedMappingList, ShardedMappingRange};
use std::assert_matches;

fn range(shard: usize, start: Option<i64>, end: Option<i64>) -> ShardedMappingConfig {
ShardedMappingConfig::Range(ShardedMappingRange {
Expand Down Expand Up @@ -215,26 +216,21 @@ mod tests {
#[test]
fn out_of_bounds() {
// shard == num_shards is out of range (0-indexed); all three variants
assert_matches!(
check_shard_range(&range(5, Some(0), Some(100)), 3),
Some(ValidationError::ShardOutOfRange {
shard: 5,
num_shards: 3
})
assert_eq!(
check_shard_range(&range(5, Some(0), Some(100)), 3)
.unwrap()
.to_string(),
"shard=5 exceeds the configured shard count (3)"
);
assert_matches!(
check_shard_range(&list(10, vec![FlexibleType::Integer(1)]), 3),
Some(ValidationError::ShardOutOfRange {
shard: 10,
num_shards: 3
})
assert_eq!(
check_shard_range(&list(10, vec![FlexibleType::Integer(1)]), 3)
.unwrap()
.to_string(),
"shard=10 exceeds the configured shard count (3)"
);
assert_matches!(
check_shard_range(&default_shard(3), 3),
Some(ValidationError::ShardOutOfRange {
shard: 3,
num_shards: 3
})
assert_eq!(
check_shard_range(&default_shard(3), 3).unwrap().to_string(),
"shard=3 exceeds the configured shard count (3)"
);
}
}
Expand All @@ -251,17 +247,21 @@ mod tests {

#[test]
fn both_none() {
assert_matches!(
check_range_bounds(&range(0, None, None)),
Some(ValidationError::RangeNoBounds { shard: 0 })
assert_eq!(
check_range_bounds(&range(0, None, None))
.unwrap()
.to_string(),
"shard=0 range must define a start, an end, or both"
);
}

#[test]
fn inverted() {
assert_matches!(
check_range_bounds(&range(0, Some(200), Some(100))),
Some(ValidationError::RangeInverted { shard: 0, .. })
assert_eq!(
check_range_bounds(&range(0, Some(200), Some(100)))
.unwrap()
.to_string(),
"shard=0 range is invalid: start (200) must not be greater than end (100)"
);
// start == end is a degenerate empty range, but not inverted.
assert!(check_range_bounds(&range(0, Some(100), Some(100))).is_none());
Expand Down Expand Up @@ -311,14 +311,19 @@ mod tests {
DataType::Bigint,
);
assert_eq!(errors.len(), 1);
assert_matches!(errors[0], ValidationError::IncompatibleType { .. });
assert_eq!(
errors[0].to_string(),
"value 'foo' is not a valid bigint value"
);
}

#[test]
fn range_both_bounds_mismatch() {
// Integer bounds but data_type = Uuid → both start and end are wrong.
let errors = check_type_compatibility(&range(0, Some(0), Some(100)), DataType::Uuid);
assert_eq!(errors.len(), 2);
assert_eq!(errors[0].to_string(), "value 0 is not a valid uuid value");
assert_eq!(errors[1].to_string(), "value 100 is not a valid uuid value");
}

#[test]
Expand All @@ -330,6 +335,10 @@ mod tests {
});
let errors = check_type_compatibility(&config, DataType::Varchar);
assert_eq!(errors.len(), 1);
assert_eq!(
errors[0].to_string(),
"value 100 is not a valid varchar value"
);
}

#[test]
Expand All @@ -356,9 +365,9 @@ mod tests {
#[test]
fn detected() {
let configs = vec![range(0, Some(0), Some(150)), range(1, Some(100), Some(200))];
assert_matches!(
check_range_overlap(&configs).as_slice(),
[ValidationError::RangesOverlap { .. }]
assert_eq!(
check_range_overlap(&configs)[0].to_string(),
"shard ranges overlap: [0, 150) -> shard=0 and [100, 200) -> shard=1"
);
}

Expand All @@ -367,18 +376,18 @@ mod tests {
// `(-inf, 50)` and `(-inf, 100)` overlap on `(-inf, 50)` yet share no
// explicit bound that falls strictly inside the other.
let configs = vec![range(0, None, Some(50)), range(1, None, Some(100))];
assert_matches!(
check_range_overlap(&configs).as_slice(),
[ValidationError::RangesOverlap { .. }]
assert_eq!(
check_range_overlap(&configs)[0].to_string(),
"shard ranges overlap: [-inf, 50) -> shard=0 and [-inf, 100) -> shard=1"
);
}

#[test]
fn unbounded_above_overlap() {
let configs = vec![range(0, Some(0), None), range(1, Some(100), None)];
assert_matches!(
check_range_overlap(&configs).as_slice(),
[ValidationError::RangesOverlap { .. }]
assert_eq!(
check_range_overlap(&configs)[0].to_string(),
"shard ranges overlap: [0, +inf) -> shard=0 and [100, +inf) -> shard=1"
);
}

Expand Down Expand Up @@ -409,9 +418,18 @@ mod tests {
range(0, None, None),
];
let errors = validate(&configs, DataType::Bigint, 3);
assert_matches!(errors[0], ValidationError::ShardOutOfRange { .. });
assert_matches!(errors[1], ValidationError::IncompatibleType { .. });
assert_matches!(errors[2], ValidationError::RangeNoBounds { .. });
assert_eq!(
errors[0].to_string(),
"shard=9 exceeds the configured shard count (3)"
);
assert_eq!(
errors[1].to_string(),
"value 'x' is not a valid bigint value"
);
assert_eq!(
errors[2].to_string(),
"shard=0 range must define a start, an end, or both"
);
}
}
}
Loading