1use arrow_schema::extension::ExtensionType;
10use arrow_schema::{ArrowError, DataType};
11
12#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
43pub struct FileType;
44
45impl ExtensionType for FileType {
46 const NAME: &'static str = "arrow.parquet.file";
47
48 type Metadata = &'static str;
49
50 fn metadata(&self) -> &Self::Metadata {
51 &""
52 }
53
54 fn serialize_metadata(&self) -> Option<String> {
55 Some(String::new())
56 }
57
58 fn deserialize_metadata(metadata: Option<&str>) -> Result<Self::Metadata, ArrowError> {
59 match metadata {
60 None | Some("") => Ok(""),
61 Some(_) => Err(ArrowError::InvalidArgumentError(
62 "FileType extension type expects no metadata".to_owned(),
63 )),
64 }
65 }
66
67 fn supports_data_type(&self, data_type: &DataType) -> Result<(), ArrowError> {
68 let DataType::Struct(fields) = data_type else {
69 return Err(ArrowError::InvalidArgumentError(format!(
70 "FileType only supports a struct, got {data_type}"
71 )));
72 };
73 for (i, field) in fields.iter().enumerate() {
74 let name = field.name();
75 if fields[..i].iter().any(|f| f.name() == name) {
76 return Err(ArrowError::InvalidArgumentError(format!(
77 "FileType struct has duplicate field '{name}'"
78 )));
79 }
80 let type_ok = match name.as_str() {
81 "uri" | "content_type" | "checksum" => matches!(
82 field.data_type(),
83 DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View
84 ),
85 "offset" | "size" => field.data_type() == &DataType::Int64,
86 "inline" => matches!(
87 field.data_type(),
88 DataType::Binary | DataType::LargeBinary | DataType::BinaryView
89 ),
90 _ => {
91 return Err(ArrowError::InvalidArgumentError(format!(
92 "FileType struct has unrecognized field '{name}'. \
93 Valid fields are: uri, offset, size, content_type, checksum, inline"
94 )));
95 }
96 };
97 if !type_ok {
98 return Err(ArrowError::InvalidArgumentError(format!(
99 "FileType field '{name}' has unsupported type {}",
100 field.data_type()
101 )));
102 }
103 if !field.is_nullable() {
104 return Err(ArrowError::InvalidArgumentError(format!(
105 "FileType field '{name}' must be nullable"
106 )));
107 }
108 }
109 Ok(())
110 }
111
112 fn try_new(data_type: &DataType, _metadata: Self::Metadata) -> Result<Self, ArrowError> {
113 Self.supports_data_type(data_type)?;
114 Ok(Self)
115 }
116
117 fn validate(data_type: &DataType, _metadata: Self::Metadata) -> Result<(), ArrowError> {
118 Self.supports_data_type(data_type)
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use std::collections::HashMap;
125 use std::sync::Arc;
126
127 use arrow_array::{
128 Array, ArrayRef, BinaryArray, Int64Array, ListArray, RecordBatch, StringArray, StructArray,
129 };
130 use arrow_buffer::OffsetBuffer;
131 use arrow_schema::{Field, Fields, Schema};
132 use bytes::Bytes;
133
134 use super::*;
135 use crate::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
136 use crate::arrow::{ArrowSchemaConverter, ArrowWriter, PARQUET_FIELD_ID_META_KEY};
137 use crate::basic::LogicalType;
138
139 fn file_fields(first_id: Option<i32>) -> Fields {
140 let field = |name: &str, data_type: DataType, offset: i32| {
141 let field = Field::new(name, data_type, true);
142 match first_id {
143 Some(id) => field.with_metadata(HashMap::from([(
144 PARQUET_FIELD_ID_META_KEY.to_string(),
145 (id + offset).to_string(),
146 )])),
147 None => field,
148 }
149 };
150 Fields::from(vec![
151 field("uri", DataType::Utf8, 1),
152 field("offset", DataType::Int64, 2),
153 field("size", DataType::Int64, 3),
154 field("content_type", DataType::Utf8, 4),
155 field("checksum", DataType::Utf8, 5),
156 field("inline", DataType::Binary, 6),
157 ])
158 }
159
160 fn file_field(name: &str, fields: Fields) -> Field {
161 Field::new(name, DataType::Struct(fields), true).with_extension_type(FileType)
162 }
163
164 #[test]
165 fn supports_valid_data_types() {
166 let cases = [
167 file_fields(None),
168 Fields::from(vec![
169 Field::new("size", DataType::Int64, true),
170 Field::new("uri", DataType::Utf8View, true),
171 ]),
172 Fields::from(vec![
173 Field::new("uri", DataType::LargeUtf8, true),
174 Field::new("inline", DataType::BinaryView, true),
175 ]),
176 ];
177 for fields in cases {
178 FileType
179 .supports_data_type(&DataType::Struct(fields))
180 .unwrap();
181 }
182 }
183
184 #[test]
185 fn rejects_invalid_data_types() {
186 let cases = [
187 (DataType::Utf8, "only supports a struct"),
188 (
189 DataType::Struct(Fields::from(vec![Field::new("path", DataType::Utf8, true)])),
190 "unrecognized field 'path'",
191 ),
192 (
193 DataType::Struct(Fields::from(vec![Field::new(
194 "size",
195 DataType::Int32,
196 true,
197 )])),
198 "field 'size' has unsupported type",
199 ),
200 (
201 DataType::Struct(Fields::from(vec![Field::new("uri", DataType::Utf8, false)])),
202 "field 'uri' must be nullable",
203 ),
204 (
205 DataType::Struct(Fields::from(vec![
206 Field::new("uri", DataType::Utf8, true),
207 Field::new("uri", DataType::Utf8, true),
208 ])),
209 "duplicate field 'uri'",
210 ),
211 ];
212 for (data_type, expected) in cases {
213 let error = FileType
214 .supports_data_type(&data_type)
215 .unwrap_err()
216 .to_string();
217 assert!(error.contains(expected), "{data_type}: {error}");
218 }
219 }
220
221 #[test]
222 fn schema_round_trip() {
223 let arrow_schema = Schema::new(vec![
224 Field::new("f", DataType::Struct(file_fields(Some(1))), true)
225 .with_metadata(HashMap::from([(
226 PARQUET_FIELD_ID_META_KEY.to_string(),
227 "1".to_string(),
228 )]))
229 .with_extension_type(FileType),
230 ]);
231 let parquet_schema = ArrowSchemaConverter::new().convert(&arrow_schema).unwrap();
232 let group = &parquet_schema.root_schema().get_fields()[0];
233 assert_eq!(
234 group.get_basic_info().logical_type_ref(),
235 Some(&LogicalType::File)
236 );
237 assert_eq!(group.get_basic_info().id(), 1);
238 let ids: Vec<_> = group
239 .get_fields()
240 .iter()
241 .map(|f| f.get_basic_info().id())
242 .collect();
243 assert_eq!(ids, (2..=7).collect::<Vec<_>>());
244
245 let arrow_schema = crate::arrow::parquet_to_arrow_schema(&parquet_schema, None).unwrap();
246 assert_eq!(
247 arrow_schema
248 .field(0)
249 .try_extension_type::<FileType>()
250 .unwrap(),
251 FileType
252 );
253 }
254
255 #[test]
256 fn struct_without_extension_is_not_annotated() {
257 let arrow_schema = Schema::new(vec![Field::new(
258 "f",
259 DataType::Struct(file_fields(None)),
260 true,
261 )]);
262 let parquet_schema = ArrowSchemaConverter::new().convert(&arrow_schema).unwrap();
263 assert_eq!(
264 parquet_schema.root_schema().get_fields()[0]
265 .get_basic_info()
266 .logical_type_ref(),
267 None
268 );
269 }
270
271 #[test]
272 fn struct_with_unknown_metadata_is_not_annotated() {
273 let field = Field::new("f", DataType::Struct(file_fields(None)), true).with_metadata(
274 HashMap::from([
275 (
276 "ARROW:extension:name".to_string(),
277 FileType::NAME.to_string(),
278 ),
279 ("ARROW:extension:metadata".to_string(), "v2".to_string()),
280 ]),
281 );
282 assert!(field.try_extension_type::<FileType>().is_err());
283 let parquet_schema = ArrowSchemaConverter::new()
284 .convert(&Schema::new(vec![field]))
285 .unwrap();
286 assert_eq!(
287 parquet_schema.root_schema().get_fields()[0]
288 .get_basic_info()
289 .logical_type_ref(),
290 None
291 );
292 }
293
294 fn file_values(fields: Fields) -> StructArray {
295 StructArray::new(
296 fields,
297 vec![
298 Arc::new(StringArray::from(vec![Some("s3://b/a.png"), None])) as ArrayRef,
299 Arc::new(Int64Array::from(vec![Some(4), None])),
300 Arc::new(Int64Array::from(vec![Some(10), None])),
301 Arc::new(StringArray::from(vec![Some("image/png"), None])),
302 Arc::new(StringArray::from(vec![None::<&str>, None])),
303 Arc::new(BinaryArray::from(vec![None, Some(b"xyz".as_slice())])),
304 ],
305 None,
306 )
307 }
308
309 fn write_and_read(batch: &RecordBatch) -> (crate::schema::types::TypePtr, RecordBatch) {
310 let mut buffer = Vec::new();
311 let mut writer = ArrowWriter::try_new(&mut buffer, batch.schema(), None).unwrap();
312 writer.write(batch).unwrap();
313 writer.close().unwrap();
314 let builder = ParquetRecordBatchReaderBuilder::try_new(Bytes::from(buffer)).unwrap();
315 let root = builder
316 .metadata()
317 .file_metadata()
318 .schema_descr()
319 .root_schema_ptr();
320 let read = builder.build().unwrap().next().unwrap().unwrap();
321 (root, read)
322 }
323
324 #[test]
325 fn writer_round_trip() {
326 let values = file_values(file_fields(None));
327 let schema = Schema::new(vec![file_field("f", values.fields().clone())]);
328 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(values.clone())]).unwrap();
329
330 let (root, read) = write_and_read(&batch);
331
332 assert_eq!(
333 root.get_fields()[0].get_basic_info().logical_type_ref(),
334 Some(&LogicalType::File)
335 );
336 assert_eq!(
337 read.schema()
338 .field(0)
339 .try_extension_type::<FileType>()
340 .unwrap(),
341 FileType
342 );
343 assert_eq!(read.column(0).as_ref(), &values as &dyn Array);
344 }
345
346 #[test]
347 fn writer_round_trip_in_list() {
348 let values = file_values(file_fields(None));
349 let element = Arc::new(file_field("element", values.fields().clone()));
350 let list = ListArray::new(
351 element,
352 OffsetBuffer::from_lengths([2]),
353 Arc::new(values),
354 None,
355 );
356 let schema = Schema::new(vec![Field::new("a", list.data_type().clone(), true)]);
357 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(list)]).unwrap();
358
359 let (root, read) = write_and_read(&batch);
360
361 let element = &root.get_fields()[0].get_fields()[0].get_fields()[0];
362 assert_eq!(
363 element.get_basic_info().logical_type_ref(),
364 Some(&LogicalType::File)
365 );
366 let DataType::List(element) = read.schema().field(0).data_type().clone() else {
367 panic!("expected a list");
368 };
369 assert_eq!(element.try_extension_type::<FileType>().unwrap(), FileType);
370 }
371}