@@ -71,7 +71,8 @@ pub const INDEX_MANIFEST_ENTRY_SCHEMA: &str = r#"{
7171 {"name": "_ROW_RANGE_END", "type": "long"},
7272 {"name": "_INDEX_FIELD_ID", "type": "int"},
7373 {"name": "_EXTRA_FIELD_IDS", "type": ["null", {"type": "array", "items": "int"}], "default": null},
74- {"name": "_INDEX_META", "type": ["null", "bytes"], "default": null}
74+ {"name": "_INDEX_META", "type": ["null", "bytes"], "default": null},
75+ {"name": "_SOURCE_META", "type": ["null", "bytes"], "default": null}
7576 ]
7677 }]
7778 }
@@ -173,7 +174,7 @@ mod tests {
173174 use indexmap:: IndexMap ;
174175
175176 use super :: * ;
176- use crate :: spec:: DeletionVectorMeta ;
177+ use crate :: spec:: { DeletionVectorMeta , GlobalIndexMeta } ;
177178
178179 #[ test]
179180 fn test_read_index_manifest_file ( ) {
@@ -276,4 +277,144 @@ mod tests {
276277 } ;
277278 assert_eq ! ( sample, decoded) ;
278279 }
280+
281+ fn global_index_entry ( source_meta : Option < Vec < u8 > > ) -> IndexManifestEntry {
282+ IndexManifestEntry {
283+ version : 1 ,
284+ kind : FileKind :: Add ,
285+ partition : vec ! [ 0 , 1 , 0 , 2 , 0 , 3 , 0 , 4 , 0 , 5 , 0 , 6 ] ,
286+ bucket : 0 ,
287+ index_file : IndexFileMeta {
288+ index_type : "GLOBAL_INDEX" . into ( ) ,
289+ file_name : "gi-1" . into ( ) ,
290+ file_size : 42 ,
291+ row_count : 7 ,
292+ deletion_vectors_ranges : None ,
293+ global_index_meta : Some ( GlobalIndexMeta {
294+ row_range_start : 10 ,
295+ row_range_end : 20 ,
296+ index_field_id : 3 ,
297+ extra_field_ids : Some ( vec ! [ 4 , 5 ] ) ,
298+ index_meta : Some ( vec ! [ 9 , 8 , 7 ] ) ,
299+ source_meta,
300+ } ) ,
301+ } ,
302+ }
303+ }
304+
305+ #[ test]
306+ fn source_meta_round_trips_through_index_manifest ( ) {
307+ // New-format writer schema carries _SOURCE_META; a Some(..) value must round-trip.
308+ let entry = global_index_entry ( Some ( vec ! [ 1 , 2 , 3 ] ) ) ;
309+ let bytes = crate :: spec:: to_avro_bytes_with_compression (
310+ INDEX_MANIFEST_ENTRY_SCHEMA ,
311+ std:: slice:: from_ref ( & entry) ,
312+ crate :: spec:: DEFAULT_AVRO_COMPRESSION ,
313+ )
314+ . unwrap ( ) ;
315+ let decoded = IndexManifest :: read_from_bytes ( & bytes) . unwrap ( ) ;
316+ assert_eq ! ( decoded[ 0 ] , entry) ;
317+ assert_eq ! (
318+ decoded[ 0 ]
319+ . index_file
320+ . global_index_meta
321+ . as_ref( )
322+ . unwrap( )
323+ . source_meta,
324+ Some ( vec![ 1 , 2 , 3 ] )
325+ ) ;
326+
327+ // A None source_meta must also round-trip as None.
328+ let entry_none = global_index_entry ( None ) ;
329+ let bytes_none = crate :: spec:: to_avro_bytes_with_compression (
330+ INDEX_MANIFEST_ENTRY_SCHEMA ,
331+ std:: slice:: from_ref ( & entry_none) ,
332+ crate :: spec:: DEFAULT_AVRO_COMPRESSION ,
333+ )
334+ . unwrap ( ) ;
335+ let decoded_none = IndexManifest :: read_from_bytes ( & bytes_none) . unwrap ( ) ;
336+ assert_eq ! ( decoded_none[ 0 ] , entry_none) ;
337+ assert_eq ! (
338+ decoded_none[ 0 ]
339+ . index_file
340+ . global_index_meta
341+ . as_ref( )
342+ . unwrap( )
343+ . source_meta,
344+ None
345+ ) ;
346+ }
347+
348+ #[ test]
349+ fn legacy_five_field_global_index_decodes_without_source_meta ( ) {
350+ // 5-field _GLOBAL_INDEX schema (pre-#8549): no _SOURCE_META. Identical to
351+ // INDEX_MANIFEST_ENTRY_SCHEMA with the trailing _SOURCE_META line removed.
352+ const LEGACY_SCHEMA : & str = r#"{
353+ "type": "record",
354+ "name": "org.apache.paimon.avro.generated.record",
355+ "fields": [
356+ {"name": "_VERSION", "type": "int"},
357+ {"name": "_KIND", "type": "int"},
358+ {"name": "_PARTITION", "type": "bytes"},
359+ {"name": "_BUCKET", "type": "int"},
360+ {"name": "_INDEX_TYPE", "type": "string"},
361+ {"name": "_FILE_NAME", "type": "string"},
362+ {"name": "_FILE_SIZE", "type": "long"},
363+ {"name": "_ROW_COUNT", "type": "long"},
364+ {
365+ "default": null,
366+ "name": "_DELETIONS_VECTORS_RANGES",
367+ "type": ["null", {
368+ "type": "array",
369+ "items": ["null", {
370+ "type": "record",
371+ "name": "org.apache.paimon.avro.generated.record__DELETIONS_VECTORS_RANGES",
372+ "fields": [
373+ {"name": "f0", "type": "string"},
374+ {"name": "f1", "type": "int"},
375+ {"name": "f2", "type": "int"},
376+ {"name": "_CARDINALITY", "type": ["null", "long"], "default": null}
377+ ]
378+ }]
379+ }]
380+ },
381+ {
382+ "default": null,
383+ "name": "_GLOBAL_INDEX",
384+ "type": ["null", {
385+ "type": "record",
386+ "name": "org.apache.paimon.avro.generated.record__GLOBAL_INDEX",
387+ "fields": [
388+ {"name": "_ROW_RANGE_START", "type": "long"},
389+ {"name": "_ROW_RANGE_END", "type": "long"},
390+ {"name": "_INDEX_FIELD_ID", "type": "int"},
391+ {"name": "_EXTRA_FIELD_IDS", "type": ["null", {"type": "array", "items": "int"}], "default": null},
392+ {"name": "_INDEX_META", "type": ["null", "bytes"], "default": null}
393+ ]
394+ }]
395+ }
396+ ]
397+ }"# ;
398+
399+ // Written by a pre-#8549 writer (5-field record, source_meta absent).
400+ let entry = global_index_entry ( None ) ;
401+ let bytes = crate :: spec:: to_avro_bytes_with_compression (
402+ LEGACY_SCHEMA ,
403+ std:: slice:: from_ref ( & entry) ,
404+ crate :: spec:: DEFAULT_AVRO_COMPRESSION ,
405+ )
406+ . unwrap ( ) ;
407+ // Decoding with the current 6-field reader must not misalign the stream.
408+ let decoded = IndexManifest :: read_from_bytes ( & bytes) . unwrap ( ) ;
409+ assert_eq ! ( decoded[ 0 ] , entry) ;
410+ assert_eq ! (
411+ decoded[ 0 ]
412+ . index_file
413+ . global_index_meta
414+ . as_ref( )
415+ . unwrap( )
416+ . source_meta,
417+ None
418+ ) ;
419+ }
279420}
0 commit comments