11//! Resolves a checkpoint shape. This falls into the following cases: no checkpoint,
22//! leaf (file actions inline, including multi-part), or manifest (which references sidecar files).
3- //! When stats are requested, also reports whether the checkpoint has compatible parsed stats.
3+ //! When typed metadata is requested, also reports whether the checkpoint has compatible parsed
4+ //! stats and partition values.
45//! Driven through a [`PlanExecutor`].
5- // No in-crate caller yet; following PRs will use this.
6- #![ allow( dead_code) ]
6+ use std:: sync:: Arc ;
77
88use url:: Url ;
99
@@ -28,23 +28,27 @@ pub(crate) enum CheckpointType {
2828 Manifest ,
2929}
3030
31- /// A snapshot's resolved checkpoint type and parsed-stats schema .
31+ /// A snapshot's resolved checkpoint type and compatible typed metadata schemas .
3232#[ derive( Clone , Debug , PartialEq ) ]
3333pub ( crate ) struct CheckpointShape {
3434 /// What kind of checkpoint this is.
3535 pub ( crate ) checkpoint_type : CheckpointType ,
3636 /// The requested stats schema when the checkpoint has a compatible `add.stats_parsed` struct
3737 /// to read it from; `None` when stats were not requested or no compatible parsed stats exist.
3838 pub ( crate ) parsed_stats_schema : Option < SchemaRef > ,
39+ /// The requested partition schema when the checkpoint has a compatible
40+ /// `add.partitionValues_parsed` struct; `None` when partitions were not requested or no
41+ /// compatible parsed values exist.
42+ pub ( crate ) parsed_partition_values_schema : Option < SchemaRef > ,
3943}
4044
4145impl CheckpointShape {
42- /// Resolve `snapshot`'s checkpoint shape. Determines the checkpoint type and, when
43- /// `stats_schema` is `Some`, whether the checkpoint contains parsed stats compatible with it.
46+ /// Resolve `snapshot`'s checkpoint shape and compatible typed metadata fields.
4447 pub ( crate ) fn try_new (
4548 exec : & dyn PlanExecutor ,
4649 snapshot : & Snapshot ,
4750 stats_schema : Option < & SchemaRef > ,
51+ partition_schema : Option < & SchemaRef > ,
4852 ) -> DeltaResult < CheckpointShape > {
4953 let segment = snapshot. log_segment ( ) ;
5054
@@ -55,15 +59,21 @@ impl CheckpointShape {
5559 return Ok ( CheckpointShape {
5660 checkpoint_type : CheckpointType :: None ,
5761 parsed_stats_schema : None ,
62+ parsed_partition_values_schema : None ,
5863 } )
5964 }
6065 } ;
6166
6267 // Classify from a V2 checkpoint's `_last_checkpoint` hint when possible, else inspect the
6368 // file.
64- if let Some ( shape) =
65- Self :: from_v2_checkpoint_hint ( exec, segment, root_checkpoint, file_type, stats_schema) ?
66- {
69+ if let Some ( shape) = Self :: from_v2_checkpoint_hint (
70+ exec,
71+ segment,
72+ root_checkpoint,
73+ file_type,
74+ stats_schema,
75+ partition_schema,
76+ ) ? {
6777 return Ok ( shape) ;
6878 }
6979
@@ -76,7 +86,11 @@ impl CheckpointShape {
7686 } ;
7787 // No `sidecar` column means the file actions are inline, so this is a leaf.
7888 if !cp_schema. contains ( SIDECAR_NAME ) {
79- return Ok ( Self :: try_new_leaf ( Some ( cp_schema) , stats_schema) ) ;
89+ return Ok ( Self :: try_new_leaf (
90+ Some ( cp_schema) ,
91+ stats_schema,
92+ partition_schema,
93+ ) ) ;
8094 }
8195 // The `sidecar` column may still be all-null (not a manifest), so scan it to
8296 // confirm whether a sidecar is actually present.
@@ -85,9 +99,14 @@ impl CheckpointShape {
8599 exec,
86100 sidecar,
87101 stats_schema,
102+ partition_schema,
88103 segment. checkpoint_hint_sidecar_file_schema ( ) ,
89104 ) ,
90- None => Ok ( Self :: try_new_leaf ( Some ( cp_schema) , stats_schema) ) ,
105+ None => Ok ( Self :: try_new_leaf (
106+ Some ( cp_schema) ,
107+ stats_schema,
108+ partition_schema,
109+ ) ) ,
91110 }
92111 }
93112 // A JSON checkpoint has no footer schema to inspect, so try to collect a sidecar to
@@ -99,9 +118,10 @@ impl CheckpointShape {
99118 exec,
100119 sidecar,
101120 stats_schema,
121+ partition_schema,
102122 segment. checkpoint_hint_sidecar_file_schema ( ) ,
103123 ) ,
104- None => Ok ( Self :: try_new_leaf ( None , stats_schema) ) ,
124+ None => Ok ( Self :: try_new_leaf ( None , stats_schema, partition_schema ) ) ,
105125 }
106126 }
107127 }
@@ -118,6 +138,7 @@ impl CheckpointShape {
118138 root_checkpoint : & FileMeta ,
119139 file_type : FileType ,
120140 stats_schema : Option < & SchemaRef > ,
141+ partition_schema : Option < & SchemaRef > ,
121142 ) -> DeltaResult < Option < CheckpointShape > > {
122143 match segment. checkpoint_hint_sidecars ( ) . map ( Vec :: as_slice) {
123144 Some ( [ sidecar, ..] ) => {
@@ -126,62 +147,67 @@ impl CheckpointShape {
126147 exec,
127148 sidecar_meta,
128149 stats_schema,
150+ partition_schema,
129151 segment. checkpoint_hint_sidecar_file_schema ( ) ,
130152 ) ?;
131153 Ok ( Some ( result) )
132154 }
133155 Some ( [ ] ) => {
134- // A parquet leaf's stats live in its own schema; read it only when stats are
135- // requested. A JSON leaf has no readable schema.
156+ // A parquet leaf's typed metadata lives in its own schema; read it only when typed
157+ // metadata was requested. A JSON leaf has no readable schema.
136158 let leaf_schema = match file_type {
137- FileType :: Parquet if stats_schema. is_some ( ) => {
159+ FileType :: Parquet if stats_schema. is_some ( ) || partition_schema . is_some ( ) => {
138160 Some ( match segment. checkpoint_hint_schema ( ) {
139161 Some ( schema) => schema,
140162 None => exec. read_parquet_footer ( root_checkpoint. clone ( ) ) ?. schema ,
141163 } )
142164 }
143165 _ => None ,
144166 } ;
145- Ok ( Some ( Self :: try_new_leaf ( leaf_schema, stats_schema) ) )
167+ Ok ( Some ( Self :: try_new_leaf (
168+ leaf_schema,
169+ stats_schema,
170+ partition_schema,
171+ ) ) )
146172 }
147173 None => Ok ( None ) ,
148174 }
149175 }
150176
151- /// Build the shape for a manifest checkpoint. Its file actions and their stats live in the
152- /// sidecars, so we need a sidecar's schema to answer the stats-compatibility question -- but
153- /// only when stats were requested. All sidecars of a checkpoint share one schema, so any one is
154- /// sufficient.
155- ///
156- /// If the `_last_checkpoint` hint carries a `sidecarFileSchema`, use it directly,
157- /// Otherwise read the sidecar's footer to get the schema.
177+ /// Build the shape for a manifest checkpoint. Its file actions and typed metadata live in the
178+ /// sidecars. Use the `_last_checkpoint` sidecar schema when present; otherwise read one
179+ /// sidecar footer when typed metadata was requested. All sidecars share one schema.
158180 fn try_new_manifest (
159181 exec : & dyn PlanExecutor ,
160182 sidecar : FileMeta ,
161183 stats_schema : Option < & SchemaRef > ,
184+ partition_schema : Option < & SchemaRef > ,
162185 hint_sidecar_schema : Option < StructType > ,
163186 ) -> DeltaResult < CheckpointShape > {
164- let parsed_stats_schema = match stats_schema {
165- Some ( stats_schema) => {
166- let compatible = match hint_sidecar_schema {
167- Some ( schema) => {
168- LogSegment :: schema_has_compatible_stats_parsed ( & schema, stats_schema)
169- }
170- None => {
171- let footer_schema = exec. read_parquet_footer ( sidecar) ?. schema ;
172- LogSegment :: schema_has_compatible_stats_parsed (
173- footer_schema. as_ref ( ) ,
174- stats_schema,
175- )
176- }
177- } ;
178- compatible. then ( || stats_schema. clone ( ) )
187+ let sidecar_schema = match hint_sidecar_schema {
188+ Some ( schema) => Some ( Arc :: new ( schema) ) ,
189+ None if stats_schema. is_some ( ) || partition_schema. is_some ( ) => {
190+ Some ( exec. read_parquet_footer ( sidecar) ?. schema )
179191 }
180192 None => None ,
181193 } ;
194+ let parsed_stats_schema = stats_schema. filter ( |stats_schema| {
195+ sidecar_schema. as_ref ( ) . is_some_and ( |sidecar_schema| {
196+ LogSegment :: schema_has_compatible_stats_parsed ( sidecar_schema, stats_schema)
197+ } )
198+ } ) ;
199+ let parsed_partition_values_schema = partition_schema. filter ( |partition_schema| {
200+ sidecar_schema. as_ref ( ) . is_some_and ( |sidecar_schema| {
201+ LogSegment :: schema_has_compatible_partition_values_parsed (
202+ sidecar_schema,
203+ partition_schema,
204+ )
205+ } )
206+ } ) ;
182207 Ok ( CheckpointShape {
183208 checkpoint_type : CheckpointType :: Manifest ,
184- parsed_stats_schema,
209+ parsed_stats_schema : parsed_stats_schema. cloned ( ) ,
210+ parsed_partition_values_schema : parsed_partition_values_schema. cloned ( ) ,
185211 } )
186212 }
187213
@@ -190,15 +216,25 @@ impl CheckpointShape {
190216 fn try_new_leaf (
191217 leaf_schema : Option < SchemaRef > ,
192218 stats_schema : Option < & SchemaRef > ,
219+ partition_schema : Option < & SchemaRef > ,
193220 ) -> CheckpointShape {
194221 let parsed_stats_schema = stats_schema. filter ( |stats_schema| {
195222 leaf_schema. as_ref ( ) . is_some_and ( |leaf_schema| {
196223 LogSegment :: schema_has_compatible_stats_parsed ( leaf_schema. as_ref ( ) , stats_schema)
197224 } )
198225 } ) ;
226+ let parsed_partition_values_schema = partition_schema. filter ( |partition_schema| {
227+ leaf_schema. as_ref ( ) . is_some_and ( |leaf_schema| {
228+ LogSegment :: schema_has_compatible_partition_values_parsed (
229+ leaf_schema,
230+ partition_schema,
231+ )
232+ } )
233+ } ) ;
199234 CheckpointShape {
200235 checkpoint_type : CheckpointType :: Leaf ,
201236 parsed_stats_schema : parsed_stats_schema. cloned ( ) ,
237+ parsed_partition_values_schema : parsed_partition_values_schema. cloned ( ) ,
202238 }
203239 }
204240}
@@ -334,8 +370,8 @@ mod tests {
334370 let exec = SyncPlanExecutor :: default ( ) ;
335371 let stats_schema = expect_parsed. map ( |_| probe_stats_schema ( ) ) ;
336372
337- let shape =
338- CheckpointShape :: try_new ( & exec , snapshot . as_ref ( ) , stats_schema . as_ref ( ) ) . unwrap ( ) ;
373+ let shape = CheckpointShape :: try_new ( & exec , snapshot . as_ref ( ) , stats_schema . as_ref ( ) , None )
374+ . unwrap ( ) ;
339375
340376 assert_eq ! (
341377 shape. checkpoint_type, expected_checkpoint,
@@ -381,8 +417,9 @@ mod tests {
381417 load_test_table ( "v2-checkpoints-parquet-with-sidecars" ) . unwrap ( ) ;
382418 let exec = CountingExecutor :: new ( ) ;
383419
384- let shape = CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & probe_stats_schema ( ) ) )
385- . unwrap ( ) ;
420+ let shape =
421+ CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & probe_stats_schema ( ) ) , None )
422+ . unwrap ( ) ;
386423
387424 assert_eq ! ( shape. checkpoint_type, CheckpointType :: Manifest ) ;
388425 assert_eq ! (
@@ -419,8 +456,9 @@ mod tests {
419456 . unwrap ( ) ;
420457
421458 let exec = CountingExecutor :: new ( ) ;
422- let shape = CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & probe_stats_schema ( ) ) )
423- . unwrap ( ) ;
459+ let shape =
460+ CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & probe_stats_schema ( ) ) , None )
461+ . unwrap ( ) ;
424462
425463 assert_eq ! ( shape. checkpoint_type, CheckpointType :: Manifest ) ;
426464 assert ! (
@@ -492,6 +530,7 @@ mod tests {
492530 root,
493531 file_type,
494532 stats_schema. as_ref ( ) ,
533+ None ,
495534 )
496535 . unwrap ( )
497536 . expect ( "an empty-sidecars hint must classify without falling through" ) ;
@@ -602,6 +641,7 @@ mod tests {
602641 root,
603642 file_type,
604643 Some ( & stats_schema) ,
644+ None ,
605645 )
606646 . unwrap ( )
607647 . expect ( "a sidecar-listing hint must classify as a manifest" ) ;
@@ -636,7 +676,7 @@ mod tests {
636676
637677 // Full resolution reads the sidecar footer to answer compatibility.
638678 let footer_shape =
639- CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & stats_schema) ) . unwrap ( ) ;
679+ CheckpointShape :: try_new ( & exec, snapshot. as_ref ( ) , Some ( & stats_schema) , None ) . unwrap ( ) ;
640680 assert_eq ! ( footer_shape. checkpoint_type, CheckpointType :: Manifest ) ;
641681
642682 // Read that same footer schema ourselves and feed it as the hint schema.
@@ -651,6 +691,7 @@ mod tests {
651691 & exec,
652692 sidecar,
653693 Some ( & stats_schema) ,
694+ None ,
654695 Some ( footer_schema. as_ref ( ) . clone ( ) ) ,
655696 )
656697 . unwrap ( ) ;
0 commit comments