Skip to content

Commit eed779e

Browse files
authored
[core] Parse primary-key vector index source metadata (_SOURCE_META) (#515)
1 parent 009039f commit eed779e

12 files changed

Lines changed: 635 additions & 4 deletions

crates/paimon/src/spec/avro/index_manifest_entry_decode.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
use super::cursor::AvroCursor;
1919
use super::decode::{neg_count_to_usize, AvroRecordDecode};
2020
use super::decode_helpers::{
21-
normalize_partition, read_bytes_field, read_int_field, read_long_field, read_string_field,
21+
extract_record_schema, normalize_partition, read_bytes_field, read_int_field, read_long_field,
22+
read_string_field,
2223
};
2324
use super::schema::{skip_nullable_field, WriterSchema};
2425
use crate::spec::index_manifest::IndexManifestEntry;
@@ -65,7 +66,8 @@ impl AvroRecordDecode for IndexManifestEntry {
6566
deletion_vectors_ranges = decode_nullable_dv_ranges(cursor, field.nullable)?;
6667
}
6768
"_GLOBAL_INDEX" => {
68-
global_index_meta = decode_nullable_global_index(cursor, field.nullable)?;
69+
global_index_meta =
70+
decode_nullable_global_index(cursor, field.nullable, &field.schema)?;
6971
}
7072
_ => skip_nullable_field(cursor, &field.schema, field.nullable)?,
7173
}
@@ -145,6 +147,7 @@ fn decode_nullable_dv_ranges(
145147
fn decode_nullable_global_index(
146148
cursor: &mut AvroCursor,
147149
nullable: bool,
150+
schema: &super::schema::FieldSchema,
148151
) -> crate::Result<Option<GlobalIndexMeta>> {
149152
if nullable {
150153
let idx = cursor.read_union_index()?;
@@ -192,11 +195,29 @@ fn decode_nullable_global_index(
192195
}
193196
};
194197

198+
// _SOURCE_META: nullable bytes — only present in >= #8549 writer schemas.
199+
// Guard on the writer's nested field list so a legacy 5-field _GLOBAL_INDEX
200+
// record does not misalign the cursor into the next record.
201+
let has_source_meta = extract_record_schema(schema)
202+
.map(|s| s.fields.iter().any(|f| f.name == "_SOURCE_META"))
203+
.unwrap_or(false);
204+
let source_meta = if has_source_meta {
205+
let u_idx = cursor.read_union_index()?;
206+
if u_idx == 0 {
207+
None
208+
} else {
209+
Some(cursor.read_bytes()?.to_vec())
210+
}
211+
} else {
212+
None
213+
};
214+
195215
Ok(Some(GlobalIndexMeta {
196216
row_range_start,
197217
row_range_end,
198218
index_field_id,
199219
extra_field_ids,
200220
index_meta,
221+
source_meta,
201222
}))
202223
}

crates/paimon/src/spec/index_file_meta.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ pub struct GlobalIndexMeta {
4646

4747
#[serde(default, rename = "_INDEX_META", with = "serde_bytes")]
4848
pub index_meta: Option<Vec<u8>>,
49+
50+
#[serde(default, rename = "_SOURCE_META", with = "serde_bytes")]
51+
pub source_meta: Option<Vec<u8>>,
4952
}
5053

5154
/// Metadata of index file.

crates/paimon/src/spec/index_manifest.rs

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

crates/paimon/src/spec/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ pub use manifest_file_meta::*;
6464
mod index_file_meta;
6565
pub use index_file_meta::*;
6666

67+
mod pk_vector_source;
68+
pub use pk_vector_source::*;
69+
6770
mod index_manifest;
6871
pub use index_manifest::{IndexManifest, IndexManifestEntry};
6972
mod manifest;

0 commit comments

Comments
 (0)