Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion crates/iceberg/src/arrow/reader/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ use crate::arrow::record_batch_transformer::RecordBatchTransformerBuilder;
use crate::arrow::scan_metrics::{CountingFileRead, ScanMetrics, ScanResult};
use crate::error::Result;
use crate::io::{FileIO, FileMetadata, FileRead};
use crate::metadata_columns::{RESERVED_FIELD_ID_FILE, is_metadata_field};
use crate::metadata_columns::{
RESERVED_FIELD_ID_FILE, RESERVED_FIELD_ID_SPEC_ID, is_metadata_field,
};
use crate::scan::{ArrowRecordBatchStream, FileScanTask, FileScanTaskStream};
use crate::spec::Datum;
use crate::{Error, ErrorKind};
Expand Down Expand Up @@ -248,6 +250,20 @@ impl FileScanTaskReader {
record_batch_transformer_builder.with_constant(RESERVED_FIELD_ID_FILE, file_datum);
}

if task
.project_field_ids()
.contains(&RESERVED_FIELD_ID_SPEC_ID)
{
let partition_spec = task
.partition_spec
.as_ref()
.ok_or_else(|| Error::new(ErrorKind::Unexpected, "Partition spec is missing"))?;

let spec_id_datum = Datum::int(partition_spec.spec_id());
record_batch_transformer_builder = record_batch_transformer_builder
.with_constant(RESERVED_FIELD_ID_SPEC_ID, spec_id_datum);
}

if let (Some(partition_spec), Some(partition_data)) =
(task.partition_spec.clone(), task.partition.clone())
{
Expand Down
105 changes: 54 additions & 51 deletions crates/iceberg/src/arrow/record_batch_transformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,49 +359,29 @@ impl RecordBatchTransformer {
let fields: Result<Vec<_>> = projected_iceberg_field_ids
.iter()
.map(|field_id| {
// Check if this is a constant field
if constant_fields.contains_key(field_id) {
// For metadata/virtual fields (like _file), get name from metadata_columns
// For partition fields, get name from schema (they exist in schema)
if let Ok(iceberg_field) = get_metadata_field(*field_id) {
// This is a metadata/virtual field - convert Iceberg field to Arrow
let datum = constant_fields.get(field_id).ok_or(Error::new(
ErrorKind::Unexpected,
"constant field not found",
))?;
let arrow_type = datum_to_arrow_type_with_ree(datum);
let arrow_field =
Field::new(&iceberg_field.name, arrow_type, !iceberg_field.required)
.with_metadata(HashMap::from([(
PARQUET_FIELD_ID_META_KEY.to_string(),
iceberg_field.id.to_string(),
)]));
Ok(Arc::new(arrow_field))
} else {
// This is a partition constant field (exists in schema but uses constant value)
let field = &field_id_to_mapped_schema_map
.get(field_id)
.ok_or(Error::new(ErrorKind::Unexpected, "field not found"))?
.0;
let datum = constant_fields.get(field_id).ok_or(Error::new(
ErrorKind::Unexpected,
"constant field not found",
))?;
let arrow_type = datum_to_arrow_type_with_ree(datum);
// Use the type from constant_fields (REE for constants)
let constant_field =
Field::new(field.name(), arrow_type, field.is_nullable())
.with_metadata(field.metadata().clone());
Ok(Arc::new(constant_field))
}
} else {
// Regular field - use schema as-is
Ok(field_id_to_mapped_schema_map
.get(field_id)
.ok_or(Error::new(ErrorKind::Unexpected, "field not found"))?
.0
.clone())
// Metadata/virtual fields (like _file, _spec_id) don't exist in the table
// schema, so build their Arrow field from the metadata column definition and
// the pre-computed constant's type.
if let Some(datum) = constant_fields.get(field_id)
&& let Ok(iceberg_field) = get_metadata_field(*field_id)
{
let arrow_type = datum_to_arrow_type_with_ree(datum);
let arrow_field =
Field::new(&iceberg_field.name, arrow_type, !iceberg_field.required)
.with_metadata(HashMap::from([(
PARQUET_FIELD_ID_META_KEY.to_string(),
iceberg_field.id.to_string(),
)]));
return Ok(Arc::new(arrow_field));
}

// Regular fields and identity-partitioned constant fields both exist in the
// table schema, so use the mapped Arrow field as-is.
Ok(field_id_to_mapped_schema_map
.get(field_id)
.ok_or(Error::new(ErrorKind::Unexpected, "field not found"))?
.0
.clone())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice refactoring!

})
.collect();

Expand Down Expand Up @@ -481,16 +461,39 @@ impl RecordBatchTransformer {
projected_iceberg_field_ids
.iter()
.map(|field_id| {
// Check if this is a constant field (metadata/virtual or identity-partitioned)
// Constant fields always use their pre-computed constant values, regardless of whether
// they exist in the Parquet file. This is per Iceberg spec rule #1: partition metadata
// is authoritative and should be preferred over file data.
// Check if this is a constant field (metadata/virtual or identity-partitioned).
//
// Metadata/virtual fields (like _file, _spec_id) never exist in the data file,
// so they always use their pre-computed constant value.
//
// For identity-partitioned fields, the Iceberg spec's "Column Projection" rules
// only apply to "field ids which are not present in a data file". When the column
// IS present in the Parquet file, it must be read from the file; the partition
// metadata constant is only a fallback for when the column is absent (e.g. add_files).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch

if let Some(datum) = constant_fields.get(field_id) {
let arrow_type = datum_to_arrow_type_with_ree(datum);
return Ok(ColumnSource::Add {
value: Some(datum.literal().clone()),
target_type: arrow_type,
});
let is_metadata_field = get_metadata_field(*field_id).is_ok();
let present_in_file = field_id_to_source_schema_map.contains_key(field_id);

if is_metadata_field || !present_in_file {
let arrow_type = if is_metadata_field {
datum_to_arrow_type_with_ree(datum)
} else {
field_id_to_mapped_schema_map
.get(field_id)
.ok_or(Error::new(
ErrorKind::Unexpected,
"could not find field in schema",
))?
.0
.data_type()
.clone()
};

return Ok(ColumnSource::Add {
value: Some(datum.literal().clone()),
target_type: arrow_type,
});
}
}

let (target_field, _) =
Expand Down
5 changes: 4 additions & 1 deletion crates/iceberg/src/arrow/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,9 @@ pub(crate) fn create_primitive_array_repeated(
let vals: Vec<Option<i32>> = vec![None; num_rows];
Arc::new(Date32Array::from(vals))
}
(DataType::Int64, Some(PrimitiveLiteral::Int(value))) => {
Arc::new(Int64Array::from(vec![i64::from(*value); num_rows]))
}
(DataType::Int64, Some(PrimitiveLiteral::Long(value))) => {
Arc::new(Int64Array::from(vec![*value; num_rows]))
}
Expand Down Expand Up @@ -969,7 +972,7 @@ pub(crate) fn create_primitive_array_repeated(
(dt, _) => {
return Err(Error::new(
ErrorKind::Unexpected,
format!("unexpected target column type {dt}"),
format!("unexpected target column type {dt}, prim_lit {prim_lit:?}"),
));
}
})
Expand Down
15 changes: 11 additions & 4 deletions crates/iceberg/src/scan/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ use crate::scan::{
PartitionFilterCache,
};
use crate::spec::{
ManifestContentType, ManifestEntryRef, ManifestFile, ManifestList, NameMapping, SchemaRef,
SnapshotRef, TableMetadataRef,
ManifestContentType, ManifestEntryRef, ManifestFile, ManifestList, NameMapping,
PartitionSpecRef, SchemaRef, SnapshotRef, TableMetadataRef,
};
use crate::{Error, ErrorKind, Result};

Expand All @@ -48,6 +48,7 @@ pub(crate) struct ManifestFileContext {
delete_file_index: DeleteFileIndex,
name_mapping: Option<Arc<NameMapping>>,
case_sensitive: bool,
partition_spec: Option<PartitionSpecRef>,
}

/// Wraps a [`ManifestEntryRef`] alongside the objects that are needed
Expand All @@ -63,6 +64,7 @@ pub(crate) struct ManifestEntryContext {
pub delete_file_index: DeleteFileIndex,
pub name_mapping: Option<Arc<NameMapping>>,
pub case_sensitive: bool,
pub partition_spec: Option<PartitionSpecRef>,
}

impl ManifestFileContext {
Expand All @@ -80,6 +82,7 @@ impl ManifestFileContext {
delete_file_index,
name_mapping,
case_sensitive,
partition_spec,
} = self;

let manifest = object_cache.get_manifest(&manifest_file).await?;
Expand All @@ -96,6 +99,7 @@ impl ManifestFileContext {
delete_file_index: delete_file_index.clone(),
name_mapping: name_mapping.clone(),
case_sensitive,
partition_spec: partition_spec.clone(),
};

sender
Expand Down Expand Up @@ -135,8 +139,7 @@ impl ManifestEntryContext {
)
.with_deletes(deletes)
.with_partition(Some(self.manifest_entry.data_file.partition.clone()))
// TODO: Pass actual PartitionSpec through context chain for native flow
.with_partition_spec(None)
.with_partition_spec(self.partition_spec.clone())
.with_name_mapping(self.name_mapping)
.with_case_sensitive(self.case_sensitive)
.build())
Expand Down Expand Up @@ -284,6 +287,10 @@ impl PlanContext {
delete_file_index,
name_mapping: self.name_mapping.clone(),
case_sensitive: self.case_sensitive,
partition_spec: self
.table_metadata
.partition_spec_by_id(manifest_file.partition_spec_id)
.cloned(),
}
}
}
Loading
Loading