uqa_sql/semantics/partition/
admission.rs1use super::bounds::parent_partition_key;
10use super::datum_text::{range_bound_text, stored_datum};
11use super::PartitionContext;
12use crate::ast::{PartitionBound, PartitionRangeDatum};
13use crate::expr::EngineHook;
14use crate::SQLError;
15use std::cmp::Ordering;
16
17struct Sibling {
19 name: String,
20 bound: PartitionBound,
21}
22
23pub fn validate_new_partition_bound(
25 context: &PartitionContext<'_>,
26 parent: &str,
27 partition: &str,
28 bound: &PartitionBound,
29) -> Result<(), SQLError> {
30 let (_, keys) = parent_partition_key(context, parent)?;
31 let siblings = sibling_bounds(context, parent)?;
32 let partition = local_relation_name(partition)?;
33 let overlapping = match bound {
34 PartitionBound::Default => {
35 if let Some(default) = siblings
36 .iter()
37 .find(|sibling| matches!(sibling.bound, PartitionBound::Default))
38 {
39 return Err(invalid_object_definition(format!(
40 "partition \"{partition}\" conflicts with existing default partition \"{}\"",
41 default.name
42 )));
43 }
44 None
45 }
46 PartitionBound::List(values) => list_overlap(values, &siblings)?,
47 PartitionBound::Range { lower, upper } => {
48 if compare_range_points(lower, upper)? != Ordering::Less {
49 let types = keys.iter().map(|key| key.ty.clone()).collect::<Vec<_>>();
50 let engine: &dyn EngineHook = context.assignment;
51 return Err(SQLError::Diagnostic {
52 sqlstate: "42P17".into(),
53 message: format!("empty range bound specified for partition \"{partition}\""),
54 detail: Some(format!(
55 "Specified lower bound {} is greater than or equal to upper bound {}.",
56 range_bound_text(lower, &types, Some(engine))?,
57 range_bound_text(upper, &types, Some(engine))?
58 )),
59 hint: None,
60 });
61 }
62 range_overlap(lower, upper, &siblings)?
63 }
64 PartitionBound::Hash { modulus, remainder } => {
65 hash_overlap(*modulus, *remainder, &siblings)?
66 }
67 };
68 match overlapping {
69 Some(sibling) => Err(invalid_object_definition(format!(
70 "partition \"{partition}\" would overlap partition \"{sibling}\""
71 ))),
72 None => Ok(()),
73 }
74}
75
76fn sibling_bounds(context: &PartitionContext<'_>, parent: &str) -> Result<Vec<Sibling>, SQLError> {
77 let mut siblings = Vec::new();
78 for child in context.catalog.direct_hierarchy_children(parent)? {
79 let hierarchy = context
80 .catalog
81 .try_table_hierarchy(&child)
82 .map_err(|error| SQLError::Internal(format!("read sibling partition: {error}")))?;
83 if let Some(bound) = hierarchy.partition_bound {
84 siblings.push(Sibling {
85 name: local_relation_name(&child)?,
86 bound,
87 });
88 }
89 }
90 Ok(siblings)
91}
92
93fn list_overlap(
95 values: &[crate::ast::Expr],
96 siblings: &[Sibling],
97) -> Result<Option<String>, SQLError> {
98 for value in values {
99 let value = stored_datum(value)?;
100 for sibling in siblings {
101 let PartitionBound::List(existing) = &sibling.bound else {
102 continue;
103 };
104 for candidate in existing {
105 if stored_datum(candidate)?.cmp(value) == Ordering::Equal {
106 return Ok(Some(sibling.name.clone()));
107 }
108 }
109 }
110 }
111 Ok(None)
112}
113
114fn range_overlap(
116 lower: &[PartitionRangeDatum],
117 upper: &[PartitionRangeDatum],
118 siblings: &[Sibling],
119) -> Result<Option<String>, SQLError> {
120 let mut ranges = Vec::new();
121 for sibling in siblings {
122 if let PartitionBound::Range {
123 lower: existing_lower,
124 upper: existing_upper,
125 } = &sibling.bound
126 {
127 ranges.push((existing_lower, existing_upper, &sibling.name));
128 }
129 }
130 let mut ordering_error = None;
131 ranges.sort_by(|left, right| {
132 compare_range_points(left.0, right.0).unwrap_or_else(|error| {
133 ordering_error.get_or_insert(error);
134 Ordering::Equal
135 })
136 });
137 if let Some(error) = ordering_error {
138 return Err(error);
139 }
140 for (existing_lower, existing_upper, name) in ranges {
141 if compare_range_points(lower, existing_upper)? == Ordering::Less
142 && compare_range_points(existing_lower, upper)? == Ordering::Less
143 {
144 return Ok(Some(name.clone()));
145 }
146 }
147 Ok(None)
148}
149
150pub(super) fn compare_range_points(
152 left: &[PartitionRangeDatum],
153 right: &[PartitionRangeDatum],
154) -> Result<Ordering, SQLError> {
155 if left.len() != right.len() {
156 return Err(SQLError::Internal(
157 "partition range points have different widths".into(),
158 ));
159 }
160 for (left, right) in left.iter().zip(right) {
161 let ordering = match (left, right) {
162 (PartitionRangeDatum::MinValue, PartitionRangeDatum::MinValue)
163 | (PartitionRangeDatum::MaxValue, PartitionRangeDatum::MaxValue) => Ordering::Equal,
164 (PartitionRangeDatum::MinValue, _) | (_, PartitionRangeDatum::MaxValue) => {
165 Ordering::Less
166 }
167 (PartitionRangeDatum::MaxValue, _) | (_, PartitionRangeDatum::MinValue) => {
168 Ordering::Greater
169 }
170 (PartitionRangeDatum::Value(left), PartitionRangeDatum::Value(right)) => {
171 stored_datum(left)?.cmp(stored_datum(right)?)
172 }
173 };
174 if ordering != Ordering::Equal {
175 return Ok(ordering);
176 }
177 }
178 Ok(Ordering::Equal)
179}
180
181fn hash_overlap(
183 modulus: i32,
184 remainder: i32,
185 siblings: &[Sibling],
186) -> Result<Option<String>, SQLError> {
187 let mut existing = siblings
188 .iter()
189 .filter_map(|sibling| match sibling.bound {
190 PartitionBound::Hash { modulus, remainder } => {
191 Some((modulus, remainder, &sibling.name))
192 }
193 _ => None,
194 })
195 .collect::<Vec<_>>();
196 if existing.is_empty() {
197 return Ok(None);
198 }
199 existing.sort_by_key(|(modulus, remainder, _)| (*modulus, *remainder));
200 let not_a_factor = |next: i32, name: &str| {
201 modulus_error(format!(
202 "The new modulus {modulus} is not a factor of {next}, the modulus of existing partition \"{name}\"."
203 ))
204 };
205 match existing
206 .iter()
207 .rposition(|(existing_modulus, existing_remainder, _)| {
208 (*existing_modulus, *existing_remainder) <= (modulus, remainder)
209 }) {
210 None => {
211 let (next, _, name) = existing[0];
212 if next % modulus != 0 {
213 return Err(not_a_factor(next, name));
214 }
215 }
216 Some(offset) => {
217 let (previous, _, name) = existing[offset];
218 if modulus % previous != 0 {
219 return Err(modulus_error(format!(
220 "The new modulus {modulus} is not divisible by {previous}, the modulus of existing partition \"{name}\"."
221 )));
222 }
223 if let Some((next, _, name)) = existing.get(offset + 1) {
224 if next % modulus != 0 {
225 return Err(not_a_factor(*next, name));
226 }
227 }
228 }
229 }
230 let greatest = existing
231 .iter()
232 .map(|(modulus, _, _)| *modulus)
233 .max()
234 .unwrap_or(modulus);
235 let mut covered = if remainder >= greatest {
236 remainder % greatest
237 } else {
238 remainder
239 };
240 loop {
241 if let Some((_, _, name)) = existing
242 .iter()
243 .find(|(modulus, remainder, _)| covered % modulus == *remainder)
244 {
245 return Ok(Some((*name).clone()));
246 }
247 covered += modulus;
248 if covered >= greatest {
249 return Ok(None);
250 }
251 }
252}
253
254fn modulus_error(detail: String) -> SQLError {
255 SQLError::Diagnostic {
256 sqlstate: "42P17".into(),
257 message: "every hash partition modulus must be a factor of the next larger modulus".into(),
258 detail: Some(detail),
259 hint: None,
260 }
261}
262
263pub(super) fn local_relation_name(table: &str) -> Result<String, SQLError> {
265 uqa_core::RelationIdentity::from_legacy_name(table)
266 .map(|identity| identity.name)
267 .map_err(SQLError::Internal)
268}
269
270fn invalid_object_definition(message: String) -> SQLError {
271 SQLError::Routine {
272 sqlstate: "42P17".into(),
273 message,
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::{hash_overlap, Sibling};
280 use crate::ast::PartitionBound;
281
282 fn hash(name: &str, modulus: i32, remainder: i32) -> Sibling {
283 Sibling {
284 name: name.into(),
285 bound: PartitionBound::Hash { modulus, remainder },
286 }
287 }
288
289 #[test]
290 fn hash_admission_follows_check_new_partition_bound() {
291 let existing = [hash("mod4_r0", 4, 0), hash("mod8_r2", 8, 2)];
292 let not_a_factor = hash_overlap(3, 1, &existing).unwrap_err();
293 assert_eq!(
294 not_a_factor.detail(),
295 Some("The new modulus 3 is not a factor of 4, the modulus of existing partition \"mod4_r0\".")
296 );
297 let not_divisible = hash_overlap(12, 1, &existing).unwrap_err();
298 assert_eq!(
299 not_divisible.detail(),
300 Some("The new modulus 12 is not divisible by 8, the modulus of existing partition \"mod8_r2\".")
301 );
302 assert_eq!(
304 hash_overlap(2, 0, &existing).unwrap(),
305 Some("mod4_r0".into())
306 );
307 assert_eq!(hash_overlap(8, 6, &existing).unwrap(), None);
308 assert_eq!(
309 hash_overlap(16, 10, &existing).unwrap(),
310 Some("mod8_r2".into())
311 );
312 }
313}