1use std::cmp::Reverse;
21use std::collections::{HashMap, HashSet};
22
23use crate::spec::{
24 NestedField, NestedFieldRef, PartitionField, PartitionSpec, Schema, StructType, Transform, Type,
25};
26use crate::{Error, ErrorKind, Result};
27
28pub fn compute_unified_partition_type<'a>(
50 partition_specs: impl Iterator<Item = &'a PartitionSpec>,
51 schema: &Schema,
52) -> Result<StructType> {
53 let mut specs: Vec<&PartitionSpec> = partition_specs.collect();
54 specs.sort_by_key(|s| Reverse(s.spec_id()));
55
56 let active_field_ids = all_active_field_ids(specs.iter().copied(), schema);
57
58 let mut field_map: HashMap<i32, &PartitionField> = HashMap::new();
59 let mut type_map: HashMap<i32, Type> = HashMap::new();
60 let mut name_map: HashMap<i32, String> = HashMap::new();
61
62 for spec in &specs {
63 for field in spec.fields() {
64 let field_id = field.field_id;
65
66 if matches!(field.transform, Transform::Unknown) {
71 return Err(Error::new(
72 ErrorKind::DataInvalid,
73 format!(
74 "Partition field '{}' uses an unknown transform whose result type \
75 cannot be determined",
76 field.name
77 ),
78 ));
79 }
80
81 if !active_field_ids.contains(&field_id) {
82 continue;
83 }
84
85 let source_field = match schema.field_by_id(field.source_id) {
86 Some(f) => f,
87 None => continue,
88 };
89
90 match field_map.get(&field_id) {
91 None => {
92 let res_type = field.transform.result_type(&source_field.field_type)?;
93 field_map.insert(field_id, field);
94 type_map.insert(field_id, res_type);
95 name_map.insert(field_id, field.name.clone());
96 }
97 Some(existing) => {
98 if !equivalent_ignoring_names(field, existing) {
101 return Err(Error::new(
102 ErrorKind::DataInvalid,
103 format!(
104 "Conflicting partition fields for field id {field_id}: \
105 '{}' and '{}'",
106 field.name, existing.name
107 ),
108 ));
109 }
110
111 if is_void_transform(existing) && !is_void_transform(field) {
115 let res_type = field.transform.result_type(&source_field.field_type)?;
116 field_map.insert(field_id, field);
117 type_map.insert(field_id, res_type);
118 }
119 }
120 }
121 }
122 }
123
124 let mut field_ids: Vec<i32> = field_map.keys().copied().collect();
125 field_ids.sort();
126
127 let struct_fields = field_ids
128 .into_iter()
129 .map(|fid| -> Result<NestedFieldRef> {
130 let name = name_map.get(&fid).ok_or_else(|| {
131 Error::new(
132 ErrorKind::Unexpected,
133 format!("Missing name for partition field {fid}"),
134 )
135 })?;
136 let ty = type_map.remove(&fid).ok_or_else(|| {
137 Error::new(
138 ErrorKind::Unexpected,
139 format!("Missing type for partition field {fid}"),
140 )
141 })?;
142 Ok(NestedField::optional(fid, name, ty).into())
143 })
144 .collect::<Result<Vec<_>>>()?;
145
146 Ok(StructType::new(struct_fields))
147}
148
149fn is_void_transform(field: &PartitionField) -> bool {
150 matches!(field.transform, Transform::Void)
151}
152
153fn equivalent_ignoring_names(field: &PartitionField, other: &PartitionField) -> bool {
157 field.field_id == other.field_id
158 && field.source_id == other.source_id
159 && compatible_transforms(&field.transform, &other.transform)
160}
161
162fn compatible_transforms(t1: &Transform, t2: &Transform) -> bool {
165 t1 == t2 || matches!(t1, Transform::Void) || matches!(t2, Transform::Void)
166}
167
168fn all_active_field_ids<'a>(
169 partition_specs: impl Iterator<Item = &'a PartitionSpec>,
170 schema: &Schema,
171) -> HashSet<i32> {
172 partition_specs
173 .flat_map(|spec| spec.fields().iter())
174 .filter(|field| schema.field_by_id(field.source_id).is_some())
175 .map(|field| field.field_id)
176 .collect()
177}
178
179#[cfg(test)]
180mod tests {
181 use std::sync::Arc;
182
183 use super::*;
184 use crate::spec::{NestedField, PrimitiveType, Transform, Type, UnboundPartitionSpec};
185
186 fn test_schema() -> Schema {
187 Schema::builder()
188 .with_fields(vec![
189 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
190 NestedField::required(2, "data", Type::Primitive(PrimitiveType::String)).into(),
191 NestedField::required(3, "ts", Type::Primitive(PrimitiveType::Timestamp)).into(),
192 NestedField::required(4, "category", Type::Primitive(PrimitiveType::String)).into(),
193 ])
194 .build()
195 .unwrap()
196 }
197
198 fn build_spec(
199 schema: &Schema,
200 spec_id: i32,
201 fields: Vec<(i32, &str, Transform)>,
202 ) -> PartitionSpec {
203 let mut builder = UnboundPartitionSpec::builder().with_spec_id(spec_id);
204 for (source_id, name, transform) in fields {
205 builder = builder
206 .add_partition_field(source_id, name, transform)
207 .unwrap();
208 }
209 builder.build().bind(schema.clone()).unwrap()
210 }
211
212 #[test]
213 fn test_single_spec_identity() {
214 let schema = test_schema();
215 let spec = build_spec(&schema, 0, vec![(4, "category", Transform::Identity)]);
216
217 let result = compute_unified_partition_type([&spec].into_iter(), &schema).unwrap();
218 assert_eq!(result.fields().len(), 1);
219 assert_eq!(result.fields()[0].name, "category");
220 assert_eq!(
221 *result.fields()[0].field_type,
222 Type::Primitive(PrimitiveType::String)
223 );
224 }
225
226 #[test]
227 fn test_single_spec_with_year_transform() {
228 let schema = test_schema();
229 let spec = build_spec(&schema, 0, vec![(3, "ts_year", Transform::Year)]);
230
231 let result = compute_unified_partition_type([&spec].into_iter(), &schema).unwrap();
232 assert_eq!(result.fields().len(), 1);
233 assert_eq!(result.fields()[0].name, "ts_year");
234 assert_eq!(
235 *result.fields()[0].field_type,
236 Type::Primitive(PrimitiveType::Int)
237 );
238 }
239
240 #[test]
241 fn test_unpartitioned() {
242 let schema = test_schema();
243 let spec = PartitionSpec::unpartition_spec();
244 let result = compute_unified_partition_type([&spec].into_iter(), &schema).unwrap();
245 assert!(result.fields().is_empty());
246 }
247
248 #[test]
249 fn test_multiple_fields_sorted_by_id() {
250 let schema = test_schema();
251 let spec = build_spec(&schema, 0, vec![
252 (3, "ts_year", Transform::Year),
253 (4, "category", Transform::Identity),
254 ]);
255
256 let result = compute_unified_partition_type([&spec].into_iter(), &schema).unwrap();
257 assert_eq!(result.fields().len(), 2);
258 assert!(result.fields()[0].id < result.fields()[1].id);
259 }
260
261 #[test]
262 fn test_newer_name_takes_precedence() {
263 let schema = test_schema();
264
265 let spec_v0 = PartitionSpec::builder(Arc::new(schema.clone()))
267 .with_spec_id(0)
268 .add_unbound_field(crate::spec::UnboundPartitionField {
269 source_id: 4,
270 field_id: Some(1000),
271 name: "cat_old".to_string(),
272 transform: Transform::Identity,
273 })
274 .unwrap()
275 .build()
276 .unwrap();
277
278 let spec_v1 = PartitionSpec::builder(Arc::new(schema.clone()))
280 .with_spec_id(1)
281 .add_unbound_field(crate::spec::UnboundPartitionField {
282 source_id: 4,
283 field_id: Some(1000),
284 name: "cat_new".to_string(),
285 transform: Transform::Identity,
286 })
287 .unwrap()
288 .build()
289 .unwrap();
290
291 let result =
292 compute_unified_partition_type([&spec_v0, &spec_v1].into_iter(), &schema).unwrap();
293 assert_eq!(result.fields().len(), 1);
294 assert_eq!(result.fields()[0].name, "cat_new");
295 }
296
297 #[test]
298 fn test_void_replaced_by_older_non_void() {
299 let schema = test_schema();
300
301 let spec_v0 = PartitionSpec::builder(Arc::new(schema.clone()))
303 .with_spec_id(0)
304 .add_unbound_field(crate::spec::UnboundPartitionField {
305 source_id: 4,
306 field_id: Some(1000),
307 name: "category".to_string(),
308 transform: Transform::Identity,
309 })
310 .unwrap()
311 .build()
312 .unwrap();
313
314 let spec_v1 = PartitionSpec::builder(Arc::new(schema.clone()))
316 .with_spec_id(1)
317 .add_unbound_field(crate::spec::UnboundPartitionField {
318 source_id: 4,
319 field_id: Some(1000),
320 name: "category_v2".to_string(),
321 transform: Transform::Void,
322 })
323 .unwrap()
324 .build()
325 .unwrap();
326
327 let result =
328 compute_unified_partition_type([&spec_v0, &spec_v1].into_iter(), &schema).unwrap();
329
330 assert_eq!(result.fields().len(), 1);
331 assert_eq!(result.fields()[0].name, "category_v2");
333 assert_eq!(
335 *result.fields()[0].field_type,
336 Type::Primitive(PrimitiveType::String)
337 );
338 }
339
340 #[test]
341 fn test_dropped_source_column_skipped() {
342 let schema = Schema::builder()
344 .with_fields(vec![
345 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
346 NestedField::required(2, "data", Type::Primitive(PrimitiveType::String)).into(),
347 ])
348 .build()
349 .unwrap();
350
351 let spec = serde_json::from_value::<PartitionSpec>(serde_json::json!({
354 "spec-id": 0,
355 "fields": [{
356 "source-id": 4,
357 "field-id": 1000,
358 "name": "category",
359 "transform": "identity"
360 }]
361 }))
362 .unwrap();
363
364 let result = compute_unified_partition_type([&spec].into_iter(), &schema).unwrap();
365 assert!(result.fields().is_empty());
366 }
367
368 #[test]
369 fn test_evolution_adds_new_field() {
370 let schema = test_schema();
371
372 let spec_v0 = build_spec(&schema, 0, vec![(4, "category", Transform::Identity)]);
374
375 let spec_v1 = PartitionSpec::builder(Arc::new(schema.clone()))
377 .with_spec_id(1)
378 .add_unbound_field(crate::spec::UnboundPartitionField {
379 source_id: 4,
380 field_id: Some(spec_v0.fields()[0].field_id),
381 name: "category".to_string(),
382 transform: Transform::Identity,
383 })
384 .unwrap()
385 .add_partition_field("ts", "ts_year", Transform::Year)
386 .unwrap()
387 .build()
388 .unwrap();
389
390 let result =
391 compute_unified_partition_type([&spec_v0, &spec_v1].into_iter(), &schema).unwrap();
392 assert_eq!(result.fields().len(), 2);
393 }
394
395 #[test]
396 fn test_unknown_transform_errors() {
397 let schema = test_schema();
398
399 let spec = serde_json::from_value::<PartitionSpec>(serde_json::json!({
402 "spec-id": 0,
403 "fields": [{
404 "source-id": 4,
405 "field-id": 1000,
406 "name": "category",
407 "transform": "unknown"
408 }]
409 }))
410 .unwrap();
411
412 let err = compute_unified_partition_type([&spec].into_iter(), &schema).unwrap_err();
413 assert_eq!(err.kind(), ErrorKind::DataInvalid);
414 }
415
416 #[test]
417 fn test_conflicting_partition_fields_error() {
418 let schema = test_schema();
419
420 let spec_v0 = serde_json::from_value::<PartitionSpec>(serde_json::json!({
422 "spec-id": 0,
423 "fields": [{
424 "source-id": 4,
425 "field-id": 1000,
426 "name": "category",
427 "transform": "identity"
428 }]
429 }))
430 .unwrap();
431
432 let spec_v1 = serde_json::from_value::<PartitionSpec>(serde_json::json!({
435 "spec-id": 1,
436 "fields": [{
437 "source-id": 3,
438 "field-id": 1000,
439 "name": "ts_year",
440 "transform": "year"
441 }]
442 }))
443 .unwrap();
444
445 let err =
446 compute_unified_partition_type([&spec_v0, &spec_v1].into_iter(), &schema).unwrap_err();
447 assert_eq!(err.kind(), ErrorKind::DataInvalid);
448 }
449}