uqa_sql/semantics/partition/
bounds.rs1use super::key::{key_columns, KeyColumn};
10use super::PartitionContext;
11use crate::ast::{Expr, PartitionBound, PartitionRangeDatum, PartitionSpec, PartitionStrategy};
12use crate::ir::ScalarExpr;
13use crate::schema::SchemaExpressionCatalog;
14use crate::SQLError;
15use uqa_core::Value;
16
17pub fn transform_partition_bound(
19 context: &PartitionContext<'_>,
20 parent: &str,
21 bound: &PartitionBound,
22) -> Result<PartitionBound, SQLError> {
23 let (spec, keys) = parent_partition_key(context, parent)?;
24 match (spec.strategy, bound) {
25 (PartitionStrategy::Hash, PartitionBound::Default) => Err(invalid_table_definition(
26 "a hash-partitioned table may not have a default partition",
27 )),
28 (_, PartitionBound::Default) => Ok(PartitionBound::Default),
29 (PartitionStrategy::Hash, PartitionBound::Hash { modulus, remainder }) => {
30 if *modulus <= 0 {
31 return Err(invalid_table_definition(
32 "modulus for hash partition must be an integer value greater than zero",
33 ));
34 }
35 if *remainder < 0 {
36 return Err(invalid_table_definition(
37 "remainder for hash partition must be an integer value greater than or equal to zero",
38 ));
39 }
40 if remainder >= modulus {
41 return Err(invalid_table_definition(
42 "remainder for hash partition must be less than modulus",
43 ));
44 }
45 Ok(bound.clone())
46 }
47 (PartitionStrategy::Hash, _) => Err(invalid_table_definition(
48 "invalid bound specification for a hash partition",
49 )),
50 (PartitionStrategy::List, PartitionBound::List(values)) => {
51 let [key] = keys.as_slice() else {
52 return Err(SQLError::Internal(
53 "LIST partitioned table has more than one partition key".into(),
54 ));
55 };
56 let mut transformed: Vec<Value> = Vec::with_capacity(values.len());
57 for expression in values {
58 let value = transform_value(context, expression, &key.name, key)?;
59 if !transformed.contains(&value) {
61 transformed.push(value);
62 }
63 }
64 Ok(PartitionBound::List(
65 transformed.into_iter().map(Expr::Literal).collect(),
66 ))
67 }
68 (PartitionStrategy::List, _) => Err(invalid_table_definition(
69 "invalid bound specification for a list partition",
70 )),
71 (PartitionStrategy::Range, PartitionBound::Range { lower, upper }) => {
72 if lower.len() != keys.len() {
73 return Err(invalid_table_definition(
74 "FROM must specify exactly one value per partitioning column",
75 ));
76 }
77 if upper.len() != keys.len() {
78 return Err(invalid_table_definition(
79 "TO must specify exactly one value per partitioning column",
80 ));
81 }
82 Ok(PartitionBound::Range {
83 lower: transform_range_datums(context, lower, &keys)?,
84 upper: transform_range_datums(context, upper, &keys)?,
85 })
86 }
87 (PartitionStrategy::Range, _) => Err(invalid_table_definition(
88 "invalid bound specification for a range partition",
89 )),
90 }
91}
92
93pub(super) fn parent_partition_key(
94 context: &PartitionContext<'_>,
95 parent: &str,
96) -> Result<(PartitionSpec, Vec<KeyColumn>), SQLError> {
97 let hierarchy = context
98 .catalog
99 .try_table_hierarchy(parent)
100 .map_err(|error| SQLError::Internal(format!("read parent partition metadata: {error}")))?;
101 let spec = hierarchy.partition_spec.ok_or_else(|| SQLError::Routine {
102 sqlstate: "42809".into(),
103 message: format!("relation \"{parent}\" is not partitioned"),
104 })?;
105 let columns = context
106 .catalog
107 .try_describe_table(parent)
108 .map_err(|error| SQLError::Internal(format!("read partition row type: {error}")))?
109 .ok_or_else(|| SQLError::UnknownTable(parent.to_string()))?;
110 let keys = key_columns(context.types, context.expressions, &spec, &columns)?;
111 Ok((spec, keys))
112}
113
114fn transform_range_datums(
116 context: &PartitionContext<'_>,
117 datums: &[PartitionRangeDatum],
118 keys: &[KeyColumn],
119) -> Result<Vec<PartitionRangeDatum>, SQLError> {
120 let expression_names = keys
121 .iter()
122 .filter(|key| key.expression)
123 .map(|key| key.name.as_str())
124 .collect::<Vec<_>>();
125 let mut next_expression = 0;
127 let mut result = Vec::with_capacity(datums.len());
128 for (position, datum) in datums.iter().enumerate() {
129 let PartitionRangeDatum::Value(expression) = datum else {
130 result.push(datum.clone());
131 continue;
132 };
133 let key = &keys[position];
134 let name = if key.expression {
135 let name = expression_names.get(next_expression).ok_or_else(|| {
136 SQLError::Internal("partition range bound names a missing key expression".into())
137 })?;
138 next_expression += 1;
139 *name
140 } else {
141 key.name.as_str()
142 };
143 let value = transform_value(context, expression, name, key)?;
144 if matches!(value, Value::Null) {
145 return Err(SQLError::Routine {
146 sqlstate: "42P17".into(),
147 message: "cannot specify NULL in range bound".into(),
148 });
149 }
150 result.push(PartitionRangeDatum::Value(Expr::Literal(value)));
151 }
152 validate_infinite_bounds(&result)?;
153 Ok(result)
154}
155
156fn validate_infinite_bounds(datums: &[PartitionRangeDatum]) -> Result<(), SQLError> {
157 let mut infinite = None;
158 for datum in datums {
159 match (infinite, datum) {
160 (None, PartitionRangeDatum::Value(_))
161 | (Some(true), PartitionRangeDatum::MinValue)
162 | (Some(false), PartitionRangeDatum::MaxValue) => {}
163 (None, PartitionRangeDatum::MinValue) => infinite = Some(true),
164 (None, PartitionRangeDatum::MaxValue) => infinite = Some(false),
165 (Some(minimum), _) => {
166 let bound = if minimum { "MINVALUE" } else { "MAXVALUE" };
167 return Err(SQLError::Routine {
168 sqlstate: "42804".into(),
169 message: format!("every bound following {bound} must also be {bound}"),
170 });
171 }
172 }
173 }
174 Ok(())
175}
176
177fn transform_value(
179 context: &PartitionContext<'_>,
180 expression: &Expr,
181 column: &str,
182 key: &KeyColumn,
183) -> Result<Value, SQLError> {
184 let plan = crate::plan::ExpressionPlan::lower(expression.clone());
185 validate_bound_expression(context.schema, &plan.scalar)?;
186 let source = crate::type_resolution::common_context_expression_type(
187 &plan.scalar,
188 &crate::RowSchema::default(),
189 &[],
190 Some(context.types),
191 )?;
192 if let Some(source) = source.as_ref() {
193 if !crate::type_resolution::assignment_type_compatible(source, &key.ty) {
194 return Err(SQLError::Routine {
195 sqlstate: "42804".into(),
196 message: format!(
197 "specified value cannot be cast to type {} for column \"{column}\"",
198 format_type(context, &key.ty)?
199 ),
200 });
201 }
202 }
203 let value = context.expressions.evaluate_bound(expression, &[])?;
204 crate::assignment::conversion::coerce_assignment_value(
205 context.assignment,
206 value,
207 &key.ty,
208 source.as_ref(),
209 )
210}
211
212pub(super) fn format_type(
214 context: &PartitionContext<'_>,
215 ty: &crate::ast::ColumnType,
216) -> Result<String, SQLError> {
217 let oid = crate::catalog::type_metadata::pg_type_oid(ty);
218 Ok(context
219 .assignment
220 .resolve_regtype_output(&crate::ast::ColumnType::Regtype, oid)
221 .map_err(SQLError::Internal)?
222 .unwrap_or_else(|| ty.regtype_name()))
223}
224
225fn validate_bound_expression(
227 catalog: &dyn SchemaExpressionCatalog,
228 expression: &ScalarExpr,
229) -> Result<(), SQLError> {
230 match expression {
232 ScalarExpr::Column(_)
233 | ScalarExpr::QualifiedColumn { .. }
234 | ScalarExpr::QualifiedStar(_)
235 | ScalarExpr::Position(_)
236 | ScalarExpr::InternalColumn(_) => {
237 return Err(feature_not_supported(
238 "cannot use column reference in partition bound expression",
239 ))
240 }
241 ScalarExpr::ScalarSubquery(_)
242 | ScalarExpr::Exists { .. }
243 | ScalarExpr::InSubquery { .. } => {
244 return Err(feature_not_supported(
245 "cannot use subquery in partition bound",
246 ))
247 }
248 ScalarExpr::Param(index) => {
249 return Err(SQLError::Routine {
250 sqlstate: "42P02".into(),
251 message: format!("there is no parameter ${index}"),
252 })
253 }
254 _ => {}
255 }
256 let mut root = true;
257 expression.try_visit(&mut |node| {
258 if std::mem::take(&mut root) {
259 return Ok(true);
260 }
261 validate_bound_expression(catalog, node)?;
262 Ok::<_, SQLError>(false)
263 })?;
264 match expression {
265 ScalarExpr::WindowCall { .. } => Err(SQLError::Routine {
266 sqlstate: "42P20".into(),
267 message: "window functions are not allowed in partition bound".into(),
268 }),
269 ScalarExpr::Func { .. }
270 if crate::semantics::aggregates::is_aggregate(catalog, expression) =>
271 {
272 Err(SQLError::Routine {
273 sqlstate: "42803".into(),
274 message: "aggregate functions are not allowed in partition bound".into(),
275 })
276 }
277 ScalarExpr::Func { .. }
278 if crate::semantics::sets::validation::expression_may_return_set(
279 catalog,
280 catalog,
281 expression,
282 &crate::RowSchema::default(),
283 &[],
284 )? =>
285 {
286 Err(feature_not_supported(
287 "set-returning functions are not allowed in partition bound",
288 ))
289 }
290 _ => Ok(()),
291 }
292}
293
294fn feature_not_supported(message: &str) -> SQLError {
295 SQLError::Routine {
296 sqlstate: "0A000".into(),
297 message: message.into(),
298 }
299}
300
301fn invalid_table_definition(message: &str) -> SQLError {
302 SQLError::Routine {
303 sqlstate: "42P16".into(),
304 message: message.into(),
305 }
306}