use super::bounds::parent_partition_key;
use super::datum_text::{range_bound_text, stored_datum};
use super::PartitionContext;
use crate::ast::{PartitionBound, PartitionRangeDatum};
use crate::expr::EngineHook;
use crate::SQLError;
use std::cmp::Ordering;
struct Sibling {
name: String,
bound: PartitionBound,
}
pub fn validate_new_partition_bound(
context: &PartitionContext<'_>,
parent: &str,
partition: &str,
bound: &PartitionBound,
) -> Result<(), SQLError> {
let (_, keys) = parent_partition_key(context, parent)?;
let siblings = sibling_bounds(context, parent)?;
let partition = local_relation_name(partition)?;
let overlapping = match bound {
PartitionBound::Default => {
if let Some(default) = siblings
.iter()
.find(|sibling| matches!(sibling.bound, PartitionBound::Default))
{
return Err(invalid_object_definition(format!(
"partition \"{partition}\" conflicts with existing default partition \"{}\"",
default.name
)));
}
None
}
PartitionBound::List(values) => list_overlap(values, &siblings)?,
PartitionBound::Range { lower, upper } => {
if compare_range_points(lower, upper)? != Ordering::Less {
let types = keys.iter().map(|key| key.ty.clone()).collect::<Vec<_>>();
let engine: &dyn EngineHook = context.assignment;
return Err(SQLError::Diagnostic {
sqlstate: "42P17".into(),
message: format!("empty range bound specified for partition \"{partition}\""),
detail: Some(format!(
"Specified lower bound {} is greater than or equal to upper bound {}.",
range_bound_text(lower, &types, Some(engine))?,
range_bound_text(upper, &types, Some(engine))?
)),
hint: None,
});
}
range_overlap(lower, upper, &siblings)?
}
PartitionBound::Hash { modulus, remainder } => {
hash_overlap(*modulus, *remainder, &siblings)?
}
};
match overlapping {
Some(sibling) => Err(invalid_object_definition(format!(
"partition \"{partition}\" would overlap partition \"{sibling}\""
))),
None => Ok(()),
}
}
fn sibling_bounds(context: &PartitionContext<'_>, parent: &str) -> Result<Vec<Sibling>, SQLError> {
let mut siblings = Vec::new();
for child in context.catalog.direct_hierarchy_children(parent)? {
let hierarchy = context
.catalog
.try_table_hierarchy(&child)
.map_err(|error| SQLError::Internal(format!("read sibling partition: {error}")))?;
if let Some(bound) = hierarchy.partition_bound {
siblings.push(Sibling {
name: local_relation_name(&child)?,
bound,
});
}
}
Ok(siblings)
}
fn list_overlap(
values: &[crate::ast::Expr],
siblings: &[Sibling],
) -> Result<Option<String>, SQLError> {
for value in values {
let value = stored_datum(value)?;
for sibling in siblings {
let PartitionBound::List(existing) = &sibling.bound else {
continue;
};
for candidate in existing {
if stored_datum(candidate)?.cmp(value) == Ordering::Equal {
return Ok(Some(sibling.name.clone()));
}
}
}
}
Ok(None)
}
fn range_overlap(
lower: &[PartitionRangeDatum],
upper: &[PartitionRangeDatum],
siblings: &[Sibling],
) -> Result<Option<String>, SQLError> {
let mut ranges = Vec::new();
for sibling in siblings {
if let PartitionBound::Range {
lower: existing_lower,
upper: existing_upper,
} = &sibling.bound
{
ranges.push((existing_lower, existing_upper, &sibling.name));
}
}
let mut ordering_error = None;
ranges.sort_by(|left, right| {
compare_range_points(left.0, right.0).unwrap_or_else(|error| {
ordering_error.get_or_insert(error);
Ordering::Equal
})
});
if let Some(error) = ordering_error {
return Err(error);
}
for (existing_lower, existing_upper, name) in ranges {
if compare_range_points(lower, existing_upper)? == Ordering::Less
&& compare_range_points(existing_lower, upper)? == Ordering::Less
{
return Ok(Some(name.clone()));
}
}
Ok(None)
}
pub(super) fn compare_range_points(
left: &[PartitionRangeDatum],
right: &[PartitionRangeDatum],
) -> Result<Ordering, SQLError> {
if left.len() != right.len() {
return Err(SQLError::Internal(
"partition range points have different widths".into(),
));
}
for (left, right) in left.iter().zip(right) {
let ordering = match (left, right) {
(PartitionRangeDatum::MinValue, PartitionRangeDatum::MinValue)
| (PartitionRangeDatum::MaxValue, PartitionRangeDatum::MaxValue) => Ordering::Equal,
(PartitionRangeDatum::MinValue, _) | (_, PartitionRangeDatum::MaxValue) => {
Ordering::Less
}
(PartitionRangeDatum::MaxValue, _) | (_, PartitionRangeDatum::MinValue) => {
Ordering::Greater
}
(PartitionRangeDatum::Value(left), PartitionRangeDatum::Value(right)) => {
stored_datum(left)?.cmp(stored_datum(right)?)
}
};
if ordering != Ordering::Equal {
return Ok(ordering);
}
}
Ok(Ordering::Equal)
}
fn hash_overlap(
modulus: i32,
remainder: i32,
siblings: &[Sibling],
) -> Result<Option<String>, SQLError> {
let mut existing = siblings
.iter()
.filter_map(|sibling| match sibling.bound {
PartitionBound::Hash { modulus, remainder } => {
Some((modulus, remainder, &sibling.name))
}
_ => None,
})
.collect::<Vec<_>>();
if existing.is_empty() {
return Ok(None);
}
existing.sort_by_key(|(modulus, remainder, _)| (*modulus, *remainder));
let not_a_factor = |next: i32, name: &str| {
modulus_error(format!(
"The new modulus {modulus} is not a factor of {next}, the modulus of existing partition \"{name}\"."
))
};
match existing
.iter()
.rposition(|(existing_modulus, existing_remainder, _)| {
(*existing_modulus, *existing_remainder) <= (modulus, remainder)
}) {
None => {
let (next, _, name) = existing[0];
if next % modulus != 0 {
return Err(not_a_factor(next, name));
}
}
Some(offset) => {
let (previous, _, name) = existing[offset];
if modulus % previous != 0 {
return Err(modulus_error(format!(
"The new modulus {modulus} is not divisible by {previous}, the modulus of existing partition \"{name}\"."
)));
}
if let Some((next, _, name)) = existing.get(offset + 1) {
if next % modulus != 0 {
return Err(not_a_factor(*next, name));
}
}
}
}
let greatest = existing
.iter()
.map(|(modulus, _, _)| *modulus)
.max()
.unwrap_or(modulus);
let mut covered = if remainder >= greatest {
remainder % greatest
} else {
remainder
};
loop {
if let Some((_, _, name)) = existing
.iter()
.find(|(modulus, remainder, _)| covered % modulus == *remainder)
{
return Ok(Some((*name).clone()));
}
covered += modulus;
if covered >= greatest {
return Ok(None);
}
}
}
fn modulus_error(detail: String) -> SQLError {
SQLError::Diagnostic {
sqlstate: "42P17".into(),
message: "every hash partition modulus must be a factor of the next larger modulus".into(),
detail: Some(detail),
hint: None,
}
}
pub(super) fn local_relation_name(table: &str) -> Result<String, SQLError> {
uqa_core::RelationIdentity::from_legacy_name(table)
.map(|identity| identity.name)
.map_err(SQLError::Internal)
}
fn invalid_object_definition(message: String) -> SQLError {
SQLError::Routine {
sqlstate: "42P17".into(),
message,
}
}
#[cfg(test)]
mod tests {
use super::{hash_overlap, Sibling};
use crate::ast::PartitionBound;
fn hash(name: &str, modulus: i32, remainder: i32) -> Sibling {
Sibling {
name: name.into(),
bound: PartitionBound::Hash { modulus, remainder },
}
}
#[test]
fn hash_admission_follows_check_new_partition_bound() {
let existing = [hash("mod4_r0", 4, 0), hash("mod8_r2", 8, 2)];
let not_a_factor = hash_overlap(3, 1, &existing).unwrap_err();
assert_eq!(
not_a_factor.detail(),
Some("The new modulus 3 is not a factor of 4, the modulus of existing partition \"mod4_r0\".")
);
let not_divisible = hash_overlap(12, 1, &existing).unwrap_err();
assert_eq!(
not_divisible.detail(),
Some("The new modulus 12 is not divisible by 8, the modulus of existing partition \"mod8_r2\".")
);
assert_eq!(
hash_overlap(2, 0, &existing).unwrap(),
Some("mod4_r0".into())
);
assert_eq!(hash_overlap(8, 6, &existing).unwrap(), None);
assert_eq!(
hash_overlap(16, 10, &existing).unwrap(),
Some("mod8_r2".into())
);
}
}