1use std::collections::HashSet;
2use std::ffi;
3use std::fs;
4use std::path;
5use std::sync::Arc;
6
7use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
8use parquet::basic::Repetition;
9use parquet::file::metadata as pq_metadata;
10use parquet::schema::types as pq_types;
11
12#[derive(thiserror::Error, Debug)]
13pub enum Error {
14 #[error("{}: {}", .0.display(), .1)]
15 Io(std::path::PathBuf, std::io::Error),
16
17 #[error("invalid non-unicode path: {}", .0.display())]
18 NonUnicode(ffi::OsString),
19
20 #[error("parquet error: {0}")]
21 Parquet(#[from] parquet::errors::ParquetError),
22
23 #[error("arrow error: {0}")]
24 Arrow(#[from] arrow::error::ArrowError),
25
26 #[error("not implemented: {feature} at {location}")]
27 NotImplemented {
28 feature: &'static str,
29 location: String,
30 },
31}
32
33pub fn get_interface(base_path: &path::Path) -> Result<String, Error> {
38 let mut r = String::new();
39 let mut paths_to_search = vec![base_path.to_path_buf()];
40
41 while let Some(path) = paths_to_search.pop() {
42 let to_io_err = |e| Error::Io(path.clone(), e);
43
44 let md = path.metadata().map_err(to_io_err)?;
45 if md.is_dir() {
46 let read_dir = path.read_dir().map_err(to_io_err)?;
47 for entry in read_dir {
48 let entry = entry.map_err(to_io_err)?;
49 paths_to_search.push(entry.path());
50 }
51 } else if md.is_file() && path.extension().is_some_and(|e| e == "parquet") {
52 let relative = path.strip_prefix(base_path).unwrap();
53
54 r += &get_interface_of_parquet_file(&path, relative)?;
55 r += "\n";
56 }
57 }
58
59 Ok(r)
60}
61
62fn get_interface_of_parquet_file(
65 path: &path::Path,
66 path_relative: &path::Path,
67) -> Result<String, Error> {
68 let file_prefix = path.file_prefix().unwrap();
70 let func_name = to_str_or_err(file_prefix)?.replace(['/', ' '], "_");
71
72 let file = fs::File::open(path).map_err(|e| Error::Io(path.to_path_buf(), e))?;
74 let mut metadata_reader = pq_metadata::ParquetMetaDataReader::new();
75 metadata_reader.try_parse(&file).unwrap();
76 let metadata = metadata_reader.finish().unwrap();
77
78 let file = fs::File::open(path).map_err(|e| Error::Io(path.to_path_buf(), e))?;
80 let nullable_columns = scan_for_nulls(file)?;
81
82 let ty = schema_to_lutra_ty(metadata.file_metadata().schema(), &nullable_columns)?;
83
84 let path = to_str_or_err(path_relative.as_os_str())?;
86
87 Ok(format!(
88 "func {func_name}(): {ty} -> std::fs::read_parquet(\"{path}\")\n"
89 ))
90}
91
92fn scan_for_nulls(file: fs::File) -> Result<HashSet<String>, Error> {
95 let builder = ParquetRecordBatchReaderBuilder::try_new(file)?;
96 let reader = builder.build()?;
97
98 let mut nullable_columns = HashSet::new();
99
100 for batch_result in reader {
101 let batch = batch_result?;
102
103 for (i, column) in batch.columns().iter().enumerate() {
105 if column.null_count() > 0 {
106 let field_name = batch.schema().field(i).name().clone();
107 nullable_columns.insert(field_name);
108 }
109 }
110 }
111
112 Ok(nullable_columns)
113}
114
115fn schema_to_lutra_ty(
116 schema: &pq_types::Type,
117 nullable_columns: &HashSet<String>,
118) -> Result<String, Error> {
119 let pq_types::Type::GroupType { fields, .. } = schema else {
120 return Err(Error::NotImplemented {
121 feature: "primitive type",
122 location: "top level".into(),
123 });
124 };
125
126 format_tuple_fields(fields, nullable_columns, " ").map_err(|e| Error::NotImplemented {
127 feature: e.message,
128 location: e.location.unwrap_or_else(|| "top level".into()),
129 })
130}
131
132struct NotImplementedError {
133 message: &'static str,
134 location: Option<String>,
135}
136
137impl NotImplementedError {
138 fn new(message: &'static str) -> Self {
139 Self {
140 message,
141 location: None,
142 }
143 }
144
145 fn with_location(mut self, location: String) -> Self {
146 self.location = Some(location);
147 self
148 }
149}
150
151fn format_tuple_fields(
153 fields: &[Arc<pq_types::Type>],
154 nullable_columns: &HashSet<String>,
155 indent: &str,
156) -> Result<String, NotImplementedError> {
157 let mut ty_fields = String::new();
158
159 for f in fields {
160 let info = match f.as_ref() {
161 pq_types::Type::PrimitiveType { basic_info, .. } => basic_info,
162 pq_types::Type::GroupType { basic_info, .. } => basic_info,
163 };
164
165 let base_ty = field_base_type(f.as_ref(), indent)
167 .map_err(|e| e.with_location(format!("field {}", info.name())))?;
168
169 let field_ty = apply_repetition(
171 base_ty,
172 info.repetition(),
173 nullable_columns.contains(info.name()),
174 );
175
176 ty_fields += indent;
178 ty_fields += info.name();
179 ty_fields += ": ";
180 ty_fields += &field_ty;
181 ty_fields += ",\n";
182 }
183
184 Ok(format!("[{{\n{ty_fields}}}]"))
185}
186
187fn apply_repetition(base_ty: String, repetition: Repetition, has_nulls: bool) -> String {
189 match repetition {
190 Repetition::OPTIONAL => {
191 if has_nulls {
192 format!("enum {{none, some: {base_ty}}}")
193 } else {
194 base_ty
195 }
196 }
197 Repetition::REQUIRED => base_ty,
198 Repetition::REPEATED => format!("[{base_ty}]"),
199 }
200}
201
202fn field_base_type(field: &pq_types::Type, indent: &str) -> Result<String, NotImplementedError> {
204 match field {
205 pq_types::Type::PrimitiveType {
206 basic_info,
207 physical_type,
208 ..
209 } => {
210 let ty_field = converted_type_to_lt(basic_info.converted_type())?;
212 let ty_field = if let Some(t) = ty_field {
213 t.to_string()
214 } else {
215 physical_type_to_lt(*physical_type)?.to_string()
216 };
217 Ok(ty_field)
218 }
219 pq_types::Type::GroupType {
220 basic_info, fields, ..
221 } => {
222 if basic_info.converted_type() == parquet::basic::ConvertedType::LIST {
224 if fields.len() == 1 {
226 let indent = indent.to_string() + " ";
227 let element_ty = field_base_type(fields[0].as_ref(), &indent)?;
228 return Ok(format!("[{element_ty}]"));
229 }
230 }
231
232 let empty_set = HashSet::new();
236 let indent = indent.to_string() + " ";
237 format_tuple_fields(fields, &empty_set, &indent)
238 }
239 }
240}
241
242fn converted_type_to_lt(
243 ty: parquet::basic::ConvertedType,
244) -> Result<Option<&'static str>, NotImplementedError> {
245 use parquet::basic::ConvertedType;
246 Ok(Some(match ty {
247 ConvertedType::NONE => return Ok(None),
248
249 ConvertedType::UINT_8 => "uint8",
250 ConvertedType::UINT_16 => "uint16",
251 ConvertedType::UINT_32 => "uint32",
252 ConvertedType::UINT_64 => "uint64",
253 ConvertedType::INT_8 => "int8",
254 ConvertedType::INT_16 => "int16",
255 ConvertedType::INT_32 => "int32",
256 ConvertedType::INT_64 => "int64",
257 ConvertedType::UTF8 => "text",
258
259 ConvertedType::MAP => return Err(NotImplementedError::new("MAP type")),
260 ConvertedType::MAP_KEY_VALUE => return Err(NotImplementedError::new("MAP_KEY_VALUE type")),
261 ConvertedType::LIST => return Ok(None), ConvertedType::ENUM => return Err(NotImplementedError::new("ENUM type")),
263 ConvertedType::DECIMAL => return Err(NotImplementedError::new("DECIMAL type")),
264
265 ConvertedType::DATE => "std::Date",
266 ConvertedType::TIME_MILLIS => return Err(NotImplementedError::new("TIME_MILLIS type")),
267 ConvertedType::TIME_MICROS => return Err(NotImplementedError::new("TIME_MICROS type")),
268 ConvertedType::TIMESTAMP_MILLIS => {
269 return Err(NotImplementedError::new("TIMESTAMP_MILLIS type"));
270 }
271 ConvertedType::TIMESTAMP_MICROS => "std::Timestamp",
272
273 ConvertedType::JSON => return Err(NotImplementedError::new("JSON type")),
274 ConvertedType::BSON => return Err(NotImplementedError::new("BSON type")),
275 ConvertedType::INTERVAL => return Err(NotImplementedError::new("INTERVAL type")),
276 }))
277}
278
279fn physical_type_to_lt(ty: parquet::basic::Type) -> Result<&'static str, NotImplementedError> {
280 use parquet::basic::Type;
281 Ok(match ty {
282 Type::BOOLEAN => "bool",
283 Type::INT32 => "int32",
284 Type::INT64 => "int64",
285 Type::INT96 => return Err(NotImplementedError::new("INT96 type")),
286 Type::FLOAT => "float32",
287 Type::DOUBLE => "float64",
288 Type::BYTE_ARRAY => return Err(NotImplementedError::new("BYTE_ARRAY type")),
289 Type::FIXED_LEN_BYTE_ARRAY => {
290 return Err(NotImplementedError::new("FIXED_LEN_BYTE_ARRAY type"));
291 }
292 })
293}
294
295fn to_str_or_err(s: &ffi::OsStr) -> Result<&str, Error> {
296 s.to_str()
297 .ok_or_else(|| Error::NonUnicode(s.to_os_string()))
298}