1use std::borrow::Cow;
6use std::sync::Arc;
7
8use polars_arrow::datatypes::{ArrowDataType, ArrowSchema, Field as ArrowField};
9use polars_error::{PolarsResult, feature_gated, polars_bail, polars_err};
10use polars_utils::aliases::{InitHashMaps, PlHashMap};
11use polars_utils::pl_str::PlSmallStr;
12
13use crate::prelude::{DataType, Field, PlIndexMap};
14
15pub const LIST_ELEMENT_DEFAULT_ID: u32 = u32::MAX;
16
17#[derive(Debug, Clone, Eq, PartialEq)]
21#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
22#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
23pub struct IcebergSchema(PlIndexMap<u32, IcebergColumn>);
24pub type IcebergSchemaRef = Arc<IcebergSchema>;
25
26impl IcebergSchema {
27 pub fn from_arrow_schema(schema: &ArrowSchema) -> PolarsResult<Self> {
29 Self::try_from_arrow_fields_iter(schema.iter_values())
30 }
31
32 pub fn try_from_arrow_fields_iter<'a, I>(iter: I) -> PolarsResult<Self>
33 where
34 I: IntoIterator<Item = &'a ArrowField>,
35 {
36 let iter = iter.into_iter();
37
38 let mut out = PlIndexMap::with_capacity(iter.size_hint().0);
39
40 for arrow_field in iter {
41 let col: IcebergColumn = arrow_field_to_iceberg_column_rec(arrow_field, None)?;
42 let existing = out.insert(col.physical_id, col);
43
44 if let Some(existing) = existing {
45 polars_bail!(
46 Duplicate:
47 "IcebergSchema: duplicate physical ID {:?}",
48 existing,
49 )
50 }
51 }
52
53 Ok(Self(out))
54 }
55}
56
57#[derive(Debug, Clone, Eq, Hash, PartialEq)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
60pub struct IcebergColumn {
61 pub name: PlSmallStr,
63 pub physical_id: u32,
65 pub type_: IcebergColumnType,
66 pub mapped_names: Vec<PlSmallStr>,
69}
70
71#[derive(Debug, Clone, Eq, Hash, PartialEq)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
73#[cfg_attr(feature = "dsl-schema", derive(schemars::JsonSchema))]
74pub enum IcebergColumnType {
75 Primitive {
76 dtype: DataType,
78 },
79 List(Box<IcebergColumn>),
80 FixedSizeList(Box<IcebergColumn>, usize),
82 Map(Box<IcebergColumn>, Box<IcebergColumn>),
84 Struct(IcebergSchema),
85}
86
87impl IcebergColumnType {
88 pub fn to_polars_dtype(&self) -> DataType {
89 use IcebergColumnType::*;
90
91 match self {
92 Primitive { dtype } => dtype.clone(),
93 List(inner) => DataType::List(Box::new(inner.type_.to_polars_dtype())),
94 FixedSizeList(inner, width) => {
95 feature_gated!("dtype-array", {
96 DataType::Array(Box::new(inner.type_.to_polars_dtype()), *width)
97 })
98 },
99 Map(key, value) => feature_gated!("dtype-map", {
100 DataType::Map(
101 Box::new(key.type_.to_polars_dtype()),
102 Box::new(value.type_.to_polars_dtype()),
103 )
104 }),
105 Struct(fields) => feature_gated!("dtype-struct", {
106 DataType::Struct(
107 fields
108 .values()
109 .map(|col| Field::new(col.name.clone(), col.type_.to_polars_dtype()))
110 .collect(),
111 )
112 }),
113 }
114 }
115
116 pub fn is_nested(&self) -> bool {
117 use IcebergColumnType::*;
118
119 match self {
120 List(_) | FixedSizeList(..) | Map(..) | Struct(_) => true,
121 Primitive { .. } => false,
122 }
123 }
124}
125
126fn arrow_field_to_iceberg_column_rec(
127 field: &ArrowField,
128 field_id_override: Option<u32>,
129) -> PolarsResult<IcebergColumn> {
130 let physical_id: u32 = field_id_override.ok_or(Cow::Borrowed("")).or_else(|_| {
131 field
132 .metadata
133 .as_deref()
134 .ok_or(Cow::Borrowed("metadata was None"))
135 .and_then(|md| {
136 md.get(PARQUET_FIELD_ID_KEY)
137 .ok_or(Cow::Borrowed("key not found in metadata"))
138 })
139 .and_then(|x| {
140 x.parse()
141 .map_err(|_| Cow::Owned(format!("could not parse value as u32: '{x}'")))
142 })
143 .map_err(|failed_reason: Cow<'_, str>| {
144 polars_err!(
145 SchemaFieldNotFound:
146 "IcebergSchema: failed to load '{PARQUET_FIELD_ID_KEY}' for field {}: {}",
147 &field.name,
148 failed_reason,
149 )
150 })
151 })?;
152
153 #[expect(unused)]
155 let field_id_override: ();
156
157 use ArrowDataType as ADT;
158
159 let name = field.name.clone();
160
161 let list_column_type = |field: &ArrowField| -> PolarsResult<IcebergColumnType> {
162 let field_id_override = field
163 .metadata
164 .as_ref()
165 .is_none_or(|x| !x.contains_key(PARQUET_FIELD_ID_KEY))
166 .then_some(LIST_ELEMENT_DEFAULT_ID);
167
168 Ok(IcebergColumnType::List(Box::new(
169 arrow_field_to_iceberg_column_rec(field, field_id_override)?,
170 )))
171 };
172
173 let type_ = match &field.dtype {
174 ADT::List(field) | ADT::LargeList(field) => list_column_type(field)?,
175
176 ADT::Map(entries, _) => {
177 #[cfg(feature = "dtype-map")]
178 {
179 let ADT::Struct(entry_fields) = &entries.dtype else {
182 polars_bail!(
183 ComputeError:
184 "IcebergSchema: expected struct under arrow map type, got: {:?}",
185 &entries.dtype,
186 )
187 };
188
189 let [key, value] = entry_fields.as_slice() else {
190 polars_bail!(
191 ComputeError:
192 "IcebergSchema: expected 2 fields under arrow map type, got: {}",
193 entry_fields.len(),
194 )
195 };
196
197 IcebergColumnType::Map(
198 Box::new(arrow_field_to_iceberg_column_rec(key, None)?),
199 Box::new(arrow_field_to_iceberg_column_rec(value, None)?),
200 )
201 }
202 #[cfg(not(feature = "dtype-map"))]
203 {
204 list_column_type(entries)?
206 }
207 },
208
209 #[cfg(feature = "dtype-array")]
210 ADT::FixedSizeList(field, width) => IcebergColumnType::FixedSizeList(
211 Box::new(arrow_field_to_iceberg_column_rec(field, None)?),
212 *width,
213 ),
214
215 #[cfg(feature = "dtype-struct")]
216 ADT::Struct(fields) => {
217 IcebergColumnType::Struct(IcebergSchema::try_from_arrow_fields_iter(fields)?)
218 },
219
220 dtype => {
221 if let ADT::Dictionary(_key_type, value_type, _is_ordered) = dtype
222 && !value_type.is_nested()
223 {
224 let mut new_field = ArrowField::new(name.clone(), dtype.clone(), true);
225 if let Some(metadata) = field.metadata.as_ref() {
226 new_field = new_field.with_metadata((**metadata).clone());
227 }
228 let dtype = DataType::from_arrow_field(&new_field);
229
230 IcebergColumnType::Primitive { dtype }
231 } else if let ADT::Extension(ext_type) = dtype
232 && let DataType::Binary = DataType::from_arrow_dtype(&ext_type.inner)
233 {
234 IcebergColumnType::Primitive {
236 dtype: DataType::Binary,
237 }
238 } else if dtype.is_nested() {
239 polars_bail!(
240 ComputeError:
241 "IcebergSchema: unsupported arrow type: {:?}",
242 dtype,
243 )
244 } else {
245 let dtype =
246 DataType::from_arrow_field(&ArrowField::new(name.clone(), dtype.clone(), true));
247
248 IcebergColumnType::Primitive { dtype }
249 }
250 },
251 };
252
253 let out = IcebergColumn {
254 name,
255 physical_id,
256 type_,
257 mapped_names: vec![],
258 };
259
260 Ok(out)
261}
262
263const PARQUET_FIELD_ID_KEY: &str = "PARQUET:field_id";
264
265impl IcebergSchema {
266 pub fn apply_name_mapping(&mut self, names: &PlHashMap<u32, Vec<PlSmallStr>>) {
269 fn apply_column(col: &mut IcebergColumn, names: &PlHashMap<u32, Vec<PlSmallStr>>) {
270 if let Some(n) = names.get(&col.physical_id) {
271 col.mapped_names = n.clone();
272 }
273 match &mut col.type_ {
274 IcebergColumnType::Primitive { .. } => {},
275 IcebergColumnType::List(inner) | IcebergColumnType::FixedSizeList(inner, _) => {
276 apply_column(inner, names)
277 },
278 IcebergColumnType::Map(key, value) => {
279 apply_column(key, names);
280 apply_column(value, names);
281 },
282 IcebergColumnType::Struct(fields) => fields.apply_name_mapping(names),
283 }
284 }
285
286 for col in self.values_mut() {
287 apply_column(col, names);
288 }
289 }
290
291 pub fn from_arrow_schema_with_name_mapping(
299 schema: &ArrowSchema,
300 table: &IcebergSchema,
301 table_has_name_mapping: bool,
302 ) -> PolarsResult<Self> {
303 let has_field_ids = schema.iter_values().any(has_field_id);
304
305 if has_field_ids {
306 let fields = with_placeholder_ids_of_level(schema.iter_values());
307 return Self::try_from_arrow_fields_iter(fields.iter());
308 }
309
310 if !table_has_name_mapping {
311 return Self::from_arrow_schema(schema);
312 }
313
314 let fields = with_mapped_ids_of_level(schema.iter_values(), table);
315 Self::try_from_arrow_fields_iter(fields.iter())
316 }
317}
318
319fn has_field_id(field: &ArrowField) -> bool {
320 field
321 .metadata
322 .as_ref()
323 .is_some_and(|md| md.contains_key(PARQUET_FIELD_ID_KEY))
324}
325
326fn with_mapped_ids_of_level<'a>(
328 fields: impl IntoIterator<Item = &'a ArrowField>,
329 table: &IcebergSchema,
330) -> Vec<ArrowField> {
331 let mut unmapped_id = LIST_ELEMENT_DEFAULT_ID;
334
335 fields
336 .into_iter()
337 .map(|field| {
338 match table
339 .values()
340 .find(|c| c.mapped_names.iter().any(|n| n == &field.name))
341 {
342 Some(column) => with_mapped_ids(field, column),
343 None => {
344 unmapped_id -= 1;
345 with_placeholder_ids(field, unmapped_id)
346 },
347 }
348 })
349 .collect()
350}
351
352fn with_placeholder_ids_of_level<'a>(
355 fields: impl IntoIterator<Item = &'a ArrowField>,
356) -> Vec<ArrowField> {
357 let mut placeholder_id = LIST_ELEMENT_DEFAULT_ID;
358
359 fields
360 .into_iter()
361 .map(|field| {
362 placeholder_id -= 1;
363 with_placeholder_ids(field, placeholder_id)
364 })
365 .collect()
366}
367
368fn with_placeholder_ids(field: &ArrowField, id: u32) -> ArrowField {
371 let field = ArrowField {
372 dtype: with_placeholder_ids_dtype(&field.dtype),
373 ..field.clone()
374 };
375
376 if has_field_id(&field) {
377 field
378 } else {
379 with_field_id(field, id)
380 }
381}
382
383fn with_placeholder_ids_dtype(dtype: &ArrowDataType) -> ArrowDataType {
384 use ArrowDataType as ADT;
385
386 let list_element = |inner: &ArrowField| ArrowField {
387 dtype: with_placeholder_ids_dtype(&inner.dtype),
388 ..inner.clone()
389 };
390
391 match dtype {
392 ADT::Struct(fields) => ADT::Struct(with_placeholder_ids_of_level(fields)),
393 ADT::List(inner) => ADT::List(Box::new(list_element(inner))),
394 ADT::LargeList(inner) => ADT::LargeList(Box::new(list_element(inner))),
395 ADT::FixedSizeList(inner, width) => ADT::FixedSizeList(
396 Box::new(with_placeholder_ids(inner, LIST_ELEMENT_DEFAULT_ID - 1)),
397 *width,
398 ),
399 ADT::Map(entries, sorted) => match &entries.dtype {
400 ADT::Struct(kv) if kv.len() == 2 => {
401 let entries = ArrowField {
402 dtype: ADT::Struct(with_placeholder_ids_of_level(kv)),
403 ..(**entries).clone()
404 };
405 ADT::Map(Box::new(entries), *sorted)
406 },
407 dtype => dtype.clone(),
408 },
409 dtype => dtype.clone(),
410 }
411}
412
413fn with_mapped_ids(field: &ArrowField, column: &IcebergColumn) -> ArrowField {
416 use ArrowDataType as ADT;
417 use IcebergColumnType as ICT;
418
419 let dtype = match (&field.dtype, &column.type_) {
420 (ADT::Struct(fields), ICT::Struct(children)) => {
421 ADT::Struct(with_mapped_ids_of_level(fields, children))
422 },
423 (ADT::List(inner), ICT::List(c)) => ADT::List(Box::new(with_mapped_ids(inner, c))),
424 (ADT::LargeList(inner), ICT::List(c)) => {
425 ADT::LargeList(Box::new(with_mapped_ids(inner, c)))
426 },
427 (ADT::FixedSizeList(inner, width), ICT::FixedSizeList(c, _)) => {
428 ADT::FixedSizeList(Box::new(with_mapped_ids(inner, c)), *width)
429 },
430 (ADT::Map(entries, sorted), ICT::Map(key, value)) => match &entries.dtype {
431 ADT::Struct(kv) if kv.len() == 2 => {
432 let entries = ArrowField {
433 dtype: ADT::Struct(vec![
434 with_mapped_ids(&kv[0], key),
435 with_mapped_ids(&kv[1], value),
436 ]),
437 ..(**entries).clone()
438 };
439 ADT::Map(Box::new(entries), *sorted)
440 },
441 dtype => dtype.clone(),
442 },
443 (dtype, _) => with_placeholder_ids_dtype(dtype),
445 };
446
447 with_field_id(
448 ArrowField {
449 dtype,
450 ..field.clone()
451 },
452 column.physical_id,
453 )
454}
455
456fn with_field_id(mut field: ArrowField, id: u32) -> ArrowField {
457 let mut metadata = field.metadata.as_deref().cloned().unwrap_or_default();
458 metadata.insert(
459 PlSmallStr::from_static(PARQUET_FIELD_ID_KEY),
460 id.to_string().into(),
461 );
462 field.metadata = Some(Arc::new(metadata));
463 field
464}
465
466impl<T> FromIterator<T> for IcebergSchema
467where
468 PlIndexMap<u32, IcebergColumn>: FromIterator<T>,
469{
470 fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
471 Self(PlIndexMap::<u32, IcebergColumn>::from_iter(iter))
472 }
473}
474
475impl std::hash::Hash for IcebergSchema {
476 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
477 for col in self.values() {
478 col.hash(state);
479 }
480 }
481}
482
483impl std::ops::Deref for IcebergSchema {
484 type Target = PlIndexMap<u32, IcebergColumn>;
485
486 fn deref(&self) -> &Self::Target {
487 &self.0
488 }
489}
490
491impl std::ops::DerefMut for IcebergSchema {
492 fn deref_mut(&mut self) -> &mut Self::Target {
493 &mut self.0
494 }
495}