33//! When stats are requested, also reports whether the checkpoint has compatible parsed stats.
44//! Driven through a [`PlanExecutor`].
55
6- // No in-crate caller yet; following PRs will use this.
7- #![ allow( dead_code) ]
8-
96use url:: Url ;
107
118use crate :: actions:: visitors:: SidecarVisitor ;
@@ -29,23 +26,27 @@ pub(crate) enum CheckpointType {
2926 Manifest ,
3027}
3128
32- /// A snapshot's resolved checkpoint type and parsed-stats schema .
29+ /// A snapshot's resolved checkpoint type and compatible parsed schemas .
3330#[ derive( Clone , Debug , PartialEq ) ]
3431pub ( crate ) struct CheckpointShape {
3532 /// What kind of checkpoint this is.
3633 pub ( crate ) checkpoint_type : CheckpointType ,
3734 /// The requested stats schema when the checkpoint has a compatible `add.stats_parsed` struct
3835 /// to read it from; `None` when stats were not requested or no compatible parsed stats exist.
3936 pub ( crate ) parsed_stats_schema : Option < SchemaRef > ,
37+ /// The requested partition schema when the checkpoint has a compatible
38+ /// `add.partitionValues_parsed` struct; `None` when partition values were not requested or no
39+ /// compatible parsed partition values exist.
40+ pub ( crate ) parsed_partition_schema : Option < SchemaRef > ,
4041}
4142
4243impl CheckpointShape {
43- /// Resolve `snapshot`'s checkpoint shape. Determines the checkpoint type and, when
44- /// `stats_schema` is `Some`, whether the checkpoint contains parsed stats compatible with it.
44+ /// Resolve `snapshot`'s checkpoint shape and compatible parsed stats and partition schemas.
4545 pub ( crate ) fn try_new (
4646 exec : & dyn PlanExecutor ,
4747 snapshot : & Snapshot ,
4848 stats_schema : Option < & SchemaRef > ,
49+ partition_schema : Option < & SchemaRef > ,
4950 ) -> DeltaResult < CheckpointShape > {
5051 let segment = snapshot. log_segment ( ) ;
5152
@@ -56,15 +57,21 @@ impl CheckpointShape {
5657 return Ok ( CheckpointShape {
5758 checkpoint_type : CheckpointType :: None ,
5859 parsed_stats_schema : None ,
60+ parsed_partition_schema : None ,
5961 } )
6062 }
6163 } ;
6264
6365 // Classify from a V2 checkpoint's `_last_checkpoint` hint when possible, else inspect the
6466 // file.
65- if let Some ( shape) =
66- Self :: from_v2_checkpoint_hint ( exec, segment, root_checkpoint, file_type, stats_schema) ?
67- {
67+ if let Some ( shape) = Self :: from_v2_checkpoint_hint (
68+ exec,
69+ segment,
70+ root_checkpoint,
71+ file_type,
72+ stats_schema,
73+ partition_schema,
74+ ) ? {
6875 return Ok ( shape) ;
6976 }
7077
@@ -77,22 +84,34 @@ impl CheckpointShape {
7784 } ;
7885 // No `sidecar` column means the file actions are inline, so this is a leaf.
7986 if !cp_schema. contains ( SIDECAR_NAME ) {
80- return Ok ( Self :: try_new_leaf ( Some ( cp_schema) , stats_schema) ) ;
87+ return Ok ( Self :: try_new_leaf (
88+ Some ( cp_schema) ,
89+ stats_schema,
90+ partition_schema,
91+ ) ) ;
8192 }
8293 // The `sidecar` column may still be all-null (not a manifest), so scan it to
8394 // confirm whether a sidecar is actually present.
8495 match collect_single_sidecar ( exec, root_checkpoint, file_type, & segment. log_root ) ? {
85- Some ( sidecar) => Self :: try_new_manifest ( exec, sidecar, stats_schema) ,
86- None => Ok ( Self :: try_new_leaf ( Some ( cp_schema) , stats_schema) ) ,
96+ Some ( sidecar) => {
97+ Self :: try_new_manifest ( exec, sidecar, stats_schema, partition_schema)
98+ }
99+ None => Ok ( Self :: try_new_leaf (
100+ Some ( cp_schema) ,
101+ stats_schema,
102+ partition_schema,
103+ ) ) ,
87104 }
88105 }
89106 // A JSON checkpoint has no footer schema to inspect, so try to collect a sidecar to
90107 // decide if it is a manifest or a leaf. A JSON leaf has no readable schema,
91108 // hence no parsed stats.
92109 FileType :: Json => {
93110 match collect_single_sidecar ( exec, root_checkpoint, file_type, & segment. log_root ) ? {
94- Some ( sidecar) => Self :: try_new_manifest ( exec, sidecar, stats_schema) ,
95- None => Ok ( Self :: try_new_leaf ( None , stats_schema) ) ,
111+ Some ( sidecar) => {
112+ Self :: try_new_manifest ( exec, sidecar, stats_schema, partition_schema)
113+ }
114+ None => Ok ( Self :: try_new_leaf ( None , stats_schema, partition_schema) ) ,
96115 }
97116 }
98117 }
@@ -109,26 +128,32 @@ impl CheckpointShape {
109128 root_checkpoint : & FileMeta ,
110129 file_type : FileType ,
111130 stats_schema : Option < & SchemaRef > ,
131+ partition_schema : Option < & SchemaRef > ,
112132 ) -> DeltaResult < Option < CheckpointShape > > {
113133 match segment. checkpoint_hint_sidecars ( ) . map ( Vec :: as_slice) {
114134 Some ( [ sidecar, ..] ) => {
115135 let sidecar_meta = sidecar. to_filemeta ( & segment. log_root ) ?;
116- let result = Self :: try_new_manifest ( exec, sidecar_meta, stats_schema) ?;
136+ let result =
137+ Self :: try_new_manifest ( exec, sidecar_meta, stats_schema, partition_schema) ?;
117138 Ok ( Some ( result) )
118139 }
119140 Some ( [ ] ) => {
120141 // A parquet leaf's stats live in its own schema; read it only when stats are
121142 // requested. A JSON leaf has no readable schema.
122143 let leaf_schema = match file_type {
123- FileType :: Parquet if stats_schema. is_some ( ) => {
144+ FileType :: Parquet if stats_schema. is_some ( ) || partition_schema . is_some ( ) => {
124145 Some ( match segment. checkpoint_hint_schema ( ) {
125146 Some ( schema) => schema,
126147 None => exec. read_parquet_footer ( root_checkpoint. clone ( ) ) ?. schema ,
127148 } )
128149 }
129150 _ => None ,
130151 } ;
131- Ok ( Some ( Self :: try_new_leaf ( leaf_schema, stats_schema) ) )
152+ Ok ( Some ( Self :: try_new_leaf (
153+ leaf_schema,
154+ stats_schema,
155+ partition_schema,
156+ ) ) )
132157 }
133158 None => Ok ( None ) ,
134159 }
@@ -141,18 +166,30 @@ impl CheckpointShape {
141166 exec : & dyn PlanExecutor ,
142167 sidecar : FileMeta ,
143168 stats_schema : Option < & SchemaRef > ,
169+ partition_schema : Option < & SchemaRef > ,
144170 ) -> DeltaResult < CheckpointShape > {
145- let parsed_stats_schema = match stats_schema {
146- Some ( stats_schema) => {
147- let footer_schema = exec. read_parquet_footer ( sidecar) ?. schema ;
148- LogSegment :: schema_has_compatible_stats_parsed ( footer_schema. as_ref ( ) , stats_schema)
149- . then ( || stats_schema. clone ( ) )
150- }
151- None => None ,
171+ let footer_schema = if stats_schema. is_some ( ) || partition_schema. is_some ( ) {
172+ Some ( exec. read_parquet_footer ( sidecar) ?. schema )
173+ } else {
174+ None
152175 } ;
176+ let parsed_stats_schema = stats_schema. filter ( |stats_schema| {
177+ footer_schema. as_ref ( ) . is_some_and ( |footer_schema| {
178+ LogSegment :: schema_has_compatible_stats_parsed ( footer_schema, stats_schema)
179+ } )
180+ } ) ;
181+ let parsed_partition_schema = partition_schema. filter ( |partition_schema| {
182+ footer_schema. as_ref ( ) . is_some_and ( |footer_schema| {
183+ LogSegment :: schema_has_compatible_partition_values_parsed (
184+ footer_schema,
185+ partition_schema,
186+ )
187+ } )
188+ } ) ;
153189 Ok ( CheckpointShape {
154190 checkpoint_type : CheckpointType :: Manifest ,
155- parsed_stats_schema,
191+ parsed_stats_schema : parsed_stats_schema. cloned ( ) ,
192+ parsed_partition_schema : parsed_partition_schema. cloned ( ) ,
156193 } )
157194 }
158195
@@ -161,15 +198,25 @@ impl CheckpointShape {
161198 fn try_new_leaf (
162199 leaf_schema : Option < SchemaRef > ,
163200 stats_schema : Option < & SchemaRef > ,
201+ partition_schema : Option < & SchemaRef > ,
164202 ) -> CheckpointShape {
165203 let parsed_stats_schema = stats_schema. filter ( |stats_schema| {
166204 leaf_schema. as_ref ( ) . is_some_and ( |leaf_schema| {
167205 LogSegment :: schema_has_compatible_stats_parsed ( leaf_schema. as_ref ( ) , stats_schema)
168206 } )
169207 } ) ;
208+ let parsed_partition_schema = partition_schema. filter ( |partition_schema| {
209+ leaf_schema. as_ref ( ) . is_some_and ( |leaf_schema| {
210+ LogSegment :: schema_has_compatible_partition_values_parsed (
211+ leaf_schema,
212+ partition_schema,
213+ )
214+ } )
215+ } ) ;
170216 CheckpointShape {
171217 checkpoint_type : CheckpointType :: Leaf ,
172218 parsed_stats_schema : parsed_stats_schema. cloned ( ) ,
219+ parsed_partition_schema : parsed_partition_schema. cloned ( ) ,
173220 }
174221 }
175222}
@@ -301,8 +348,8 @@ mod tests {
301348 let exec = SyncPlanExecutor :: default ( ) ;
302349 let stats_schema = expect_parsed. map ( |_| probe_stats_schema ( ) ) ;
303350
304- let shape =
305- CheckpointShape :: try_new ( & exec , snapshot . as_ref ( ) , stats_schema . as_ref ( ) ) . unwrap ( ) ;
351+ let shape = CheckpointShape :: try_new ( & exec , snapshot . as_ref ( ) , stats_schema . as_ref ( ) , None )
352+ . unwrap ( ) ;
306353
307354 assert_eq ! (
308355 shape. checkpoint_type, expected_checkpoint,
@@ -353,8 +400,9 @@ mod tests {
353400 load_test_table ( "v2-checkpoints-parquet-with-sidecars" ) . unwrap ( ) ;
354401 let exec = CountingExecutor :: new ( ) ;
355402
356- let shape = CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & probe_stats_schema ( ) ) )
357- . unwrap ( ) ;
403+ let shape =
404+ CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & probe_stats_schema ( ) ) , None )
405+ . unwrap ( ) ;
358406
359407 assert_eq ! ( shape. checkpoint_type, CheckpointType :: Manifest ) ;
360408 assert_eq ! (
@@ -391,8 +439,9 @@ mod tests {
391439 . unwrap ( ) ;
392440
393441 let exec = CountingExecutor :: new ( ) ;
394- let shape = CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & probe_stats_schema ( ) ) )
395- . unwrap ( ) ;
442+ let shape =
443+ CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & probe_stats_schema ( ) ) , None )
444+ . unwrap ( ) ;
396445
397446 assert_eq ! ( shape. checkpoint_type, CheckpointType :: Manifest ) ;
398447 assert ! (
@@ -468,6 +517,7 @@ mod tests {
468517 root,
469518 file_type,
470519 stats_schema. as_ref ( ) ,
520+ None ,
471521 )
472522 . unwrap ( )
473523 . expect ( "an empty-sidecars hint must classify without falling through" ) ;
0 commit comments