1use std::sync::Arc;
11
12use ailake_catalog::SchemaField;
13use ailake_core::{AilakeError, AilakeResult};
14use arrow_array::{
15 Array, ArrayRef, BooleanArray, Date32Array, Float32Array, Float64Array, Int32Array, Int64Array,
16 StringArray, TimestampMicrosecondArray,
17};
18use arrow_schema::{DataType, Field, Schema, TimeUnit};
19
20pub struct SchemaFiller;
21
22impl SchemaFiller {
23 pub fn fill(
30 batch: arrow_array::RecordBatch,
31 schema_fields: &[SchemaField],
32 ) -> AilakeResult<arrow_array::RecordBatch> {
33 if schema_fields.is_empty() {
34 return Ok(batch);
35 }
36
37 let batch_schema = batch.schema();
38 let existing: std::collections::HashSet<&str> = batch_schema
39 .fields()
40 .iter()
41 .map(|f| f.name().as_str())
42 .collect();
43
44 let missing: Vec<&SchemaField> = schema_fields
45 .iter()
46 .filter(|sf| !existing.contains(sf.name.as_str()))
47 .collect();
48
49 if missing.is_empty() {
50 return Ok(batch);
51 }
52
53 let n = batch.num_rows();
54 let mut new_fields: Vec<arrow_schema::FieldRef> =
55 batch.schema().fields().iter().cloned().collect();
56 let mut new_cols: Vec<Arc<dyn Array>> = batch.columns().to_vec();
57
58 for sf in missing {
59 let dtype = iceberg_type_to_arrow(&sf.iceberg_type);
60 let arr = make_default_array(&dtype, sf.initial_default.as_ref(), n)?;
61 new_fields.push(Arc::new(Field::new(sf.name.clone(), dtype, !sf.required)));
62 new_cols.push(arr);
63 }
64
65 let new_schema = Arc::new(Schema::new(new_fields));
66 arrow_array::RecordBatch::try_new(new_schema, new_cols)
67 .map_err(|e| AilakeError::Arrow(e.to_string()))
68 }
69}
70
71pub fn iceberg_type_to_arrow(typ: &str) -> DataType {
76 match typ.trim() {
77 "boolean" => DataType::Boolean,
78 "int" | "integer" => DataType::Int32,
79 "long" => DataType::Int64,
80 "float" => DataType::Float32,
81 "double" => DataType::Float64,
82 "date" => DataType::Date32,
83 "time" => DataType::Time64(TimeUnit::Microsecond),
84 "timestamp" => DataType::Timestamp(TimeUnit::Microsecond, None),
85 "timestamptz" => DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())),
86 "string" | "uuid" => DataType::Utf8,
87 "binary" | "fixed" => DataType::Binary,
88 _ => DataType::Utf8,
89 }
90}
91
92fn make_default_array(
94 dtype: &DataType,
95 default_val: Option<&serde_json::Value>,
96 n: usize,
97) -> AilakeResult<ArrayRef> {
98 use serde_json::Value;
99
100 Ok(match dtype {
101 DataType::Boolean => {
102 let v = default_val.and_then(Value::as_bool);
103 Arc::new(BooleanArray::from(vec![v; n]))
104 }
105 DataType::Int32 => {
106 let v = default_val.and_then(Value::as_i64).map(|i| i as i32);
107 Arc::new(Int32Array::from(vec![v; n]))
108 }
109 DataType::Int64 => {
110 let v = default_val.and_then(Value::as_i64);
111 Arc::new(Int64Array::from(vec![v; n]))
112 }
113 DataType::Float32 => {
114 let v = default_val.and_then(Value::as_f64).map(|f| f as f32);
115 Arc::new(Float32Array::from(vec![v; n]))
116 }
117 DataType::Float64 => {
118 let v = default_val.and_then(Value::as_f64);
119 Arc::new(Float64Array::from(vec![v; n]))
120 }
121 DataType::Date32 => {
122 let v = default_val.and_then(Value::as_i64).map(|d| d as i32);
124 Arc::new(Date32Array::from(vec![v; n]))
125 }
126 DataType::Timestamp(TimeUnit::Microsecond, tz) => {
127 let v = default_val.and_then(Value::as_i64);
129 let arr = TimestampMicrosecondArray::from(vec![v; n]);
130 Arc::new(if tz.is_some() {
131 arr.with_timezone("UTC")
132 } else {
133 arr
134 })
135 }
136 DataType::Utf8 => {
137 let v: Option<&str> = default_val.and_then(Value::as_str);
138 Arc::new(StringArray::from(vec![v; n]))
139 }
140 _ => {
141 Arc::new(StringArray::from(vec![None::<&str>; n]))
143 }
144 })
145}
146
147#[cfg(test)]
148mod tests {
149 use std::sync::Arc;
150
151 use ailake_catalog::SchemaField;
152 use arrow_array::{Array, Float32Array, Int32Array, RecordBatch, StringArray};
153 use arrow_schema::{DataType, Field, Schema};
154
155 use super::SchemaFiller;
156
157 fn make_base_batch() -> RecordBatch {
158 let schema = Arc::new(Schema::new(vec![
159 Field::new("id", DataType::Int32, false),
160 Field::new("text", DataType::Utf8, true),
161 ]));
162 RecordBatch::try_new(
163 schema,
164 vec![
165 Arc::new(Int32Array::from(vec![1, 2, 3])),
166 Arc::new(StringArray::from(vec!["a", "b", "c"])),
167 ],
168 )
169 .unwrap()
170 }
171
172 #[test]
173 fn no_op_when_no_schema_fields() {
174 let batch = make_base_batch();
175 let filled = SchemaFiller::fill(batch.clone(), &[]).unwrap();
176 assert_eq!(filled.num_columns(), batch.num_columns());
177 assert_eq!(filled.num_rows(), batch.num_rows());
178 }
179
180 #[test]
181 fn no_op_when_all_columns_present() {
182 let batch = make_base_batch();
183 let fields = vec![
184 SchemaField {
185 id: 1,
186 name: "id".into(),
187 required: true,
188 iceberg_type: "int".into(),
189 initial_default: None,
190 write_default: None,
191 },
192 SchemaField {
193 id: 2,
194 name: "text".into(),
195 required: false,
196 iceberg_type: "string".into(),
197 initial_default: None,
198 write_default: None,
199 },
200 ];
201 let filled = SchemaFiller::fill(batch.clone(), &fields).unwrap();
202 assert_eq!(filled.num_columns(), 2);
203 }
204
205 #[test]
206 fn injects_missing_column_with_null_default() {
207 let batch = make_base_batch();
208 let fields = vec![SchemaField {
209 id: 3,
210 name: "score".into(),
211 required: false,
212 iceberg_type: "float".into(),
213 initial_default: None,
214 write_default: None,
215 }];
216 let filled = SchemaFiller::fill(batch, &fields).unwrap();
217 assert_eq!(filled.num_columns(), 3);
218 assert_eq!(filled.num_rows(), 3);
219 let score_col = filled
220 .column_by_name("score")
221 .unwrap()
222 .as_any()
223 .downcast_ref::<Float32Array>()
224 .unwrap();
225 assert!(!score_col.is_valid(0));
227 assert!(!score_col.is_valid(1));
228 }
229
230 #[test]
231 fn injects_missing_column_with_value_default() {
232 let batch = make_base_batch();
233 let fields = vec![SchemaField {
234 id: 4,
235 name: "score".into(),
236 required: false,
237 iceberg_type: "float".into(),
238 initial_default: Some(serde_json::json!(0.5)),
239 write_default: None,
240 }];
241 let filled = SchemaFiller::fill(batch, &fields).unwrap();
242 let score_col = filled
243 .column_by_name("score")
244 .unwrap()
245 .as_any()
246 .downcast_ref::<Float32Array>()
247 .unwrap();
248 assert!((score_col.value(0) - 0.5).abs() < 1e-6);
249 assert!((score_col.value(2) - 0.5).abs() < 1e-6);
250 }
251
252 #[test]
253 fn injects_string_column_with_default() {
254 let batch = make_base_batch();
255 let fields = vec![SchemaField {
256 id: 5,
257 name: "category".into(),
258 required: false,
259 iceberg_type: "string".into(),
260 initial_default: Some(serde_json::json!("uncategorized")),
261 write_default: None,
262 }];
263 let filled = SchemaFiller::fill(batch, &fields).unwrap();
264 let cat = filled
265 .column_by_name("category")
266 .unwrap()
267 .as_any()
268 .downcast_ref::<StringArray>()
269 .unwrap();
270 assert_eq!(cat.value(0), "uncategorized");
271 assert_eq!(cat.value(2), "uncategorized");
272 }
273}