1use crate::{
10 ast::{ColumnDef, Expr, TableHierarchy},
11 type_resolution::FunctionTypeResolver,
12 ResultRow, RowSchema, SQLError, SQLParam,
13};
14use std::cmp::Ordering;
15use uqa_core::Value;
16
17pub trait PartitionCatalog {
19 fn try_table_hierarchy(&self, table: &str) -> Result<TableHierarchy, String>;
20 fn direct_hierarchy_children(&self, parent: &str) -> Result<Vec<String>, SQLError>;
21 fn try_resolve_table_name(&self, name: &str) -> Result<Option<String>, String>;
22 fn try_describe_table(&self, table: &str) -> Result<Option<Vec<ColumnDef>>, String>;
23}
24
25pub trait PartitionExpressions {
27 fn evaluate_bound(&self, expression: &Expr, params: &[SQLParam]) -> Result<Value, SQLError>;
28 fn evaluate_row(
29 &self,
30 expression: &Expr,
31 row: &ResultRow,
32 schema: &RowSchema,
33 params: &[SQLParam],
34 ) -> Result<Value, SQLError>;
35}
36
37#[derive(Clone, Copy)]
38pub struct PartitionContext<'a> {
39 pub catalog: &'a dyn PartitionCatalog,
40 pub expressions: &'a dyn PartitionExpressions,
41 pub types: &'a dyn FunctionTypeResolver,
42}
43
44mod hash;
45
46pub fn validate_hash_partition_spec(
47 context: &PartitionContext<'_>,
48 spec: &crate::ast::PartitionSpec,
49 columns: &[crate::ast::ColumnDef],
50) -> Result<(), SQLError> {
51 hash::validate_partition_spec(context.types, spec, columns)
52}
53
54pub fn validate_new_partition_bound(
55 context: &PartitionContext<'_>,
56 parent: &str,
57 bound: &crate::ast::PartitionBound,
58) -> Result<(), SQLError> {
59 let hierarchy = context
60 .catalog
61 .try_table_hierarchy(parent)
62 .map_err(|error| SQLError::Internal(format!("read parent partition metadata: {error}")))?;
63 let spec = hierarchy
64 .partition_spec
65 .as_ref()
66 .ok_or_else(|| SQLError::Routine {
67 sqlstate: "42809".into(),
68 message: format!("relation \"{parent}\" is not partitioned"),
69 })?;
70 validate_partition_bound_width(spec, bound)?;
71 if let crate::ast::PartitionBound::Hash { modulus, remainder } = bound {
72 hash::validate_bound(*modulus, *remainder)?;
73 let mut existing_moduli = Vec::new();
74 for sibling in context.catalog.direct_hierarchy_children(parent)? {
75 let sibling_hierarchy = context
76 .catalog
77 .try_table_hierarchy(&sibling)
78 .map_err(|error| SQLError::Internal(format!("read sibling partition: {error}")))?;
79 match sibling_hierarchy.partition_bound.as_ref() {
80 Some(crate::ast::PartitionBound::Hash { modulus, remainder }) => {
81 hash::validate_bound(*modulus, *remainder)?;
82 existing_moduli.push(*modulus);
83 }
84 Some(crate::ast::PartitionBound::Default) => {
85 return Err(SQLError::Internal(format!(
86 "HASH-partitioned table `{parent}` has a default partition"
87 )))
88 }
89 Some(_) => {
90 return Err(SQLError::Internal(
91 "partition siblings use different bound strategies".into(),
92 ))
93 }
94 None => {}
95 }
96 }
97 hash::validate_modulus_chain(*modulus, existing_moduli)?;
98 }
99 if let crate::ast::PartitionBound::Range { lower, upper } = bound {
100 if compare_partition_points(context, lower, upper)? != Ordering::Less {
101 return Err(invalid_partition_bound(
102 "empty range bound specified for partition",
103 ));
104 }
105 }
106 for sibling in context.catalog.direct_hierarchy_children(parent)? {
107 let sibling_hierarchy = context
108 .catalog
109 .try_table_hierarchy(&sibling)
110 .map_err(|error| SQLError::Internal(format!("read sibling partition: {error}")))?;
111 let Some(sibling_bound) = sibling_hierarchy.partition_bound.as_ref() else {
112 continue;
113 };
114 if partition_bounds_overlap(context, bound, sibling_bound)? {
115 return Err(invalid_partition_bound(format!(
116 "partition would overlap partition \"{sibling}\""
117 )));
118 }
119 }
120 Ok(())
121}
122
123pub fn prospective_partition_bound_accepts_document(
127 context: &PartitionContext<'_>,
128 parent: &str,
129 bound: &crate::ast::PartitionBound,
130 document: &ResultRow,
131) -> Result<bool, SQLError> {
132 let hierarchy = context
133 .catalog
134 .try_table_hierarchy(parent)
135 .map_err(|error| SQLError::Internal(format!("read parent partition metadata: {error}")))?;
136 let spec = hierarchy
137 .partition_spec
138 .as_ref()
139 .ok_or_else(|| SQLError::Routine {
140 sqlstate: "42809".into(),
141 message: format!("relation \"{parent}\" is not partitioned"),
142 })?;
143 let (keys, row_hash) = partition_key_values_and_hash(context, parent, spec, document)?;
144 if !matches!(bound, crate::ast::PartitionBound::Default) {
145 return partition_bound_matches(context, bound, &keys, &[], row_hash);
146 }
147 for sibling in context.catalog.direct_hierarchy_children(parent)? {
148 let sibling_hierarchy = context
149 .catalog
150 .try_table_hierarchy(&sibling)
151 .map_err(|error| SQLError::Internal(format!("read child partition: {error}")))?;
152 let Some(sibling_bound) = sibling_hierarchy.partition_bound.as_ref() else {
153 continue;
154 };
155 if matches!(sibling_bound, crate::ast::PartitionBound::Default) {
156 continue;
157 }
158 if partition_bound_matches(context, sibling_bound, &keys, &[], row_hash)? {
159 return Ok(false);
160 }
161 }
162 Ok(true)
163}
164
165pub fn partition_constraint_accepts_document(
169 context: &PartitionContext<'_>,
170 table: &str,
171 spec: &crate::ast::PartitionSpec,
172 bound: &crate::ast::PartitionBound,
173 document: &ResultRow,
174) -> Result<bool, SQLError> {
175 let (keys, row_hash) = partition_key_values_and_hash(context, table, spec, document)?;
176 partition_bound_matches(context, bound, &keys, &[], row_hash)
177}
178
179fn partition_key_values_and_hash(
180 context: &PartitionContext<'_>,
181 table: &str,
182 spec: &crate::ast::PartitionSpec,
183 document: &ResultRow,
184) -> Result<(Vec<Value>, Option<u64>), SQLError> {
185 let (keys, definitions) = evaluate_partition_keys(context, table, &spec.keys, document, &[])?;
186 let row_hash = (spec.strategy == crate::ast::PartitionStrategy::Hash)
187 .then(|| hash::row_hash(context.types, spec, &definitions, &keys))
188 .transpose()?;
189 Ok((keys, row_hash))
190}
191
192fn validate_partition_bound_width(
193 spec: &crate::ast::PartitionSpec,
194 bound: &crate::ast::PartitionBound,
195) -> Result<(), SQLError> {
196 use crate::ast::{PartitionBound, PartitionStrategy};
197 match (spec.strategy, bound) {
198 (_, PartitionBound::Default) => Ok(()),
199 (PartitionStrategy::List, PartitionBound::List(_)) if spec.keys.len() != 1 => {
200 Err(invalid_partition_bound(
201 "cannot use list partition bounds with more than one partition key",
202 ))
203 }
204 (PartitionStrategy::List, PartitionBound::List(_)) => Ok(()),
205 (PartitionStrategy::Range, PartitionBound::Range { lower, upper })
206 if lower.len() != spec.keys.len() || upper.len() != spec.keys.len() =>
207 {
208 Err(invalid_partition_bound(
209 "partition bound has the wrong number of columns",
210 ))
211 }
212 (PartitionStrategy::Range, PartitionBound::Range { .. })
213 | (PartitionStrategy::Hash, PartitionBound::Hash { .. }) => Ok(()),
214 (strategy, _) => Err(invalid_partition_bound(format!(
215 "invalid bound specification for a {} partitioned table",
216 match strategy {
217 PartitionStrategy::List => "list",
218 PartitionStrategy::Range => "range",
219 PartitionStrategy::Hash => "hash",
220 }
221 ))),
222 }
223}
224
225fn partition_bounds_overlap(
226 context: &PartitionContext<'_>,
227 left: &crate::ast::PartitionBound,
228 right: &crate::ast::PartitionBound,
229) -> Result<bool, SQLError> {
230 use crate::ast::PartitionBound;
231 match (left, right) {
232 (PartitionBound::Default, PartitionBound::Default) => Ok(true),
233 (PartitionBound::Default, _) | (_, PartitionBound::Default) => Ok(false),
234 (PartitionBound::List(left), PartitionBound::List(right)) => {
235 let left = evaluate_bound_values(context, left)?;
236 let right = evaluate_bound_values(context, right)?;
237 Ok(left.iter().any(|value| right.contains(value)))
238 }
239 (
240 PartitionBound::Range {
241 lower: left_lower,
242 upper: left_upper,
243 },
244 PartitionBound::Range {
245 lower: right_lower,
246 upper: right_upper,
247 },
248 ) => Ok(
249 compare_partition_points(context, left_lower, right_upper)? == Ordering::Less
250 && compare_partition_points(context, right_lower, left_upper)? == Ordering::Less,
251 ),
252 (
253 PartitionBound::Hash {
254 modulus: left_modulus,
255 remainder: left_remainder,
256 },
257 PartitionBound::Hash {
258 modulus: right_modulus,
259 remainder: right_remainder,
260 },
261 ) => hash::bounds_overlap(
262 *left_modulus,
263 *left_remainder,
264 *right_modulus,
265 *right_remainder,
266 ),
267 _ => Err(SQLError::Internal(
268 "partition siblings use different bound strategies".into(),
269 )),
270 }
271}
272
273fn evaluate_bound_values(
274 context: &PartitionContext<'_>,
275 expressions: &[crate::ast::Expr],
276) -> Result<Vec<Value>, SQLError> {
277 expressions
278 .iter()
279 .map(|expression| context.expressions.evaluate_bound(expression, &[]))
280 .collect()
281}
282
283fn compare_partition_points(
284 context: &PartitionContext<'_>,
285 left: &[crate::ast::PartitionRangeDatum],
286 right: &[crate::ast::PartitionRangeDatum],
287) -> Result<Ordering, SQLError> {
288 if left.len() != right.len() {
289 return Err(invalid_partition_bound(
290 "partition range points have different widths",
291 ));
292 }
293 for (left, right) in left.iter().zip(right) {
294 let ordering = match (left, right) {
295 (
296 crate::ast::PartitionRangeDatum::MinValue,
297 crate::ast::PartitionRangeDatum::MinValue,
298 )
299 | (
300 crate::ast::PartitionRangeDatum::MaxValue,
301 crate::ast::PartitionRangeDatum::MaxValue,
302 ) => Ordering::Equal,
303 (crate::ast::PartitionRangeDatum::MinValue, _)
304 | (_, crate::ast::PartitionRangeDatum::MaxValue) => Ordering::Less,
305 (crate::ast::PartitionRangeDatum::MaxValue, _)
306 | (_, crate::ast::PartitionRangeDatum::MinValue) => Ordering::Greater,
307 (
308 crate::ast::PartitionRangeDatum::Value(left),
309 crate::ast::PartitionRangeDatum::Value(right),
310 ) => context
311 .expressions
312 .evaluate_bound(left, &[])?
313 .cmp(&context.expressions.evaluate_bound(right, &[])?),
314 };
315 if ordering != Ordering::Equal {
316 return Ok(ordering);
317 }
318 }
319 Ok(Ordering::Equal)
320}
321
322fn invalid_partition_bound(message: impl Into<String>) -> SQLError {
323 SQLError::Routine {
324 sqlstate: "42P17".into(),
325 message: message.into(),
326 }
327}
328
329pub fn partition_insert_target(
330 context: &PartitionContext<'_>,
331 requested_table: &str,
332 document: &ResultRow,
333 params: &[SQLParam],
334 include_descendants: bool,
335) -> Result<String, SQLError> {
336 let table = context
337 .catalog
338 .try_resolve_table_name(requested_table)
339 .map_err(|error| SQLError::Internal(format!("resolve INSERT table: {error}")))?
340 .ok_or_else(|| SQLError::UnknownTable(requested_table.to_string()))?;
341 let hierarchy = context
342 .catalog
343 .try_table_hierarchy(&table)
344 .map_err(|error| SQLError::Internal(format!("read partition metadata: {error}")))?;
345 validate_partition_ancestor_path(context, requested_table, &table, document, params)?;
346 if let Some(spec) = hierarchy.partition_spec.as_ref() {
347 if !include_descendants {
348 return Err(SQLError::Routine {
349 sqlstate: "42809".into(),
350 message: format!("cannot insert into partitioned table \"{requested_table}\""),
351 });
352 }
353 let child = select_direct_partition_with_spec(context, &table, spec, document, params)?
354 .ok_or_else(|| no_partition_for_row(requested_table))?;
355 return route_partition_tree(context, &child, document, params);
356 }
357 Ok(table)
358}
359
360fn validate_partition_ancestor_path(
361 context: &PartitionContext<'_>,
362 requested_table: &str,
363 table: &str,
364 document: &ResultRow,
365 params: &[SQLParam],
366) -> Result<(), SQLError> {
367 let mut child = table.to_string();
368 let mut visited = std::collections::BTreeSet::new();
369 loop {
370 if !visited.insert(child.clone()) {
371 return Err(SQLError::Internal(format!(
372 "partition hierarchy cycle reaches `{child}`"
373 )));
374 }
375 let hierarchy = context
376 .catalog
377 .try_table_hierarchy(&child)
378 .map_err(|error| SQLError::Internal(format!("read partition metadata: {error}")))?;
379 if hierarchy.partition_bound.is_none() {
380 return Ok(());
381 }
382 let parent = hierarchy.parents.first().ok_or_else(|| {
383 SQLError::Internal(format!("partition `{child}` has no parent relation"))
384 })?;
385 let selected = select_direct_partition(context, parent, document, params)?;
386 if selected.as_deref() != Some(child.as_str()) {
387 return Err(SQLError::Routine {
388 sqlstate: "23514".into(),
389 message: format!(
390 "new row for relation \"{requested_table}\" violates partition constraint"
391 ),
392 });
393 }
394 child.clone_from(parent);
395 }
396}
397
398fn route_partition_tree(
399 context: &PartitionContext<'_>,
400 table: &str,
401 document: &ResultRow,
402 params: &[SQLParam],
403) -> Result<String, SQLError> {
404 let hierarchy = context
405 .catalog
406 .try_table_hierarchy(table)
407 .map_err(|error| SQLError::Internal(format!("read partition metadata: {error}")))?;
408 let Some(spec) = hierarchy.partition_spec.as_ref() else {
409 return Ok(table.to_string());
410 };
411 let child = select_direct_partition_with_spec(context, table, spec, document, params)?
412 .ok_or_else(|| no_partition_for_row(table))?;
413 route_partition_tree(context, &child, document, params)
414}
415
416fn select_direct_partition(
417 context: &PartitionContext<'_>,
418 parent: &str,
419 document: &ResultRow,
420 params: &[SQLParam],
421) -> Result<Option<String>, SQLError> {
422 let hierarchy = context
423 .catalog
424 .try_table_hierarchy(parent)
425 .map_err(|error| SQLError::Internal(format!("read parent partition metadata: {error}")))?;
426 let spec = hierarchy.partition_spec.as_ref().ok_or_else(|| {
427 SQLError::Internal(format!("partition parent `{parent}` has no partition key"))
428 })?;
429 select_direct_partition_with_spec(context, parent, spec, document, params)
430}
431
432fn select_direct_partition_with_spec(
433 context: &PartitionContext<'_>,
434 parent: &str,
435 spec: &crate::ast::PartitionSpec,
436 document: &ResultRow,
437 params: &[SQLParam],
438) -> Result<Option<String>, SQLError> {
439 let (keys, definitions) =
440 evaluate_partition_keys(context, parent, &spec.keys, document, params)?;
441 let row_hash = (spec.strategy == crate::ast::PartitionStrategy::Hash)
442 .then(|| hash::row_hash(context.types, spec, &definitions, &keys))
443 .transpose()?;
444 let mut default = None;
445 for child in context.catalog.direct_hierarchy_children(parent)? {
446 let child_hierarchy = context
447 .catalog
448 .try_table_hierarchy(&child)
449 .map_err(|error| SQLError::Internal(format!("read child partition: {error}")))?;
450 let Some(bound) = child_hierarchy.partition_bound.as_ref() else {
451 continue;
452 };
453 if matches!(bound, crate::ast::PartitionBound::Default) {
454 if default.replace(child).is_some() {
455 return Err(SQLError::Internal(format!(
456 "partitioned table `{parent}` has more than one default partition"
457 )));
458 }
459 continue;
460 }
461 if partition_bound_matches(context, bound, &keys, params, row_hash)? {
462 return Ok(Some(child));
463 }
464 }
465 Ok(default)
466}
467
468fn evaluate_partition_keys(
469 context: &PartitionContext<'_>,
470 table: &str,
471 expressions: &[crate::ast::Expr],
472 document: &ResultRow,
473 params: &[SQLParam],
474) -> Result<(Vec<Value>, Vec<crate::ast::ColumnDef>), SQLError> {
475 let definitions = context
476 .catalog
477 .try_describe_table(table)
478 .map_err(|error| SQLError::Internal(format!("read partition row type: {error}")))?
479 .ok_or_else(|| SQLError::UnknownTable(table.to_string()))?;
480 let schema = crate::RowSchema::with_types(
481 definitions
482 .iter()
483 .map(|definition| definition.name.clone())
484 .collect(),
485 definitions
486 .iter()
487 .map(|definition| Some(definition.ty.clone()))
488 .collect(),
489 );
490 let values = expressions
491 .iter()
492 .map(|expression| {
493 context
494 .expressions
495 .evaluate_row(expression, document, &schema, params)
496 })
497 .collect::<Result<Vec<_>, _>>()?;
498 Ok((values, definitions))
499}
500
501fn partition_bound_matches(
502 context: &PartitionContext<'_>,
503 bound: &crate::ast::PartitionBound,
504 keys: &[Value],
505 params: &[SQLParam],
506 row_hash: Option<u64>,
507) -> Result<bool, SQLError> {
508 use crate::ast::PartitionBound;
509 match bound {
510 PartitionBound::Default => Ok(true),
511 PartitionBound::List(values) => {
512 let [key] = keys else {
513 return Err(SQLError::Internal(
514 "LIST partition has more than one partition key".into(),
515 ));
516 };
517 for expression in values {
518 if context.expressions.evaluate_bound(expression, params)? == *key {
519 return Ok(true);
520 }
521 }
522 Ok(false)
523 }
524 PartitionBound::Range { lower, upper } => {
525 if keys.iter().any(|value| matches!(value, Value::Null)) {
526 return Ok(false);
527 }
528 Ok(
529 compare_key_to_bound(context, keys, lower, params)? != Ordering::Less
530 && compare_key_to_bound(context, keys, upper, params)? == Ordering::Less,
531 )
532 }
533 PartitionBound::Hash { modulus, remainder } => hash::bound_matches(
534 row_hash.ok_or_else(|| {
535 SQLError::Internal("HASH partition bound has no computed row hash".into())
536 })?,
537 *modulus,
538 *remainder,
539 ),
540 }
541}
542
543fn compare_key_to_bound(
544 context: &PartitionContext<'_>,
545 keys: &[Value],
546 bound: &[crate::ast::PartitionRangeDatum],
547 params: &[SQLParam],
548) -> Result<Ordering, SQLError> {
549 if keys.len() != bound.len() {
550 return Err(SQLError::Internal(format!(
551 "partition key width {} differs from bound width {}",
552 keys.len(),
553 bound.len()
554 )));
555 }
556 for (key, datum) in keys.iter().zip(bound) {
557 let ordering = match datum {
558 crate::ast::PartitionRangeDatum::MinValue => Ordering::Greater,
559 crate::ast::PartitionRangeDatum::MaxValue => Ordering::Less,
560 crate::ast::PartitionRangeDatum::Value(expression) => {
561 key.cmp(&context.expressions.evaluate_bound(expression, params)?)
562 }
563 };
564 if ordering != Ordering::Equal {
565 return Ok(ordering);
566 }
567 }
568 Ok(Ordering::Equal)
569}
570
571fn no_partition_for_row(table: &str) -> SQLError {
572 SQLError::Routine {
573 sqlstate: "23514".into(),
574 message: format!("no partition of relation \"{table}\" found for row"),
575 }
576}
577
578mod identity;
579pub use identity::{partition_hierarchy_root, partition_identity_owner};