1#![doc(
8 html_logo_url = "https://raw.githubusercontent.com/apache/parquet-format/25f05e73d8cd7f5c83532ce51cb4f4de8ba5f2a2/logo/parquet-logos_1.svg",
9 html_favicon_url = "https://raw.githubusercontent.com/apache/parquet-format/25f05e73d8cd7f5c83532ce51cb4f4de8ba5f2a2/logo/parquet-logos_1.svg"
10)]
11#![cfg_attr(docsrs, feature(doc_cfg))]
12#![allow(clippy::approx_constant)]
13
14use parquet_derive::{ParquetRecordReader, ParquetRecordWriter};
15use std::sync::Arc;
16
17#[derive(ParquetRecordWriter)]
18#[expect(
19 clippy::ref_option_ref,
20 reason = "the point of this struct is to cover every field type the derive supports, including `&Option<&T>`"
21)]
22struct ACompleteRecord<'a> {
23 pub a_bool: bool,
24 pub a_str: &'a str,
25 pub a_string: String,
26 pub a_borrowed_string: &'a String,
27 pub a_arc_str: Arc<str>,
28 pub maybe_a_str: Option<&'a str>,
29 pub maybe_a_string: Option<String>,
30 pub maybe_a_arc_str: Option<Arc<str>>,
31 pub i16: i16,
32 pub i32: i32,
33 pub u64: u64,
34 pub maybe_u8: Option<u8>,
35 pub maybe_i16: Option<i16>,
36 pub maybe_u32: Option<u32>,
37 pub maybe_usize: Option<usize>,
38 pub isize: isize,
39 pub float: f32,
40 pub double: f64,
41 pub maybe_float: Option<f32>,
42 pub maybe_double: Option<f64>,
43 pub borrowed_maybe_a_string: &'a Option<String>,
44 pub borrowed_maybe_a_str: &'a Option<&'a str>,
45 pub now: chrono::NaiveDateTime,
46 pub uuid: uuid::Uuid,
47 pub byte_vec: Vec<u8>,
48 pub maybe_byte_vec: Option<Vec<u8>>,
49 pub borrowed_byte_vec: &'a [u8],
50 pub borrowed_maybe_byte_vec: &'a Option<Vec<u8>>,
51 pub borrowed_maybe_borrowed_byte_vec: &'a Option<&'a [u8]>,
52}
53
54#[derive(PartialEq, ParquetRecordWriter, ParquetRecordReader, Debug)]
55struct APartiallyCompleteRecord {
56 pub bool: bool,
57 pub string: String,
58 pub i16: i16,
59 pub i32: i32,
60 pub u64: u64,
61 pub isize: isize,
62 pub float: f32,
63 pub double: f64,
64 pub now: chrono::NaiveDateTime,
65 pub date: chrono::NaiveDate,
66 pub uuid: uuid::Uuid,
67 pub byte_vec: Vec<u8>,
68}
69
70#[derive(PartialEq, ParquetRecordWriter, Debug)]
74struct APartiallyOptionalRecord {
75 pub bool: bool,
76 pub string: String,
77 pub i16: Option<i16>,
78 pub i32: Option<i32>,
79 pub u64: Option<u64>,
80 pub isize: isize,
81 pub float: f32,
82 pub double: f64,
83 pub now: chrono::NaiveDateTime,
84 pub date: chrono::NaiveDate,
85 pub uuid: uuid::Uuid,
86 pub byte_vec: Vec<u8>,
87}
88
89#[derive(PartialEq, ParquetRecordReader, Debug)]
93struct APrunedRecord {
94 pub bool: bool,
95 pub string: String,
96 pub byte_vec: Vec<u8>,
97 pub float: f32,
98 pub double: f64,
99 pub i16: i16,
100 pub i32: i32,
101 pub u64: u64,
102 pub isize: isize,
103}
104
105#[derive(PartialEq, ParquetRecordWriter, ParquetRecordReader, Debug)]
109struct ARecordWithRawIdentifiers {
110 pub r#type: i32,
111 pub count: i32,
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 use chrono::SubsecRound;
119 use std::{env, fs, io::Write, sync::Arc};
120
121 use parquet::{
122 file::writer::SerializedFileWriter,
123 record::{RecordReader, RecordWriter},
124 schema::parser::parse_message_type,
125 };
126
127 #[test]
128 fn test_parquet_derive_hello() {
129 let file = get_temp_file("test_parquet_derive_hello", &[]);
130
131 let schema_str = "message rust_schema {
134 REQUIRED boolean a_bool;
135 REQUIRED BINARY a_str (STRING);
136 REQUIRED BINARY a_string (STRING);
137 REQUIRED BINARY a_borrowed_string (STRING);
138 REQUIRED BINARY a_arc_str (STRING);
139 OPTIONAL BINARY maybe_a_str (STRING);
140 OPTIONAL BINARY maybe_a_string (STRING);
141 OPTIONAL BINARY maybe_a_arc_str (STRING);
142 REQUIRED INT32 i16 (INTEGER(16,true));
143 REQUIRED INT32 i32;
144 REQUIRED INT64 u64 (INTEGER(64,false));
145 OPTIONAL INT32 maybe_u8 (INTEGER(8,false));
146 OPTIONAL INT32 maybe_i16 (INTEGER(16,true));
147 OPTIONAL INT32 maybe_u32 (INTEGER(32,false));
148 OPTIONAL INT64 maybe_usize (INTEGER(64,false));
149 REQUIRED INT64 isize (INTEGER(64,true));
150 REQUIRED FLOAT float;
151 REQUIRED DOUBLE double;
152 OPTIONAL FLOAT maybe_float;
153 OPTIONAL DOUBLE maybe_double;
154 OPTIONAL BINARY borrowed_maybe_a_string (STRING);
155 OPTIONAL BINARY borrowed_maybe_a_str (STRING);
156 REQUIRED INT64 now (TIMESTAMP_MILLIS);
157 REQUIRED FIXED_LEN_BYTE_ARRAY (16) uuid (UUID);
158 REQUIRED BINARY byte_vec;
159 OPTIONAL BINARY maybe_byte_vec;
160 REQUIRED BINARY borrowed_byte_vec;
161 OPTIONAL BINARY borrowed_maybe_byte_vec;
162 OPTIONAL BINARY borrowed_maybe_borrowed_byte_vec;
163 }";
164
165 let schema = Arc::new(parse_message_type(schema_str).unwrap());
166
167 let a_str = "hello mother".to_owned();
168 let a_borrowed_string = "cool news".to_owned();
169 let a_arc_str: Arc<str> = "hello arc".into();
170 let maybe_a_string = Some("it's true, I'm a string".to_owned());
171 let maybe_a_str = Some(&a_str[..]);
172 let maybe_a_arc_str = Some(a_arc_str.clone());
173 let borrowed_byte_vec = vec![0x68, 0x69, 0x70];
174 let borrowed_maybe_byte_vec = Some(vec![0x71, 0x72]);
175 let borrowed_maybe_borrowed_byte_vec = Some(&borrowed_byte_vec[..]);
176
177 let drs: Vec<ACompleteRecord> = vec![ACompleteRecord {
178 a_bool: true,
179 a_str: &a_str[..],
180 a_string: "hello father".into(),
181 a_borrowed_string: &a_borrowed_string,
182 a_arc_str,
183 maybe_a_str: Some(&a_str[..]),
184 maybe_a_string: Some(a_str.clone()),
185 maybe_a_arc_str,
186 i16: -45,
187 i32: 456,
188 u64: 4563424,
189 maybe_u8: None,
190 maybe_i16: Some(3),
191 maybe_u32: None,
192 maybe_usize: Some(4456),
193 isize: -365,
194 float: 3.5,
195 double: f64::NAN,
196 maybe_float: None,
197 maybe_double: Some(f64::MAX),
198 borrowed_maybe_a_string: &maybe_a_string,
199 borrowed_maybe_a_str: &maybe_a_str,
200 now: chrono::Utc::now().naive_local(),
201 uuid: uuid::Uuid::new_v4(),
202 byte_vec: vec![0x65, 0x66, 0x67],
203 maybe_byte_vec: Some(vec![0x88, 0x89, 0x90]),
204 borrowed_byte_vec: &borrowed_byte_vec,
205 borrowed_maybe_byte_vec: &borrowed_maybe_byte_vec,
206 borrowed_maybe_borrowed_byte_vec: &borrowed_maybe_borrowed_byte_vec,
207 }];
208
209 let generated_schema = drs.as_slice().schema().unwrap();
210
211 assert_eq!(&schema, &generated_schema);
212
213 let props = Default::default();
214 let mut writer = SerializedFileWriter::new(file, generated_schema, props).unwrap();
215
216 let mut row_group = writer.next_row_group().unwrap();
217 drs.as_slice().write_to_row_group(&mut row_group).unwrap();
218 row_group.close().unwrap();
219 writer.close().unwrap();
220 }
221
222 #[test]
223 fn test_parquet_derive_read_write_combined() {
224 let file = get_temp_file("test_parquet_derive_combined", &[]);
225
226 let mut drs: Vec<APartiallyCompleteRecord> = vec![APartiallyCompleteRecord {
227 bool: true,
228 string: "a string".into(),
229 i16: -45,
230 i32: 456,
231 u64: 4563424,
232 isize: -365,
233 float: 3.5,
234 double: f64::NAN,
235 now: chrono::Utc::now().naive_local(),
236 date: chrono::naive::NaiveDate::from_ymd_opt(2015, 3, 14).unwrap(),
237 uuid: uuid::Uuid::new_v4(),
238 byte_vec: vec![0x65, 0x66, 0x67],
239 }];
240
241 let mut out: Vec<APartiallyCompleteRecord> = Vec::new();
242
243 use parquet::file::{reader::FileReader, serialized_reader::SerializedFileReader};
244
245 let generated_schema = drs.as_slice().schema().unwrap();
246
247 let props = Default::default();
248 let mut writer =
249 SerializedFileWriter::new(file.try_clone().unwrap(), generated_schema, props).unwrap();
250
251 let mut row_group = writer.next_row_group().unwrap();
252 drs.as_slice().write_to_row_group(&mut row_group).unwrap();
253 row_group.close().unwrap();
254 writer.close().unwrap();
255
256 let reader = SerializedFileReader::new(file).unwrap();
257
258 let mut row_group = reader.get_row_group(0).unwrap();
259 out.read_from_row_group(&mut *row_group, 1).unwrap();
260
261 drs[0].now = drs[0].now.trunc_subsecs(3);
264
265 assert!(out[0].double.is_nan()); out[0].double = 0.;
267 drs[0].double = 0.;
268
269 assert_eq!(drs[0], out[0]);
270 }
271
272 #[test]
273 fn test_parquet_derive_read_optional_but_valid_column() {
274 let file = get_temp_file("test_parquet_derive_read_optional", &[]);
275 let drs = vec![APartiallyOptionalRecord {
276 bool: true,
277 string: "a string".into(),
278 i16: Some(-45),
279 i32: Some(456),
280 u64: Some(4563424),
281 isize: -365,
282 float: 3.5,
283 double: f64::NAN,
284 now: chrono::Utc::now().naive_local(),
285 date: chrono::naive::NaiveDate::from_ymd_opt(2015, 3, 14).unwrap(),
286 uuid: uuid::Uuid::new_v4(),
287 byte_vec: vec![0x65, 0x66, 0x67],
288 }];
289
290 let generated_schema = drs.as_slice().schema().unwrap();
291
292 let props = Default::default();
293 let mut writer =
294 SerializedFileWriter::new(file.try_clone().unwrap(), generated_schema, props).unwrap();
295
296 let mut row_group = writer.next_row_group().unwrap();
297 drs.as_slice().write_to_row_group(&mut row_group).unwrap();
298 row_group.close().unwrap();
299 writer.close().unwrap();
300
301 use parquet::file::{reader::FileReader, serialized_reader::SerializedFileReader};
302 let reader = SerializedFileReader::new(file).unwrap();
303 let mut out: Vec<APartiallyCompleteRecord> = Vec::new();
304
305 let mut row_group = reader.get_row_group(0).unwrap();
306 out.read_from_row_group(&mut *row_group, 1).unwrap();
307
308 assert_eq!(drs[0].i16.unwrap(), out[0].i16);
309 assert_eq!(drs[0].i32.unwrap(), out[0].i32);
310 assert_eq!(drs[0].u64.unwrap(), out[0].u64);
311 }
312
313 #[test]
314 fn test_parquet_derive_read_pruned_and_shuffled_columns() {
315 let file = get_temp_file("test_parquet_derive_read_pruned", &[]);
316 let drs = vec![APartiallyCompleteRecord {
317 bool: true,
318 string: "a string".into(),
319 i16: -45,
320 i32: 456,
321 u64: 4563424,
322 isize: -365,
323 float: 3.5,
324 double: f64::NAN,
325 now: chrono::Utc::now().naive_local(),
326 date: chrono::naive::NaiveDate::from_ymd_opt(2015, 3, 14).unwrap(),
327 uuid: uuid::Uuid::new_v4(),
328 byte_vec: vec![0x65, 0x66, 0x67],
329 }];
330
331 let generated_schema = drs.as_slice().schema().unwrap();
332
333 let props = Default::default();
334 let mut writer =
335 SerializedFileWriter::new(file.try_clone().unwrap(), generated_schema, props).unwrap();
336
337 let mut row_group = writer.next_row_group().unwrap();
338 drs.as_slice().write_to_row_group(&mut row_group).unwrap();
339 row_group.close().unwrap();
340 writer.close().unwrap();
341
342 use parquet::file::{reader::FileReader, serialized_reader::SerializedFileReader};
343 let reader = SerializedFileReader::new(file).unwrap();
344 let mut out: Vec<APrunedRecord> = Vec::new();
345
346 let mut row_group = reader.get_row_group(0).unwrap();
347 out.read_from_row_group(&mut *row_group, 1).unwrap();
348
349 assert_eq!(drs[0].bool, out[0].bool);
350 assert_eq!(drs[0].string, out[0].string);
351 assert_eq!(drs[0].byte_vec, out[0].byte_vec);
352 assert_eq!(drs[0].float, out[0].float);
353 assert!(drs[0].double.is_nan());
354 assert!(out[0].double.is_nan());
355 assert_eq!(drs[0].i16, out[0].i16);
356 assert_eq!(drs[0].i32, out[0].i32);
357 assert_eq!(drs[0].u64, out[0].u64);
358 assert_eq!(drs[0].isize, out[0].isize);
359 }
360
361 #[test]
362 fn test_parquet_derive_raw_identifiers() {
363 let file = get_temp_file("test_parquet_derive_raw_identifiers", &[]);
364 let drs = vec![ARecordWithRawIdentifiers {
365 r#type: 456,
366 count: 123,
367 }];
368
369 let generated_schema = drs.as_slice().schema().unwrap();
370
371 assert_eq!(
374 vec!["type", "count"],
375 generated_schema
376 .get_fields()
377 .iter()
378 .map(|field| field.name())
379 .collect::<Vec<_>>()
380 );
381
382 let props = Default::default();
383 let mut writer =
384 SerializedFileWriter::new(file.try_clone().unwrap(), generated_schema, props).unwrap();
385
386 let mut row_group = writer.next_row_group().unwrap();
387 drs.as_slice().write_to_row_group(&mut row_group).unwrap();
388 row_group.close().unwrap();
389 writer.close().unwrap();
390
391 use parquet::file::{reader::FileReader, serialized_reader::SerializedFileReader};
392 let reader = SerializedFileReader::new(file).unwrap();
393 let mut out: Vec<ARecordWithRawIdentifiers> = Vec::new();
394
395 let mut row_group = reader.get_row_group(0).unwrap();
396 out.read_from_row_group(&mut *row_group, 1).unwrap();
397
398 assert_eq!(drs, out);
399 }
400
401 #[test]
402 fn test_aliased_result() {
403 mod aliased_result {
407 use parquet_derive::{ParquetRecordReader, ParquetRecordWriter};
408
409 pub type Result = ();
415
416 #[derive(ParquetRecordReader, ParquetRecordWriter, Debug)]
417 pub struct ARecord {
418 pub bool: bool,
419 pub string: String,
420 }
421
422 impl ARecord {
423 pub fn do_nothing(&self) -> Result {}
424 pub fn validate(&self) -> std::result::Result<(), Box<dyn std::error::Error>> {
425 Ok(())
426 }
427 }
428 }
429
430 use aliased_result::ARecord;
431 let foo = ARecord {
432 bool: true,
433 string: "test".to_string(),
434 };
435 foo.do_nothing();
436 assert!(foo.validate().is_ok());
437 }
438
439 pub fn get_temp_file(file_name: &str, content: &[u8]) -> fs::File {
441 let dir = env::current_dir().unwrap().join("target/debug/testdata");
443 fs::create_dir_all(&dir).unwrap();
444 let path_buf = dir.join(file_name);
445
446 let mut tmp_file = fs::File::create(path_buf.as_path()).unwrap();
448 tmp_file.write_all(content).unwrap();
449 tmp_file.sync_all().unwrap();
450
451 let file = fs::OpenOptions::new()
453 .read(true)
454 .write(true)
455 .open(path_buf.as_path());
456 assert!(file.is_ok());
457 file.unwrap()
458 }
459}