1use std::sync::Arc;
22
23use itertools::Itertools;
24use serde::{Deserialize, Serialize};
25use typed_builder::TypedBuilder;
26
27use super::transform::Transform;
28use super::{NestedField, PrimitiveType, Schema, SchemaRef, StructType};
29use crate::Result;
30use crate::error::invalid_data;
31use crate::spec::Struct;
32
33pub(crate) const UNPARTITIONED_LAST_ASSIGNED_ID: i32 = 999;
34pub(crate) const DEFAULT_PARTITION_SPEC_ID: i32 = 0;
35
36#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, TypedBuilder)]
38#[serde(rename_all = "kebab-case")]
39pub struct PartitionField {
40 pub source_id: i32,
42 pub field_id: i32,
45 pub name: String,
47 pub transform: Transform,
49}
50
51impl PartitionField {
52 pub fn into_unbound(self) -> UnboundPartitionField {
54 self.into()
55 }
56}
57
58pub type PartitionSpecRef = Arc<PartitionSpec>;
60#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
67#[serde(rename_all = "kebab-case")]
68pub struct PartitionSpec {
69 spec_id: i32,
71 fields: Vec<PartitionField>,
73}
74
75impl PartitionSpec {
76 pub fn builder(schema: impl Into<SchemaRef>) -> PartitionSpecBuilder {
78 PartitionSpecBuilder::new(schema)
79 }
80
81 pub fn fields(&self) -> &[PartitionField] {
83 &self.fields
84 }
85
86 pub fn spec_id(&self) -> i32 {
88 self.spec_id
89 }
90
91 pub fn unpartition_spec() -> Self {
93 Self {
94 spec_id: DEFAULT_PARTITION_SPEC_ID,
95 fields: vec![],
96 }
97 }
98
99 pub fn is_unpartitioned(&self) -> bool {
103 self.fields.is_empty() || self.fields.iter().all(|f| f.transform == Transform::Void)
104 }
105
106 pub fn partition_type(&self, schema: &Schema) -> Result<StructType> {
110 let mut struct_fields = Vec::with_capacity(self.fields.len());
111 for partition_field in &self.fields {
112 let res_type = match schema.field_by_id(partition_field.source_id) {
113 Some(field) => partition_field.transform.result_type(&field.field_type)?,
114 None => match partition_field.transform {
117 Transform::Bucket(_) | Transform::Year | Transform::Month | Transform::Hour => {
118 PrimitiveType::Int.into()
119 }
120 Transform::Day => PrimitiveType::Date.into(),
121 Transform::Unknown => PrimitiveType::String.into(),
122 Transform::Identity | Transform::Truncate(_) | Transform::Void => {
123 PrimitiveType::Unknown.into()
124 }
125 },
126 };
127 struct_fields.push(
128 NestedField::optional(partition_field.field_id, &partition_field.name, res_type)
129 .into(),
130 );
131 }
132 Ok(StructType::new(struct_fields))
133 }
134
135 pub fn into_unbound(self) -> UnboundPartitionSpec {
137 self.into()
138 }
139
140 pub fn with_spec_id(self, spec_id: i32) -> Self {
142 Self { spec_id, ..self }
143 }
144
145 pub fn has_sequential_ids(&self) -> bool {
149 has_sequential_ids(self.fields.iter().map(|f| f.field_id))
150 }
151
152 pub fn highest_field_id(&self) -> Option<i32> {
154 self.fields.iter().map(|f| f.field_id).max()
155 }
156
157 pub fn is_compatible_with(&self, other: &PartitionSpec) -> bool {
167 if self.fields.len() != other.fields.len() {
168 return false;
169 }
170
171 for (this_field, other_field) in self.fields.iter().zip(other.fields.iter()) {
172 if this_field.source_id != other_field.source_id
173 || this_field.name != other_field.name
174 || this_field.transform != other_field.transform
175 {
176 return false;
177 }
178 }
179
180 true
181 }
182
183 pub fn partition_to_path(&self, data: &Struct, schema: SchemaRef) -> String {
186 let partition_type = self.partition_type(&schema).unwrap();
187 let field_types = partition_type.fields();
188
189 self.fields
190 .iter()
191 .enumerate()
192 .map(|(i, field)| {
193 let value = data[i].as_ref();
194 form_urlencoded::Serializer::new(String::new())
195 .append_pair(
196 &field.name,
197 &field
198 .transform
199 .to_human_string(&field_types[i].field_type, value),
200 )
201 .finish()
202 })
203 .join("/")
204 }
205}
206
207#[derive(Clone, Debug)]
210pub struct PartitionKey {
211 spec: PartitionSpec,
213 schema: SchemaRef,
215 data: Struct,
217}
218
219impl PartitionKey {
220 pub fn new(spec: PartitionSpec, schema: SchemaRef, data: Struct) -> Self {
222 Self { spec, schema, data }
223 }
224
225 pub fn copy_with_data(&self, data: Struct) -> Self {
227 Self {
228 spec: self.spec.clone(),
229 schema: self.schema.clone(),
230 data,
231 }
232 }
233
234 pub fn to_path(&self) -> String {
236 self.spec.partition_to_path(&self.data, self.schema.clone())
237 }
238
239 pub fn is_effectively_none(partition_key: Option<&PartitionKey>) -> bool {
242 match partition_key {
243 None => true,
244 Some(pk) => pk.spec.is_unpartitioned(),
245 }
246 }
247
248 pub fn spec(&self) -> &PartitionSpec {
250 &self.spec
251 }
252
253 pub fn schema(&self) -> &SchemaRef {
255 &self.schema
256 }
257
258 pub fn data(&self) -> &Struct {
260 &self.data
261 }
262}
263
264pub type UnboundPartitionSpecRef = Arc<UnboundPartitionSpec>;
266#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, TypedBuilder)]
272#[serde(
273 try_from = "self::_serde::UnboundPartitionFieldSerde",
274 into = "self::_serde::UnboundPartitionFieldSerde"
275)]
276#[builder(build_method(into = Result<UnboundPartitionField>))]
277pub struct UnboundPartitionField {
278 source_ids: Vec<i32>,
281 #[builder(default, setter(strip_option(fallback = field_id_opt)))]
284 field_id: Option<i32>,
285 #[builder(setter(into))]
287 name: String,
288 transform: Transform,
290}
291
292impl UnboundPartitionField {
293 pub fn source_id(&self) -> Result<i32> {
298 match self.source_ids.as_slice() {
299 [source_id] => Ok(*source_id),
300 source_ids => Err(invalid_data!(
301 "Partition field '{}' reads {} source columns and has no single source id",
302 self.name,
303 source_ids.len()
304 )),
305 }
306 }
307
308 pub fn source_ids(&self) -> &[i32] {
310 &self.source_ids
311 }
312
313 pub fn field_id(&self) -> Option<i32> {
315 self.field_id
316 }
317
318 pub fn name(&self) -> &str {
320 &self.name
321 }
322
323 pub fn transform(&self) -> Transform {
325 self.transform
326 }
327
328 pub fn with_field_id(self, field_id: i32) -> Self {
330 Self {
331 field_id: Some(field_id),
332 ..self
333 }
334 }
335
336 fn validate(&self) -> Result<()> {
337 if self.source_ids.is_empty() {
338 return Err(invalid_data!("Empty source-ids is not allowed"));
339 }
340 Ok(())
341 }
342}
343
344impl From<UnboundPartitionField> for Result<UnboundPartitionField> {
345 fn from(field: UnboundPartitionField) -> Self {
346 field.validate()?;
347 Ok(field)
348 }
349}
350
351mod _serde {
352 use serde::{Deserialize, Serialize};
353
354 use super::UnboundPartitionField;
355 use crate::Error;
356 use crate::error::invalid_data;
357 use crate::spec::Transform;
358
359 #[derive(Serialize, Deserialize)]
363 #[serde(rename_all = "kebab-case")]
364 pub(super) struct UnboundPartitionFieldSerde {
365 #[serde(default, skip_serializing_if = "Option::is_none")]
366 source_id: Option<i32>,
367 #[serde(default, skip_serializing_if = "Option::is_none")]
368 source_ids: Option<Vec<i32>>,
369 #[serde(default, skip_serializing_if = "Option::is_none")]
370 field_id: Option<i32>,
371 name: String,
372 transform: Transform,
373 }
374
375 impl TryFrom<UnboundPartitionFieldSerde> for UnboundPartitionField {
376 type Error = Error;
377
378 fn try_from(value: UnboundPartitionFieldSerde) -> Result<Self, Error> {
379 let source_ids = match (value.source_id, value.source_ids) {
380 (Some(source_id), None) => vec![source_id],
381 (None, Some(source_ids)) => source_ids,
382 (Some(_), Some(_)) => {
383 return Err(invalid_data!(
384 "source-id and source-ids are mutually exclusive"
385 ));
386 }
387 (None, None) => {
388 return Err(invalid_data!(
389 "Either `source-id` or `source-ids` must be present"
390 ));
391 }
392 };
393
394 UnboundPartitionField::builder()
395 .source_ids(source_ids)
396 .field_id_opt(value.field_id)
397 .name(value.name)
398 .transform(value.transform)
399 .build()
400 }
401 }
402
403 impl From<UnboundPartitionField> for UnboundPartitionFieldSerde {
404 fn from(value: UnboundPartitionField) -> Self {
405 let (source_id, source_ids) = match value.source_ids.as_slice() {
406 [source_id] => (Some(*source_id), None),
407 _ => (None, Some(value.source_ids)),
408 };
409 Self {
410 source_id,
411 source_ids,
412 field_id: value.field_id,
413 name: value.name,
414 transform: value.transform,
415 }
416 }
417 }
418}
419
420#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]
424#[serde(rename_all = "kebab-case")]
425pub struct UnboundPartitionSpec {
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub(crate) spec_id: Option<i32>,
429 pub(crate) fields: Vec<UnboundPartitionField>,
431}
432
433impl UnboundPartitionSpec {
434 pub fn builder() -> UnboundPartitionSpecBuilder {
436 UnboundPartitionSpecBuilder::default()
437 }
438
439 pub fn bind(self, schema: impl Into<SchemaRef>) -> Result<PartitionSpec> {
444 PartitionSpecBuilder::new_from_unbound(self, schema)?.build()
445 }
446
447 pub fn spec_id(&self) -> Option<i32> {
449 self.spec_id
450 }
451
452 pub fn fields(&self) -> &[UnboundPartitionField] {
454 &self.fields
455 }
456
457 pub fn with_spec_id(self, spec_id: i32) -> Self {
459 Self {
460 spec_id: Some(spec_id),
461 ..self
462 }
463 }
464}
465
466fn has_sequential_ids(field_ids: impl Iterator<Item = i32>) -> bool {
467 for (index, field_id) in field_ids.enumerate() {
468 let expected_id = (UNPARTITIONED_LAST_ASSIGNED_ID as i64)
469 .checked_add(1)
470 .and_then(|id| id.checked_add(index as i64))
471 .unwrap_or(i64::MAX);
472
473 if field_id as i64 != expected_id {
474 return false;
475 }
476 }
477
478 true
479}
480
481impl From<PartitionField> for UnboundPartitionField {
482 fn from(field: PartitionField) -> Self {
483 UnboundPartitionField {
484 source_ids: vec![field.source_id],
485 field_id: Some(field.field_id),
486 name: field.name,
487 transform: field.transform,
488 }
489 }
490}
491
492impl From<PartitionSpec> for UnboundPartitionSpec {
493 fn from(spec: PartitionSpec) -> Self {
494 UnboundPartitionSpec {
495 spec_id: Some(spec.spec_id),
496 fields: spec.fields.into_iter().map(Into::into).collect(),
497 }
498 }
499}
500
501#[derive(Debug, Default)]
503pub struct UnboundPartitionSpecBuilder {
504 spec_id: Option<i32>,
505 fields: Vec<UnboundPartitionField>,
506}
507
508impl UnboundPartitionSpecBuilder {
509 pub fn new() -> Self {
511 Self {
512 spec_id: None,
513 fields: vec![],
514 }
515 }
516
517 pub fn with_spec_id(mut self, spec_id: i32) -> Self {
519 self.spec_id = Some(spec_id);
520 self
521 }
522
523 pub fn add_partition_field(mut self, field: UnboundPartitionField) -> Result<Self> {
525 self.check_name_set_and_unique(&field.name)?;
526 self.check_for_redundant_partitions(&field.source_ids, &field.transform)?;
527 if let Some(partition_field_id) = field.field_id {
528 self.check_partition_id_unique(partition_field_id)?;
529 }
530 self.fields.push(field);
531 Ok(self)
532 }
533
534 pub fn add_partition_fields(
536 self,
537 fields: impl IntoIterator<Item = UnboundPartitionField>,
538 ) -> Result<Self> {
539 let mut builder = self;
540 for field in fields {
541 builder = builder.add_partition_field(field)?;
542 }
543 Ok(builder)
544 }
545
546 pub fn build(self) -> UnboundPartitionSpec {
548 UnboundPartitionSpec {
549 spec_id: self.spec_id,
550 fields: self.fields,
551 }
552 }
553}
554
555#[derive(Debug)]
557pub struct PartitionSpecBuilder {
558 spec_id: Option<i32>,
559 last_assigned_field_id: i32,
560 fields: Vec<UnboundPartitionField>,
561 schema: SchemaRef,
562}
563
564impl PartitionSpecBuilder {
565 pub fn new(schema: impl Into<SchemaRef>) -> Self {
567 Self {
568 spec_id: None,
569 fields: vec![],
570 last_assigned_field_id: UNPARTITIONED_LAST_ASSIGNED_ID,
571 schema: schema.into(),
572 }
573 }
574
575 pub fn new_from_unbound(
577 unbound: UnboundPartitionSpec,
578 schema: impl Into<SchemaRef>,
579 ) -> Result<Self> {
580 let mut builder =
581 Self::new(schema).with_spec_id(unbound.spec_id.unwrap_or(DEFAULT_PARTITION_SPEC_ID));
582
583 for field in unbound.fields {
584 builder = builder.add_field_allowing_redundancy(field)?;
585 }
586 Ok(builder)
587 }
588
589 pub fn with_last_assigned_field_id(mut self, last_assigned_field_id: i32) -> Self {
595 self.last_assigned_field_id = last_assigned_field_id;
596 self
597 }
598
599 pub fn with_spec_id(mut self, spec_id: i32) -> Self {
601 self.spec_id = Some(spec_id);
602 self
603 }
604
605 pub fn add_partition_field(
607 self,
608 source_name: impl AsRef<str>,
609 target_name: impl Into<String>,
610 transform: Transform,
611 ) -> Result<Self> {
612 let source_id = self
613 .schema
614 .field_by_name(source_name.as_ref())
615 .ok_or_else(|| {
616 invalid_data!(
617 "Cannot find source column with name: {} in schema",
618 source_name.as_ref()
619 )
620 })?
621 .id;
622 let field = UnboundPartitionField {
623 source_ids: vec![source_id],
624 field_id: None,
625 name: target_name.into(),
626 transform,
627 };
628
629 self.add_unbound_field(field)
630 }
631
632 pub fn add_unbound_field(self, field: UnboundPartitionField) -> Result<Self> {
640 self.check_name_set_and_unique(&field.name)?;
641 self.check_for_redundant_partitions(&field.source_ids, &field.transform)?;
642 self.add_field_allowing_redundancy(field)
643 }
644
645 fn add_field_allowing_redundancy(mut self, field: UnboundPartitionField) -> Result<Self> {
649 self.check_name_set_and_unique(&field.name)?;
650 Self::check_name_does_not_collide_with_schema(&field, &self.schema)?;
651 Self::check_transform_compatibility(&field, &self.schema)?;
652 if let Some(partition_field_id) = field.field_id {
653 self.check_partition_id_unique(partition_field_id)?;
654 }
655
656 self.fields.push(field);
658 Ok(self)
659 }
660
661 pub fn add_unbound_fields(
663 self,
664 fields: impl IntoIterator<Item = UnboundPartitionField>,
665 ) -> Result<Self> {
666 let mut builder = self;
667 for field in fields {
668 builder = builder.add_unbound_field(field)?;
669 }
670 Ok(builder)
671 }
672
673 pub fn build(self) -> Result<PartitionSpec> {
675 let fields = Self::set_field_ids(self.fields, self.last_assigned_field_id)?;
676 Ok(PartitionSpec {
677 spec_id: self.spec_id.unwrap_or(DEFAULT_PARTITION_SPEC_ID),
678 fields,
679 })
680 }
681
682 fn set_field_ids(
683 fields: Vec<UnboundPartitionField>,
684 last_assigned_field_id: i32,
685 ) -> Result<Vec<PartitionField>> {
686 let mut last_assigned_field_id = last_assigned_field_id;
687 let assigned_ids = fields
690 .iter()
691 .filter_map(|f| f.field_id)
692 .collect::<std::collections::HashSet<_>>();
693
694 fn _check_add_1(prev: i32) -> Result<i32> {
695 prev.checked_add(1)
696 .ok_or_else(|| invalid_data!("Cannot assign more partition ids. Overflow."))
697 }
698
699 let mut bound_fields = Vec::with_capacity(fields.len());
700 for field in fields.into_iter() {
701 let partition_field_id = if let Some(partition_field_id) = field.field_id {
702 last_assigned_field_id = std::cmp::max(last_assigned_field_id, partition_field_id);
703 partition_field_id
704 } else {
705 last_assigned_field_id = _check_add_1(last_assigned_field_id)?;
706 while assigned_ids.contains(&last_assigned_field_id) {
707 last_assigned_field_id = _check_add_1(last_assigned_field_id)?;
708 }
709 last_assigned_field_id
710 };
711
712 bound_fields.push(PartitionField {
713 source_id: field.source_id()?,
714 field_id: partition_field_id,
715 name: field.name,
716 transform: field.transform,
717 })
718 }
719
720 Ok(bound_fields)
721 }
722
723 fn check_name_does_not_collide_with_schema(
728 field: &UnboundPartitionField,
729 schema: &Schema,
730 ) -> Result<()> {
731 match schema.field_by_name(field.name.as_str()) {
732 Some(schema_collision) => {
733 if field.transform.may_share_name_with_source() {
734 if schema_collision.id == field.source_id()? {
735 Ok(())
736 } else {
737 Err(invalid_data!(
738 "Cannot create identity or void partition sourced from different field in schema. Field name '{}' has id `{}` in schema but partition source id is `{}`",
739 field.name,
740 schema_collision.id,
741 field.source_id()?
742 ))
743 }
744 } else {
745 Err(invalid_data!(
746 "Cannot create partition with name: '{}' that conflicts with schema field and is not an identity or void transform.",
747 field.name
748 ))
749 }
750 }
751 None => Ok(()),
752 }
753 }
754
755 fn check_transform_compatibility(field: &UnboundPartitionField, schema: &Schema) -> Result<()> {
758 let source_id = field.source_id()?;
761 let schema_field = schema.field_by_id(source_id).ok_or_else(|| {
762 invalid_data!("Cannot find partition source field with id `{source_id}` in schema")
763 })?;
764
765 if field.transform != Transform::Void {
766 if !schema_field.field_type.is_primitive() {
767 return Err(invalid_data!(
768 "Cannot partition by non-primitive source field: '{}'.",
769 schema_field.field_type
770 ));
771 }
772
773 if field
774 .transform
775 .result_type(&schema_field.field_type)
776 .is_err()
777 {
778 return Err(invalid_data!(
779 "Invalid source type: '{}' for transform: '{}'.",
780 schema_field.field_type,
781 field.transform.dedup_name()
782 ));
783 }
784 }
785
786 Ok(())
787 }
788}
789
790trait CorePartitionSpecValidator {
792 fn check_name_set_and_unique(&self, name: &str) -> Result<()> {
794 if name.is_empty() {
795 return Err(invalid_data!("Cannot use empty partition name"));
796 }
797
798 if self.fields().iter().any(|f| f.name == name) {
799 return Err(invalid_data!(
800 "Cannot use partition name more than once: {name}"
801 ));
802 }
803 Ok(())
804 }
805
806 fn check_for_redundant_partitions(
808 &self,
809 source_ids: &[i32],
810 transform: &Transform,
811 ) -> Result<()> {
812 let collision = self.fields().iter().find(|f| {
813 f.source_ids == source_ids && f.transform.dedup_name() == transform.dedup_name()
814 });
815
816 if let Some(collision) = collision {
817 Err(invalid_data!(
818 "Cannot add redundant partition with source ids `{:?}` and transform `{}`. A partition with the same source ids and transform already exists with name `{}`",
819 source_ids,
820 transform.dedup_name(),
821 collision.name
822 ))
823 } else {
824 Ok(())
825 }
826 }
827
828 fn check_partition_id_unique(&self, field_id: i32) -> Result<()> {
830 if self.fields().iter().any(|f| f.field_id == Some(field_id)) {
831 return Err(invalid_data!(
832 "Cannot use field id more than once in one PartitionSpec: {field_id}"
833 ));
834 }
835
836 Ok(())
837 }
838
839 fn fields(&self) -> &Vec<UnboundPartitionField>;
840}
841
842impl CorePartitionSpecValidator for PartitionSpecBuilder {
843 fn fields(&self) -> &Vec<UnboundPartitionField> {
844 &self.fields
845 }
846}
847
848impl CorePartitionSpecValidator for UnboundPartitionSpecBuilder {
849 fn fields(&self) -> &Vec<UnboundPartitionField> {
850 &self.fields
851 }
852}
853
854#[cfg(test)]
855mod tests {
856 use super::*;
857 use crate::ErrorKind;
858 use crate::spec::{Literal, PrimitiveType, Type};
859
860 #[test]
861 fn test_partition_spec() {
862 let spec = r#"
863 {
864 "spec-id": 1,
865 "fields": [ {
866 "source-id": 4,
867 "field-id": 1000,
868 "name": "ts_day",
869 "transform": "day"
870 }, {
871 "source-id": 1,
872 "field-id": 1001,
873 "name": "id_bucket",
874 "transform": "bucket[16]"
875 }, {
876 "source-id": 2,
877 "field-id": 1002,
878 "name": "id_truncate",
879 "transform": "truncate[4]"
880 } ]
881 }
882 "#;
883
884 let partition_spec: PartitionSpec = serde_json::from_str(spec).unwrap();
885 assert_eq!(4, partition_spec.fields[0].source_id);
886 assert_eq!(1000, partition_spec.fields[0].field_id);
887 assert_eq!("ts_day", partition_spec.fields[0].name);
888 assert_eq!(Transform::Day, partition_spec.fields[0].transform);
889
890 assert_eq!(1, partition_spec.fields[1].source_id);
891 assert_eq!(1001, partition_spec.fields[1].field_id);
892 assert_eq!("id_bucket", partition_spec.fields[1].name);
893 assert_eq!(Transform::Bucket(16), partition_spec.fields[1].transform);
894
895 assert_eq!(2, partition_spec.fields[2].source_id);
896 assert_eq!(1002, partition_spec.fields[2].field_id);
897 assert_eq!("id_truncate", partition_spec.fields[2].name);
898 assert_eq!(Transform::Truncate(4), partition_spec.fields[2].transform);
899 }
900
901 #[test]
902 fn test_is_unpartitioned() {
903 let schema = Schema::builder()
904 .with_fields(vec![
905 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
906 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
907 ])
908 .build()
909 .unwrap();
910 let partition_spec = PartitionSpec::builder(schema.clone())
911 .with_spec_id(1)
912 .build()
913 .unwrap();
914 assert!(
915 partition_spec.is_unpartitioned(),
916 "Empty partition spec should be unpartitioned"
917 );
918
919 let partition_spec = PartitionSpec::builder(schema.clone())
920 .add_unbound_fields(vec![
921 UnboundPartitionField::builder()
922 .source_ids(vec![1])
923 .name("id".to_string())
924 .transform(Transform::Identity)
925 .build()
926 .unwrap(),
927 UnboundPartitionField::builder()
928 .source_ids(vec![2])
929 .name("name_string".to_string())
930 .transform(Transform::Void)
931 .build()
932 .unwrap(),
933 ])
934 .unwrap()
935 .with_spec_id(1)
936 .build()
937 .unwrap();
938 assert!(
939 !partition_spec.is_unpartitioned(),
940 "Partition spec with one non void transform should not be unpartitioned"
941 );
942
943 let partition_spec = PartitionSpec::builder(schema.clone())
944 .with_spec_id(1)
945 .add_unbound_fields(vec![
946 UnboundPartitionField::builder()
947 .source_ids(vec![1])
948 .name("id_void".to_string())
949 .transform(Transform::Void)
950 .build()
951 .unwrap(),
952 UnboundPartitionField::builder()
953 .source_ids(vec![2])
954 .name("name_void".to_string())
955 .transform(Transform::Void)
956 .build()
957 .unwrap(),
958 ])
959 .unwrap()
960 .build()
961 .unwrap();
962 assert!(
963 partition_spec.is_unpartitioned(),
964 "Partition spec with all void field should be unpartitioned"
965 );
966 }
967
968 #[test]
969 fn test_unbound_partition_spec() {
970 let spec = r#"
971 {
972 "spec-id": 1,
973 "fields": [ {
974 "source-id": 4,
975 "field-id": 1000,
976 "name": "ts_day",
977 "transform": "day"
978 }, {
979 "source-id": 1,
980 "field-id": 1001,
981 "name": "id_bucket",
982 "transform": "bucket[16]"
983 }, {
984 "source-id": 2,
985 "field-id": 1002,
986 "name": "id_truncate",
987 "transform": "truncate[4]"
988 } ]
989 }
990 "#;
991
992 let partition_spec: UnboundPartitionSpec = serde_json::from_str(spec).unwrap();
993 assert_eq!(Some(1), partition_spec.spec_id);
994
995 assert_eq!(4, partition_spec.fields[0].source_id().unwrap());
996 assert_eq!(Some(1000), partition_spec.fields[0].field_id);
997 assert_eq!("ts_day", partition_spec.fields[0].name);
998 assert_eq!(Transform::Day, partition_spec.fields[0].transform);
999
1000 assert_eq!(1, partition_spec.fields[1].source_id().unwrap());
1001 assert_eq!(Some(1001), partition_spec.fields[1].field_id);
1002 assert_eq!("id_bucket", partition_spec.fields[1].name);
1003 assert_eq!(Transform::Bucket(16), partition_spec.fields[1].transform);
1004
1005 assert_eq!(2, partition_spec.fields[2].source_id().unwrap());
1006 assert_eq!(Some(1002), partition_spec.fields[2].field_id);
1007 assert_eq!("id_truncate", partition_spec.fields[2].name);
1008 assert_eq!(Transform::Truncate(4), partition_spec.fields[2].transform);
1009
1010 let spec = r#"
1011 {
1012 "fields": [ {
1013 "source-id": 4,
1014 "name": "ts_day",
1015 "transform": "day"
1016 } ]
1017 }
1018 "#;
1019 let partition_spec: UnboundPartitionSpec = serde_json::from_str(spec).unwrap();
1020 assert_eq!(None, partition_spec.spec_id);
1021
1022 assert_eq!(4, partition_spec.fields[0].source_id().unwrap());
1023 assert_eq!(None, partition_spec.fields[0].field_id);
1024 assert_eq!("ts_day", partition_spec.fields[0].name);
1025 assert_eq!(Transform::Day, partition_spec.fields[0].transform);
1026 }
1027
1028 #[test]
1029 fn test_unbound_partition_spec_serialization_skips_none_fields() {
1030 let spec = UnboundPartitionSpec::builder()
1031 .add_partition_field(
1032 UnboundPartitionField::builder()
1033 .source_ids(vec![4])
1034 .name("ts_day")
1035 .transform(Transform::Day)
1036 .build()
1037 .unwrap(),
1038 )
1039 .unwrap()
1040 .build();
1041
1042 let value = serde_json::to_value(&spec).unwrap();
1043 let object = value.as_object().unwrap();
1044 assert!(!object.contains_key("spec-id"));
1045 let field = object["fields"][0].as_object().unwrap();
1046 assert!(!field.contains_key("field-id"));
1047
1048 let value = serde_json::to_value(spec.with_spec_id(1)).unwrap();
1049 let object = value.as_object().unwrap();
1050 assert_eq!(Some(&serde_json::json!(1)), object.get("spec-id"));
1051
1052 let spec: UnboundPartitionSpec = serde_json::from_str(
1055 r#"{
1056 "spec-id": null,
1057 "fields": [
1058 {"source-id": 4, "name": "ts_day", "transform": "day", "field-id": null}
1059 ]
1060 }"#,
1061 )
1062 .unwrap();
1063 assert_eq!(None, spec.spec_id);
1064 assert_eq!(None, spec.fields[0].field_id);
1065 }
1066
1067 #[test]
1068 fn test_new_unpartition() {
1069 let schema = Schema::builder()
1070 .with_fields(vec![
1071 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1072 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1073 ])
1074 .build()
1075 .unwrap();
1076 let partition_spec = PartitionSpec::builder(schema.clone())
1077 .with_spec_id(0)
1078 .build()
1079 .unwrap();
1080 let partition_type = partition_spec.partition_type(&schema).unwrap();
1081 assert_eq!(0, partition_type.fields().len());
1082
1083 let unpartition_spec = PartitionSpec::unpartition_spec();
1084 assert_eq!(partition_spec, unpartition_spec);
1085 }
1086
1087 #[test]
1088 fn test_partition_type() {
1089 let spec = r#"
1090 {
1091 "spec-id": 1,
1092 "fields": [ {
1093 "source-id": 4,
1094 "field-id": 1000,
1095 "name": "ts_day",
1096 "transform": "day"
1097 }, {
1098 "source-id": 1,
1099 "field-id": 1001,
1100 "name": "id_bucket",
1101 "transform": "bucket[16]"
1102 }, {
1103 "source-id": 2,
1104 "field-id": 1002,
1105 "name": "id_truncate",
1106 "transform": "truncate[4]"
1107 } ]
1108 }
1109 "#;
1110
1111 let partition_spec: PartitionSpec = serde_json::from_str(spec).unwrap();
1112 let schema = Schema::builder()
1113 .with_fields(vec![
1114 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1115 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1116 NestedField::required(3, "ts", Type::Primitive(PrimitiveType::Timestamp)).into(),
1117 NestedField::required(4, "ts_day", Type::Primitive(PrimitiveType::Timestamp))
1118 .into(),
1119 NestedField::required(5, "id_bucket", Type::Primitive(PrimitiveType::Int)).into(),
1120 NestedField::required(6, "id_truncate", Type::Primitive(PrimitiveType::Int)).into(),
1121 ])
1122 .build()
1123 .unwrap();
1124
1125 let partition_type = partition_spec.partition_type(&schema).unwrap();
1126 assert_eq!(3, partition_type.fields().len());
1127 assert_eq!(
1128 *partition_type.fields()[0],
1129 NestedField::optional(
1130 partition_spec.fields[0].field_id,
1131 &partition_spec.fields[0].name,
1132 Type::Primitive(PrimitiveType::Date)
1133 )
1134 );
1135 assert_eq!(
1136 *partition_type.fields()[1],
1137 NestedField::optional(
1138 partition_spec.fields[1].field_id,
1139 &partition_spec.fields[1].name,
1140 Type::Primitive(PrimitiveType::Int)
1141 )
1142 );
1143 assert_eq!(
1144 *partition_type.fields()[2],
1145 NestedField::optional(
1146 partition_spec.fields[2].field_id,
1147 &partition_spec.fields[2].name,
1148 Type::Primitive(PrimitiveType::String)
1149 )
1150 );
1151 }
1152
1153 #[test]
1154 fn test_partition_empty() {
1155 let spec = r#"
1156 {
1157 "spec-id": 1,
1158 "fields": []
1159 }
1160 "#;
1161
1162 let partition_spec: PartitionSpec = serde_json::from_str(spec).unwrap();
1163 let schema = Schema::builder()
1164 .with_fields(vec![
1165 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1166 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1167 NestedField::required(3, "ts", Type::Primitive(PrimitiveType::Timestamp)).into(),
1168 NestedField::required(4, "ts_day", Type::Primitive(PrimitiveType::Timestamp))
1169 .into(),
1170 NestedField::required(5, "id_bucket", Type::Primitive(PrimitiveType::Int)).into(),
1171 NestedField::required(6, "id_truncate", Type::Primitive(PrimitiveType::Int)).into(),
1172 ])
1173 .build()
1174 .unwrap();
1175
1176 let partition_type = partition_spec.partition_type(&schema).unwrap();
1177 assert_eq!(0, partition_type.fields().len());
1178 }
1179
1180 #[test]
1181 fn test_partition_type_with_dropped_source_column() {
1182 let spec = r#"
1183 {
1184 "spec-id": 1,
1185 "fields": [ {
1186 "source-id": 4,
1187 "field-id": 1000,
1188 "name": "ts_day",
1189 "transform": "day"
1190 }, {
1191 "source-id": 1,
1192 "field-id": 1001,
1193 "name": "id_bucket",
1194 "transform": "bucket[16]"
1195 }, {
1196 "source-id": 2,
1197 "field-id": 1002,
1198 "name": "id_truncate",
1199 "transform": "truncate[4]"
1200 } ]
1201 }
1202 "#;
1203
1204 let partition_spec: PartitionSpec = serde_json::from_str(spec).unwrap();
1205 let schema = Schema::builder()
1206 .with_fields(vec![
1207 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1208 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1209 ])
1210 .build()
1211 .unwrap();
1212
1213 assert_eq!(
1214 partition_spec.partition_type(&schema).unwrap(),
1215 StructType::new(vec![
1216 NestedField::optional(1000, "ts_day", PrimitiveType::Date).into(),
1217 NestedField::optional(1001, "id_bucket", PrimitiveType::Int).into(),
1218 NestedField::optional(1002, "id_truncate", PrimitiveType::String).into(),
1219 ])
1220 );
1221
1222 let empty_schema = Schema::builder().build().unwrap();
1224 assert_eq!(
1225 partition_spec.partition_type(&empty_schema).unwrap(),
1226 StructType::new(vec![
1227 NestedField::optional(1000, "ts_day", PrimitiveType::Date).into(),
1228 NestedField::optional(1001, "id_bucket", PrimitiveType::Int).into(),
1229 NestedField::optional(1002, "id_truncate", PrimitiveType::Unknown).into(),
1230 ])
1231 );
1232
1233 let incompatible_schema = Schema::builder()
1235 .with_fields(vec![
1236 NestedField::required(1, "id", PrimitiveType::Boolean).into(),
1237 ])
1238 .build()
1239 .unwrap();
1240 let err = partition_spec
1241 .partition_type(&incompatible_schema)
1242 .unwrap_err();
1243 assert_eq!(err.kind(), ErrorKind::DataInvalid);
1244 assert_eq!(
1245 err.message(),
1246 "boolean is not a valid input type of bucket transform"
1247 );
1248 }
1249
1250 #[test]
1251 fn test_partition_type_without_source_type() {
1252 let schema = Schema::builder().build().unwrap();
1253 for (transform, expected_type) in [
1254 (Transform::Identity, PrimitiveType::Unknown),
1255 (Transform::Truncate(4), PrimitiveType::Unknown),
1256 (Transform::Void, PrimitiveType::Unknown),
1257 (Transform::Bucket(16), PrimitiveType::Int),
1258 (Transform::Year, PrimitiveType::Int),
1259 (Transform::Month, PrimitiveType::Int),
1260 (Transform::Day, PrimitiveType::Date),
1261 (Transform::Hour, PrimitiveType::Int),
1262 (Transform::Unknown, PrimitiveType::String),
1263 ] {
1264 let spec = PartitionSpec {
1265 spec_id: 0,
1266 fields: vec![PartitionField {
1267 source_id: 1,
1268 field_id: 1000,
1269 name: "partition".to_string(),
1270 transform,
1271 }],
1272 };
1273 assert_eq!(
1274 spec.partition_type(&schema).unwrap(),
1275 StructType::new(vec![
1276 NestedField::optional(1000, "partition", expected_type).into(),
1277 ])
1278 );
1279 }
1280 }
1281
1282 #[test]
1283 fn test_builder_disallow_duplicate_names() {
1284 UnboundPartitionSpec::builder()
1285 .add_partition_field(
1286 UnboundPartitionField::builder()
1287 .source_ids(vec![1])
1288 .name("ts_day")
1289 .transform(Transform::Day)
1290 .build()
1291 .unwrap(),
1292 )
1293 .unwrap()
1294 .add_partition_field(
1295 UnboundPartitionField::builder()
1296 .source_ids(vec![2])
1297 .name("ts_day")
1298 .transform(Transform::Day)
1299 .build()
1300 .unwrap(),
1301 )
1302 .unwrap_err();
1303 }
1304
1305 #[test]
1306 fn test_builder_disallow_duplicate_field_ids() {
1307 let schema = Schema::builder()
1308 .with_fields(vec![
1309 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1310 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1311 ])
1312 .build()
1313 .unwrap();
1314 PartitionSpec::builder(schema.clone())
1315 .add_unbound_field(UnboundPartitionField {
1316 source_ids: vec![1],
1317 field_id: Some(1000),
1318 name: "id".to_string(),
1319 transform: Transform::Identity,
1320 })
1321 .unwrap()
1322 .add_unbound_field(UnboundPartitionField {
1323 source_ids: vec![2],
1324 field_id: Some(1000),
1325 name: "id_bucket".to_string(),
1326 transform: Transform::Bucket(16),
1327 })
1328 .unwrap_err();
1329 }
1330
1331 #[test]
1332 fn test_builder_auto_assign_field_ids() {
1333 let schema = Schema::builder()
1334 .with_fields(vec![
1335 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1336 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1337 NestedField::required(3, "ts", Type::Primitive(PrimitiveType::Timestamp)).into(),
1338 ])
1339 .build()
1340 .unwrap();
1341 let spec = PartitionSpec::builder(schema.clone())
1342 .with_spec_id(1)
1343 .add_unbound_field(UnboundPartitionField {
1344 source_ids: vec![1],
1345 name: "id".to_string(),
1346 transform: Transform::Identity,
1347 field_id: Some(1012),
1348 })
1349 .unwrap()
1350 .add_unbound_field(UnboundPartitionField {
1351 source_ids: vec![2],
1352 name: "name_void".to_string(),
1353 transform: Transform::Void,
1354 field_id: None,
1355 })
1356 .unwrap()
1357 .add_unbound_field(UnboundPartitionField {
1359 source_ids: vec![3],
1360 name: "year".to_string(),
1361 transform: Transform::Year,
1362 field_id: Some(1),
1363 })
1364 .unwrap()
1365 .build()
1366 .unwrap();
1367
1368 assert_eq!(1012, spec.fields[0].field_id);
1369 assert_eq!(1013, spec.fields[1].field_id);
1370 assert_eq!(1, spec.fields[2].field_id);
1371 }
1372
1373 #[test]
1374 fn test_builder_valid_schema() {
1375 let schema = Schema::builder()
1376 .with_fields(vec![
1377 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1378 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1379 ])
1380 .build()
1381 .unwrap();
1382
1383 PartitionSpec::builder(schema.clone())
1384 .with_spec_id(1)
1385 .build()
1386 .unwrap();
1387
1388 let spec = PartitionSpec::builder(schema.clone())
1389 .with_spec_id(1)
1390 .add_partition_field("id", "id_bucket[16]", Transform::Bucket(16))
1391 .unwrap()
1392 .build()
1393 .unwrap();
1394
1395 assert_eq!(spec, PartitionSpec {
1396 spec_id: 1,
1397 fields: vec![PartitionField {
1398 source_id: 1,
1399 field_id: 1000,
1400 name: "id_bucket[16]".to_string(),
1401 transform: Transform::Bucket(16),
1402 }],
1403 });
1404 assert_eq!(
1405 spec.partition_type(&schema).unwrap(),
1406 StructType::new(vec![
1407 NestedField::optional(1000, "id_bucket[16]", Type::Primitive(PrimitiveType::Int))
1408 .into()
1409 ])
1410 )
1411 }
1412
1413 #[test]
1414 fn test_collision_with_schema_name() {
1415 let schema = Schema::builder()
1416 .with_fields(vec![
1417 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1418 ])
1419 .build()
1420 .unwrap();
1421
1422 PartitionSpec::builder(schema.clone())
1423 .with_spec_id(1)
1424 .build()
1425 .unwrap();
1426
1427 let err = PartitionSpec::builder(schema)
1428 .with_spec_id(1)
1429 .add_unbound_field(UnboundPartitionField {
1430 source_ids: vec![1],
1431 field_id: None,
1432 name: "id".to_string(),
1433 transform: Transform::Bucket(16),
1434 })
1435 .unwrap_err();
1436 assert!(err.message().contains("conflicts with schema"))
1437 }
1438
1439 #[test]
1440 fn test_builder_collision_is_ok_for_identity_transforms() {
1441 let schema = Schema::builder()
1442 .with_fields(vec![
1443 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1444 NestedField::required(2, "number", Type::Primitive(PrimitiveType::Int)).into(),
1445 ])
1446 .build()
1447 .unwrap();
1448
1449 PartitionSpec::builder(schema.clone())
1450 .with_spec_id(1)
1451 .build()
1452 .unwrap();
1453
1454 PartitionSpec::builder(schema.clone())
1455 .with_spec_id(1)
1456 .add_unbound_field(UnboundPartitionField {
1457 source_ids: vec![1],
1458 field_id: None,
1459 name: "id".to_string(),
1460 transform: Transform::Identity,
1461 })
1462 .unwrap()
1463 .build()
1464 .unwrap();
1465
1466 PartitionSpec::builder(schema)
1468 .with_spec_id(1)
1469 .add_unbound_field(UnboundPartitionField {
1470 source_ids: vec![2],
1471 field_id: None,
1472 name: "id".to_string(),
1473 transform: Transform::Identity,
1474 })
1475 .unwrap_err();
1476 }
1477
1478 #[test]
1479 fn test_builder_all_source_ids_must_exist() {
1480 let schema = Schema::builder()
1481 .with_fields(vec![
1482 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1483 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1484 NestedField::required(3, "ts", Type::Primitive(PrimitiveType::Timestamp)).into(),
1485 ])
1486 .build()
1487 .unwrap();
1488
1489 PartitionSpec::builder(schema.clone())
1491 .with_spec_id(1)
1492 .add_unbound_fields(vec![
1493 UnboundPartitionField {
1494 source_ids: vec![1],
1495 field_id: None,
1496 name: "id_bucket".to_string(),
1497 transform: Transform::Bucket(16),
1498 },
1499 UnboundPartitionField {
1500 source_ids: vec![2],
1501 field_id: None,
1502 name: "name".to_string(),
1503 transform: Transform::Identity,
1504 },
1505 ])
1506 .unwrap()
1507 .build()
1508 .unwrap();
1509
1510 PartitionSpec::builder(schema)
1512 .with_spec_id(1)
1513 .add_unbound_fields(vec![
1514 UnboundPartitionField {
1515 source_ids: vec![1],
1516 field_id: None,
1517 name: "id_bucket".to_string(),
1518 transform: Transform::Bucket(16),
1519 },
1520 UnboundPartitionField {
1521 source_ids: vec![4],
1522 field_id: None,
1523 name: "name".to_string(),
1524 transform: Transform::Identity,
1525 },
1526 ])
1527 .unwrap_err();
1528 }
1529
1530 #[test]
1531 fn test_builder_disallows_variant_source() {
1532 let schema = Schema::builder()
1533 .with_fields(vec![
1534 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1535 NestedField::optional(2, "v", Type::Variant(crate::spec::VariantType)).into(),
1536 ])
1537 .build()
1538 .unwrap();
1539
1540 let err = PartitionSpec::builder(schema)
1541 .with_spec_id(1)
1542 .add_unbound_fields(vec![UnboundPartitionField {
1543 source_ids: vec![2],
1544 field_id: None,
1545 name: "v_part".to_string(),
1546 transform: Transform::Identity,
1547 }])
1548 .expect_err("variant must not be allowed as a partition source");
1549
1550 assert_eq!(
1551 err.message(),
1552 "Cannot partition by non-primitive source field: 'variant'."
1553 );
1554 }
1555
1556 #[test]
1557 fn test_builder_disallows_redundant() {
1558 let err = UnboundPartitionSpec::builder()
1559 .with_spec_id(1)
1560 .add_partition_field(
1561 UnboundPartitionField::builder()
1562 .source_ids(vec![1])
1563 .name("id_bucket[16]")
1564 .transform(Transform::Bucket(16))
1565 .build()
1566 .unwrap(),
1567 )
1568 .unwrap()
1569 .add_partition_field(
1570 UnboundPartitionField::builder()
1571 .source_ids(vec![1])
1572 .name("id_bucket_with_other_name")
1573 .transform(Transform::Bucket(16))
1574 .build()
1575 .unwrap(),
1576 )
1577 .unwrap_err();
1578 assert!(err.message().contains("redundant partition"));
1579 }
1580
1581 fn ts_schema() -> Schema {
1582 Schema::builder()
1583 .with_fields(vec![
1584 NestedField::required(1, "ts", Type::Primitive(PrimitiveType::Timestamptz)).into(),
1585 NestedField::required(2, "id", Type::Primitive(PrimitiveType::Long)).into(),
1586 ])
1587 .build()
1588 .unwrap()
1589 }
1590
1591 fn ts_field(field_id: i32, name: &str, transform: Transform) -> UnboundPartitionField {
1592 UnboundPartitionField {
1593 source_ids: vec![1],
1594 field_id: Some(field_id),
1595 name: name.to_string(),
1596 transform,
1597 }
1598 }
1599
1600 #[test]
1601 fn test_add_unbound_field_rejects_second_time_granularity() {
1602 let err = PartitionSpec::builder(ts_schema())
1603 .add_unbound_field(ts_field(1000, "ts_day", Transform::Day))
1604 .unwrap()
1605 .add_unbound_field(ts_field(1001, "ts_hour", Transform::Hour))
1606 .unwrap_err();
1607 assert!(err.message().contains("redundant partition"));
1608 }
1609
1610 #[test]
1611 fn test_bind_accepts_evolved_spec_fields() {
1612 let fields = vec![
1613 ts_field(1000, "ts_day", Transform::Day),
1614 ts_field(1001, "ts_hour", Transform::Hour),
1615 ts_field(1002, "ts", Transform::Void),
1616 ts_field(1003, "ts_null", Transform::Void),
1617 ];
1618 let unbound = UnboundPartitionSpec {
1619 spec_id: Some(1),
1620 fields: fields.clone(),
1621 };
1622
1623 let spec = unbound.bind(ts_schema()).unwrap();
1624
1625 let expected = PartitionSpec {
1626 spec_id: 1,
1627 fields: fields
1628 .into_iter()
1629 .map(|f| PartitionField {
1630 source_id: 1,
1631 field_id: f.field_id.unwrap(),
1632 name: f.name,
1633 transform: f.transform,
1634 })
1635 .collect(),
1636 };
1637 assert_eq!(spec, expected);
1638 }
1639
1640 #[test]
1641 fn test_bind_rejects_void_field_named_after_other_column() {
1642 let unbound = UnboundPartitionSpec {
1643 spec_id: Some(1),
1644 fields: vec![ts_field(1000, "id", Transform::Void)],
1645 };
1646
1647 let err = unbound.bind(ts_schema()).unwrap_err();
1648
1649 assert_eq!(err.kind(), ErrorKind::DataInvalid);
1650 assert!(err.message().contains("Field name 'id'"));
1651 }
1652
1653 #[test]
1654 fn test_bind_accepts_identical_fields_with_different_names() {
1655 let bucket_field = |field_id: i32, name: &str| UnboundPartitionField {
1656 source_ids: vec![2],
1657 field_id: Some(field_id),
1658 name: name.to_string(),
1659 transform: Transform::Bucket(16),
1660 };
1661 let unbound = UnboundPartitionSpec {
1662 spec_id: Some(1),
1663 fields: vec![
1664 bucket_field(1000, "id_bucket"),
1665 bucket_field(1001, "id_bucket_2"),
1666 ],
1667 };
1668
1669 let spec = unbound.bind(ts_schema()).unwrap();
1670
1671 let expected = PartitionSpec {
1672 spec_id: 1,
1673 fields: vec![
1674 PartitionField {
1675 source_id: 2,
1676 field_id: 1000,
1677 name: "id_bucket".to_string(),
1678 transform: Transform::Bucket(16),
1679 },
1680 PartitionField {
1681 source_id: 2,
1682 field_id: 1001,
1683 name: "id_bucket_2".to_string(),
1684 transform: Transform::Bucket(16),
1685 },
1686 ],
1687 };
1688 assert_eq!(spec, expected);
1689 }
1690
1691 #[test]
1692 fn test_bind_rejects_repeated_partition_name() {
1693 let unbound = UnboundPartitionSpec {
1694 spec_id: Some(1),
1695 fields: vec![
1696 ts_field(1000, "ts_part", Transform::Day),
1697 ts_field(1001, "ts_part", Transform::Hour),
1698 ],
1699 };
1700
1701 let err = unbound.bind(ts_schema()).unwrap_err();
1702
1703 assert!(
1704 err.message()
1705 .contains("Cannot use partition name more than once")
1706 );
1707 }
1708
1709 #[test]
1710 fn test_add_unbound_field_checks_name_before_redundancy() {
1711 let err = PartitionSpec::builder(ts_schema())
1712 .add_unbound_field(ts_field(1000, "ts_part", Transform::Day))
1713 .unwrap()
1714 .add_unbound_field(ts_field(1001, "ts_part", Transform::Hour))
1715 .unwrap_err();
1716
1717 assert!(
1718 err.message()
1719 .contains("Cannot use partition name more than once")
1720 );
1721 }
1722
1723 #[test]
1724 fn test_builder_incompatible_transforms_disallowed() {
1725 let schema = Schema::builder()
1726 .with_fields(vec![
1727 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1728 ])
1729 .build()
1730 .unwrap();
1731
1732 PartitionSpec::builder(schema)
1733 .with_spec_id(1)
1734 .add_unbound_field(UnboundPartitionField {
1735 source_ids: vec![1],
1736 field_id: None,
1737 name: "id_year".to_string(),
1738 transform: Transform::Year,
1739 })
1740 .unwrap_err();
1741 }
1742
1743 #[test]
1744 fn test_build_unbound_specs_without_partition_id() {
1745 let spec = UnboundPartitionSpec::builder()
1746 .with_spec_id(1)
1747 .add_partition_fields(vec![UnboundPartitionField {
1748 source_ids: vec![1],
1749 field_id: None,
1750 name: "id_bucket[16]".to_string(),
1751 transform: Transform::Bucket(16),
1752 }])
1753 .unwrap()
1754 .build();
1755
1756 assert_eq!(spec, UnboundPartitionSpec {
1757 spec_id: Some(1),
1758 fields: vec![UnboundPartitionField {
1759 source_ids: vec![1],
1760 field_id: None,
1761 name: "id_bucket[16]".to_string(),
1762 transform: Transform::Bucket(16),
1763 }]
1764 });
1765 }
1766
1767 #[test]
1768 fn test_is_compatible_with() {
1769 let schema = Schema::builder()
1770 .with_fields(vec![
1771 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1772 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1773 ])
1774 .build()
1775 .unwrap();
1776
1777 let partition_spec_1 = PartitionSpec::builder(schema.clone())
1778 .with_spec_id(1)
1779 .add_unbound_field(UnboundPartitionField {
1780 source_ids: vec![1],
1781 field_id: None,
1782 name: "id_bucket".to_string(),
1783 transform: Transform::Bucket(16),
1784 })
1785 .unwrap()
1786 .build()
1787 .unwrap();
1788
1789 let partition_spec_2 = PartitionSpec::builder(schema)
1790 .with_spec_id(1)
1791 .add_unbound_field(UnboundPartitionField {
1792 source_ids: vec![1],
1793 field_id: None,
1794 name: "id_bucket".to_string(),
1795 transform: Transform::Bucket(16),
1796 })
1797 .unwrap()
1798 .build()
1799 .unwrap();
1800
1801 assert!(partition_spec_1.is_compatible_with(&partition_spec_2));
1802 }
1803
1804 #[test]
1805 fn test_not_compatible_with_transform_different() {
1806 let schema = Schema::builder()
1807 .with_fields(vec![
1808 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1809 ])
1810 .build()
1811 .unwrap();
1812
1813 let partition_spec_1 = PartitionSpec::builder(schema.clone())
1814 .with_spec_id(1)
1815 .add_unbound_field(UnboundPartitionField {
1816 source_ids: vec![1],
1817 field_id: None,
1818 name: "id_bucket".to_string(),
1819 transform: Transform::Bucket(16),
1820 })
1821 .unwrap()
1822 .build()
1823 .unwrap();
1824
1825 let partition_spec_2 = PartitionSpec::builder(schema)
1826 .with_spec_id(1)
1827 .add_unbound_field(UnboundPartitionField {
1828 source_ids: vec![1],
1829 field_id: None,
1830 name: "id_bucket".to_string(),
1831 transform: Transform::Bucket(32),
1832 })
1833 .unwrap()
1834 .build()
1835 .unwrap();
1836
1837 assert!(!partition_spec_1.is_compatible_with(&partition_spec_2));
1838 }
1839
1840 #[test]
1841 fn test_not_compatible_with_source_id_different() {
1842 let schema = Schema::builder()
1843 .with_fields(vec![
1844 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1845 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1846 ])
1847 .build()
1848 .unwrap();
1849
1850 let partition_spec_1 = PartitionSpec::builder(schema.clone())
1851 .with_spec_id(1)
1852 .add_unbound_field(UnboundPartitionField {
1853 source_ids: vec![1],
1854 field_id: None,
1855 name: "id_bucket".to_string(),
1856 transform: Transform::Bucket(16),
1857 })
1858 .unwrap()
1859 .build()
1860 .unwrap();
1861
1862 let partition_spec_2 = PartitionSpec::builder(schema)
1863 .with_spec_id(1)
1864 .add_unbound_field(UnboundPartitionField {
1865 source_ids: vec![2],
1866 field_id: None,
1867 name: "id_bucket".to_string(),
1868 transform: Transform::Bucket(16),
1869 })
1870 .unwrap()
1871 .build()
1872 .unwrap();
1873
1874 assert!(!partition_spec_1.is_compatible_with(&partition_spec_2));
1875 }
1876
1877 #[test]
1878 fn test_not_compatible_with_order_different() {
1879 let schema = Schema::builder()
1880 .with_fields(vec![
1881 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1882 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1883 ])
1884 .build()
1885 .unwrap();
1886
1887 let partition_spec_1 = PartitionSpec::builder(schema.clone())
1888 .with_spec_id(1)
1889 .add_unbound_field(UnboundPartitionField {
1890 source_ids: vec![1],
1891 field_id: None,
1892 name: "id_bucket".to_string(),
1893 transform: Transform::Bucket(16),
1894 })
1895 .unwrap()
1896 .add_unbound_field(UnboundPartitionField {
1897 source_ids: vec![2],
1898 field_id: None,
1899 name: "name".to_string(),
1900 transform: Transform::Identity,
1901 })
1902 .unwrap()
1903 .build()
1904 .unwrap();
1905
1906 let partition_spec_2 = PartitionSpec::builder(schema)
1907 .with_spec_id(1)
1908 .add_unbound_field(UnboundPartitionField {
1909 source_ids: vec![2],
1910 field_id: None,
1911 name: "name".to_string(),
1912 transform: Transform::Identity,
1913 })
1914 .unwrap()
1915 .add_unbound_field(UnboundPartitionField {
1916 source_ids: vec![1],
1917 field_id: None,
1918 name: "id_bucket".to_string(),
1919 transform: Transform::Bucket(16),
1920 })
1921 .unwrap()
1922 .build()
1923 .unwrap();
1924
1925 assert!(!partition_spec_1.is_compatible_with(&partition_spec_2));
1926 }
1927
1928 #[test]
1929 fn test_highest_field_id_unpartitioned() {
1930 let spec = PartitionSpec::builder(Schema::builder().with_fields(vec![]).build().unwrap())
1931 .with_spec_id(1)
1932 .build()
1933 .unwrap();
1934
1935 assert!(spec.highest_field_id().is_none());
1936 }
1937
1938 #[test]
1939 fn test_highest_field_id() {
1940 let schema = Schema::builder()
1941 .with_fields(vec![
1942 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1943 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1944 ])
1945 .build()
1946 .unwrap();
1947
1948 let spec = PartitionSpec::builder(schema)
1949 .with_spec_id(1)
1950 .add_unbound_field(UnboundPartitionField {
1951 source_ids: vec![1],
1952 field_id: Some(1001),
1953 name: "id".to_string(),
1954 transform: Transform::Identity,
1955 })
1956 .unwrap()
1957 .add_unbound_field(UnboundPartitionField {
1958 source_ids: vec![2],
1959 field_id: Some(1000),
1960 name: "name".to_string(),
1961 transform: Transform::Identity,
1962 })
1963 .unwrap()
1964 .build()
1965 .unwrap();
1966
1967 assert_eq!(Some(1001), spec.highest_field_id());
1968 }
1969
1970 #[test]
1971 fn test_has_sequential_ids() {
1972 let schema = Schema::builder()
1973 .with_fields(vec![
1974 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
1975 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
1976 ])
1977 .build()
1978 .unwrap();
1979
1980 let spec = PartitionSpec::builder(schema)
1981 .with_spec_id(1)
1982 .add_unbound_field(UnboundPartitionField {
1983 source_ids: vec![1],
1984 field_id: Some(1000),
1985 name: "id".to_string(),
1986 transform: Transform::Identity,
1987 })
1988 .unwrap()
1989 .add_unbound_field(UnboundPartitionField {
1990 source_ids: vec![2],
1991 field_id: Some(1001),
1992 name: "name".to_string(),
1993 transform: Transform::Identity,
1994 })
1995 .unwrap()
1996 .build()
1997 .unwrap();
1998
1999 assert_eq!(1000, spec.fields[0].field_id);
2000 assert_eq!(1001, spec.fields[1].field_id);
2001 assert!(spec.has_sequential_ids());
2002 }
2003
2004 #[test]
2005 fn test_sequential_ids_must_start_at_1000() {
2006 let schema = Schema::builder()
2007 .with_fields(vec![
2008 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
2009 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
2010 ])
2011 .build()
2012 .unwrap();
2013
2014 let spec = PartitionSpec::builder(schema)
2015 .with_spec_id(1)
2016 .add_unbound_field(UnboundPartitionField {
2017 source_ids: vec![1],
2018 field_id: Some(999),
2019 name: "id".to_string(),
2020 transform: Transform::Identity,
2021 })
2022 .unwrap()
2023 .add_unbound_field(UnboundPartitionField {
2024 source_ids: vec![2],
2025 field_id: Some(1000),
2026 name: "name".to_string(),
2027 transform: Transform::Identity,
2028 })
2029 .unwrap()
2030 .build()
2031 .unwrap();
2032
2033 assert_eq!(999, spec.fields[0].field_id);
2034 assert_eq!(1000, spec.fields[1].field_id);
2035 assert!(!spec.has_sequential_ids());
2036 }
2037
2038 #[test]
2039 fn test_sequential_ids_must_have_no_gaps() {
2040 let schema = Schema::builder()
2041 .with_fields(vec![
2042 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
2043 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
2044 ])
2045 .build()
2046 .unwrap();
2047
2048 let spec = PartitionSpec::builder(schema)
2049 .with_spec_id(1)
2050 .add_unbound_field(UnboundPartitionField {
2051 source_ids: vec![1],
2052 field_id: Some(1000),
2053 name: "id".to_string(),
2054 transform: Transform::Identity,
2055 })
2056 .unwrap()
2057 .add_unbound_field(UnboundPartitionField {
2058 source_ids: vec![2],
2059 field_id: Some(1002),
2060 name: "name".to_string(),
2061 transform: Transform::Identity,
2062 })
2063 .unwrap()
2064 .build()
2065 .unwrap();
2066
2067 assert_eq!(1000, spec.fields[0].field_id);
2068 assert_eq!(1002, spec.fields[1].field_id);
2069 assert!(!spec.has_sequential_ids());
2070 }
2071
2072 #[test]
2073 fn test_partition_to_path_with_dropped_source_column() {
2074 for (source_type, transform, value, expected) in [
2075 (
2076 PrimitiveType::Int,
2077 Transform::Identity,
2078 Literal::int(1),
2079 "p=1",
2080 ),
2081 (
2082 PrimitiveType::Timestamp,
2083 Transform::Identity,
2084 Literal::long(1),
2085 "p=1",
2086 ),
2087 (
2088 PrimitiveType::Int,
2089 Transform::Truncate(10),
2090 Literal::int(10),
2091 "p=10",
2092 ),
2093 ] {
2094 let schema = Schema::builder()
2095 .with_fields(vec![
2096 NestedField::required(1, "kept", PrimitiveType::Int).into(),
2097 NestedField::required(2, "dropped", source_type).into(),
2098 ])
2099 .build()
2100 .unwrap();
2101 let spec = PartitionSpec::builder(schema)
2102 .add_partition_field("dropped", "p", transform)
2103 .unwrap()
2104 .build()
2105 .unwrap();
2106 let current_schema = Schema::builder()
2107 .with_fields(vec![
2108 NestedField::required(1, "kept", PrimitiveType::Int).into(),
2109 ])
2110 .build()
2111 .unwrap();
2112 assert_eq!(
2113 spec.partition_to_path(&Struct::from_iter([Some(value)]), current_schema.into()),
2114 expected
2115 );
2116 }
2117 }
2118
2119 #[test]
2120 fn test_partition_to_path() {
2121 let schema = Schema::builder()
2122 .with_fields(vec![
2123 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
2124 NestedField::required(2, "name", Type::Primitive(PrimitiveType::String)).into(),
2125 NestedField::required(3, "timestamp", Type::Primitive(PrimitiveType::Timestamp))
2126 .into(),
2127 NestedField::required(4, "empty", Type::Primitive(PrimitiveType::String)).into(),
2128 ])
2129 .build()
2130 .unwrap();
2131
2132 let spec = PartitionSpec::builder(schema.clone())
2133 .add_partition_field("id", "id", Transform::Identity)
2134 .unwrap()
2135 .add_partition_field("name", "name", Transform::Identity)
2136 .unwrap()
2137 .add_partition_field("timestamp", "ts_hour", Transform::Hour)
2138 .unwrap()
2139 .add_partition_field("empty", "empty_void", Transform::Void)
2140 .unwrap()
2141 .build()
2142 .unwrap();
2143
2144 let data = Struct::from_iter([
2145 Some(Literal::int(42)),
2146 Some(Literal::string("alice")),
2147 Some(Literal::int(1000)),
2148 Some(Literal::string("empty")),
2149 ]);
2150
2151 assert_eq!(
2152 spec.partition_to_path(&data, schema.into()),
2153 "id=42/name=alice/ts_hour=1970-02-11-16/empty_void=null"
2154 );
2155 }
2156
2157 #[test]
2158 fn test_partition_to_path_float_matches_java() {
2159 let schema = Schema::builder()
2160 .with_fields(vec![
2161 NestedField::required(1, "d", Type::Primitive(PrimitiveType::Double)).into(),
2162 NestedField::required(2, "f", Type::Primitive(PrimitiveType::Float)).into(),
2163 ])
2164 .build()
2165 .unwrap();
2166
2167 let spec = PartitionSpec::builder(schema.clone())
2168 .add_partition_field("d", "d", Transform::Identity)
2169 .unwrap()
2170 .add_partition_field("f", "f", Transform::Identity)
2171 .unwrap()
2172 .build()
2173 .unwrap();
2174
2175 let data = Struct::from_iter([Some(Literal::double(1.0)), Some(Literal::float(1.0e7_f32))]);
2176
2177 assert_eq!(
2178 spec.partition_to_path(&data, schema.into()),
2179 "d=1.0/f=1.0E7"
2180 );
2181 }
2182
2183 #[test]
2184 fn test_partition_to_path_escaped_strings() {
2185 let schema = Schema::builder()
2186 .with_fields(vec![
2187 NestedField::required(1, "\"esc\"#1", Type::Primitive(PrimitiveType::String))
2188 .into(),
2189 NestedField::required(2, "data", Type::Primitive(PrimitiveType::String)).into(),
2190 ])
2191 .build()
2192 .unwrap();
2193
2194 let spec = PartitionSpec::builder(schema.clone())
2195 .add_partition_field("\"esc\"#1", "\"esc\"#1", Transform::Identity)
2196 .unwrap()
2197 .build()
2198 .unwrap();
2199
2200 let data = Struct::from_iter([
2201 Some(Literal::string("a/b/c/d")),
2202 Some(Literal::string("val#1")),
2203 ]);
2204
2205 assert_eq!(
2206 spec.partition_to_path(&data, schema.into()),
2207 "%22esc%22%231=a%2Fb%2Fc%2Fd"
2208 );
2209 }
2210
2211 #[test]
2212 fn test_partition_to_path_escaped_field_name() {
2213 let schema = Schema::builder()
2214 .with_fields(vec![
2215 NestedField::required(1, "\"esc\"#1", Type::Primitive(PrimitiveType::String))
2216 .into(),
2217 NestedField::required(2, "data", Type::Primitive(PrimitiveType::String)).into(),
2218 ])
2219 .build()
2220 .unwrap();
2221
2222 let spec = PartitionSpec::builder(schema.clone())
2223 .add_partition_field("data", "data", Transform::Identity)
2224 .unwrap()
2225 .add_partition_field("data", "data_truc_10", Transform::Truncate(10))
2226 .unwrap()
2227 .build()
2228 .unwrap();
2229
2230 let data = Struct::from_iter([
2231 Some(Literal::string("a/b/c/d")),
2232 Some(Literal::string("a/b/c/d")),
2233 ]);
2234
2235 assert_eq!(
2236 spec.partition_to_path(&data, schema.into()),
2237 "data=a%2Fb%2Fc%2Fd/data_truc_10=a%2Fb%2Fc%2Fd"
2238 );
2239 }
2240
2241 #[test]
2242 fn test_unbound_partition_field_reads_source_ids_only() {
2243 let field: UnboundPartitionField = serde_json::from_str(
2244 r#"{"source-ids": [1, 2], "name": "m", "transform": "bucket[4]"}"#,
2245 )
2246 .unwrap();
2247
2248 assert_eq!([1, 2], field.source_ids());
2249 assert!(
2251 field
2252 .source_id()
2253 .unwrap_err()
2254 .to_string()
2255 .contains("has no single source id")
2256 );
2257
2258 let serialized = serde_json::to_value(&field).unwrap();
2259 assert_eq!(
2260 Some(&serde_json::json!([1, 2])),
2261 serialized.get("source-ids")
2262 );
2263 assert!(serialized.get("source-id").is_none());
2264 }
2265
2266 #[test]
2267 fn test_unbound_partition_field_single_source_id_round_trip() {
2268 let field: UnboundPartitionField =
2269 serde_json::from_str(r#"{"source-id": 1, "name": "m", "transform": "identity"}"#)
2270 .unwrap();
2271
2272 assert_eq!(1, field.source_id().unwrap());
2273 assert_eq!([1], field.source_ids());
2274
2275 let serialized = serde_json::to_value(&field).unwrap();
2277 assert_eq!(Some(&serde_json::json!(1)), serialized.get("source-id"));
2278 assert!(serialized.get("source-ids").is_none());
2279 }
2280
2281 #[test]
2282 fn test_unbound_partition_field_rejects_malformed_source_ids() {
2283 for (input, expected) in [
2284 (
2285 r#"{"source-ids": [], "name": "m", "transform": "identity"}"#,
2286 "Empty source-ids is not allowed",
2287 ),
2288 (
2289 r#"{"name": "m", "transform": "identity"}"#,
2290 "Either `source-id` or `source-ids` must be present",
2291 ),
2292 (
2293 r#"{"source-id": 1, "source-ids": [1], "name": "m", "transform": "identity"}"#,
2294 "mutually exclusive",
2295 ),
2296 ] {
2297 let err = serde_json::from_str::<UnboundPartitionField>(input).unwrap_err();
2298 assert!(
2299 err.to_string().contains(expected),
2300 "unexpected error for {input}: {err}"
2301 );
2302 }
2303 }
2304
2305 #[test]
2306 fn test_binding_a_multi_argument_field_fails_loudly() {
2307 let schema = Schema::builder()
2308 .with_fields(vec![
2309 NestedField::required(1, "a", Type::Primitive(PrimitiveType::Int)).into(),
2310 NestedField::required(2, "b", Type::Primitive(PrimitiveType::Int)).into(),
2311 ])
2312 .build()
2313 .unwrap();
2314
2315 let spec: UnboundPartitionSpec = serde_json::from_str(
2316 r#"{"spec-id": 1, "fields": [{"source-ids": [1, 2], "field-id": 1000, "name": "m", "transform": "bucket[4]"}]}"#,
2317 )
2318 .unwrap();
2319
2320 let err = spec.bind(schema).unwrap_err();
2323 assert!(
2324 err.to_string().contains("has no single source id"),
2325 "unexpected error: {err}"
2326 );
2327 }
2328
2329 #[test]
2330 fn test_unbound_partition_field_builder_rejects_empty_source_ids() {
2331 let err = UnboundPartitionField::builder()
2332 .source_ids(vec![])
2333 .name("m")
2334 .transform(Transform::Identity)
2335 .build()
2336 .unwrap_err();
2337 assert!(
2338 err.to_string().contains("Empty source-ids is not allowed"),
2339 "unexpected error: {err}"
2340 );
2341 }
2342}