1use crate::column::chunker::ContentDefinedChunker;
21
22use bytes::Bytes;
23use std::io::Write;
24use std::ops::Range;
25use std::slice::Iter;
26use std::sync::{Arc, Mutex};
27use std::vec::IntoIter;
28
29use arrow_array::cast::AsArray;
30use arrow_array::{ArrayRef, Int32Array, RecordBatch, RecordBatchWriter};
31use arrow_array::{PrimitiveArray, types::*};
32use arrow_schema::{
33 ArrowError, DataType as ArrowDataType, Field, IntervalUnit, SchemaRef, TimeUnit,
34};
35
36use super::schema::{
37 add_encoded_arrow_schema_to_metadata, decimal_length_from_precision, validate_map_key_type,
38};
39
40use crate::arrow::ArrowSchemaConverter;
41use crate::arrow::arrow_writer::byte_array::ByteArrayEncoder;
42use crate::basic::PageType;
43use crate::column::page::{CompressedPage, PageWriteSpec, PageWriter};
44use crate::column::page_encryption::PageEncryptor;
45use crate::column::writer::encoder::ColumnValueEncoder;
46use crate::column::writer::{
47 ColumnCloseResult, ColumnWriter, ColumnWriterImpl, GenericColumnWriter, get_column_writer,
48};
49use crate::data_type::{ByteArray, FixedLenByteArray, FixedLenByteArrayType};
50use std::collections::HashSet;
51type DistinctValuesSet = HashSet<u64>;
52#[cfg(feature = "encryption")]
53use crate::encryption::encrypt::FileEncryptor;
54use crate::errors::{ParquetError, Result};
55use crate::file::metadata::{KeyValue, ParquetMetaData, RowGroupMetaData};
56use crate::file::properties::{WriterProperties, WriterPropertiesPtr};
57use crate::file::writer::{SerializedFileWriter, SerializedRowGroupWriter};
58use crate::parquet_thrift::{ThriftCompactOutputProtocol, WriteThrift};
59use crate::schema::types::{ColumnDescPtr, SchemaDescPtr, SchemaDescriptor};
60use levels::{ArrayLevels, calculate_array_levels};
61
62mod byte_array;
63mod levels;
64
65#[doc(inline)]
66pub use crate::column::page_store::{
67 InMemoryPageStore, InMemoryPageStoreFactory, PageKey, PageStore, PageStoreArgs,
68 PageStoreFactory,
69};
70
71pub struct ArrowWriter<W: Write> {
188 writer: SerializedFileWriter<W>,
190
191 in_progress: Option<ArrowRowGroupWriter>,
193
194 arrow_schema: SchemaRef,
198
199 row_group_writer_factory: ArrowRowGroupWriterFactory,
201
202 max_row_group_row_count: Option<usize>,
204
205 max_row_group_bytes: Option<usize>,
207
208 cdc_chunkers: Option<Vec<ContentDefinedChunker>>,
210}
211
212impl<W: Write + Send> std::fmt::Debug for ArrowWriter<W> {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 let buffered_memory = self.in_progress_size();
215 f.debug_struct("ArrowWriter")
216 .field("writer", &self.writer)
217 .field("in_progress_size", &format_args!("{buffered_memory} bytes"))
218 .field("in_progress_rows", &self.in_progress_rows())
219 .field("arrow_schema", &self.arrow_schema)
220 .field("max_row_group_row_count", &self.max_row_group_row_count)
221 .field("max_row_group_bytes", &self.max_row_group_bytes)
222 .finish()
223 }
224}
225
226impl<W: Write + Send> ArrowWriter<W> {
227 pub fn try_new(
233 writer: W,
234 arrow_schema: SchemaRef,
235 props: Option<WriterProperties>,
236 ) -> Result<Self> {
237 let options = ArrowWriterOptions::new().with_properties(props.unwrap_or_default());
238 Self::try_new_with_options(writer, arrow_schema, options)
239 }
240
241 pub fn try_new_with_options(
247 writer: W,
248 arrow_schema: SchemaRef,
249 options: ArrowWriterOptions,
250 ) -> Result<Self> {
251 for field in arrow_schema.fields() {
252 validate_map_key_type(field.data_type())?;
253 }
254 let mut props = options.properties;
255
256 let schema = if let Some(parquet_schema) = options.schema_descr {
257 parquet_schema.clone()
258 } else {
259 let mut converter = ArrowSchemaConverter::new().with_coerce_types(props.coerce_types());
260 if let Some(schema_root) = &options.schema_root {
261 converter = converter.schema_root(schema_root);
262 }
263
264 converter.convert(&arrow_schema)?
265 };
266
267 if !options.skip_arrow_metadata {
268 add_encoded_arrow_schema_to_metadata(&arrow_schema, &mut props);
270 }
271
272 let max_row_group_row_count = props.max_row_group_row_count();
273 let max_row_group_bytes = props.max_row_group_bytes();
274
275 let props_ptr = Arc::new(props);
276 let file_writer =
277 SerializedFileWriter::new(writer, schema.root_schema_ptr(), Arc::clone(&props_ptr))?;
278
279 let mut row_group_writer_factory =
280 ArrowRowGroupWriterFactory::new(&file_writer, arrow_schema.clone());
281 if let Some(page_store_factory) = options.page_store_factory {
282 row_group_writer_factory =
283 row_group_writer_factory.with_page_store_factory(page_store_factory);
284 }
285
286 let cdc_chunkers = props_ptr
287 .content_defined_chunking()
288 .map(|opts| {
289 file_writer
290 .schema_descr()
291 .columns()
292 .iter()
293 .map(|desc| ContentDefinedChunker::new(desc, opts))
294 .collect::<Result<Vec<_>>>()
295 })
296 .transpose()?;
297
298 Ok(Self {
299 writer: file_writer,
300 in_progress: None,
301 arrow_schema,
302 row_group_writer_factory,
303 max_row_group_row_count,
304 max_row_group_bytes,
305 cdc_chunkers,
306 })
307 }
308
309 pub fn flushed_row_groups(&self) -> &[RowGroupMetaData] {
311 self.writer.flushed_row_groups()
312 }
313
314 pub fn memory_size(&self) -> usize {
319 match &self.in_progress {
320 Some(in_progress) => in_progress.writers.iter().map(|x| x.memory_size()).sum(),
321 None => 0,
322 }
323 }
324
325 pub fn in_progress_size(&self) -> usize {
332 match &self.in_progress {
333 Some(in_progress) => in_progress
334 .writers
335 .iter()
336 .map(|x| x.get_estimated_total_bytes())
337 .sum(),
338 None => 0,
339 }
340 }
341
342 pub fn in_progress_rows(&self) -> usize {
344 self.in_progress
345 .as_ref()
346 .map(|x| x.buffered_rows)
347 .unwrap_or_default()
348 }
349
350 pub fn bytes_written(&self) -> usize {
352 self.writer.bytes_written()
353 }
354
355 pub fn write(&mut self, batch: &RecordBatch) -> Result<()> {
369 if batch.num_rows() == 0 {
370 return Ok(());
371 }
372
373 let mut remaining = batch.clone();
376
377 loop {
378 let in_progress = match &mut self.in_progress {
379 Some(in_progress) => in_progress,
380 x => x.insert(
381 self.row_group_writer_factory
382 .create_row_group_writer(self.writer.flushed_row_groups().len())?,
383 ),
384 };
385 let buffered_rows = in_progress.buffered_rows;
386
387 let mut split_at = match self.max_row_group_row_count {
390 Some(max_rows) if buffered_rows + remaining.num_rows() > max_rows => {
391 Some(max_rows - buffered_rows)
392 }
393 _ => None,
394 };
395
396 let candidate_rows = split_at.unwrap_or_else(|| remaining.num_rows());
401
402 if self.max_row_group_bytes.is_some() && buffered_rows == 0 {
403 if candidate_rows > 1 {
407 split_at = Some(1);
408 }
409 } else if let Some(max_bytes) = self.max_row_group_bytes {
410 let current_bytes = in_progress.get_estimated_total_bytes();
411
412 if current_bytes >= max_bytes {
413 self.flush()?;
414 continue;
415 }
416
417 let avg_row_bytes = current_bytes / buffered_rows;
418 if let Some(rows_that_fit) = (max_bytes - current_bytes).checked_div(avg_row_bytes)
419 {
420 if candidate_rows > rows_that_fit {
422 if rows_that_fit > 0 {
423 split_at = Some(rows_that_fit);
424 } else {
425 self.flush()?;
426 continue;
427 }
428 }
429 }
430 }
431
432 let rest = split_at.map(|to_write| {
433 let rest = remaining.slice(to_write, remaining.num_rows() - to_write);
434 remaining = remaining.slice(0, to_write);
435 rest
436 });
437
438 let in_progress = self.in_progress.as_mut().unwrap();
439 match self.cdc_chunkers.as_mut() {
440 Some(chunkers) => in_progress.write_with_chunkers(&remaining, chunkers)?,
441 None => in_progress.write(&remaining)?,
442 }
443
444 let should_flush = self
445 .max_row_group_row_count
446 .is_some_and(|max| in_progress.buffered_rows >= max)
447 || self
448 .max_row_group_bytes
449 .is_some_and(|max| in_progress.get_estimated_total_bytes() >= max);
450
451 if should_flush {
452 self.flush()?
453 }
454
455 match rest {
456 Some(rest) => remaining = rest,
457 None => return Ok(()),
458 }
459 }
460 }
461
462 pub fn write_all(&mut self, buf: &[u8]) -> std::io::Result<()> {
467 self.writer.write_all(buf)
468 }
469
470 pub fn sync(&mut self) -> std::io::Result<()> {
472 self.writer.flush()
473 }
474
475 pub fn flush(&mut self) -> Result<()> {
480 let Some(in_progress) = self.in_progress.take() else {
481 return Ok(());
482 };
483
484 let mut row_group_writer = self.writer.next_row_group()?;
485 for chunk in in_progress.close()? {
486 chunk.append_to_row_group(&mut row_group_writer)?;
487 }
488 row_group_writer.close()?;
489 Ok(())
490 }
491
492 pub fn append_key_value_metadata(&mut self, kv_metadata: KeyValue) {
496 self.writer.append_key_value_metadata(kv_metadata)
497 }
498
499 pub fn inner(&self) -> &W {
501 self.writer.inner()
502 }
503
504 pub fn inner_mut(&mut self) -> &mut W {
513 self.writer.inner_mut()
514 }
515
516 pub fn into_inner(mut self) -> Result<W> {
518 self.flush()?;
519 self.writer.into_inner()
520 }
521
522 pub fn finish(&mut self) -> Result<ParquetMetaData> {
528 self.flush()?;
529 self.writer.finish()
530 }
531
532 pub fn close(mut self) -> Result<ParquetMetaData> {
534 self.finish()
535 }
536
537 pub fn into_serialized_writer(
544 mut self,
545 ) -> Result<(SerializedFileWriter<W>, ArrowRowGroupWriterFactory)> {
546 self.flush()?;
547 Ok((self.writer, self.row_group_writer_factory))
548 }
549}
550
551impl<W: Write + Send> RecordBatchWriter for ArrowWriter<W> {
552 fn write(&mut self, batch: &RecordBatch) -> Result<(), ArrowError> {
553 self.write(batch).map_err(|e| e.into())
554 }
555
556 fn close(self) -> std::result::Result<(), ArrowError> {
557 self.close()?;
558 Ok(())
559 }
560}
561
562#[derive(Debug, Clone, Default)]
566pub struct ArrowWriterOptions {
567 properties: WriterProperties,
568 skip_arrow_metadata: bool,
569 schema_root: Option<String>,
570 schema_descr: Option<SchemaDescriptor>,
571 page_store_factory: Option<Arc<dyn PageStoreFactory>>,
572}
573
574impl ArrowWriterOptions {
575 pub fn new() -> Self {
577 Self::default()
578 }
579
580 pub fn with_properties(self, properties: WriterProperties) -> Self {
582 Self { properties, ..self }
583 }
584
585 pub fn with_page_store_factory(self, page_store_factory: Arc<dyn PageStoreFactory>) -> Self {
671 Self {
672 page_store_factory: Some(page_store_factory),
673 ..self
674 }
675 }
676
677 pub fn with_skip_arrow_metadata(self, skip_arrow_metadata: bool) -> Self {
684 Self {
685 skip_arrow_metadata,
686 ..self
687 }
688 }
689
690 pub fn with_schema_root(self, schema_root: String) -> Self {
692 Self {
693 schema_root: Some(schema_root),
694 ..self
695 }
696 }
697
698 pub fn with_parquet_schema(self, schema_descr: SchemaDescriptor) -> Self {
704 Self {
705 schema_descr: Some(schema_descr),
706 ..self
707 }
708 }
709}
710
711struct ArrowColumnChunkData {
717 length: usize,
718 store: Box<dyn PageStore>,
719 keys: Vec<PageKey>,
720 dictionary_keys: Vec<PageKey>,
731 dictionary_len: usize,
735}
736
737impl ArrowColumnChunkData {
738 fn new(store: Box<dyn PageStore>) -> Self {
739 Self {
740 length: 0,
741 store,
742 keys: Vec::new(),
743 dictionary_keys: Vec::new(),
744 dictionary_len: 0,
745 }
746 }
747
748 fn push(&mut self, value: Bytes) -> Result<()> {
751 let key = self.store.put(value)?;
752 self.keys.push(key);
753 Ok(())
754 }
755
756 fn push_dictionary(&mut self, value: Bytes) -> Result<()> {
760 self.dictionary_len += value.len();
761 let key = self.store.put(value)?;
762 self.dictionary_keys.push(key);
763 Ok(())
764 }
765
766 fn memory_size(&self) -> usize {
769 self.store.memory_size()
770 }
771}
772
773struct StreamingColumnChunkPages {
782 store: Box<dyn PageStore>,
783 keys: IntoIter<PageKey>,
786}
787
788impl StreamingColumnChunkPages {
789 fn new(data: ArrowColumnChunkData) -> Self {
790 let keys = if data.dictionary_keys.is_empty() {
793 data.keys
794 } else {
795 let mut keys = Vec::with_capacity(data.dictionary_keys.len() + data.keys.len());
796 keys.extend(data.dictionary_keys);
797 keys.extend(data.keys);
798 keys
799 };
800 Self {
801 store: data.store,
802 keys: keys.into_iter(),
803 }
804 }
805}
806
807impl Iterator for StreamingColumnChunkPages {
808 type Item = Result<Bytes>;
809
810 fn next(&mut self) -> Option<Self::Item> {
811 let key = self.keys.next()?;
812 Some(self.store.take(key))
813 }
814}
815
816type SharedColumnChunk = Arc<Mutex<ArrowColumnChunkData>>;
821
822struct ArrowPageWriter {
823 buffer: SharedColumnChunk,
824 #[cfg(feature = "encryption")]
825 page_encryptor: Option<PageEncryptor>,
826}
827
828impl ArrowPageWriter {
829 fn new(store: Box<dyn PageStore>) -> Self {
831 Self {
832 buffer: Arc::new(Mutex::new(ArrowColumnChunkData::new(store))),
833 #[cfg(feature = "encryption")]
834 page_encryptor: None,
835 }
836 }
837
838 #[cfg(feature = "encryption")]
839 pub fn with_encryptor(mut self, page_encryptor: Option<PageEncryptor>) -> Self {
840 self.page_encryptor = page_encryptor;
841 self
842 }
843
844 #[cfg(feature = "encryption")]
845 fn page_encryptor_mut(&mut self) -> Option<&mut PageEncryptor> {
846 self.page_encryptor.as_mut()
847 }
848
849 #[cfg(not(feature = "encryption"))]
852 #[expect(
853 clippy::needless_pass_by_ref_mut,
854 reason = "mirrors the encryption-enabled signature"
855 )]
856 fn page_encryptor_mut(&mut self) -> Option<&mut PageEncryptor> {
857 None
858 }
859}
860
861impl PageWriter for ArrowPageWriter {
862 fn write_page(&mut self, page: CompressedPage) -> Result<PageWriteSpec> {
863 let page = match self.page_encryptor_mut() {
864 Some(page_encryptor) => page_encryptor.encrypt_compressed_page(page)?,
865 None => page,
866 };
867
868 let page_header = page.to_thrift_header()?;
869 let header = {
870 let mut header = Vec::with_capacity(1024);
871
872 match self.page_encryptor_mut() {
873 Some(page_encryptor) => {
874 page_encryptor.encrypt_page_header(&page_header, &mut header)?;
875 if page.compressed_page().is_data_page() {
876 page_encryptor.increment_page();
877 }
878 }
879 None => {
880 let mut protocol = ThriftCompactOutputProtocol::new(&mut header);
881 page_header.write_thrift(&mut protocol)?;
882 }
883 }
884
885 Bytes::from(header)
886 };
887
888 let mut buf = self.buffer.try_lock().unwrap();
889
890 let data = page.compressed_page().buffer().clone();
891 let compressed_size = data.len() + header.len();
892
893 let mut spec = PageWriteSpec::new();
894 spec.page_type = page.page_type();
895 spec.num_values = page.num_values();
896 spec.uncompressed_size = page.uncompressed_size() + header.len();
897 spec.offset = buf.length as u64;
898 spec.compressed_size = compressed_size;
899 spec.bytes_written = compressed_size as u64;
900
901 buf.length += compressed_size;
902 if spec.page_type == PageType::DICTIONARY_PAGE {
903 buf.push_dictionary(header)?;
906 buf.push_dictionary(data)?;
907 } else {
908 buf.push(header)?;
909 buf.push(data)?;
910 }
911
912 Ok(spec)
913 }
914
915 fn defers_dictionary_ordering(&self) -> bool {
916 true
921 }
922
923 fn buffered_memory_size(&self) -> usize {
924 self.buffer.try_lock().unwrap().memory_size()
927 }
928
929 fn close(&mut self) -> Result<()> {
930 Ok(())
931 }
932}
933
934#[derive(Debug)]
936pub struct ArrowLeafColumn(ArrayLevels);
937
938pub fn compute_leaves(field: &Field, array: &ArrayRef) -> Result<Vec<ArrowLeafColumn>> {
944 let levels = calculate_array_levels(array, field)?;
945 Ok(levels.into_iter().map(ArrowLeafColumn).collect())
946}
947
948pub struct ArrowColumnChunk {
950 data: ArrowColumnChunkData,
951 close: ColumnCloseResult,
952}
953
954impl std::fmt::Debug for ArrowColumnChunk {
955 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
956 f.debug_struct("ArrowColumnChunk")
957 .field("length", &self.data.length)
958 .finish_non_exhaustive()
959 }
960}
961
962impl ArrowColumnChunk {
963 pub fn close(&self) -> &ColumnCloseResult {
970 &self.close
971 }
972
973 pub fn close_mut(&mut self) -> &mut ColumnCloseResult {
980 &mut self.close
981 }
982
983 pub fn append_to_row_group<W: Write + Send>(
986 self,
987 writer: &mut SerializedRowGroupWriter<'_, W>,
988 ) -> Result<()> {
989 let ArrowColumnChunk { data, close } = self;
990
991 let close = close.update_dictionary_location(data.dictionary_len)?;
995
996 let pages = StreamingColumnChunkPages::new(data);
997 writer.append_column_from_pages(pages, close)
998 }
999}
1000
1001pub struct ArrowColumnWriter {
1099 writer: ArrowColumnWriterImpl,
1100 chunk: SharedColumnChunk,
1101 distinct_values_seen: Option<DistinctValuesSet>,
1104}
1105
1106impl std::fmt::Debug for ArrowColumnWriter {
1107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1108 f.debug_struct("ArrowColumnWriter").finish_non_exhaustive()
1109 }
1110}
1111
1112enum ArrowColumnWriterImpl {
1113 ByteArray(GenericColumnWriter<'static, ByteArrayEncoder>),
1114 Column(ColumnWriter<'static>),
1115}
1116
1117impl ArrowColumnWriter {
1118 pub fn write(&mut self, col: &ArrowLeafColumn) -> Result<()> {
1120 col.0.validate()?;
1121 self.write_internal(&col.0)
1122 }
1123
1124 fn write_with_chunker(
1126 &mut self,
1127 col: &ArrowLeafColumn,
1128 chunker: &mut ContentDefinedChunker,
1129 ) -> Result<()> {
1130 let levels = &col.0;
1131 levels.validate()?;
1132 let chunks = chunker.get_arrow_chunks(
1133 levels.def_level_data().as_ref(),
1134 levels.rep_level_data().as_ref(),
1135 levels.array(),
1136 )?;
1137
1138 let num_chunks = chunks.len();
1139 for (i, chunk) in chunks.iter().enumerate() {
1140 let chunk_levels = levels.slice_for_chunk(chunk);
1141 self.write_internal(&chunk_levels)?;
1142
1143 if i + 1 < num_chunks {
1145 match &mut self.writer {
1146 ArrowColumnWriterImpl::Column(c) => c.add_data_page()?,
1147 ArrowColumnWriterImpl::ByteArray(c) => c.add_data_page()?,
1148 }
1149 }
1150 }
1151 Ok(())
1152 }
1153
1154 fn write_internal(&mut self, levels: &ArrayLevels) -> Result<()> {
1155 if let Some(seen) = &mut self.distinct_values_seen {
1156 let array = levels.array();
1157 let non_null = levels.non_null_indices();
1158 match array.as_any_dictionary_opt() {
1159 Some(dict) => {
1160 let values = dict.values();
1163 let keys = dict.normalized_keys();
1164 let referenced_value_indices: Vec<usize> = non_null
1165 .iter()
1166 .map(|&pos| keys[pos])
1167 .filter(|&val_idx| values.is_valid(val_idx))
1168 .collect();
1169 update_distinct_values_seen(values.as_ref(), &referenced_value_indices, seen);
1170 }
1171 None => update_distinct_values_seen(array.as_ref(), non_null, seen),
1173 }
1174 }
1175
1176 match &mut self.writer {
1177 ArrowColumnWriterImpl::Column(c) => {
1178 let leaf = levels.array();
1179 match leaf.as_any_dictionary_opt() {
1180 Some(dictionary) => {
1181 let materialized =
1182 arrow_select::take::take(dictionary.values(), dictionary.keys(), None)?;
1183 write_leaf(c, &materialized, levels)?
1184 }
1185 None => write_leaf(c, leaf, levels)?,
1186 };
1187 }
1188 ArrowColumnWriterImpl::ByteArray(c) => {
1189 write_primitive(c, levels.array().as_ref(), levels)?;
1190 }
1191 }
1192 Ok(())
1193 }
1194
1195 pub fn close(self) -> Result<ArrowColumnChunk> {
1202 let distinct_count = self
1203 .distinct_values_seen
1204 .as_ref()
1205 .filter(|s| !s.is_empty())
1206 .map(|s| s.len() as u64);
1207 let close = match self.writer {
1208 ArrowColumnWriterImpl::ByteArray(mut c) => {
1209 if let Some(count) = distinct_count {
1210 c.set_distinct_count_override(count);
1211 }
1212 c.close()?
1213 }
1214 ArrowColumnWriterImpl::Column(mut c) => {
1215 if let Some(count) = distinct_count {
1216 c.set_distinct_count_override(count);
1217 }
1218 c.close()?
1219 }
1220 };
1221 let chunk = Arc::try_unwrap(self.chunk)
1223 .map_err(|_| general_err!("Internal Error: the column chunk is still shared"))?;
1224 let data = chunk
1225 .into_inner()
1226 .map_err(|_| general_err!("The column chunk lock is poisoned"))?;
1227 Ok(ArrowColumnChunk { data, close })
1228 }
1229
1230 pub fn memory_size(&self) -> usize {
1241 match &self.writer {
1242 ArrowColumnWriterImpl::ByteArray(c) => c.memory_size(),
1243 ArrowColumnWriterImpl::Column(c) => c.memory_size(),
1244 }
1245 }
1246
1247 pub fn get_estimated_total_bytes(&self) -> usize {
1255 match &self.writer {
1256 ArrowColumnWriterImpl::ByteArray(c) => c.get_estimated_total_bytes() as _,
1257 ArrowColumnWriterImpl::Column(c) => c.get_estimated_total_bytes() as _,
1258 }
1259 }
1260}
1261
1262fn arrow_leaf_ranges(schema: &SchemaRef) -> Result<Vec<Range<usize>>> {
1263 let mut count = 0;
1264 schema
1265 .fields
1266 .iter()
1267 .map(|field| {
1268 validate_map_key_type(field.data_type())?;
1269 let start = count;
1270 count += compute_leaves(field, &arrow_array::new_empty_array(field.data_type()))?.len();
1271 Ok(start..count)
1272 })
1273 .collect()
1274}
1275
1276#[derive(Debug)]
1283struct ArrowRowGroupWriter {
1284 writers: Vec<ArrowColumnWriter>,
1285 schema: SchemaRef,
1286 buffered_rows: usize,
1287}
1288
1289impl ArrowRowGroupWriter {
1290 fn new(writers: Vec<ArrowColumnWriter>, arrow: &SchemaRef) -> Self {
1291 Self {
1292 writers,
1293 schema: arrow.clone(),
1294 buffered_rows: 0,
1295 }
1296 }
1297
1298 fn write(&mut self, batch: &RecordBatch) -> Result<()> {
1299 let ranges = self.validate_batch_shape(batch, None)?;
1300 self.buffered_rows += batch.num_rows();
1301 for (column_idx, (field, range)) in self.schema.fields.iter().zip(ranges).enumerate() {
1302 let leaves = compute_leaves(field, batch.column(column_idx))?;
1303 Self::validate_leaf_count(field, &range, leaves.len())?;
1304 for (offset, leaf) in leaves.into_iter().enumerate() {
1305 self.writers[range.start + offset].write(&leaf)?;
1306 }
1307 }
1308 Ok(())
1309 }
1310
1311 fn write_with_chunkers(
1312 &mut self,
1313 batch: &RecordBatch,
1314 chunkers: &mut [ContentDefinedChunker],
1315 ) -> Result<()> {
1316 let ranges = self.validate_batch_shape(batch, Some(chunkers.len()))?;
1317 self.buffered_rows += batch.num_rows();
1318 for (column_idx, (field, range)) in self.schema.fields.iter().zip(ranges).enumerate() {
1319 let leaves = compute_leaves(field, batch.column(column_idx))?;
1320 Self::validate_leaf_count(field, &range, leaves.len())?;
1321 for (offset, leaf) in leaves.into_iter().enumerate() {
1322 let index = range.start + offset;
1323 self.writers[index].write_with_chunker(&leaf, &mut chunkers[index])?;
1324 }
1325 }
1326 Ok(())
1327 }
1328
1329 fn validate_batch_shape(
1330 &self,
1331 batch: &RecordBatch,
1332 chunker_count: Option<usize>,
1333 ) -> Result<Vec<Range<usize>>> {
1334 if batch.num_columns() != self.schema.fields.len() {
1335 return Err(ParquetError::ArrowError(format!(
1336 "Incompatible schema: writer has {} top-level fields but batch has {} columns",
1337 self.schema.fields.len(),
1338 batch.num_columns()
1339 )));
1340 }
1341 let ranges = arrow_leaf_ranges(&self.schema)?;
1342 let leaf_count = ranges.last().map_or(0, |r| r.end);
1343 if self.writers.len() != leaf_count {
1344 return Err(ParquetError::General(format!(
1345 "Arrow row-group writer has {} column writers for {} planned leaves",
1346 self.writers.len(),
1347 leaf_count
1348 )));
1349 }
1350 if let Some(actual) = chunker_count
1351 && actual != leaf_count
1352 {
1353 return Err(ParquetError::General(format!(
1354 "content-defined chunking has {actual} chunkers for {leaf_count} planned leaves"
1355 )));
1356 }
1357 Ok(ranges)
1358 }
1359
1360 fn validate_leaf_count(field: &Field, range: &Range<usize>, actual: usize) -> Result<()> {
1361 let expected = range.len();
1362 if actual != expected {
1363 return Err(ParquetError::ArrowError(format!(
1364 "Incompatible schema: field '{}' produced {actual} leaf columns but writer expects {expected}",
1365 field.name()
1366 )));
1367 }
1368 Ok(())
1369 }
1370
1371 fn get_estimated_total_bytes(&self) -> usize {
1373 self.writers
1374 .iter()
1375 .map(|x| x.get_estimated_total_bytes())
1376 .sum()
1377 }
1378
1379 fn close(self) -> Result<Vec<ArrowColumnChunk>> {
1380 self.writers
1381 .into_iter()
1382 .map(|writer| writer.close())
1383 .collect()
1384 }
1385}
1386
1387#[derive(Debug)]
1392pub struct ArrowRowGroupWriterFactory {
1393 schema: SchemaDescPtr,
1394 arrow_schema: SchemaRef,
1395 props: WriterPropertiesPtr,
1396 page_store_factory: Arc<dyn PageStoreFactory>,
1397 #[cfg(feature = "encryption")]
1398 file_encryptor: Option<Arc<FileEncryptor>>,
1399}
1400
1401impl ArrowRowGroupWriterFactory {
1402 pub fn new<W: Write + Send>(
1404 file_writer: &SerializedFileWriter<W>,
1405 arrow_schema: SchemaRef,
1406 ) -> Self {
1407 let schema = Arc::clone(file_writer.schema_descr_ptr());
1408 let props = Arc::clone(file_writer.properties());
1409 Self {
1410 schema,
1411 arrow_schema,
1412 props,
1413 page_store_factory: Arc::new(InMemoryPageStoreFactory),
1414 #[cfg(feature = "encryption")]
1415 file_encryptor: file_writer.file_encryptor(),
1416 }
1417 }
1418
1419 pub fn with_page_store_factory(
1423 mut self,
1424 page_store_factory: Arc<dyn PageStoreFactory>,
1425 ) -> Self {
1426 self.page_store_factory = page_store_factory;
1427 self
1428 }
1429
1430 fn create_row_group_writer(&self, row_group_index: usize) -> Result<ArrowRowGroupWriter> {
1431 let writers = self.create_column_writers(row_group_index)?;
1432 Ok(ArrowRowGroupWriter::new(writers, &self.arrow_schema))
1433 }
1434
1435 pub fn create_column_writers(&self, row_group_index: usize) -> Result<Vec<ArrowColumnWriter>> {
1437 let ranges = arrow_leaf_ranges(&self.arrow_schema)?;
1438 let leaf_count = ranges.last().map_or(0, |r| r.end);
1439 if leaf_count != self.schema.num_columns() {
1440 return Err(ParquetError::General(format!(
1441 "Arrow schema maps to {} leaf columns but Parquet schema has {}",
1442 leaf_count,
1443 self.schema.num_columns()
1444 )));
1445 }
1446 let mut writers = Vec::with_capacity(self.arrow_schema.fields.len());
1447 let mut leaves = self.schema.columns().iter();
1448 let column_factory = self.column_writer_factory(row_group_index);
1449 for field in &self.arrow_schema.fields {
1450 column_factory.get_arrow_column_writer(
1451 field.data_type(),
1452 &self.props,
1453 &mut leaves,
1454 &mut writers,
1455 )?;
1456 }
1457 Ok(writers)
1458 }
1459
1460 #[cfg(feature = "encryption")]
1461 fn column_writer_factory(&self, row_group_idx: usize) -> ArrowColumnWriterFactory {
1462 ArrowColumnWriterFactory::new()
1463 .with_page_store_factory(self.page_store_factory.clone())
1464 .with_file_encryptor(row_group_idx, self.file_encryptor.clone())
1465 }
1466
1467 #[cfg(not(feature = "encryption"))]
1468 fn column_writer_factory(&self, _row_group_idx: usize) -> ArrowColumnWriterFactory {
1469 ArrowColumnWriterFactory::new().with_page_store_factory(self.page_store_factory.clone())
1470 }
1471}
1472
1473struct ArrowColumnWriterFactory {
1475 page_store_factory: Arc<dyn PageStoreFactory>,
1477 #[cfg(feature = "encryption")]
1478 row_group_index: usize,
1479 #[cfg(feature = "encryption")]
1480 file_encryptor: Option<Arc<FileEncryptor>>,
1481}
1482
1483impl ArrowColumnWriterFactory {
1484 pub fn new() -> Self {
1485 Self {
1486 page_store_factory: Arc::new(InMemoryPageStoreFactory),
1487 #[cfg(feature = "encryption")]
1488 row_group_index: 0,
1489 #[cfg(feature = "encryption")]
1490 file_encryptor: None,
1491 }
1492 }
1493
1494 pub fn with_page_store_factory(
1496 mut self,
1497 page_store_factory: Arc<dyn PageStoreFactory>,
1498 ) -> Self {
1499 self.page_store_factory = page_store_factory;
1500 self
1501 }
1502
1503 #[cfg(feature = "encryption")]
1504 pub fn with_file_encryptor(
1505 mut self,
1506 row_group_index: usize,
1507 file_encryptor: Option<Arc<FileEncryptor>>,
1508 ) -> Self {
1509 self.row_group_index = row_group_index;
1510 self.file_encryptor = file_encryptor;
1511 self
1512 }
1513
1514 #[cfg(feature = "encryption")]
1515 fn create_page_writer(
1516 &self,
1517 column_descriptor: &ColumnDescPtr,
1518 column_index: usize,
1519 ) -> Result<Box<ArrowPageWriter>> {
1520 let column_path = column_descriptor.path().string();
1521 let page_encryptor = PageEncryptor::create_if_column_encrypted(
1522 self.file_encryptor.as_ref(),
1523 self.row_group_index,
1524 column_index,
1525 &column_path,
1526 )?;
1527 let args = PageStoreArgs::new(column_index, column_descriptor);
1528 let store = self.page_store_factory.create(&args)?;
1529 Ok(Box::new(
1530 ArrowPageWriter::new(store).with_encryptor(page_encryptor),
1531 ))
1532 }
1533
1534 #[cfg(not(feature = "encryption"))]
1535 fn create_page_writer(
1536 &self,
1537 column_descriptor: &ColumnDescPtr,
1538 column_index: usize,
1539 ) -> Result<Box<ArrowPageWriter>> {
1540 let args = PageStoreArgs::new(column_index, column_descriptor);
1541 let store = self.page_store_factory.create(&args)?;
1542 Ok(Box::new(ArrowPageWriter::new(store)))
1543 }
1544
1545 fn get_arrow_column_writer(
1548 &self,
1549 data_type: &ArrowDataType,
1550 props: &WriterPropertiesPtr,
1551 leaves: &mut Iter<'_, ColumnDescPtr>,
1552 out: &mut Vec<ArrowColumnWriter>,
1553 ) -> Result<()> {
1554 let write_distinct_values = props.write_row_group_number_distinct_values();
1555
1556 let col = |desc: &ColumnDescPtr| -> Result<ArrowColumnWriter> {
1558 let page_writer = self.create_page_writer(desc, out.len())?;
1559 let chunk = page_writer.buffer.clone();
1560 let writer = get_column_writer(desc.clone(), props.clone(), page_writer);
1561 Ok(ArrowColumnWriter {
1562 chunk,
1563 writer: ArrowColumnWriterImpl::Column(writer),
1564 distinct_values_seen: write_distinct_values.then(HashSet::new),
1565 })
1566 };
1567
1568 let bytes = |desc: &ColumnDescPtr| -> Result<ArrowColumnWriter> {
1570 let page_writer = self.create_page_writer(desc, out.len())?;
1571 let chunk = page_writer.buffer.clone();
1572 let writer = GenericColumnWriter::new(desc.clone(), props.clone(), page_writer);
1573 Ok(ArrowColumnWriter {
1574 chunk,
1575 writer: ArrowColumnWriterImpl::ByteArray(writer),
1576 distinct_values_seen: write_distinct_values.then(HashSet::new),
1577 })
1578 };
1579
1580 match data_type {
1581 _ if data_type.is_primitive() => out.push(col(leaves.next().unwrap())?),
1582 ArrowDataType::FixedSizeBinary(_) | ArrowDataType::Boolean | ArrowDataType::Null => {
1583 out.push(col(leaves.next().unwrap())?)
1584 }
1585 ArrowDataType::LargeBinary
1586 | ArrowDataType::Binary
1587 | ArrowDataType::Utf8
1588 | ArrowDataType::LargeUtf8
1589 | ArrowDataType::BinaryView
1590 | ArrowDataType::Utf8View => out.push(bytes(leaves.next().unwrap())?),
1591 ArrowDataType::List(f)
1592 | ArrowDataType::LargeList(f)
1593 | ArrowDataType::FixedSizeList(f, _)
1594 | ArrowDataType::ListView(f)
1595 | ArrowDataType::LargeListView(f) => {
1596 self.get_arrow_column_writer(f.data_type(), props, leaves, out)?
1597 }
1598 ArrowDataType::Struct(fields) => {
1599 for field in fields {
1600 self.get_arrow_column_writer(field.data_type(), props, leaves, out)?
1601 }
1602 }
1603 ArrowDataType::Map(f, _) => match f.data_type() {
1604 ArrowDataType::Struct(f) => {
1605 self.get_arrow_column_writer(f[0].data_type(), props, leaves, out)?;
1606 self.get_arrow_column_writer(f[1].data_type(), props, leaves, out)?
1607 }
1608 _ => unreachable!("invalid map type"),
1609 },
1610 ArrowDataType::Dictionary(_, value_type) => match value_type.as_ref() {
1611 ArrowDataType::Utf8
1612 | ArrowDataType::LargeUtf8
1613 | ArrowDataType::Binary
1614 | ArrowDataType::LargeBinary => out.push(bytes(leaves.next().unwrap())?),
1615 ArrowDataType::Utf8View | ArrowDataType::BinaryView => {
1616 out.push(bytes(leaves.next().unwrap())?)
1617 }
1618 _ => self.get_arrow_column_writer(value_type, props, leaves, out)?,
1619 },
1620 ArrowDataType::RunEndEncoded(_, value_field) => {
1621 self.get_arrow_column_writer(value_field.data_type(), props, leaves, out)?
1622 }
1623 _ => {
1624 return Err(ParquetError::NYI(format!(
1625 "Attempting to write an Arrow type {data_type} to parquet that is not yet implemented"
1626 )));
1627 }
1628 }
1629 Ok(())
1630 }
1631}
1632
1633fn write_leaf(
1634 writer: &mut ColumnWriter<'_>,
1635 column: &dyn arrow_array::Array,
1636 levels: &ArrayLevels,
1637) -> Result<usize> {
1638 let indices = levels.non_null_indices();
1639
1640 match writer {
1641 ColumnWriter::Int32ColumnWriter(typed) => visit_i32(
1643 column,
1644 WritePrimitive {
1645 writer: typed,
1646 levels,
1647 },
1648 ),
1649 ColumnWriter::BoolColumnWriter(typed) => {
1650 let array = column.as_boolean();
1651 let values = get_bool_array_slice(array, indices.iter().copied());
1652 typed.write_batch_internal(
1653 values.as_slice(),
1654 None,
1655 levels.def_level_data().as_ref(),
1656 levels.rep_level_data().as_ref(),
1657 None,
1658 None,
1659 None,
1660 )
1661 }
1662 ColumnWriter::Int64ColumnWriter(typed) => visit_i64(
1663 column,
1664 WritePrimitive {
1665 writer: typed,
1666 levels,
1667 },
1668 ),
1669 ColumnWriter::Int96ColumnWriter(_typed) => {
1670 unreachable!("Currently unreachable because data type not supported")
1671 }
1672 ColumnWriter::FloatColumnWriter(typed) => {
1673 let array = column.as_primitive::<Float32Type>();
1674 write_primitive(typed, array.values(), levels)
1675 }
1676 ColumnWriter::DoubleColumnWriter(typed) => {
1677 let array = column.as_primitive::<Float64Type>();
1678 write_primitive(typed, array.values(), levels)
1679 }
1680 ColumnWriter::ByteArrayColumnWriter(_) => {
1681 unreachable!("should use ByteArrayWriter")
1682 }
1683 ColumnWriter::FixedLenByteArrayColumnWriter(typed) => visit_fixed_len_byte_array(
1684 column,
1685 WriteFixedLenByteArray {
1686 writer: typed,
1687 levels,
1688 },
1689 ),
1690 }
1691}
1692
1693pub(crate) trait PrimitiveVisitor<T> {
1702 type Output;
1703
1704 fn slice(self, values: &[T]) -> Result<Self::Output>;
1706
1707 fn map<S: Copy>(self, values: &[S], convert: impl Fn(S) -> T) -> Result<Self::Output>;
1709}
1710
1711pub(crate) fn visit_i32<V: PrimitiveVisitor<i32>>(
1713 column: &dyn arrow_array::Array,
1714 visitor: V,
1715) -> Result<V::Output> {
1716 match column.data_type() {
1718 ArrowDataType::Null => visitor.slice(Int32Array::new_null(column.len()).values()),
1719 ArrowDataType::Int8 => {
1720 visitor.map(column.as_primitive::<Int8Type>().values(), |x| x as i32)
1721 }
1722 ArrowDataType::Int16 => {
1723 visitor.map(column.as_primitive::<Int16Type>().values(), |x| x as i32)
1724 }
1725 ArrowDataType::Int32 => visitor.slice(column.as_primitive::<Int32Type>().values()),
1726 ArrowDataType::UInt8 => {
1727 visitor.map(column.as_primitive::<UInt8Type>().values(), |x| x as i32)
1728 }
1729 ArrowDataType::UInt16 => {
1730 visitor.map(column.as_primitive::<UInt16Type>().values(), |x| x as i32)
1731 }
1732 ArrowDataType::UInt32 => {
1733 let values = column.as_primitive::<UInt32Type>().values();
1736 visitor.slice(values.inner().typed_data())
1737 }
1738 ArrowDataType::Date32 => visitor.slice(column.as_primitive::<Date32Type>().values()),
1739 ArrowDataType::Time32(TimeUnit::Second) => {
1740 visitor.slice(column.as_primitive::<Time32SecondType>().values())
1741 }
1742 ArrowDataType::Time32(TimeUnit::Millisecond) => {
1743 visitor.slice(column.as_primitive::<Time32MillisecondType>().values())
1744 }
1745 ArrowDataType::Date64 => {
1746 visitor.map(column.as_primitive::<Date64Type>().values(), |x| {
1748 (x / 86_400_000) as i32
1749 })
1750 }
1751 ArrowDataType::Decimal32(_, _) => {
1752 visitor.slice(column.as_primitive::<Decimal32Type>().values())
1753 }
1754 ArrowDataType::Decimal64(_, _) => {
1755 visitor.map(column.as_primitive::<Decimal64Type>().values(), |v| {
1757 v as i32
1758 })
1759 }
1760 ArrowDataType::Decimal128(_, _) => {
1761 visitor.map(column.as_primitive::<Decimal128Type>().values(), |v| {
1763 v as i32
1764 })
1765 }
1766 ArrowDataType::Decimal256(_, _) => {
1767 visitor.map(column.as_primitive::<Decimal256Type>().values(), |v| {
1769 v.as_i128() as i32
1770 })
1771 }
1772 d => Err(ParquetError::General(format!("Cannot coerce {d} to I32"))),
1773 }
1774}
1775
1776pub(crate) fn visit_i64<V: PrimitiveVisitor<i64>>(
1778 column: &dyn arrow_array::Array,
1779 visitor: V,
1780) -> Result<V::Output> {
1781 match column.data_type() {
1783 ArrowDataType::Date64 => visitor.slice(column.as_primitive::<Date64Type>().values()),
1784 ArrowDataType::Int64 => visitor.slice(column.as_primitive::<Int64Type>().values()),
1785 ArrowDataType::UInt64 => {
1786 let values = column.as_primitive::<UInt64Type>().values();
1789 visitor.slice(values.inner().typed_data())
1790 }
1791 ArrowDataType::Time64(TimeUnit::Microsecond) => {
1792 visitor.slice(column.as_primitive::<Time64MicrosecondType>().values())
1793 }
1794 ArrowDataType::Time64(TimeUnit::Nanosecond) => {
1795 visitor.slice(column.as_primitive::<Time64NanosecondType>().values())
1796 }
1797 ArrowDataType::Timestamp(unit, _) => match unit {
1798 TimeUnit::Second => {
1799 visitor.slice(column.as_primitive::<TimestampSecondType>().values())
1800 }
1801 TimeUnit::Millisecond => {
1802 visitor.slice(column.as_primitive::<TimestampMillisecondType>().values())
1803 }
1804 TimeUnit::Microsecond => {
1805 visitor.slice(column.as_primitive::<TimestampMicrosecondType>().values())
1806 }
1807 TimeUnit::Nanosecond => {
1808 visitor.slice(column.as_primitive::<TimestampNanosecondType>().values())
1809 }
1810 },
1811 ArrowDataType::Duration(unit) => match unit {
1812 TimeUnit::Second => visitor.slice(column.as_primitive::<DurationSecondType>().values()),
1813 TimeUnit::Millisecond => {
1814 visitor.slice(column.as_primitive::<DurationMillisecondType>().values())
1815 }
1816 TimeUnit::Microsecond => {
1817 visitor.slice(column.as_primitive::<DurationMicrosecondType>().values())
1818 }
1819 TimeUnit::Nanosecond => {
1820 visitor.slice(column.as_primitive::<DurationNanosecondType>().values())
1821 }
1822 },
1823 ArrowDataType::Decimal64(_, _) => {
1824 visitor.slice(column.as_primitive::<Decimal64Type>().values())
1825 }
1826 ArrowDataType::Decimal128(_, _) => {
1827 visitor.map(column.as_primitive::<Decimal128Type>().values(), |v| {
1829 v as i64
1830 })
1831 }
1832 ArrowDataType::Decimal256(_, _) => {
1833 visitor.map(column.as_primitive::<Decimal256Type>().values(), |v| {
1835 v.as_i128() as i64
1836 })
1837 }
1838 d => Err(ParquetError::General(format!("Cannot coerce {d} to I64"))),
1839 }
1840}
1841
1842struct WritePrimitive<'a, 'b, E: ColumnValueEncoder> {
1844 writer: &'a mut GenericColumnWriter<'b, E>,
1845 levels: &'a ArrayLevels,
1846}
1847
1848impl<T, E> PrimitiveVisitor<T> for WritePrimitive<'_, '_, E>
1849where
1850 E: ColumnValueEncoder<Values = [T]>,
1851{
1852 type Output = usize;
1853
1854 fn slice(self, values: &[T]) -> Result<usize> {
1855 write_primitive(self.writer, values, self.levels)
1856 }
1857
1858 fn map<S: Copy>(self, values: &[S], convert: impl Fn(S) -> T) -> Result<usize> {
1859 let values: Vec<T> = values.iter().map(|v| convert(*v)).collect();
1860 write_primitive(self.writer, &values, self.levels)
1861 }
1862}
1863
1864pub(crate) trait FixedLenByteArrayVisitor {
1870 type Output;
1871
1872 fn decimal_length(&self, precision: u8, native_length: usize) -> usize;
1877
1878 fn values(self, length: usize, fill: impl Fn(usize, &mut [u8])) -> Result<Self::Output>;
1883}
1884
1885pub(crate) fn visit_fixed_len_byte_array<V: FixedLenByteArrayVisitor>(
1888 column: &dyn arrow_array::Array,
1889 visitor: V,
1890) -> Result<V::Output> {
1891 match column.data_type() {
1892 ArrowDataType::Interval(interval_unit) => match interval_unit {
1893 IntervalUnit::YearMonth => {
1894 let array = column.as_primitive::<IntervalYearMonthType>();
1897 visitor.values(12, |i, value| {
1898 value[0..4].copy_from_slice(&array.value(i).to_le_bytes());
1899 })
1900 }
1901 IntervalUnit::DayTime => {
1902 let array = column.as_primitive::<IntervalDayTimeType>();
1906 visitor.values(12, |i, value| {
1907 let interval = array.value(i);
1908 value[4..8].copy_from_slice(&interval.days.to_le_bytes());
1909 value[8..12].copy_from_slice(&interval.milliseconds.to_le_bytes());
1910 })
1911 }
1912 IntervalUnit::MonthDayNano => Err(ParquetError::NYI(format!(
1913 "Attempting to write an Arrow interval type {interval_unit:?} to parquet that is not yet implemented"
1914 ))),
1915 },
1916 ArrowDataType::FixedSizeBinary(_) => {
1917 let array = column.as_fixed_size_binary();
1918 visitor.values(array.value_size(), |i, value| {
1919 value.copy_from_slice(array.value(i));
1920 })
1921 }
1922 ArrowDataType::Decimal32(_, _) => {
1923 visit_decimal(column.as_primitive::<Decimal32Type>(), visitor)
1924 }
1925 ArrowDataType::Decimal64(_, _) => {
1926 visit_decimal(column.as_primitive::<Decimal64Type>(), visitor)
1927 }
1928 ArrowDataType::Decimal128(_, _) => {
1929 visit_decimal(column.as_primitive::<Decimal128Type>(), visitor)
1930 }
1931 ArrowDataType::Decimal256(_, _) => {
1932 visit_decimal(column.as_primitive::<Decimal256Type>(), visitor)
1933 }
1934 ArrowDataType::Float16 => {
1935 let array = column.as_primitive::<Float16Type>();
1936 visitor.values(2, |i, value| {
1937 value.copy_from_slice(&array.value(i).to_le_bytes());
1938 })
1939 }
1940 _ => Err(ParquetError::NYI(
1941 "Attempting to write an Arrow type that is not yet implemented".to_string(),
1942 )),
1943 }
1944}
1945
1946fn visit_decimal<T: NativeDecimalType, V: FixedLenByteArrayVisitor>(
1953 array: &PrimitiveArray<T>,
1954 visitor: V,
1955) -> Result<V::Output> {
1956 let length = visitor.decimal_length(array.precision(), T::BYTE_LENGTH);
1957 let be_bytes = |i| T::to_be_bytes(array.value(i));
1958 if length == T::BYTE_LENGTH {
1959 visitor.values(length, |i, value| {
1961 value.copy_from_slice(be_bytes(i).as_ref());
1962 })
1963 } else {
1964 visitor.values(length, |i, value| {
1965 value.copy_from_slice(&be_bytes(i).as_ref()[T::BYTE_LENGTH - length..]);
1966 })
1967 }
1968}
1969
1970struct WriteFixedLenByteArray<'a, 'b> {
1972 writer: &'a mut ColumnWriterImpl<'b, FixedLenByteArrayType>,
1973 levels: &'a ArrayLevels,
1974}
1975
1976impl FixedLenByteArrayVisitor for WriteFixedLenByteArray<'_, '_> {
1977 type Output = usize;
1978
1979 fn decimal_length(&self, precision: u8, native_length: usize) -> usize {
1980 let length = decimal_length_from_precision(precision);
1981 assert!(length <= native_length);
1983 length
1984 }
1985
1986 fn values(self, length: usize, fill: impl Fn(usize, &mut [u8])) -> Result<usize> {
1987 let indices = self.levels.non_null_indices().iter().copied();
1988 let values = chunk_array_slice(length, indices, fill);
1989 self.writer.write_batch_internal(
1990 values.as_slice(),
1991 None,
1992 self.levels.def_level_data().as_ref(),
1993 self.levels.rep_level_data().as_ref(),
1994 None,
1995 None,
1996 None,
1997 )
1998 }
1999}
2000
2001fn write_primitive<E: ColumnValueEncoder>(
2002 writer: &mut GenericColumnWriter<E>,
2003 values: &E::Values,
2004 levels: &ArrayLevels,
2005) -> Result<usize> {
2006 writer.write_batch_internal(
2007 values,
2008 Some(levels.non_null_indices()),
2009 levels.def_level_data().as_ref(),
2010 levels.rep_level_data().as_ref(),
2011 None,
2012 None,
2013 None,
2014 )
2015}
2016
2017fn get_bool_array_slice(
2018 array: &arrow_array::BooleanArray,
2019 indices: impl ExactSizeIterator<Item = usize>,
2020) -> Vec<bool> {
2021 let mut values = Vec::with_capacity(indices.len());
2022 for i in indices {
2023 values.push(array.value(i))
2024 }
2025 values
2026}
2027
2028trait NativeDecimalType: DecimalType {
2029 type NativeBytes: AsRef<[u8]>;
2030
2031 fn to_be_bytes(value: Self::Native) -> Self::NativeBytes;
2032}
2033impl NativeDecimalType for Decimal32Type {
2034 type NativeBytes = [u8; Self::BYTE_LENGTH];
2035
2036 fn to_be_bytes(value: Self::Native) -> Self::NativeBytes {
2037 value.to_be_bytes()
2038 }
2039}
2040impl NativeDecimalType for Decimal64Type {
2041 type NativeBytes = [u8; Self::BYTE_LENGTH];
2042
2043 fn to_be_bytes(value: Self::Native) -> Self::NativeBytes {
2044 value.to_be_bytes()
2045 }
2046}
2047impl NativeDecimalType for Decimal128Type {
2048 type NativeBytes = [u8; Self::BYTE_LENGTH];
2049
2050 fn to_be_bytes(value: Self::Native) -> Self::NativeBytes {
2051 value.to_be_bytes()
2052 }
2053}
2054impl NativeDecimalType for Decimal256Type {
2055 type NativeBytes = [u8; Self::BYTE_LENGTH];
2056
2057 fn to_be_bytes(value: Self::Native) -> Self::NativeBytes {
2058 value.to_be_bytes()
2059 }
2060}
2061
2062#[inline]
2063fn chunk_array_slice(
2064 chunk_size: usize,
2065 indices: impl ExactSizeIterator<Item = usize>,
2066 writer: impl Fn(usize, &mut [u8]),
2067) -> Vec<FixedLenByteArray> {
2068 let capacity = indices.len() * chunk_size;
2069 let mut arena = vec![0; capacity];
2072 for (i, chunk) in indices.zip(arena.chunks_exact_mut(chunk_size)) {
2073 writer(i, chunk);
2074 }
2075 chunk_contiguous_vec(arena, chunk_size)
2076}
2077
2078fn chunk_contiguous_vec(arena: Vec<u8>, chunk_size: usize) -> Vec<FixedLenByteArray> {
2079 let mut values = Vec::with_capacity(arena.len() / chunk_size);
2080 let mut arena = Bytes::from(arena);
2081 while arena.len() >= chunk_size {
2082 let slice = arena.split_to(chunk_size);
2083 values.push(FixedLenByteArray::from(ByteArray::from(slice)));
2084 }
2085 values
2086}
2087
2088#[inline]
2090fn hash_bytes(bytes: &[u8]) -> u64 {
2091 twox_hash::XxHash64::oneshot(0, bytes)
2092}
2093
2094fn fixed_byte_width(dt: &ArrowDataType) -> Option<usize> {
2096 use ArrowDataType::*;
2097 match dt {
2098 Int8 | UInt8 => Some(1),
2099 Int16 | UInt16 | Float16 => Some(2),
2100 Int32 | UInt32 | Float32 | Date32 | Time32(_) | Decimal32(_, _) => Some(4),
2101 Int64
2102 | UInt64
2103 | Float64
2104 | Date64
2105 | Time64(_)
2106 | Timestamp(_, _)
2107 | Duration(_)
2108 | Decimal64(_, _) => Some(8),
2109 Interval(IntervalUnit::YearMonth) => Some(4),
2110 Interval(IntervalUnit::DayTime) => Some(8),
2111 Interval(IntervalUnit::MonthDayNano) => Some(16),
2112 Decimal128(_, _) => Some(16),
2113 Decimal256(_, _) => Some(32),
2114 _ => None,
2115 }
2116}
2117
2118fn update_distinct_values_seen(
2124 array: &dyn arrow_array::Array,
2125 non_null_indices: &[usize],
2126 seen: &mut DistinctValuesSet,
2127) {
2128 let data = array.to_data();
2129 let offset = data.offset();
2130
2131 match array.data_type() {
2132 ArrowDataType::Boolean => {
2133 let arr = array
2134 .as_any()
2135 .downcast_ref::<arrow_array::BooleanArray>()
2136 .unwrap();
2137 for &row in non_null_indices {
2138 seen.insert(arr.value(row) as u64);
2139 }
2140 }
2141 ArrowDataType::Utf8 | ArrowDataType::Binary => {
2142 let offsets = data.buffers()[0].typed_data::<i32>();
2143 let values = data.buffers()[1].as_slice();
2144 for &row in non_null_indices {
2145 let start = offsets[offset + row] as usize;
2146 let end = offsets[offset + row + 1] as usize;
2147 seen.insert(hash_bytes(&values[start..end]));
2148 }
2149 }
2150 ArrowDataType::LargeUtf8 | ArrowDataType::LargeBinary => {
2151 let offsets = data.buffers()[0].typed_data::<i64>();
2152 let values = data.buffers()[1].as_slice();
2153 for &row in non_null_indices {
2154 let start = offsets[offset + row] as usize;
2155 let end = offsets[offset + row + 1] as usize;
2156 seen.insert(hash_bytes(&values[start..end]));
2157 }
2158 }
2159 ArrowDataType::FixedSizeBinary(byte_width) => {
2160 let byte_width = *byte_width as usize;
2161 let buffer = data.buffers()[0].as_slice();
2162 for &row in non_null_indices {
2163 let start = (offset + row) * byte_width;
2164 seen.insert(hash_bytes(&buffer[start..start + byte_width]));
2165 }
2166 }
2167 ArrowDataType::Utf8View => {
2168 let string_view_array = array.as_string_view();
2169 for &row in non_null_indices {
2170 seen.insert(hash_bytes(string_view_array.value(row).as_bytes()));
2171 }
2172 }
2173 ArrowDataType::BinaryView => {
2174 let binary_view_array = array.as_binary_view();
2175 for &row in non_null_indices {
2176 seen.insert(hash_bytes(binary_view_array.value(row)));
2177 }
2178 }
2179 data_type => {
2180 if let Some(width) = fixed_byte_width(data_type) {
2181 let buffer = data.buffers()[0].as_slice();
2182 for &row in non_null_indices {
2183 let pos = (offset + row) * width;
2184 seen.insert(hash_bytes(&buffer[pos..pos + width]));
2185 }
2186 }
2187 }
2189 }
2190}
2191
2192#[cfg(test)]
2194use crate as parquet_crate;
2195
2196#[cfg(test)]
2197#[path = "../../../tests/arrow_writer/roundtrip_helpers.rs"]
2198mod roundtrip_helpers;
2199
2200#[cfg(test)]
2201mod tests {
2202 use super::roundtrip_helpers::{
2203 RoundTripTest, SMALL_SIZE, required_and_optional, roundtrip_opts,
2204 roundtrip_opts_with_array_validation,
2205 };
2206 use super::*;
2207 use std::cmp::Ordering;
2208 use std::collections::HashMap;
2209
2210 use std::fs::File;
2211
2212 use crate::arrow::ARROW_SCHEMA_META_KEY;
2213 use crate::arrow::arrow_reader::{ParquetRecordBatchReader, ParquetRecordBatchReaderBuilder};
2214 use crate::column::page::{Page, PageReader};
2215 use crate::file::metadata::thrift::PageHeader;
2216 use crate::file::page_index::column_index::ColumnIndexMetaData;
2217 use crate::file::reader::SerializedPageReader;
2218 use crate::parquet_thrift::{ReadThrift, ThriftSliceInputProtocol};
2219 use crate::schema::types::ColumnPath;
2220 use arrow::datatypes::{DataType, Schema};
2221 use arrow::error::Result as ArrowResult;
2222 use arrow::util::data_gen::create_random_array;
2223 use arrow::util::pretty::pretty_format_batches;
2224 use arrow::{array::*, buffer::Buffer};
2225 use arrow_buffer::{IntervalMonthDayNano, NullBuffer, OffsetBuffer};
2226 use arrow_schema::Fields;
2227 use arrow_select::concat::concat;
2228
2229 use crate::basic::{Encoding, EncodingMask};
2230 use crate::data_type::AsBytes;
2231 use crate::file::metadata::{ColumnChunkMetaData, ParquetMetaData, ParquetMetaDataReader};
2232 use crate::file::properties::{
2233 BloomFilterPosition, EnabledStatistics, ReaderProperties, WriterVersion,
2234 };
2235 use crate::file::serialized_reader::ReadOptionsBuilder;
2236 use crate::file::{
2237 reader::{FileReader, SerializedFileReader},
2238 statistics::Statistics,
2239 };
2240
2241 #[derive(Debug, Default)]
2246 struct RecordingPageStore {
2247 next: u64,
2248 blobs: HashMap<u64, Bytes>,
2249 puts: Arc<std::sync::atomic::AtomicUsize>,
2250 }
2251
2252 impl PageStore for RecordingPageStore {
2253 fn put(&mut self, value: Bytes) -> Result<PageKey> {
2254 let id = 100 + self.next * 7;
2256 self.next += 1;
2257 self.puts.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2258 self.blobs.insert(id, value);
2259 Ok(PageKey::new(id))
2260 }
2261
2262 fn take(&mut self, key: PageKey) -> Result<Bytes> {
2263 self.blobs
2264 .remove(&key.get())
2265 .ok_or_else(|| ParquetError::General(format!("missing key {}", key.get())))
2266 }
2267 }
2268
2269 #[derive(Debug)]
2270 struct RecordingPageStoreFactory {
2271 puts: Arc<std::sync::atomic::AtomicUsize>,
2272 }
2273
2274 impl PageStoreFactory for RecordingPageStoreFactory {
2275 fn create(&self, _args: &PageStoreArgs<'_>) -> Result<Box<dyn PageStore>> {
2276 Ok(Box::new(RecordingPageStore {
2277 puts: self.puts.clone(),
2278 ..Default::default()
2279 }))
2280 }
2281 }
2282
2283 #[test]
2287 fn custom_page_store_is_byte_identical_to_default() {
2288 let schema = Arc::new(Schema::new(vec![
2289 Field::new("i", DataType::Int32, true),
2290 Field::new("s", DataType::Utf8, true),
2292 ]));
2293 let i = Int32Array::from(vec![Some(1), None, Some(3), Some(4), Some(5), Some(6)]);
2294 let s = StringArray::from(vec![
2295 Some("a"),
2296 Some("bb"),
2297 Some("a"),
2298 None,
2299 Some("bb"),
2300 Some("ccc"),
2301 ]);
2302 let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(i), Arc::new(s)]).unwrap();
2303
2304 let props = WriterProperties::builder()
2307 .set_max_row_group_row_count(Some(3))
2308 .build();
2309
2310 let write = |factory: Option<Arc<dyn PageStoreFactory>>| {
2311 let mut buffer = Vec::new();
2312 let mut opts = ArrowWriterOptions::new().with_properties(props.clone());
2313 if let Some(factory) = factory {
2314 opts = opts.with_page_store_factory(factory);
2315 }
2316 let mut writer =
2317 ArrowWriter::try_new_with_options(&mut buffer, schema.clone(), opts).unwrap();
2318 writer.write(&batch).unwrap();
2319 writer.close().unwrap();
2320 buffer
2321 };
2322
2323 let default_bytes = write(None);
2324
2325 let puts = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2326 let custom_bytes = write(Some(Arc::new(RecordingPageStoreFactory {
2327 puts: puts.clone(),
2328 })));
2329
2330 assert!(
2331 puts.load(std::sync::atomic::Ordering::Relaxed) > 0,
2332 "custom PageStore was never written to"
2333 );
2334 assert_eq!(
2335 default_bytes, custom_bytes,
2336 "a custom PageStore must produce byte-identical output to the default"
2337 );
2338 }
2339
2340 #[test]
2346 #[cfg_attr(miri, ignore)] fn dictionary_column_round_trips_with_offset_index_disabled() {
2348 let schema = Arc::new(Schema::new(vec![Field::new("k", DataType::Int32, true)]));
2349
2350 let values: Vec<Option<i32>> = (0..50_000).map(|i| Some(i % 8)).collect();
2353 let array = Int32Array::from(values.clone());
2354 let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(array)]).unwrap();
2355
2356 let props = WriterProperties::builder()
2357 .set_offset_index_disabled(true)
2358 .set_data_page_row_count_limit(4096)
2359 .build();
2360 let opts = ArrowWriterOptions::new().with_properties(props);
2361
2362 let mut buffer = Vec::new();
2363 let mut writer =
2364 ArrowWriter::try_new_with_options(&mut buffer, schema.clone(), opts).unwrap();
2365 writer.write(&batch).unwrap();
2366 writer.close().unwrap();
2367
2368 let reader = ParquetRecordBatchReader::try_new(Bytes::from(buffer), values.len()).unwrap();
2369 let read: Vec<RecordBatch> = reader.collect::<ArrowResult<_>>().unwrap();
2370 let read_values: Vec<Option<i32>> = read
2371 .iter()
2372 .flat_map(|b| b.column(0).as_primitive::<Int32Type>().iter())
2373 .collect();
2374 assert_eq!(read_values, values);
2375 }
2376
2377 #[test]
2382 fn dictionary_page_is_routed_through_the_store() {
2383 #[derive(Debug, Default)]
2385 struct SizeRecordingPageStore {
2386 blobs: Vec<Bytes>,
2387 bytes_put: Arc<std::sync::atomic::AtomicUsize>,
2388 }
2389 impl PageStore for SizeRecordingPageStore {
2390 fn put(&mut self, value: Bytes) -> Result<PageKey> {
2391 self.bytes_put
2392 .fetch_add(value.len(), std::sync::atomic::Ordering::Relaxed);
2393 let key = PageKey::new(self.blobs.len() as u64);
2394 self.blobs.push(value);
2395 Ok(key)
2396 }
2397 fn take(&mut self, key: PageKey) -> Result<Bytes> {
2398 Ok(std::mem::take(&mut self.blobs[key.get() as usize]))
2399 }
2400 }
2401 #[derive(Debug)]
2402 struct Factory {
2403 bytes_put: Arc<std::sync::atomic::AtomicUsize>,
2404 }
2405 impl PageStoreFactory for Factory {
2406 fn create(&self, _args: &PageStoreArgs<'_>) -> Result<Box<dyn PageStore>> {
2407 Ok(Box::new(SizeRecordingPageStore {
2408 bytes_put: self.bytes_put.clone(),
2409 ..Default::default()
2410 }))
2411 }
2412 }
2413
2414 let schema = Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]));
2415 let values: Vec<&str> = (0..2048)
2418 .map(|i| ["alpha", "beta", "gamma", "delta"][i % 4])
2419 .collect();
2420 let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(StringArray::from(values))])
2421 .unwrap();
2422
2423 let bytes_put = Arc::new(std::sync::atomic::AtomicUsize::new(0));
2424 let opts = ArrowWriterOptions::new().with_page_store_factory(Arc::new(Factory {
2425 bytes_put: bytes_put.clone(),
2426 }));
2427
2428 let mut buffer = Vec::new();
2431 let mut writer =
2432 ArrowWriter::try_new_with_options(&mut buffer, schema.clone(), opts).unwrap();
2433 writer.write(&batch).unwrap();
2434 writer.close().unwrap();
2435
2436 let reader = SerializedFileReader::new(Bytes::from(buffer)).unwrap();
2437 let column = reader.metadata().row_group(0).column(0);
2438 assert!(
2439 column.dictionary_page_offset().is_some(),
2440 "expected the column to be dictionary-encoded"
2441 );
2442
2443 assert_eq!(
2447 bytes_put.load(std::sync::atomic::Ordering::Relaxed) as i64,
2448 column.compressed_size(),
2449 "the dictionary page must pass through the store like any other page"
2450 );
2451 }
2452
2453 #[test]
2458 fn dictionary_page_encoding_stats_lists_dictionary_first() {
2459 use crate::basic::PageType;
2460 use crate::file::metadata::ParquetMetaDataOptions;
2461
2462 let schema = Arc::new(Schema::new(vec![Field::new("k", DataType::Int32, true)]));
2465 let values: Vec<Option<i32>> = (0..4096).map(|i| Some(i % 4)).collect();
2466 let array = Int32Array::from(values);
2467 let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(array)]).unwrap();
2468
2469 let mut buffer = Vec::new();
2470 let mut writer = ArrowWriter::try_new(&mut buffer, schema.clone(), None).unwrap();
2471 writer.write(&batch).unwrap();
2472 writer.close().unwrap();
2473 let buffer = Bytes::from(buffer);
2474
2475 let options = ParquetMetaDataOptions::new().with_encoding_stats_as_mask(false);
2479 let metadata = ParquetMetaDataReader::new()
2480 .with_metadata_options(Some(options))
2481 .parse_and_finish(&buffer)
2482 .unwrap();
2483
2484 let col = metadata.row_group(0).column(0);
2485 assert!(
2486 col.dictionary_page_offset().is_some(),
2487 "column should be dictionary encoded"
2488 );
2489 let stats = col
2490 .page_encoding_stats()
2491 .expect("full (non-mask) page encoding stats should be present");
2492
2493 assert_eq!(
2496 stats[0].page_type,
2497 PageType::DICTIONARY_PAGE,
2498 "the DICTIONARY_PAGE encoding stat must come first, got {stats:?}"
2499 );
2500 assert!(
2501 stats[1..]
2502 .iter()
2503 .all(|s| s.page_type == PageType::DATA_PAGE)
2504 );
2505 }
2506
2507 fn get_bytes_after_close(schema: SchemaRef, expected_batch: &RecordBatch) -> Vec<u8> {
2508 let mut buffer = vec![];
2509
2510 let mut writer = ArrowWriter::try_new(&mut buffer, schema, None).unwrap();
2511 writer.write(expected_batch).unwrap();
2512 writer.close().unwrap();
2513
2514 buffer
2515 }
2516
2517 fn get_bytes_by_into_inner(schema: SchemaRef, expected_batch: &RecordBatch) -> Vec<u8> {
2518 let mut writer = ArrowWriter::try_new(Vec::new(), schema, None).unwrap();
2519 writer.write(expected_batch).unwrap();
2520 writer.into_inner().unwrap()
2521 }
2522
2523 #[test]
2524 fn roundtrip_bytes() {
2525 let schema = Arc::new(Schema::new(vec![
2527 Field::new("a", DataType::Int32, false),
2528 Field::new("b", DataType::Int32, true),
2529 ]));
2530
2531 let a = Int32Array::from(vec![1, 2, 3, 4, 5]);
2533 let b = Int32Array::from(vec![Some(1), None, None, Some(4), Some(5)]);
2534
2535 let expected_batch =
2537 RecordBatch::try_new(schema.clone(), vec![Arc::new(a), Arc::new(b)]).unwrap();
2538
2539 for buffer in [
2540 get_bytes_after_close(schema.clone(), &expected_batch),
2541 get_bytes_by_into_inner(schema, &expected_batch),
2542 ] {
2543 let cursor = Bytes::from(buffer);
2544 let mut record_batch_reader = ParquetRecordBatchReader::try_new(cursor, 1024).unwrap();
2545
2546 let actual_batch = record_batch_reader
2547 .next()
2548 .expect("No batch found")
2549 .expect("Unable to get batch");
2550
2551 assert_eq!(expected_batch.schema(), actual_batch.schema());
2552 assert_eq!(expected_batch.num_columns(), actual_batch.num_columns());
2553 assert_eq!(expected_batch.num_rows(), actual_batch.num_rows());
2554 for i in 0..expected_batch.num_columns() {
2555 let expected_data = expected_batch.column(i).to_data();
2556 let actual_data = actual_batch.column(i).to_data();
2557
2558 assert_eq!(expected_data, actual_data);
2559 }
2560 }
2561 }
2562
2563 fn read_column(file: Vec<u8>) -> ArrayRef {
2564 let reader = ParquetRecordBatchReader::try_new(Bytes::from(file), 4096).unwrap();
2565 let batches: Vec<RecordBatch> = reader.map(|b| b.unwrap()).collect();
2566 let arrays: Vec<&dyn Array> = batches.iter().map(|b| b.column(0).as_ref()).collect();
2567 concat(&arrays).unwrap()
2568 }
2569
2570 fn roundtrip_compatible_column(field: Field, col: ArrayRef) -> ArrayRef {
2571 let writer_schema = Arc::new(Schema::new(vec![field]));
2572 let batch_schema = Arc::new(Schema::new(vec![Field::new(
2573 "c",
2574 col.data_type().clone(),
2575 col.logical_null_count() != 0,
2576 )]));
2577 let batch = RecordBatch::try_new(batch_schema, vec![col]).unwrap();
2578 let options = ArrowWriterOptions::new().with_skip_arrow_metadata(true);
2581 let mut file = vec![];
2582 let mut writer =
2583 ArrowWriter::try_new_with_options(&mut file, writer_schema, options).unwrap();
2584 writer.write(&batch).unwrap();
2585 writer.close().unwrap();
2586 read_column(file)
2587 }
2588
2589 fn assert_compatible_roundtrip(field: Field, actual: ArrayRef, expected: &ArrayRef) {
2590 assert!(!compute_leaves(&field, &actual).unwrap().is_empty());
2591 assert_eq!(
2592 roundtrip_compatible_column(field, actual).as_ref(),
2593 expected.as_ref()
2594 );
2595 }
2596
2597 fn run_end_type(value_type: DataType, nullable_values: bool) -> DataType {
2598 DataType::RunEndEncoded(
2599 Arc::new(Field::new(
2600 Field::REE_RUN_ENDS_FIELD_DEFAULT_NAME,
2601 DataType::Int32,
2602 false,
2603 )),
2604 Arc::new(Field::new(
2605 Field::REE_VALUES_FIELD_DEFAULT_NAME,
2606 value_type,
2607 nullable_values,
2608 )),
2609 )
2610 }
2611
2612 #[test]
2613 fn arrow_writer_required_ree_schema_rejects_nullable_dense_input() {
2614 let writer_schema = Arc::new(Schema::new(vec![Field::new(
2615 "c",
2616 run_end_type(DataType::Int32, false),
2617 false,
2618 )]));
2619 let batch_schema = Arc::new(Schema::new(vec![Field::new("c", DataType::Int32, true)]));
2620 let batch = RecordBatch::try_new(
2621 batch_schema.clone(),
2622 vec![Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]))],
2623 )
2624 .unwrap();
2625
2626 for skip_metadata in [false, true] {
2627 for cdc in [false, true] {
2628 let props = WriterProperties::builder()
2629 .set_content_defined_chunking(cdc.then(Default::default))
2630 .build();
2631 let options = ArrowWriterOptions::new()
2632 .with_properties(props)
2633 .with_skip_arrow_metadata(skip_metadata);
2634 let mut writer = ArrowWriter::try_new_with_options(
2635 Vec::new(),
2636 writer_schema.clone(),
2637 options.clone(),
2638 )
2639 .unwrap();
2640 let error = writer.write(&batch).unwrap_err().to_string();
2641 assert!(
2642 error.contains("Found null at index 1 for required field 'c'"),
2643 "skip_metadata={skip_metadata}, cdc={cdc}: {error}"
2644 );
2645
2646 let values: ArrayRef = Arc::new(Int32Array::from(vec![7, 8, 9]));
2648 let batch =
2649 RecordBatch::try_new(batch_schema.clone(), vec![values.clone()]).unwrap();
2650 let mut file = vec![];
2651 let mut writer =
2652 ArrowWriter::try_new_with_options(&mut file, writer_schema.clone(), options)
2653 .unwrap();
2654 writer.write(&batch).unwrap();
2655 writer.close().unwrap();
2656 assert_eq!(read_column(file).as_ref(), values.as_ref());
2657 }
2658 }
2659 }
2660
2661 #[test]
2662 fn ree_nulls_respect_dense_schema_nullability() {
2663 let run_ends = Int32Array::from(vec![2, 4, 5]);
2664 let values = Int32Array::from(vec![Some(1), None, Some(2)]);
2665 let ree: ArrayRef = Arc::new(Int32RunArray::try_new(&run_ends, &values).unwrap());
2666 let ree_batch = RecordBatch::try_new(
2667 Arc::new(Schema::new(vec![Field::new(
2668 "c",
2669 ree.data_type().clone(),
2670 true,
2671 )])),
2672 vec![ree],
2673 )
2674 .unwrap();
2675
2676 let nullable_schema = Arc::new(Schema::new(vec![Field::new("c", DataType::Int32, true)]));
2677 let mut file = vec![];
2678 let mut writer = ArrowWriter::try_new(&mut file, nullable_schema, None).unwrap();
2679 writer.write(&ree_batch).unwrap();
2680 writer.close().unwrap();
2681 assert_eq!(
2682 read_column(file).as_primitive::<Int32Type>(),
2683 &Int32Array::from(vec![Some(1), Some(1), None, None, Some(2)])
2684 );
2685
2686 let required_schema = Arc::new(Schema::new(vec![Field::new("c", DataType::Int32, false)]));
2687 let mut writer = ArrowWriter::try_new(Vec::new(), required_schema, None).unwrap();
2688 let error = writer.write(&ree_batch).unwrap_err().to_string();
2689 assert!(error.contains("Found null") && error.contains("required field 'c'"));
2690 }
2691
2692 #[test]
2693 fn required_null_validation_respects_reachability_and_write_timing() {
2694 let required = Field::new("c", DataType::Int32, false);
2695 let nullable: ArrayRef = Arc::new(Int32Array::from(vec![Some(1), None]));
2696 let leaves = compute_leaves(&required, &nullable).unwrap();
2697 let schema = Arc::new(Schema::new(vec![required.clone()]));
2698 let writer = ArrowWriter::try_new(Vec::new(), schema, None).unwrap();
2699 let (_file, factory) = writer.into_serialized_writer().unwrap();
2700 let mut columns = factory.create_column_writers(0).unwrap();
2701 assert!(
2702 columns[0]
2703 .write(&leaves[0])
2704 .unwrap_err()
2705 .to_string()
2706 .contains("required field 'c'")
2707 );
2708
2709 let values: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None]));
2711 let dict: ArrayRef = Arc::new(DictionaryArray::new(
2712 Int8Array::from(vec![0, 0]),
2713 values.clone(),
2714 ));
2715 assert_eq!(
2716 roundtrip_compatible_column(required.clone(), dict).as_primitive::<Int32Type>(),
2717 &Int32Array::from(vec![7, 7])
2718 );
2719 let dict: ArrayRef = Arc::new(DictionaryArray::new(Int8Array::from(vec![0, 1]), values));
2720 let leaves = compute_leaves(&required, &dict).unwrap();
2721 assert!(leaves[0].0.validate().is_err());
2722 let dict: ArrayRef = Arc::new(DictionaryArray::new(
2723 Int8Array::from(vec![Some(0), None]),
2724 Arc::new(Int32Array::from(vec![7])),
2725 ));
2726 let leaves = compute_leaves(&required, &dict).unwrap();
2727 assert!(leaves[0].0.validate().is_err());
2728
2729 let actual_fields = Fields::from(vec![Field::new("source_child", DataType::Int32, true)]);
2732 let array: ArrayRef = Arc::new(StructArray::new(
2733 actual_fields.clone(),
2734 vec![nullable.clone()],
2735 Some(NullBuffer::from(vec![true, false])),
2736 ));
2737 let unmasked: ArrayRef = Arc::new(StructArray::new(
2738 actual_fields,
2739 vec![nullable.clone()],
2740 None,
2741 ));
2742 for child_type in [DataType::Int32, run_end_type(DataType::Int32, false)] {
2743 let target = Field::new(
2744 "c",
2745 DataType::Struct(Fields::from(vec![Field::new(
2746 "target_child",
2747 child_type,
2748 false,
2749 )])),
2750 true,
2751 );
2752 let leaves = compute_leaves(&target, &unmasked).unwrap();
2753 assert!(leaves[0].0.validate().is_err());
2754 let leaves = compute_leaves(&target, &array).unwrap();
2755 assert!(leaves[0].0.validate().is_ok());
2756 let result = roundtrip_compatible_column(target, array.clone());
2757 assert_eq!(result.as_struct().fields()[0].name(), "target_child");
2758 assert!(result.is_null(1));
2759 }
2760
2761 let values: ArrayRef = Arc::new(Int32Array::from(vec![None, Some(9), None]));
2763 let list: ArrayRef = Arc::new(ListArray::new(
2764 Arc::new(Field::new_list_field(DataType::Int32, true)),
2765 OffsetBuffer::new(vec![1_i32, 2].into()),
2766 values,
2767 None,
2768 ));
2769 let target = Field::new(
2770 "c",
2771 DataType::List(Arc::new(Field::new_list_field(DataType::Int32, false))),
2772 false,
2773 );
2774 assert!(
2775 compute_leaves(&target, &list).unwrap()[0]
2776 .0
2777 .validate()
2778 .is_ok()
2779 );
2780 assert_eq!(
2781 roundtrip_compatible_column(required, nullable.slice(0, 1)).as_primitive::<Int32Type>(),
2782 &Int32Array::from(vec![1])
2783 );
2784 }
2785
2786 fn check_required_list_validation(list_type: fn(Arc<Field>) -> DataType) {
2787 let make_array = |values: Vec<Option<i32>>,
2788 offsets: Vec<i32>,
2789 sizes: Vec<i32>,
2790 nulls: Option<NullBuffer>| {
2791 let child = Arc::new(Field::new("source_child", DataType::Int32, true));
2792 let values: ArrayRef = Arc::new(Int32Array::from(values));
2793 match list_type(child.clone()) {
2794 DataType::List(_) => Arc::new(ListArray::new(
2795 child,
2796 OffsetBuffer::new(offsets.into()),
2797 values,
2798 nulls,
2799 )) as ArrayRef,
2800 DataType::LargeList(_) => Arc::new(LargeListArray::new(
2801 child,
2802 OffsetBuffer::new(
2803 offsets
2804 .into_iter()
2805 .map(i64::from)
2806 .collect::<Vec<_>>()
2807 .into(),
2808 ),
2809 values,
2810 nulls,
2811 )) as ArrayRef,
2812 DataType::ListView(_) => Arc::new(ListViewArray::new(
2813 child,
2814 offsets.into(),
2815 sizes.into(),
2816 values,
2817 nulls,
2818 )) as ArrayRef,
2819 DataType::LargeListView(_) => Arc::new(LargeListViewArray::new(
2820 child,
2821 offsets
2822 .into_iter()
2823 .map(i64::from)
2824 .collect::<Vec<_>>()
2825 .into(),
2826 sizes.into_iter().map(i64::from).collect::<Vec<_>>().into(),
2827 values,
2828 nulls,
2829 )) as ArrayRef,
2830 _ => unreachable!(),
2831 }
2832 };
2833 let is_view = matches!(
2834 list_type(Arc::new(Field::new("item", DataType::Int32, true))),
2835 DataType::ListView(_) | DataType::LargeListView(_)
2836 );
2837 let offsets = |starts: Vec<i32>, end: i32| {
2838 let mut result = starts;
2839 if !is_view {
2840 result.push(end);
2841 }
2842 result
2843 };
2844 let target = Field::new(
2845 "c",
2846 list_type(Arc::new(Field::new("target_child", DataType::Int32, false))),
2847 true,
2848 );
2849 let invalid = make_array(vec![None, Some(7)], offsets(vec![0], 2), vec![2], None);
2850 let valid = make_array(vec![Some(7), Some(8)], offsets(vec![0], 2), vec![2], None);
2851 let masked = make_array(
2852 vec![None, Some(7)],
2853 offsets(vec![0, 1], 2),
2854 vec![1, 1],
2855 Some(NullBuffer::from(vec![false, true])),
2856 );
2857 let unused = make_array(
2858 vec![None, Some(7), None],
2859 offsets(vec![1], 2),
2860 vec![1],
2861 None,
2862 );
2863 let empty = make_array(vec![None], offsets(vec![0], 0), vec![0], None);
2864 let sliced = make_array(
2865 vec![None, Some(7)],
2866 offsets(vec![0, 1], 2),
2867 vec![1, 1],
2868 None,
2869 )
2870 .slice(1, 1);
2871 let schema = Arc::new(Schema::new(vec![target.clone()]));
2872
2873 for (name, array, invalid) in [
2875 ("reachable null", invalid, true),
2876 ("valid required child", valid.clone(), false),
2877 ("null ancestor", masked, false),
2878 ("unused child slots", unused, false),
2879 ("empty list", empty, false),
2880 ("out-of-slice null", sliced, false),
2881 ] {
2882 let leaves = compute_leaves(&target, &array).unwrap();
2883 assert_eq!(leaves.len(), 1);
2884 for cdc in [false, true] {
2885 let props = WriterProperties::builder()
2886 .set_content_defined_chunking(cdc.then(Default::default))
2887 .build();
2888 let mut writer =
2889 ArrowWriter::try_new(Vec::new(), schema.clone(), Some(props)).unwrap();
2890 let mut chunkers = writer.cdc_chunkers.take();
2891 let (_file, factory) = writer.into_serialized_writer().unwrap();
2892 let mut columns = factory.create_column_writers(0).unwrap();
2893 let before = (
2894 columns[0].memory_size(),
2895 columns[0].get_estimated_total_bytes(),
2896 );
2897 let result = match &mut chunkers {
2898 Some(chunkers) => columns[0].write_with_chunker(&leaves[0], &mut chunkers[0]),
2899 None => columns[0].write(&leaves[0]),
2900 };
2901 if invalid {
2902 let error = result.unwrap_err().to_string();
2903 assert!(
2904 error.contains("Found null at index 0 for required field 'target_child'"),
2905 "{name}, cdc={cdc}: {error}"
2906 );
2907 assert_eq!(
2908 before,
2909 (
2910 columns[0].memory_size(),
2911 columns[0].get_estimated_total_bytes()
2912 ),
2913 "invalid leaf must be rejected before encoding"
2914 );
2915 let valid_leaves = compute_leaves(&target, &valid).unwrap();
2917 match &mut chunkers {
2918 Some(chunkers) => columns[0]
2919 .write_with_chunker(&valid_leaves[0], &mut chunkers[0])
2920 .unwrap(),
2921 None => columns[0].write(&valid_leaves[0]).unwrap(),
2922 }
2923 assert_eq!(
2924 columns
2925 .pop()
2926 .unwrap()
2927 .close()
2928 .unwrap()
2929 .close
2930 .metadata
2931 .num_values(),
2932 2
2933 );
2934 } else {
2935 result.unwrap();
2936 columns.pop().unwrap().close().unwrap();
2937 }
2938
2939 let batch = RecordBatch::try_new(
2941 Arc::new(Schema::new(vec![Field::new(
2942 "c",
2943 array.data_type().clone(),
2944 true,
2945 )])),
2946 vec![array.clone()],
2947 )
2948 .unwrap();
2949 let props = WriterProperties::builder()
2950 .set_content_defined_chunking(cdc.then(Default::default))
2951 .build();
2952 let mut writer =
2953 ArrowWriter::try_new(Vec::new(), schema.clone(), Some(props)).unwrap();
2954 let result = writer.write(&batch);
2955 if invalid {
2956 assert!(
2957 result
2958 .unwrap_err()
2959 .to_string()
2960 .contains("required field 'target_child'")
2961 );
2962 } else {
2963 result.unwrap();
2964 writer.close().unwrap();
2965 }
2966 }
2967 }
2968 }
2969
2970 #[test]
2971 fn required_list_validation_preserves_level_generation() {
2972 check_required_list_validation(DataType::List);
2973 }
2974
2975 #[test]
2976 fn required_large_list_validation_preserves_level_generation() {
2977 check_required_list_validation(DataType::LargeList);
2978 }
2979
2980 #[test]
2981 fn required_list_view_validation_preserves_level_generation() {
2982 check_required_list_validation(DataType::ListView);
2983 }
2984
2985 #[test]
2986 fn required_large_list_view_validation_preserves_level_generation() {
2987 check_required_list_validation(DataType::LargeListView);
2988 }
2989
2990 fn nullable_int32_list() -> ArrayRef {
2991 Arc::new(ListArray::new(
2992 Arc::new(Field::new_list_field(DataType::Int32, true)),
2993 OffsetBuffer::new(vec![0_i32, 2, 3].into()),
2994 Arc::new(Int32Array::from(vec![Some(1), None, Some(3)])),
2995 None,
2996 ))
2997 }
2998
2999 #[test]
3000 fn arrow_writer_dense_list_under_ree_schema() {
3001 let list = nullable_int32_list();
3002 let field = Field::new("c", run_end_type(list.data_type().clone(), false), false);
3004 assert_compatible_roundtrip(field, list.clone(), &list);
3005 }
3006
3007 #[test]
3008 fn arrow_writer_dense_list_under_dictionary_schema() {
3009 let list = nullable_int32_list();
3010 let field = Field::new(
3011 "c",
3012 DataType::Dictionary(Box::new(DataType::Int8), Box::new(list.data_type().clone())),
3013 false,
3014 );
3015 assert_compatible_roundtrip(field, list.clone(), &list);
3016 }
3017
3018 #[test]
3019 fn arrow_writer_dense_values_under_nested_ree_schema() {
3020 let dense: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]));
3021 let field = Field::new(
3022 "c",
3023 run_end_type(run_end_type(DataType::Int32, true), false),
3024 false,
3025 );
3026 assert_compatible_roundtrip(field, dense.clone(), &dense);
3027 }
3028
3029 #[test]
3030 fn arrow_writer_dense_values_under_dictionary_ree_schema() {
3031 let dense: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]));
3032 let field = Field::new(
3033 "c",
3034 DataType::Dictionary(
3035 Box::new(DataType::Int8),
3036 Box::new(run_end_type(DataType::Int32, true)),
3037 ),
3038 false,
3039 );
3040 assert_compatible_roundtrip(field, dense.clone(), &dense);
3041 }
3042
3043 #[test]
3044 fn arrow_writer_dense_values_under_ree_dictionary_schema() {
3045 let dense: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]));
3046 let field = Field::new(
3049 "c",
3050 run_end_type(
3051 DataType::Dictionary(Box::new(DataType::Int8), Box::new(DataType::Int32)),
3052 true,
3053 ),
3054 false,
3055 );
3056 assert_compatible_roundtrip(field, dense.clone(), &dense);
3057 }
3058
3059 #[test]
3060 fn arrow_writer_dense_struct_under_ree_child_schema() {
3061 let dense: ArrayRef = Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]));
3062 let struct_fields = Fields::from(vec![Field::new("value", DataType::Int32, true)]);
3063 let dense_struct: ArrayRef = Arc::new(StructArray::new(struct_fields, vec![dense], None));
3064 let field = Field::new(
3066 "c",
3067 DataType::Struct(Fields::from(vec![Field::new(
3068 "value",
3069 run_end_type(DataType::Int32, true),
3070 false,
3071 )])),
3072 false,
3073 );
3074 assert_compatible_roundtrip(field, dense_struct.clone(), &dense_struct);
3075 }
3076
3077 #[test]
3078 fn arrow_writer_dense_list_under_ree_child_schema() {
3079 let list = nullable_int32_list();
3080 let field = Field::new(
3081 "c",
3082 DataType::List(Arc::new(Field::new(
3083 "item",
3084 run_end_type(DataType::Int32, true),
3085 false,
3086 ))),
3087 false,
3088 );
3089 assert_compatible_roundtrip(field, list.clone(), &list);
3090 }
3091
3092 #[test]
3093 fn arrow_writer_dense_list_under_dictionary_child_schema() {
3094 let list = nullable_int32_list();
3095 let field = Field::new(
3096 "c",
3097 DataType::List(Arc::new(Field::new_list_field(
3098 DataType::Dictionary(Box::new(DataType::Int8), Box::new(DataType::Int32)),
3099 true,
3100 ))),
3101 false,
3102 );
3103 assert_compatible_roundtrip(field, list.clone(), &list);
3104 }
3105
3106 #[test]
3107 fn arrow_writer_dictionary_list_under_dense_child_schema() {
3108 let dict: ArrayRef = Arc::new(DictionaryArray::new(
3109 Int8Array::from(vec![Some(0), None, Some(1)]),
3110 Arc::new(Int32Array::from(vec![1, 3])),
3111 ));
3112 let dict_list: ArrayRef = Arc::new(ListArray::new(
3113 Arc::new(Field::new_list_field(dict.data_type().clone(), true)),
3114 OffsetBuffer::new(vec![0_i32, 2, 3].into()),
3115 dict,
3116 None,
3117 ));
3118 let expected = nullable_int32_list();
3119 assert_compatible_roundtrip(
3120 Field::new("c", expected.data_type().clone(), false),
3121 dict_list,
3122 &expected,
3123 );
3124 }
3125
3126 #[test]
3127 fn arrow_writer_dense_map_under_ree_value_schema() {
3128 let key_field = Arc::new(Field::new("keys", DataType::Utf8, false));
3129 let entries = StructArray::new(
3130 Fields::from(vec![
3131 key_field.clone(),
3132 Arc::new(Field::new("values", DataType::Int32, true)),
3133 ]),
3134 vec![
3135 Arc::new(StringArray::from(vec!["a", "b", "c"])) as ArrayRef,
3136 Arc::new(Int32Array::from(vec![Some(7), None, Some(9)])),
3137 ],
3138 None,
3139 );
3140 let map: ArrayRef = Arc::new(MapArray::new(
3141 Arc::new(Field::new("entries", entries.data_type().clone(), false)),
3142 OffsetBuffer::new(vec![0_i32, 2, 3].into()),
3143 entries,
3144 None,
3145 false,
3146 ));
3147 let field = Field::new(
3149 "c",
3150 DataType::Map(
3151 Arc::new(Field::new(
3152 "entries",
3153 DataType::Struct(Fields::from(vec![
3154 key_field,
3155 Arc::new(Field::new(
3156 "values",
3157 run_end_type(DataType::Int32, true),
3158 false,
3159 )),
3160 ])),
3161 false,
3162 )),
3163 false,
3164 ),
3165 false,
3166 );
3167 assert_compatible_roundtrip(field, map.clone(), &map);
3168 }
3169
3170 #[test]
3171 fn arrow_writer_nested_dictionary_metadata_roundtrips_with_default_reader() {
3172 let dense: ArrayRef = Arc::new(StructArray::new(
3173 Fields::from(vec![Field::new("item", DataType::Int32, true)]),
3174 vec![Arc::new(Int32Array::from(vec![Some(7), None, Some(9)]))],
3175 None,
3176 ));
3177 let field = Field::new(
3178 "value",
3179 DataType::Dictionary(
3180 Box::new(DataType::Int8),
3181 Box::new(dense.data_type().clone()),
3182 ),
3183 false,
3184 )
3185 .with_metadata(HashMap::from([(
3186 "description".to_string(),
3187 "nested dictionary".to_string(),
3188 )]));
3189 let writer_schema = Arc::new(Schema::new(vec![field.clone()]));
3190 let expected_schema = Arc::new(Schema::new(vec![
3191 field.with_data_type(dense.data_type().clone()),
3192 ]));
3193 let batch = RecordBatch::try_new(expected_schema.clone(), vec![dense]).unwrap();
3194
3195 let mut file = vec![];
3198 let mut writer = ArrowWriter::try_new(&mut file, writer_schema, None).unwrap();
3199 writer.write(&batch).unwrap();
3200 writer.close().unwrap();
3201
3202 let builder = ParquetRecordBatchReaderBuilder::try_new(Bytes::from(file)).unwrap();
3203 assert!(
3204 builder
3205 .metadata()
3206 .file_metadata()
3207 .key_value_metadata()
3208 .unwrap()
3209 .iter()
3210 .any(|kv| kv.key == ARROW_SCHEMA_META_KEY)
3211 );
3212 assert_eq!(builder.schema(), &expected_schema);
3213 let decoded = builder
3214 .build()
3215 .unwrap()
3216 .collect::<ArrowResult<Vec<_>>>()
3217 .unwrap();
3218 assert_eq!(decoded, vec![batch]);
3219 }
3220
3221 #[test]
3222 fn arrow_writer_rejects_missing_or_extra_top_level_columns_cleanly() {
3223 let one_field = Arc::new(Schema::new(vec![Field::new(
3224 "a",
3225 ArrowDataType::Int32,
3226 false,
3227 )]));
3228 let two_fields = Arc::new(Schema::new(vec![
3229 Field::new("a", ArrowDataType::Int32, false),
3230 Field::new("b", ArrowDataType::Int32, false),
3231 ]));
3232 let one_column = RecordBatch::try_new(
3233 Arc::clone(&one_field),
3234 vec![Arc::new(Int32Array::from(vec![1]))],
3235 )
3236 .unwrap();
3237 let two_columns = RecordBatch::try_new(
3238 Arc::clone(&two_fields),
3239 vec![
3240 Arc::new(Int32Array::from(vec![1])),
3241 Arc::new(Int32Array::from(vec![2])),
3242 ],
3243 )
3244 .unwrap();
3245
3246 let mut writer = ArrowWriter::try_new(Vec::new(), Arc::clone(&two_fields), None).unwrap();
3247 let err = writer.write(&one_column).unwrap_err();
3248 assert!(
3249 err.to_string()
3250 .contains("writer has 2 top-level fields but batch has 1 columns"),
3251 "{err}"
3252 );
3253 assert_eq!(writer.in_progress_rows(), 0);
3254
3255 let mut writer = ArrowWriter::try_new(Vec::new(), Arc::clone(&one_field), None).unwrap();
3256 let err = writer.write(&two_columns).unwrap_err();
3257 assert!(
3258 err.to_string()
3259 .contains("writer has 1 top-level fields but batch has 2 columns"),
3260 "{err}"
3261 );
3262 assert_eq!(writer.in_progress_rows(), 0);
3263 }
3264
3265 #[test]
3266 fn arrow_row_group_factory_rejects_parquet_leaf_count_mismatch_cleanly() {
3267 let one_field = Arc::new(Schema::new(vec![Field::new(
3268 "a",
3269 ArrowDataType::Int32,
3270 false,
3271 )]));
3272 let two_fields = Arc::new(Schema::new(vec![
3273 Field::new("a", ArrowDataType::Int32, false),
3274 Field::new("b", ArrowDataType::Int32, false),
3275 ]));
3276 let parquet = ArrowSchemaConverter::new().convert(&one_field).unwrap();
3277 let props = Arc::new(WriterProperties::default());
3278 let file_writer =
3279 SerializedFileWriter::new(Vec::new(), parquet.root_schema_ptr(), Arc::clone(&props))
3280 .unwrap();
3281
3282 let factory = ArrowRowGroupWriterFactory::new(&file_writer, Arc::clone(&two_fields));
3283 let Err(err) = factory.create_column_writers(0) else {
3284 panic!("mismatched schemas should not create column writers")
3285 };
3286 assert!(
3287 err.to_string()
3288 .contains("Arrow schema maps to 2 leaf columns but Parquet schema has 1"),
3289 "{err}"
3290 );
3291
3292 let parquet = ArrowSchemaConverter::new().convert(&two_fields).unwrap();
3293 let file_writer =
3294 SerializedFileWriter::new(Vec::new(), parquet.root_schema_ptr(), props).unwrap();
3295 let factory = ArrowRowGroupWriterFactory::new(&file_writer, one_field);
3296 let Err(err) = factory.create_column_writers(0) else {
3297 panic!("mismatched schemas should not create column writers")
3298 };
3299 assert!(
3300 err.to_string()
3301 .contains("Arrow schema maps to 1 leaf columns but Parquet schema has 2"),
3302 "{err}"
3303 );
3304 }
3305
3306 #[test]
3307 fn arrow_writer_required_map_keys_reject_only_reachable_nulls() {
3308 let keys: ArrayRef = Arc::new(DictionaryArray::<Int32Type>::new(
3309 Int32Array::from(vec![1, 0, 1]),
3310 Arc::new(Int32Array::from(vec![None, Some(7)])),
3311 ));
3312 let values = Int32Array::from(vec![Some(10), Some(20), None]);
3313 let fields = Fields::from(vec![
3314 Field::new("key", keys.data_type().clone(), false),
3315 Field::new("value", DataType::Int32, true),
3316 ]);
3317 assert!(
3318 StructArray::try_new(
3319 fields.clone(),
3320 vec![keys.clone(), Arc::new(values.clone())],
3321 None
3322 )
3323 .is_err()
3324 );
3325
3326 let data = ArrayData::builder(DataType::Struct(fields))
3329 .len(3)
3330 .add_child_data(keys.to_data())
3331 .add_child_data(values.to_data())
3332 .build()
3333 .unwrap();
3334 data.validate_full().unwrap();
3335 let entries = StructArray::from(data);
3336 let entries_field = Arc::new(Field::new("entries", entries.data_type().clone(), false));
3337 let offsets = OffsetBuffer::new(vec![0i32, 1, 2, 3].into());
3338 let map = MapArray::try_new(
3339 entries_field.clone(),
3340 offsets.clone(),
3341 entries.clone(),
3342 None,
3343 false,
3344 )
3345 .unwrap();
3346 assert_eq!(map.keys().logical_null_count(), 1);
3347 let masked = MapArray::try_new(
3348 entries_field,
3349 offsets,
3350 entries,
3351 Some(NullBuffer::from(vec![true, false, true])),
3352 false,
3353 )
3354 .unwrap();
3355 let writer_schema = Arc::new(Schema::new(vec![Field::new(
3356 "map",
3357 DataType::Map(
3358 Arc::new(Field::new(
3359 "entries",
3360 DataType::Struct(
3361 vec![
3362 Field::new("key", DataType::Int32, false),
3363 Field::new("value", DataType::Int32, true),
3364 ]
3365 .into(),
3366 ),
3367 false,
3368 )),
3369 false,
3370 ),
3371 true,
3372 )]));
3373 for cdc in [false, true] {
3374 let props = WriterProperties::builder()
3375 .set_content_defined_chunking(cdc.then(Default::default))
3376 .build();
3377 for (array, expected_keys, expected_null_maps) in [
3378 (map.clone(), None, 0),
3379 (map.slice(0, 1), Some(1), 0),
3380 (map.slice(2, 1), Some(1), 0),
3381 (masked.clone(), Some(2), 1),
3382 ] {
3383 let schema = Arc::new(Schema::new(vec![Field::new(
3384 "map",
3385 array.data_type().clone(),
3386 true,
3387 )]));
3388 let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap();
3389 let mut bytes = Vec::new();
3390 let mut writer =
3391 ArrowWriter::try_new(&mut bytes, writer_schema.clone(), Some(props.clone()))
3392 .unwrap();
3393 let result = writer.write(&batch);
3394 let Some(expected_keys) = expected_keys else {
3395 let err = result.unwrap_err();
3396 assert!(
3397 err.to_string().contains("Found null")
3398 && err.to_string().contains("required field 'key'"),
3399 "{err}"
3400 );
3401 continue;
3402 };
3403 result.unwrap();
3404 writer.close().unwrap();
3405 let actual = ParquetRecordBatchReader::try_new(Bytes::from(bytes), 1024)
3406 .unwrap()
3407 .next()
3408 .unwrap()
3409 .unwrap();
3410 let actual = actual.column(0).as_map();
3411 assert_eq!(actual.null_count(), expected_null_maps);
3412 assert_eq!(
3413 actual.keys().as_primitive::<Int32Type>().values().as_ref(),
3414 vec![7; expected_keys]
3415 );
3416 }
3417 }
3418 }
3419
3420 #[test]
3421 fn arrow_writer_rejects_nullable_map_key_schemas_early() {
3422 let schema_with_key = |key| {
3423 Arc::new(Schema::new(vec![Field::new(
3424 "map",
3425 DataType::Map(
3426 Arc::new(Field::new(
3427 "entries",
3428 DataType::Struct(
3429 vec![key, Field::new("value", DataType::Int32, true)].into(),
3430 ),
3431 false,
3432 )),
3433 false,
3434 ),
3435 true,
3436 )]))
3437 };
3438 let required = schema_with_key(Field::new("key", DataType::Int32, false));
3439 let parquet = ArrowSchemaConverter::new().convert(&required).unwrap();
3440 let ree = DataType::RunEndEncoded(
3441 Arc::new(Field::new("run_ends", DataType::Int32, false)),
3442 Arc::new(Field::new("values", DataType::Int32, true)),
3443 );
3444 for key in [
3445 Field::new("key", DataType::Int32, true),
3446 Field::new("key", ree.clone(), false),
3447 Field::new(
3448 "key",
3449 DataType::Dictionary(Box::new(DataType::Int8), Box::new(ree)),
3450 false,
3451 ),
3452 ] {
3453 let schema = schema_with_key(key);
3454 for explicit_schema in [false, true] {
3455 for skip_metadata in [false, true] {
3456 let mut options =
3457 ArrowWriterOptions::new().with_skip_arrow_metadata(skip_metadata);
3458 if explicit_schema {
3459 options = options.with_parquet_schema(parquet.clone());
3460 }
3461 let mut bytes = Vec::new();
3462 let err =
3463 ArrowWriter::try_new_with_options(&mut bytes, schema.clone(), options)
3464 .unwrap_err();
3465 assert!(
3466 err.to_string()
3467 .contains("Map key field 'key' must be non-nullable"),
3468 "{err}"
3469 );
3470 assert!(
3471 bytes.is_empty(),
3472 "schema rejection must precede file initialization"
3473 );
3474 }
3475 }
3476 let file = SerializedFileWriter::new(
3477 Vec::new(),
3478 parquet.root_schema_ptr(),
3479 Arc::new(WriterProperties::default()),
3480 )
3481 .unwrap();
3482 let factory = ArrowRowGroupWriterFactory::new(&file, schema);
3483 let Err(err) = factory.create_column_writers(0) else {
3484 panic!("nullable map-key schema should not create column writers")
3485 };
3486 assert!(
3487 err.to_string()
3488 .contains("Map key field 'key' must be non-nullable"),
3489 "{err}"
3490 );
3491 }
3492 }
3493
3494 #[test]
3495 fn eager_row_group_validation_rejects_writer_and_chunker_count_mismatches() {
3496 let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
3497 let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(Int32Array::from(vec![1]))])
3498 .unwrap();
3499 let writer = ArrowWriter::try_new(Vec::new(), schema, None).unwrap();
3500 let (_file, factory) = writer.into_serialized_writer().unwrap();
3501 let mut group = factory.create_row_group_writer(0).unwrap();
3502 assert!(
3503 group
3504 .write_with_chunkers(&batch, &mut [])
3505 .unwrap_err()
3506 .to_string()
3507 .contains("chunkers")
3508 );
3509 assert_eq!(group.buffered_rows, 0);
3510 group.writers.clear();
3511 assert!(
3512 group
3513 .write(&batch)
3514 .unwrap_err()
3515 .to_string()
3516 .contains("column writers")
3517 );
3518 assert_eq!(group.buffered_rows, 0);
3519 }
3520
3521 #[test]
3522 fn arrow_writer_page_size() {
3523 let schema = Arc::new(Schema::new(vec![Field::new("col", DataType::Utf8, false)]));
3524
3525 let mut builder = StringBuilder::with_capacity(100, 329 * 10_000);
3526
3527 for i in 0..10 {
3529 let value = i
3530 .to_string()
3531 .repeat(10)
3532 .chars()
3533 .take(10)
3534 .collect::<String>();
3535
3536 builder.append_value(value);
3537 }
3538
3539 let array = Arc::new(builder.finish());
3540
3541 let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
3542
3543 let file = tempfile::tempfile().unwrap();
3544
3545 let props = WriterProperties::builder()
3547 .set_data_page_size_limit(1)
3548 .set_dictionary_page_size_limit(1)
3549 .set_write_batch_size(1)
3550 .build();
3551
3552 let mut writer =
3553 ArrowWriter::try_new(file.try_clone().unwrap(), batch.schema(), Some(props))
3554 .expect("Unable to write file");
3555 writer.write(&batch).unwrap();
3556 writer.close().unwrap();
3557
3558 let options = ReadOptionsBuilder::new().with_page_index().build();
3559 let reader =
3560 SerializedFileReader::new_with_options(file.try_clone().unwrap(), options).unwrap();
3561
3562 let column = reader.metadata().row_group(0).columns();
3563
3564 assert_eq!(column.len(), 1);
3565
3566 assert!(
3569 column[0].dictionary_page_offset().is_some(),
3570 "Expected a dictionary page"
3571 );
3572
3573 let page_index = reader
3574 .metadata()
3575 .page_index()
3576 .expect("page index should be present");
3577 let page_locations = page_index
3578 .page_locations(0, 0)
3579 .expect("page locations should exist");
3580
3581 assert_eq!(
3584 page_locations.len(),
3585 10,
3586 "Expected 10 pages but got {page_locations:#?}"
3587 );
3588 }
3589
3590 const MEDIUM_SIZE: usize = 63;
3591
3592 fn check_bloom_filter<T: AsBytes>(
3593 files: Vec<Bytes>,
3594 file_column: String,
3595 positive_values: Vec<T>,
3596 negative_values: Vec<T>,
3597 ) {
3598 files.into_iter().take(1).for_each(|file| {
3599 let file_reader = SerializedFileReader::new_with_options(
3600 file,
3601 ReadOptionsBuilder::new()
3602 .with_reader_properties(
3603 ReaderProperties::builder()
3604 .set_read_bloom_filter(true)
3605 .build(),
3606 )
3607 .build(),
3608 )
3609 .expect("Unable to open file as Parquet");
3610 let metadata = file_reader.metadata();
3611
3612 let mut bloom_filters: Vec<_> = vec![];
3614 for (ri, row_group) in metadata.row_groups().iter().enumerate() {
3615 if let Some((column_index, _)) = row_group
3616 .columns()
3617 .iter()
3618 .enumerate()
3619 .find(|(_, column)| column.column_path().string() == file_column)
3620 {
3621 let row_group_reader = file_reader
3622 .get_row_group(ri)
3623 .expect("Unable to read row group");
3624 if let Some(sbbf) = row_group_reader.get_column_bloom_filter(column_index) {
3625 bloom_filters.push(sbbf.clone());
3626 } else {
3627 panic!("No bloom filter for column named {file_column} found");
3628 }
3629 } else {
3630 panic!("No column named {file_column} found");
3631 }
3632 }
3633
3634 positive_values.iter().for_each(|value| {
3635 let found = bloom_filters.iter().find(|sbbf| sbbf.check(value));
3636 assert!(
3637 found.is_some(),
3638 "{}",
3639 format!("Value {:?} should be in bloom filter", value.as_bytes())
3640 );
3641 });
3642
3643 negative_values.iter().for_each(|value| {
3644 let found = bloom_filters.iter().find(|sbbf| sbbf.check(value));
3645 assert!(
3646 found.is_none(),
3647 "{}",
3648 format!("Value {:?} should not be in bloom filter", value.as_bytes())
3649 );
3650 });
3651 });
3652 }
3653
3654 #[test]
3655 #[cfg_attr(miri, ignore)] fn bool_large_single_column() {
3657 let values = Arc::new(
3658 [None, Some(true), Some(false)]
3659 .iter()
3660 .cycle()
3661 .copied()
3662 .take(200_000)
3663 .collect::<BooleanArray>(),
3664 );
3665 let schema = Schema::new(vec![Field::new("col", values.data_type().clone(), true)]);
3666 let expected_batch = RecordBatch::try_new(Arc::new(schema), vec![values]).unwrap();
3667 let file = tempfile::tempfile().unwrap();
3668
3669 let mut writer =
3670 ArrowWriter::try_new(file.try_clone().unwrap(), expected_batch.schema(), None)
3671 .expect("Unable to write file");
3672 writer.write(&expected_batch).unwrap();
3673 writer.close().unwrap();
3674 }
3675
3676 #[test]
3677 fn check_page_offset_index_with_nan() {
3678 let values = Arc::new(Float64Array::from(vec![f64::NAN; 10]));
3679 let schema = Schema::new(vec![Field::new("col", DataType::Float64, true)]);
3680 let batch = RecordBatch::try_new(Arc::new(schema), vec![values]).unwrap();
3681
3682 let mut out = Vec::with_capacity(1024);
3683 let mut writer =
3684 ArrowWriter::try_new(&mut out, batch.schema(), None).expect("Unable to write file");
3685 writer.write(&batch).unwrap();
3686 let file_meta_data = writer.close().unwrap();
3687 for row_group in file_meta_data.row_groups() {
3688 for column in row_group.columns() {
3689 assert!(column.offset_index_offset().is_some());
3690 assert!(column.offset_index_length().is_some());
3691 assert!(column.column_index_offset().is_some());
3692 assert!(column.column_index_length().is_some());
3693 }
3694 }
3695 if let Some(page_index) = file_meta_data.page_index() {
3696 for rg in 0..file_meta_data.num_row_groups() {
3697 for col in 0..file_meta_data.row_group(rg).num_columns() {
3698 let idx = page_index
3699 .column_index(rg, col)
3700 .expect("column index should exist");
3701 assert!(idx.nan_counts().is_some());
3702 let ColumnIndexMetaData::DOUBLE(float_idx) = idx else {
3703 panic!("expected double statistics")
3704 };
3705 for i in 0..idx.num_pages() as usize {
3706 assert_eq!(float_idx.nan_count(i), Some(10));
3707 assert_eq!(
3708 f64::NAN.total_cmp(float_idx.min_value(i).unwrap()),
3709 Ordering::Equal
3710 );
3711 assert_eq!(
3712 f64::NAN.total_cmp(float_idx.max_value(i).unwrap()),
3713 Ordering::Equal
3714 );
3715 }
3716 }
3717 }
3718 } else {
3719 panic!("page index should be present");
3720 }
3721 }
3722
3723 #[test]
3724 fn check_page_offset_index_with_mixed_nan() {
3725 let schema = Arc::new(Schema::new(vec![Field::new(
3726 "col",
3727 DataType::Float64,
3728 true,
3729 )]));
3730
3731 let mut out = Vec::with_capacity(1024);
3732 let props = WriterProperties::builder()
3733 .set_data_page_row_count_limit(10)
3734 .build();
3735 let mut writer = ArrowWriter::try_new(&mut out, schema.clone(), Some(props))
3736 .expect("Unable to write file");
3737
3738 let values = Arc::new(Float64Array::from(vec![f64::NAN; 10]));
3740 let batch = RecordBatch::try_new(schema.clone(), vec![values]).unwrap();
3741 writer.write(&batch).unwrap();
3742
3743 let values = Arc::new(Float64Array::from(vec![-f64::NAN; 10]));
3745 let batch = RecordBatch::try_new(schema.clone(), vec![values]).unwrap();
3746 writer.write(&batch).unwrap();
3747
3748 let values = Arc::new(Float64Array::from(vec![0_f64; 10]));
3750 let batch = RecordBatch::try_new(schema.clone(), vec![values]).unwrap();
3751 writer.write(&batch).unwrap();
3752
3753 let values = Arc::new(Float64Array::from(vec![
3755 -1.0,
3756 0.0,
3757 f64::NAN,
3758 -f64::NAN,
3759 1.0,
3760 ]));
3761 let batch = RecordBatch::try_new(schema.clone(), vec![values]).unwrap();
3762 writer.write(&batch).unwrap();
3763
3764 let file_meta_data = writer.close().unwrap();
3765
3766 let col_stats = file_meta_data
3768 .row_group(0)
3769 .column(0)
3770 .statistics()
3771 .expect("missing column chunk statistics");
3772
3773 assert_eq!(col_stats.nan_count_opt(), Some(22));
3774 assert_eq!(col_stats.min_bytes_opt(), Some((-1.0f64).as_bytes()));
3775 assert_eq!(col_stats.max_bytes_opt(), Some(1.0f64.as_bytes()));
3776
3777 assert!(file_meta_data.page_index().is_some());
3778 let col_idx = &file_meta_data.page_index().unwrap().column_index(0, 0);
3779 assert_eq!(col_idx.as_ref().unwrap().num_pages(), 4);
3780
3781 let Some(ColumnIndexMetaData::DOUBLE(float_idx)) = col_idx else {
3783 panic!("expected double statistics")
3784 };
3785
3786 assert_eq!(float_idx.nan_counts, Some(vec![10, 10, 0, 2]));
3787 assert_eq!(
3788 f64::NAN.total_cmp(float_idx.min_value(0).unwrap()),
3789 Ordering::Equal
3790 );
3791 assert_eq!(
3792 f64::NAN.total_cmp(float_idx.max_value(0).unwrap()),
3793 Ordering::Equal
3794 );
3795 assert_eq!(
3796 (-f64::NAN).total_cmp(float_idx.min_value(1).unwrap()),
3797 Ordering::Equal
3798 );
3799 assert_eq!(
3800 (-f64::NAN).total_cmp(float_idx.max_value(1).unwrap()),
3801 Ordering::Equal
3802 );
3803 assert_eq!(float_idx.min_value(2), Some(&0.0));
3804 assert_eq!(float_idx.max_value(2), Some(&0.0));
3805 assert_eq!(float_idx.min_value(3), Some(&-1.0));
3806 assert_eq!(float_idx.max_value(3), Some(&1.0));
3807 }
3808
3809 #[test]
3810 #[should_panic(
3811 expected = "Attempting to write an Arrow interval type MonthDayNano to parquet that is not yet implemented"
3812 )]
3813 fn interval_month_day_nano_single_column() {
3814 required_and_optional::<IntervalMonthDayNanoArray, _>(vec![
3815 IntervalMonthDayNano::new(0, 1, 5),
3816 IntervalMonthDayNano::new(0, 3, 2),
3817 IntervalMonthDayNano::new(3, -2, -5),
3818 IntervalMonthDayNano::new(-200, 4, -1),
3819 ]);
3820 }
3821
3822 #[test]
3823 #[cfg_attr(miri, ignore)] fn i32_column_bloom_filter_at_end() {
3825 let array = Arc::new(Int32Array::from_iter(0..SMALL_SIZE as i32));
3826 let files = RoundTripTest::new(array)
3827 .with_nullable(false)
3828 .with_bloom_filter(true)
3829 .with_bloom_filter_position(BloomFilterPosition::End)
3830 .run();
3831
3832 check_bloom_filter(
3833 files,
3834 "col".to_string(),
3835 (0..SMALL_SIZE as i32).collect(),
3836 (SMALL_SIZE as i32 + 1..SMALL_SIZE as i32 + 10).collect(),
3837 );
3838 }
3839
3840 #[test]
3841 #[cfg_attr(miri, ignore)] fn i32_column_bloom_filter() {
3843 let array = Arc::new(Int32Array::from_iter(0..SMALL_SIZE as i32));
3844 let files = RoundTripTest::new(array)
3845 .with_nullable(false)
3846 .with_bloom_filter(true)
3847 .run();
3848
3849 check_bloom_filter(
3850 files,
3851 "col".to_string(),
3852 (0..SMALL_SIZE as i32).collect(),
3853 (SMALL_SIZE as i32 + 1..SMALL_SIZE as i32 + 10).collect(),
3854 );
3855 }
3856
3857 fn write_with_bloom_filter(array: ArrayRef, dictionary_page_size_limit: usize) -> Bytes {
3858 let schema = Arc::new(Schema::new(vec![Field::new(
3859 "col",
3860 array.data_type().clone(),
3861 false,
3862 )]));
3863 let batch = RecordBatch::try_new(schema.clone(), vec![array]).unwrap();
3864 let props = WriterProperties::builder()
3865 .set_dictionary_enabled(true)
3866 .set_dictionary_page_size_limit(dictionary_page_size_limit)
3867 .set_write_batch_size(256)
3868 .set_bloom_filter_enabled(true)
3869 .build();
3870 let mut buf = Vec::new();
3871 let mut writer = ArrowWriter::try_new(&mut buf, schema, Some(props)).unwrap();
3872 writer.write(&batch).unwrap();
3873 writer.close().unwrap();
3874 Bytes::from(buf)
3875 }
3876
3877 fn data_page_encoding_mask(file: &Bytes) -> EncodingMask {
3878 let metadata = ParquetMetaDataReader::new().parse_and_finish(file).unwrap();
3879 *metadata
3880 .row_group(0)
3881 .column(0)
3882 .page_encoding_stats_mask()
3883 .unwrap()
3884 }
3885
3886 #[test]
3889 fn string_column_bloom_filter_populated_from_dictionary() {
3890 let values: Vec<String> = (0..2000).map(|i| format!("value-{}", i % 10)).collect();
3891 let array = Arc::new(StringArray::from_iter_values(&values));
3892 let file = write_with_bloom_filter(array, 1024 * 1024);
3893 assert!(data_page_encoding_mask(&file).is_only(Encoding::RLE_DICTIONARY));
3894
3895 check_bloom_filter(
3896 vec![file],
3897 "col".to_string(),
3898 (0..10).map(|i| format!("value-{i}").into_bytes()).collect(),
3899 (10..20)
3900 .map(|i| format!("value-{i}").into_bytes())
3901 .collect(),
3902 );
3903 }
3904
3905 #[test]
3908 fn string_column_bloom_filter_across_dictionary_fallback() {
3909 let values: Vec<String> = (0..2000).map(|i| format!("value-{i}")).collect();
3910 let array = Arc::new(StringArray::from_iter_values(&values));
3911 let file = write_with_bloom_filter(array, 1024);
3912 let encodings = data_page_encoding_mask(&file);
3913 assert!(
3914 encodings.is_set(Encoding::RLE_DICTIONARY) && encodings.is_set(Encoding::PLAIN),
3915 "expected dictionary and plain data pages, got {encodings:?}"
3916 );
3917
3918 check_bloom_filter(
3919 vec![file],
3920 "col".to_string(),
3921 values.into_iter().map(String::into_bytes).collect(),
3922 (2000..2010)
3923 .map(|i| format!("value-{i}").into_bytes())
3924 .collect(),
3925 );
3926 }
3927
3928 #[test]
3929 fn i64_column_bloom_filter_populated_from_dictionary() {
3930 let array = Arc::new(Int64Array::from_iter_values((0..2000).map(|i| i % 10)));
3931 let file = write_with_bloom_filter(array, 1024 * 1024);
3932 assert!(data_page_encoding_mask(&file).is_only(Encoding::RLE_DICTIONARY));
3933
3934 check_bloom_filter(
3935 vec![file],
3936 "col".to_string(),
3937 (0..10i64).collect(),
3938 (10..20i64).collect(),
3939 );
3940 }
3941
3942 #[test]
3943 fn i64_column_bloom_filter_across_dictionary_fallback() {
3944 let array = Arc::new(Int64Array::from_iter_values(0..2000i64));
3945 let file = write_with_bloom_filter(array, 1024);
3946 let encodings = data_page_encoding_mask(&file);
3947 assert!(
3948 encodings.is_set(Encoding::RLE_DICTIONARY) && encodings.is_set(Encoding::PLAIN),
3949 "expected dictionary and plain data pages, got {encodings:?}"
3950 );
3951
3952 check_bloom_filter(
3953 vec![file],
3954 "col".to_string(),
3955 (0..2000i64).collect(),
3956 (2000..2010i64).collect(),
3957 );
3958 }
3959
3960 #[test]
3965 #[cfg_attr(miri, ignore)] fn i32_column_bloom_filter_fixed_ndv() {
3967 let array = Arc::new(Int32Array::from_iter(0..SMALL_SIZE as i32));
3968
3969 let files = RoundTripTest::new(array.clone())
3971 .with_nullable(false)
3972 .with_bloom_filter(true)
3973 .with_bloom_filter_ndv(1_000_000)
3974 .run();
3975
3976 check_bloom_filter(
3977 files,
3978 "col".to_string(),
3979 (0..SMALL_SIZE as i32).collect(),
3980 (SMALL_SIZE as i32 + 1..SMALL_SIZE as i32 + 10).collect(),
3981 );
3982
3983 let files = RoundTripTest::new(array)
3985 .with_nullable(false)
3986 .with_bloom_filter(true)
3987 .with_bloom_filter_ndv(3)
3988 .run();
3989
3990 check_bloom_filter(
3991 files,
3992 "col".to_string(),
3993 (0..SMALL_SIZE as i32).collect(),
3994 (SMALL_SIZE as i32 + 1..SMALL_SIZE as i32 + 10).collect(),
3995 );
3996 }
3997
3998 #[test]
3999 #[cfg_attr(miri, ignore)] fn binary_column_bloom_filter() {
4001 let one_vec: Vec<u8> = (0..SMALL_SIZE as u8).collect();
4002 let many_vecs: Vec<_> = std::iter::repeat_n(one_vec, SMALL_SIZE).collect();
4003 let many_vecs_iter = many_vecs.iter().map(|v| v.as_slice());
4004
4005 let array = Arc::new(BinaryArray::from_iter_values(many_vecs_iter));
4006 let files = RoundTripTest::new(array)
4007 .with_nullable(false)
4008 .with_bloom_filter(true)
4009 .run();
4010
4011 check_bloom_filter(
4012 files,
4013 "col".to_string(),
4014 many_vecs,
4015 vec![vec![(SMALL_SIZE + 1) as u8]],
4016 );
4017 }
4018
4019 #[test]
4020 #[cfg_attr(miri, ignore)] fn empty_string_null_column_bloom_filter() {
4022 let raw_values: Vec<_> = (0..SMALL_SIZE).map(|i| i.to_string()).collect();
4023 let raw_strs = raw_values.iter().map(|s| s.as_str());
4024
4025 let array = Arc::new(StringArray::from_iter_values(raw_strs));
4026 let files = RoundTripTest::new(array)
4027 .with_nullable(false)
4028 .with_bloom_filter(true)
4029 .run();
4030
4031 let optional_raw_values: Vec<_> = raw_values
4032 .iter()
4033 .enumerate()
4034 .filter_map(|(i, v)| if i % 2 == 0 { None } else { Some(v.as_str()) })
4035 .collect();
4036 check_bloom_filter(files, "col".to_string(), optional_raw_values, vec![""]);
4038 }
4039
4040 #[test]
4041 fn list_and_map_coerced_names() {
4042 let list_field =
4044 Field::new_list("my_list", Field::new("item", DataType::Int32, false), false);
4045 let map_field = Field::new_map(
4046 "my_map",
4047 "my_entries",
4048 Field::new("my_keys", DataType::Int32, false),
4049 Field::new("my_values", DataType::Int32, true),
4050 false,
4051 true,
4052 );
4053
4054 let list_array = create_random_array(&list_field, 100, 0.0, 0.0).unwrap();
4055 let map_array = create_random_array(&map_field, 100, 0.0, 0.0).unwrap();
4056
4057 let arrow_schema = Arc::new(Schema::new(vec![list_field, map_field]));
4058
4059 let props = Some(WriterProperties::builder().set_coerce_types(true).build());
4061 let file = tempfile::tempfile().unwrap();
4062 let mut writer =
4063 ArrowWriter::try_new(file.try_clone().unwrap(), arrow_schema.clone(), props).unwrap();
4064
4065 let batch = RecordBatch::try_new(arrow_schema, vec![list_array, map_array]).unwrap();
4066 writer.write(&batch).unwrap();
4067 let file_metadata = writer.close().unwrap();
4068
4069 let schema = file_metadata.file_metadata().schema();
4070 let list_field = &schema.get_fields()[0].get_fields()[0];
4072 assert_eq!(list_field.get_fields()[0].name(), "element");
4073
4074 let map_field = &schema.get_fields()[1].get_fields()[0];
4075 assert_eq!(map_field.name(), "key_value");
4077 assert_eq!(map_field.get_fields()[0].name(), "key");
4079 assert_eq!(map_field.get_fields()[1].name(), "value");
4081
4082 let reader = SerializedFileReader::new(file).unwrap();
4084 let file_schema = reader.metadata().file_metadata().schema();
4085 let fields = file_schema.get_fields();
4086 let list_field = &fields[0].get_fields()[0];
4087 assert_eq!(list_field.get_fields()[0].name(), "element");
4088 let map_field = &fields[1].get_fields()[0];
4089 assert_eq!(map_field.name(), "key_value");
4090 assert_eq!(map_field.get_fields()[0].name(), "key");
4091 assert_eq!(map_field.get_fields()[1].name(), "value");
4092 }
4093
4094 #[test]
4095 #[cfg_attr(miri, ignore)] fn fallback_flush_data_page() {
4097 let raw_values: Vec<_> = (0..MEDIUM_SIZE).map(|i| i.to_string()).collect();
4099 let values = Arc::new(StringArray::from(raw_values));
4100 let encodings = vec![
4101 Encoding::DELTA_BYTE_ARRAY,
4102 Encoding::DELTA_LENGTH_BYTE_ARRAY,
4103 ];
4104 let data_type = values.data_type().clone();
4105 let schema = Arc::new(Schema::new(vec![Field::new("col", data_type, false)]));
4106 let expected_batch = RecordBatch::try_new(schema, vec![values]).unwrap();
4107
4108 let row_group_sizes = [1024, SMALL_SIZE, SMALL_SIZE / 2, SMALL_SIZE / 2 + 1, 10];
4109 let data_page_size_limit: usize = 32;
4110 let write_batch_size: usize = 16;
4111
4112 for encoding in &encodings {
4113 for row_group_size in row_group_sizes {
4114 let props = WriterProperties::builder()
4115 .set_writer_version(WriterVersion::PARQUET_2_0)
4116 .set_max_row_group_row_count(Some(row_group_size))
4117 .set_dictionary_enabled(false)
4118 .set_encoding(*encoding)
4119 .set_data_page_size_limit(data_page_size_limit)
4120 .set_write_batch_size(write_batch_size)
4121 .build();
4122
4123 roundtrip_opts_with_array_validation(&expected_batch, props, |a, b| {
4124 let string_array_a = StringArray::from(a.clone());
4125 let string_array_b = StringArray::from(b.clone());
4126 let vec_a: Vec<&str> = string_array_a.iter().map(|v| v.unwrap()).collect();
4127 let vec_b: Vec<&str> = string_array_b.iter().map(|v| v.unwrap()).collect();
4128 assert_eq!(
4129 vec_a, vec_b,
4130 "failed for encoder: {encoding:?} and row_group_size: {row_group_size:?}"
4131 );
4132 });
4133 }
4134 }
4135 }
4136
4137 #[test]
4138 #[cfg_attr(miri, ignore)] fn u32_min_max() {
4140 let src = [
4142 u32::MIN,
4143 1,
4144 (i32::MAX as u32) - 1,
4145 i32::MAX as u32,
4146 (i32::MAX as u32) + 1,
4147 u32::MAX - 1,
4148 u32::MAX,
4149 ];
4150 let values = Arc::new(UInt32Array::from_iter_values(src.iter().copied()));
4151 let files = RoundTripTest::new(values).with_nullable(false).run();
4152
4153 for file in files {
4154 let reader = SerializedFileReader::new(file).unwrap();
4156 let metadata = reader.metadata();
4157
4158 let mut row_offset = 0;
4159 for row_group in metadata.row_groups() {
4160 assert_eq!(row_group.num_columns(), 1);
4161 let column = row_group.column(0);
4162
4163 let num_values = column.num_values() as usize;
4164 let src_slice = &src[row_offset..row_offset + num_values];
4165 row_offset += column.num_values() as usize;
4166
4167 let stats = column.statistics().unwrap();
4168 if let Statistics::Int32(stats) = stats {
4169 assert_eq!(
4170 *stats.min_opt().unwrap() as u32,
4171 *src_slice.iter().min().unwrap()
4172 );
4173 assert_eq!(
4174 *stats.max_opt().unwrap() as u32,
4175 *src_slice.iter().max().unwrap()
4176 );
4177 } else {
4178 panic!("Statistics::Int32 missing")
4179 }
4180 }
4181 }
4182 }
4183
4184 #[test]
4185 #[cfg_attr(miri, ignore)] fn u64_min_max() {
4187 let src = [
4189 u64::MIN,
4190 1,
4191 (i64::MAX as u64) - 1,
4192 i64::MAX as u64,
4193 (i64::MAX as u64) + 1,
4194 u64::MAX - 1,
4195 u64::MAX,
4196 ];
4197 let values = Arc::new(UInt64Array::from_iter_values(src.iter().copied()));
4198 let files = RoundTripTest::new(values).with_nullable(false).run();
4199
4200 for file in files {
4201 let reader = SerializedFileReader::new(file).unwrap();
4203 let metadata = reader.metadata();
4204
4205 let mut row_offset = 0;
4206 for row_group in metadata.row_groups() {
4207 assert_eq!(row_group.num_columns(), 1);
4208 let column = row_group.column(0);
4209
4210 let num_values = column.num_values() as usize;
4211 let src_slice = &src[row_offset..row_offset + num_values];
4212 row_offset += column.num_values() as usize;
4213
4214 let stats = column.statistics().unwrap();
4215 if let Statistics::Int64(stats) = stats {
4216 assert_eq!(
4217 *stats.min_opt().unwrap() as u64,
4218 *src_slice.iter().min().unwrap()
4219 );
4220 assert_eq!(
4221 *stats.max_opt().unwrap() as u64,
4222 *src_slice.iter().max().unwrap()
4223 );
4224 } else {
4225 panic!("Statistics::Int64 missing")
4226 }
4227 }
4228 }
4229 }
4230
4231 #[test]
4232 #[cfg_attr(miri, ignore)] fn statistics_null_counts_only_nulls() {
4234 let values = Arc::new(UInt64Array::from(vec![None, None]));
4236 let files = RoundTripTest::new(values).run();
4237
4238 for file in files {
4239 let reader = SerializedFileReader::new(file).unwrap();
4241 let metadata = reader.metadata();
4242 assert_eq!(metadata.num_row_groups(), 1);
4243 let row_group = metadata.row_group(0);
4244 assert_eq!(row_group.num_columns(), 1);
4245 let column = row_group.column(0);
4246 let stats = column.statistics().unwrap();
4247 assert_eq!(stats.null_count_opt(), Some(2));
4248 }
4249 }
4250
4251 fn row_group_sizes(metadata: &ParquetMetaData) -> Vec<i64> {
4252 metadata.row_groups().iter().map(|x| x.num_rows()).collect()
4253 }
4254
4255 #[test]
4256 fn test_aggregates_records() {
4257 let arrays = [
4258 Int32Array::from((0..100).collect::<Vec<_>>()),
4259 Int32Array::from((0..50).collect::<Vec<_>>()),
4260 Int32Array::from((200..500).collect::<Vec<_>>()),
4261 ];
4262
4263 let schema = Arc::new(Schema::new(vec![Field::new(
4264 "int",
4265 ArrowDataType::Int32,
4266 false,
4267 )]));
4268
4269 let file = tempfile::tempfile().unwrap();
4270
4271 let props = WriterProperties::builder()
4272 .set_max_row_group_row_count(Some(200))
4273 .build();
4274
4275 let mut writer =
4276 ArrowWriter::try_new(file.try_clone().unwrap(), schema.clone(), Some(props)).unwrap();
4277
4278 for array in arrays {
4279 let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(array)]).unwrap();
4280 writer.write(&batch).unwrap();
4281 }
4282
4283 writer.close().unwrap();
4284
4285 let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
4286 assert_eq!(&row_group_sizes(builder.metadata()), &[200, 200, 50]);
4287
4288 let batches = builder
4289 .with_batch_size(100)
4290 .build()
4291 .unwrap()
4292 .collect::<ArrowResult<Vec<_>>>()
4293 .unwrap();
4294
4295 assert_eq!(batches.len(), 5);
4296 assert!(batches.iter().all(|x| x.num_columns() == 1));
4297
4298 let batch_sizes: Vec<_> = batches.iter().map(|x| x.num_rows()).collect();
4299
4300 assert_eq!(&batch_sizes, &[100, 100, 100, 100, 50]);
4301
4302 let values: Vec<_> = batches
4303 .iter()
4304 .flat_map(|x| {
4305 x.column(0)
4306 .as_any()
4307 .downcast_ref::<Int32Array>()
4308 .unwrap()
4309 .values()
4310 .iter()
4311 .copied()
4312 })
4313 .collect();
4314
4315 let expected_values: Vec<_> = [0..100, 0..50, 200..500].into_iter().flatten().collect();
4316 assert_eq!(&values, &expected_values)
4317 }
4318
4319 #[test]
4320 fn complex_aggregate() {
4321 let field_a = Arc::new(Field::new("leaf_a", DataType::Int32, false));
4323 let field_b = Arc::new(Field::new("leaf_b", DataType::Int32, true));
4324 let struct_a = Arc::new(Field::new(
4325 "struct_a",
4326 DataType::Struct(vec![field_a.clone(), field_b.clone()].into()),
4327 true,
4328 ));
4329
4330 let list_a = Arc::new(Field::new("list", DataType::List(struct_a), true));
4331 let struct_b = Arc::new(Field::new(
4332 "struct_b",
4333 DataType::Struct(vec![list_a.clone()].into()),
4334 false,
4335 ));
4336
4337 let schema = Arc::new(Schema::new(vec![struct_b]));
4338
4339 let field_a_array = Int32Array::from(vec![1, 2, 3, 4, 5, 6]);
4341 let field_b_array =
4342 Int32Array::from_iter(vec![Some(1), None, Some(2), None, None, Some(6)]);
4343
4344 let struct_a_array = StructArray::from(vec![
4345 (field_a.clone(), Arc::new(field_a_array) as ArrayRef),
4346 (field_b.clone(), Arc::new(field_b_array) as ArrayRef),
4347 ]);
4348
4349 let list_data = ArrayDataBuilder::new(list_a.data_type().clone())
4350 .len(5)
4351 .add_buffer(Buffer::from_iter(vec![
4352 0_i32, 1_i32, 1_i32, 3_i32, 3_i32, 5_i32,
4353 ]))
4354 .null_bit_buffer(Some(Buffer::from_iter(vec![
4355 true, false, true, false, true,
4356 ])))
4357 .child_data(vec![struct_a_array.into_data()])
4358 .build()
4359 .unwrap();
4360
4361 let list_a_array = Arc::new(ListArray::from(list_data)) as ArrayRef;
4362 let struct_b_array = StructArray::from(vec![(list_a.clone(), list_a_array)]);
4363
4364 let batch1 =
4365 RecordBatch::try_from_iter(vec![("struct_b", Arc::new(struct_b_array) as ArrayRef)])
4366 .unwrap();
4367
4368 let field_a_array = Int32Array::from(vec![6, 7, 8, 9, 10]);
4369 let field_b_array = Int32Array::from_iter(vec![None, None, None, Some(1), None]);
4370
4371 let struct_a_array = StructArray::from(vec![
4372 (field_a, Arc::new(field_a_array) as ArrayRef),
4373 (field_b, Arc::new(field_b_array) as ArrayRef),
4374 ]);
4375
4376 let list_data = ArrayDataBuilder::new(list_a.data_type().clone())
4377 .len(2)
4378 .add_buffer(Buffer::from_iter(vec![0_i32, 4_i32, 5_i32]))
4379 .child_data(vec![struct_a_array.into_data()])
4380 .build()
4381 .unwrap();
4382
4383 let list_a_array = Arc::new(ListArray::from(list_data)) as ArrayRef;
4384 let struct_b_array = StructArray::from(vec![(list_a, list_a_array)]);
4385
4386 let batch2 =
4387 RecordBatch::try_from_iter(vec![("struct_b", Arc::new(struct_b_array) as ArrayRef)])
4388 .unwrap();
4389
4390 let batches = &[batch1, batch2];
4391
4392 let expected = r"
4395 +-------------------------------------------------------------------------------------------------------+
4396 | struct_b |
4397 +-------------------------------------------------------------------------------------------------------+
4398 | {list: [{leaf_a: 1, leaf_b: 1}]} |
4399 | {list: } |
4400 | {list: [{leaf_a: 2, leaf_b: }, {leaf_a: 3, leaf_b: 2}]} |
4401 | {list: } |
4402 | {list: [{leaf_a: 4, leaf_b: }, {leaf_a: 5, leaf_b: }]} |
4403 | {list: [{leaf_a: 6, leaf_b: }, {leaf_a: 7, leaf_b: }, {leaf_a: 8, leaf_b: }, {leaf_a: 9, leaf_b: 1}]} |
4404 | {list: [{leaf_a: 10, leaf_b: }]} |
4405 +-------------------------------------------------------------------------------------------------------+
4406 ".trim().split('\n').map(|x| x.trim()).collect::<Vec<_>>().join("\n");
4407
4408 let actual = pretty_format_batches(batches).unwrap().to_string();
4409 assert_eq!(actual, expected);
4410
4411 let file = tempfile::tempfile().unwrap();
4413 let props = WriterProperties::builder()
4414 .set_max_row_group_row_count(Some(6))
4415 .build();
4416
4417 let mut writer =
4418 ArrowWriter::try_new(file.try_clone().unwrap(), schema, Some(props)).unwrap();
4419
4420 for batch in batches {
4421 writer.write(batch).unwrap();
4422 }
4423 writer.close().unwrap();
4424
4425 let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
4430 assert_eq!(&row_group_sizes(builder.metadata()), &[6, 1]);
4431
4432 let batches = builder
4433 .with_batch_size(2)
4434 .build()
4435 .unwrap()
4436 .collect::<ArrowResult<Vec<_>>>()
4437 .unwrap();
4438
4439 assert_eq!(batches.len(), 4);
4440 let batch_counts: Vec<_> = batches.iter().map(|x| x.num_rows()).collect();
4441 assert_eq!(&batch_counts, &[2, 2, 2, 1]);
4442
4443 let actual = pretty_format_batches(&batches).unwrap().to_string();
4444 assert_eq!(actual, expected);
4445 }
4446
4447 #[test]
4448 fn test_arrow_writer_metadata() {
4449 let batch_schema = Schema::new(vec![Field::new("int32", DataType::Int32, false)]);
4450 let file_schema = batch_schema.clone().with_metadata([("foo", "bar")]);
4451
4452 let batch = RecordBatch::try_new(
4453 Arc::new(batch_schema),
4454 vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4455 )
4456 .unwrap();
4457
4458 let mut buf = Vec::with_capacity(1024);
4459 let mut writer = ArrowWriter::try_new(&mut buf, Arc::new(file_schema), None).unwrap();
4460 writer.write(&batch).unwrap();
4461 writer.close().unwrap();
4462 }
4463
4464 #[test]
4465 fn in_progress_accounting() {
4466 let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
4468
4469 let a = Int32Array::from(vec![1, 2, 3, 4, 5]);
4471
4472 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(a)]).unwrap();
4474
4475 let mut writer = ArrowWriter::try_new(vec![], batch.schema(), None).unwrap();
4476
4477 assert_eq!(writer.in_progress_size(), 0);
4479 assert_eq!(writer.in_progress_rows(), 0);
4480 assert_eq!(writer.memory_size(), 0);
4481 assert_eq!(writer.bytes_written(), 4); writer.write(&batch).unwrap();
4483
4484 let initial_size = writer.in_progress_size();
4486 assert!(initial_size > 0);
4487 assert_eq!(writer.in_progress_rows(), 5);
4488 let initial_memory = writer.memory_size();
4489 assert!(initial_memory > 0);
4490 assert!(
4492 initial_size <= initial_memory,
4493 "{initial_size} <= {initial_memory}"
4494 );
4495
4496 writer.write(&batch).unwrap();
4498 assert!(writer.in_progress_size() > initial_size);
4499 assert_eq!(writer.in_progress_rows(), 10);
4500 assert!(writer.memory_size() > initial_memory);
4501 assert!(
4502 writer.in_progress_size() <= writer.memory_size(),
4503 "in_progress_size {} <= memory_size {}",
4504 writer.in_progress_size(),
4505 writer.memory_size()
4506 );
4507
4508 let pre_flush_bytes_written = writer.bytes_written();
4510 writer.flush().unwrap();
4511 assert_eq!(writer.in_progress_size(), 0);
4512 assert_eq!(writer.memory_size(), 0);
4513 assert!(writer.bytes_written() > pre_flush_bytes_written);
4514
4515 writer.close().unwrap();
4516 }
4517
4518 #[test]
4519 fn test_writer_all_null() {
4520 let a = Int32Array::from(vec![1, 2, 3, 4, 5]);
4521 let b = Int32Array::new(vec![0; 5].into(), Some(NullBuffer::new_null(5)));
4522 let batch = RecordBatch::try_from_iter(vec![
4523 ("a", Arc::new(a) as ArrayRef),
4524 ("b", Arc::new(b) as ArrayRef),
4525 ])
4526 .unwrap();
4527
4528 let mut buf = Vec::with_capacity(1024);
4529 let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), None).unwrap();
4530 writer.write(&batch).unwrap();
4531 writer.close().unwrap();
4532
4533 let bytes = Bytes::from(buf);
4534 let options = ReadOptionsBuilder::new().with_page_index().build();
4535 let reader = SerializedFileReader::new_with_options(bytes, options).unwrap();
4536 let index = reader.metadata().page_index().unwrap();
4537
4538 assert_eq!(index.num_data_pages(0, 0), Some(1)); assert_eq!(index.num_data_pages(0, 1), Some(1)); }
4541
4542 #[test]
4543 fn test_disabled_statistics_with_page() {
4544 let file_schema = Schema::new(vec![
4545 Field::new("a", DataType::Utf8, true),
4546 Field::new("b", DataType::Utf8, true),
4547 ]);
4548 let file_schema = Arc::new(file_schema);
4549
4550 let batch = RecordBatch::try_new(
4551 file_schema.clone(),
4552 vec![
4553 Arc::new(StringArray::from(vec!["a", "b", "c", "d"])) as _,
4554 Arc::new(StringArray::from(vec!["w", "x", "y", "z"])) as _,
4555 ],
4556 )
4557 .unwrap();
4558
4559 let props = WriterProperties::builder()
4560 .set_statistics_enabled(EnabledStatistics::None)
4561 .set_column_statistics_enabled("a".into(), EnabledStatistics::Page)
4562 .build();
4563
4564 let mut buf = Vec::with_capacity(1024);
4565 let mut writer = ArrowWriter::try_new(&mut buf, file_schema.clone(), Some(props)).unwrap();
4566 writer.write(&batch).unwrap();
4567
4568 let metadata = writer.close().unwrap();
4569 assert_eq!(metadata.num_row_groups(), 1);
4570 let row_group = metadata.row_group(0);
4571 assert_eq!(row_group.num_columns(), 2);
4572 assert!(row_group.column(0).offset_index_offset().is_some());
4574 assert!(row_group.column(0).column_index_offset().is_some());
4575 assert!(row_group.column(1).offset_index_offset().is_some());
4577 assert!(row_group.column(1).column_index_offset().is_none());
4578
4579 let options = ReadOptionsBuilder::new().with_page_index().build();
4580 let reader = SerializedFileReader::new_with_options(Bytes::from(buf), options).unwrap();
4581
4582 let row_group = reader.get_row_group(0).unwrap();
4583 let a_col = row_group.metadata().column(0);
4584 let b_col = row_group.metadata().column(1);
4585
4586 if let Statistics::ByteArray(byte_array_stats) = a_col.statistics().unwrap() {
4588 let min = byte_array_stats.min_opt().unwrap();
4589 let max = byte_array_stats.max_opt().unwrap();
4590
4591 assert_eq!(min.as_bytes(), b"a");
4592 assert_eq!(max.as_bytes(), b"d");
4593 } else {
4594 panic!("expecting Statistics::ByteArray");
4595 }
4596
4597 assert!(b_col.statistics().is_none());
4599
4600 let page_index = reader.metadata().page_index().unwrap();
4601
4602 let a_idx = page_index.column_index(0, 0);
4603 assert!(
4604 matches!(a_idx, Some(ColumnIndexMetaData::BYTE_ARRAY(_))),
4605 "{a_idx:?}"
4606 );
4607 let b_idx = page_index.column_index(0, 1);
4608 assert!(b_idx.is_none(), "{b_idx:?}");
4609 }
4610
4611 #[test]
4612 fn test_disabled_statistics_with_chunk() {
4613 let file_schema = Schema::new(vec![
4614 Field::new("a", DataType::Utf8, true),
4615 Field::new("b", DataType::Utf8, true),
4616 ]);
4617 let file_schema = Arc::new(file_schema);
4618
4619 let batch = RecordBatch::try_new(
4620 file_schema.clone(),
4621 vec![
4622 Arc::new(StringArray::from(vec!["a", "b", "c", "d"])) as _,
4623 Arc::new(StringArray::from(vec!["w", "x", "y", "z"])) as _,
4624 ],
4625 )
4626 .unwrap();
4627
4628 let props = WriterProperties::builder()
4629 .set_statistics_enabled(EnabledStatistics::None)
4630 .set_column_statistics_enabled("a".into(), EnabledStatistics::Chunk)
4631 .build();
4632
4633 let mut buf = Vec::with_capacity(1024);
4634 let mut writer = ArrowWriter::try_new(&mut buf, file_schema.clone(), Some(props)).unwrap();
4635 writer.write(&batch).unwrap();
4636
4637 let metadata = writer.close().unwrap();
4638 assert_eq!(metadata.num_row_groups(), 1);
4639 let row_group = metadata.row_group(0);
4640 assert_eq!(row_group.num_columns(), 2);
4641 assert!(row_group.column(0).offset_index_offset().is_some());
4643 assert!(row_group.column(0).column_index_offset().is_none());
4644 assert!(row_group.column(1).offset_index_offset().is_some());
4646 assert!(row_group.column(1).column_index_offset().is_none());
4647
4648 let options = ReadOptionsBuilder::new().with_page_index().build();
4649 let reader = SerializedFileReader::new_with_options(Bytes::from(buf), options).unwrap();
4650
4651 let row_group = reader.get_row_group(0).unwrap();
4652 let a_col = row_group.metadata().column(0);
4653 let b_col = row_group.metadata().column(1);
4654
4655 if let Statistics::ByteArray(byte_array_stats) = a_col.statistics().unwrap() {
4657 let min = byte_array_stats.min_opt().unwrap();
4658 let max = byte_array_stats.max_opt().unwrap();
4659
4660 assert_eq!(min.as_bytes(), b"a");
4661 assert_eq!(max.as_bytes(), b"d");
4662 } else {
4663 panic!("expecting Statistics::ByteArray");
4664 }
4665
4666 assert!(b_col.statistics().is_none());
4668
4669 let page_index = reader.metadata().page_index().unwrap();
4670
4671 let a_idx = page_index.column_index(0, 0);
4672 assert!(a_idx.is_none(), "{a_idx:?}");
4673 let b_idx = page_index.column_index(0, 1);
4674 assert!(b_idx.is_none(), "{b_idx:?}");
4675 }
4676
4677 #[test]
4678 fn test_arrow_writer_skip_metadata() {
4679 let batch_schema = Schema::new(vec![Field::new("int32", DataType::Int32, false)]);
4680 let file_schema = Arc::new(batch_schema.clone());
4681
4682 let batch = RecordBatch::try_new(
4683 Arc::new(batch_schema),
4684 vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4685 )
4686 .unwrap();
4687 let skip_options = ArrowWriterOptions::new().with_skip_arrow_metadata(true);
4688
4689 let mut buf = Vec::with_capacity(1024);
4690 let mut writer =
4691 ArrowWriter::try_new_with_options(&mut buf, file_schema.clone(), skip_options).unwrap();
4692 writer.write(&batch).unwrap();
4693 writer.close().unwrap();
4694
4695 let bytes = Bytes::from(buf);
4696 let reader_builder = ParquetRecordBatchReaderBuilder::try_new(bytes).unwrap();
4697 assert_eq!(file_schema, *reader_builder.schema());
4698 if let Some(key_value_metadata) = reader_builder
4699 .metadata()
4700 .file_metadata()
4701 .key_value_metadata()
4702 {
4703 assert!(
4704 !key_value_metadata
4705 .iter()
4706 .any(|kv| kv.key.as_str() == ARROW_SCHEMA_META_KEY)
4707 );
4708 }
4709 }
4710
4711 #[test]
4712 fn test_arrow_writer_skip_path_in_schema() {
4713 let batch_schema = Schema::new(vec![Field::new("int32", DataType::Int32, false)]);
4714 let file_schema = Arc::new(batch_schema.clone());
4715
4716 let batch = RecordBatch::try_new(
4717 Arc::new(batch_schema),
4718 vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4719 )
4720 .unwrap();
4721
4722 let skip_options = ArrowWriterOptions::new();
4724
4725 let mut buf = Vec::with_capacity(1024);
4726 let mut writer =
4727 ArrowWriter::try_new_with_options(&mut buf, file_schema.clone(), skip_options).unwrap();
4728 writer.write(&batch).unwrap();
4729 writer.close().unwrap();
4730
4731 let skip_options = ArrowWriterOptions::new().with_properties(
4733 WriterProperties::builder()
4734 .set_write_path_in_schema(false)
4735 .build(),
4736 );
4737
4738 let mut buf2 = Vec::with_capacity(1024);
4739 let mut writer =
4740 ArrowWriter::try_new_with_options(&mut buf2, file_schema.clone(), skip_options)
4741 .unwrap();
4742 writer.write(&batch).unwrap();
4743 writer.close().unwrap();
4744
4745 assert!(buf.len() > buf2.len());
4747 }
4748
4749 #[test]
4750 fn mismatched_schemas() {
4751 let batch_schema = Schema::new(vec![Field::new("count", DataType::Int32, false)]);
4752 let file_schema = Arc::new(Schema::new(vec![Field::new(
4753 "temperature",
4754 DataType::Float64,
4755 false,
4756 )]));
4757
4758 let batch = RecordBatch::try_new(
4759 Arc::new(batch_schema),
4760 vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4761 )
4762 .unwrap();
4763
4764 let mut buf = Vec::with_capacity(1024);
4765 let mut writer = ArrowWriter::try_new(&mut buf, file_schema.clone(), None).unwrap();
4766
4767 let err = writer.write(&batch).unwrap_err().to_string();
4768 assert_eq!(
4769 err,
4770 "Arrow: Incompatible type. Field 'temperature' has type Float64, array has type Int32"
4771 );
4772 }
4773
4774 #[test]
4775 fn test_page_stats_not_written_by_default() {
4776 let string_field = Field::new("a", DataType::Utf8, false);
4777 let schema = Schema::new(vec![string_field]);
4778 let raw_string_values = vec!["Blart Versenwald III"];
4779 let string_values = StringArray::from(raw_string_values.clone());
4780 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(string_values)]).unwrap();
4781
4782 let props = WriterProperties::builder()
4783 .set_statistics_enabled(EnabledStatistics::Page)
4784 .set_dictionary_enabled(false)
4785 .set_encoding(Encoding::PLAIN)
4786 .set_compression(crate::basic::Compression::UNCOMPRESSED)
4787 .build();
4788
4789 let file = roundtrip_opts(&batch, props);
4790
4791 let first_page = &file[4..];
4796 let mut prot = ThriftSliceInputProtocol::new(first_page);
4797 let hdr = PageHeader::read_thrift(&mut prot).unwrap();
4798 let stats = hdr.data_page_header.unwrap().statistics;
4799
4800 assert!(stats.is_none());
4801 }
4802
4803 #[test]
4804 fn test_page_stats_when_enabled() {
4805 let string_field = Field::new("a", DataType::Utf8, false);
4806 let schema = Schema::new(vec![string_field]);
4807 let raw_string_values = vec!["Blart Versenwald III", "Andrew Lamb"];
4808 let string_values = StringArray::from(raw_string_values.clone());
4809 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(string_values)]).unwrap();
4810
4811 let props = WriterProperties::builder()
4812 .set_statistics_enabled(EnabledStatistics::Page)
4813 .set_dictionary_enabled(false)
4814 .set_encoding(Encoding::PLAIN)
4815 .set_write_page_header_statistics(true)
4816 .set_compression(crate::basic::Compression::UNCOMPRESSED)
4817 .build();
4818
4819 let file = roundtrip_opts(&batch, props);
4820
4821 let first_page = &file[4..];
4826 let mut prot = ThriftSliceInputProtocol::new(first_page);
4827 let hdr = PageHeader::read_thrift(&mut prot).unwrap();
4828 let stats = hdr.data_page_header.unwrap().statistics;
4829
4830 let stats = stats.unwrap();
4831 assert!(stats.is_max_value_exact.unwrap());
4833 assert!(stats.is_min_value_exact.unwrap());
4834 assert_eq!(stats.max_value.unwrap(), b"Blart Versenwald III");
4835 assert_eq!(stats.min_value.unwrap(), b"Andrew Lamb");
4836 }
4837
4838 #[test]
4839 fn test_page_stats_truncation() {
4840 let string_field = Field::new("a", DataType::Utf8, false);
4841 let binary_field = Field::new("b", DataType::Binary, false);
4842 let schema = Schema::new(vec![string_field, binary_field]);
4843
4844 let raw_string_values = vec!["Blart Versenwald III"];
4845 let raw_binary_values = [b"Blart Versenwald III".to_vec()];
4846 let raw_binary_value_refs = raw_binary_values
4847 .iter()
4848 .map(|x| x.as_slice())
4849 .collect::<Vec<_>>();
4850
4851 let string_values = StringArray::from(raw_string_values.clone());
4852 let binary_values = BinaryArray::from(raw_binary_value_refs);
4853 let batch = RecordBatch::try_new(
4854 Arc::new(schema),
4855 vec![Arc::new(string_values), Arc::new(binary_values)],
4856 )
4857 .unwrap();
4858
4859 let props = WriterProperties::builder()
4860 .set_statistics_truncate_length(Some(2))
4861 .set_dictionary_enabled(false)
4862 .set_encoding(Encoding::PLAIN)
4863 .set_write_page_header_statistics(true)
4864 .set_compression(crate::basic::Compression::UNCOMPRESSED)
4865 .build();
4866
4867 let file = roundtrip_opts(&batch, props);
4868
4869 let first_page = &file[4..];
4874 let mut prot = ThriftSliceInputProtocol::new(first_page);
4875 let hdr = PageHeader::read_thrift(&mut prot).unwrap();
4876 let stats = hdr.data_page_header.unwrap().statistics;
4877 assert!(stats.is_some());
4878 let stats = stats.unwrap();
4879 assert!(!stats.is_max_value_exact.unwrap());
4881 assert!(!stats.is_min_value_exact.unwrap());
4882 assert_eq!(stats.max_value.unwrap(), b"Bm");
4883 assert_eq!(stats.min_value.unwrap(), b"Bl");
4884
4885 let second_page = &prot.as_slice()[hdr.compressed_page_size as usize..];
4887 let mut prot = ThriftSliceInputProtocol::new(second_page);
4888 let hdr = PageHeader::read_thrift(&mut prot).unwrap();
4889 let stats = hdr.data_page_header.unwrap().statistics;
4890 assert!(stats.is_some());
4891 let stats = stats.unwrap();
4892 assert!(!stats.is_max_value_exact.unwrap());
4894 assert!(!stats.is_min_value_exact.unwrap());
4895 assert_eq!(stats.max_value.unwrap(), b"Bm");
4896 assert_eq!(stats.min_value.unwrap(), b"Bl");
4897 }
4898
4899 #[test]
4900 fn test_page_encoding_statistics_roundtrip() {
4901 let batch_schema = Schema::new(vec![Field::new(
4902 "int32",
4903 arrow_schema::DataType::Int32,
4904 false,
4905 )]);
4906
4907 let batch = RecordBatch::try_new(
4908 Arc::new(batch_schema.clone()),
4909 vec![Arc::new(Int32Array::from(vec![1, 2, 3, 4])) as _],
4910 )
4911 .unwrap();
4912
4913 let mut file: File = tempfile::tempfile().unwrap();
4914 let mut writer = ArrowWriter::try_new(&mut file, Arc::new(batch_schema), None).unwrap();
4915 writer.write(&batch).unwrap();
4916 let file_metadata = writer.close().unwrap();
4917
4918 assert_eq!(file_metadata.num_row_groups(), 1);
4919 assert_eq!(file_metadata.row_group(0).num_columns(), 1);
4920 assert!(
4921 file_metadata
4922 .row_group(0)
4923 .column(0)
4924 .page_encoding_stats()
4925 .is_some()
4926 );
4927 let chunk_page_stats = file_metadata
4928 .row_group(0)
4929 .column(0)
4930 .page_encoding_stats()
4931 .unwrap();
4932
4933 let options = ReadOptionsBuilder::new()
4935 .with_page_index()
4936 .with_encoding_stats_as_mask(false)
4937 .build();
4938 let reader = SerializedFileReader::new_with_options(file, options).unwrap();
4939
4940 let rowgroup = reader.get_row_group(0).expect("row group missing");
4941 assert_eq!(rowgroup.num_columns(), 1);
4942 let column = rowgroup.metadata().column(0);
4943 assert!(column.page_encoding_stats().is_some());
4944 let file_page_stats = column.page_encoding_stats().unwrap();
4945 assert_eq!(chunk_page_stats, file_page_stats);
4946 }
4947
4948 #[test]
4949 #[cfg_attr(miri, ignore)] fn test_different_dict_page_size_limit() {
4951 let array = Arc::new(Int64Array::from_iter(0..1024 * 1024));
4952 let schema = Arc::new(Schema::new(vec![
4953 Field::new("col0", arrow_schema::DataType::Int64, false),
4954 Field::new("col1", arrow_schema::DataType::Int64, false),
4955 ]));
4956 let batch =
4957 arrow_array::RecordBatch::try_new(schema.clone(), vec![array.clone(), array]).unwrap();
4958
4959 let props = WriterProperties::builder()
4960 .set_dictionary_page_size_limit(1024 * 1024)
4961 .set_column_dictionary_page_size_limit(ColumnPath::from("col1"), 1024 * 1024 * 4)
4962 .build();
4963 let mut writer = ArrowWriter::try_new(Vec::new(), schema, Some(props)).unwrap();
4964 writer.write(&batch).unwrap();
4965 let data = Bytes::from(writer.into_inner().unwrap());
4966
4967 let mut metadata = ParquetMetaDataReader::new();
4968 metadata.try_parse(&data).unwrap();
4969 let metadata = metadata.finish().unwrap();
4970 let col0_meta = metadata.row_group(0).column(0);
4971 let col1_meta = metadata.row_group(0).column(1);
4972
4973 let get_dict_page_size = move |meta: &ColumnChunkMetaData| {
4974 let mut reader =
4975 SerializedPageReader::new(Arc::new(data.clone()), meta, 0, None).unwrap();
4976 let page = reader.get_next_page().unwrap().unwrap();
4977 match page {
4978 Page::DictionaryPage { buf, .. } => buf.len(),
4979 _ => panic!("expected DictionaryPage"),
4980 }
4981 };
4982
4983 assert_eq!(get_dict_page_size(col0_meta), 1024 * 1024);
4984 assert_eq!(get_dict_page_size(col1_meta), 1024 * 1024 * 4);
4985 }
4986
4987 #[test]
4988 #[cfg_attr(miri, ignore)] fn test_arrow_writer_granular_mode_roundtrip() {
4990 let small = "tiny".to_string();
4999 let big = "x".repeat(64 * 1024);
5000 let strings: Vec<String> = (0..256)
5001 .map(|i| {
5002 if i % 16 == 0 {
5003 big.clone()
5004 } else {
5005 small.clone()
5006 }
5007 })
5008 .collect();
5009
5010 let schema = Arc::new(Schema::new(vec![Field::new(
5011 "col",
5012 ArrowDataType::Utf8,
5013 false,
5014 )]));
5015 let batch = RecordBatch::try_new(
5016 schema.clone(),
5017 vec![Arc::new(StringArray::from(strings.clone())) as _],
5018 )
5019 .unwrap();
5020
5021 let props = WriterProperties::builder()
5022 .set_dictionary_enabled(false)
5023 .set_data_page_size_limit(16 * 1024)
5024 .build();
5025 let mut writer = ArrowWriter::try_new(Vec::new(), schema, Some(props)).unwrap();
5026 writer.write(&batch).unwrap();
5027 let data = Bytes::from(writer.into_inner().unwrap());
5028
5029 let mut reader = ParquetRecordBatchReader::try_new(data, 1024).unwrap();
5030 let read = reader.next().unwrap().unwrap();
5031 assert!(reader.next().is_none(), "expected one batch");
5032 let col = read
5033 .column(0)
5034 .as_any()
5035 .downcast_ref::<StringArray>()
5036 .unwrap();
5037 assert_eq!(col.len(), strings.len());
5038 for (i, expected) in strings.iter().enumerate() {
5039 assert_eq!(
5040 col.value(i),
5041 expected.as_str(),
5042 "value mismatch at index {i}"
5043 );
5044 }
5045 }
5046
5047 #[test]
5048 fn test_arrow_writer_all_null_string_column() {
5049 let num_rows = 1024;
5054 let schema = Arc::new(Schema::new(vec![Field::new(
5055 "col",
5056 ArrowDataType::Utf8,
5057 true,
5058 )]));
5059 let nulls: Vec<Option<&str>> = vec![None; num_rows];
5060 let batch = RecordBatch::try_new(
5061 schema.clone(),
5062 vec![Arc::new(StringArray::from(nulls)) as _],
5063 )
5064 .unwrap();
5065
5066 let props = WriterProperties::builder()
5067 .set_dictionary_enabled(false)
5068 .set_data_page_size_limit(16 * 1024)
5069 .build();
5070 let mut writer = ArrowWriter::try_new(Vec::new(), schema, Some(props)).unwrap();
5071 writer.write(&batch).unwrap();
5072 let data = Bytes::from(writer.into_inner().unwrap());
5073
5074 let mut metadata = ParquetMetaDataReader::new();
5077 metadata.try_parse(&data).unwrap();
5078 let metadata = metadata.finish().unwrap();
5079 let row_group = metadata.row_group(0);
5080 let col_meta = row_group.column(0);
5081 assert_eq!(row_group.num_rows() as usize, num_rows);
5082 if let Some(stats) = col_meta.statistics() {
5085 assert_eq!(
5086 stats.null_count_opt().unwrap_or(0) as usize,
5087 num_rows,
5088 "expected all-null column to report null_count = num_rows"
5089 );
5090 }
5091
5092 let mut reader =
5093 SerializedPageReader::new(Arc::new(data.clone()), col_meta, num_rows, None).unwrap();
5094 let mut total_values = 0u32;
5095 while let Some(page) = reader.get_next_page().unwrap() {
5096 if matches!(page, Page::DataPage { .. } | Page::DataPageV2 { .. }) {
5097 total_values += page.num_values();
5098 }
5099 }
5100 assert_eq!(
5101 total_values as usize, num_rows,
5102 "expected every level position to be represented in some page"
5103 );
5104 }
5105
5106 struct WriteBatchesShape {
5107 num_batches: usize,
5108 rows_per_batch: usize,
5109 row_size: usize,
5110 }
5111
5112 fn write_batches(
5114 WriteBatchesShape {
5115 num_batches,
5116 rows_per_batch,
5117 row_size,
5118 }: WriteBatchesShape,
5119 props: WriterProperties,
5120 ) -> ParquetRecordBatchReaderBuilder<File> {
5121 let schema = Arc::new(Schema::new(vec![Field::new(
5122 "str",
5123 ArrowDataType::Utf8,
5124 false,
5125 )]));
5126 let file = tempfile::tempfile().unwrap();
5127 let mut writer =
5128 ArrowWriter::try_new(file.try_clone().unwrap(), schema.clone(), Some(props)).unwrap();
5129
5130 for batch_idx in 0..num_batches {
5131 let strings: Vec<String> = (0..rows_per_batch)
5132 .map(|i| format!("{:0>width$}", batch_idx * 10 + i, width = row_size))
5133 .collect();
5134 let array = StringArray::from(strings);
5135 let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(array)]).unwrap();
5136 writer.write(&batch).unwrap();
5137 }
5138 writer.close().unwrap();
5139 ParquetRecordBatchReaderBuilder::try_new(file).unwrap()
5140 }
5141
5142 #[test]
5143 fn test_row_group_limit_none_writes_single_row_group() {
5145 let props = WriterProperties::builder()
5146 .set_max_row_group_row_count(None)
5147 .set_max_row_group_bytes(None)
5148 .build();
5149
5150 let builder = write_batches(
5151 WriteBatchesShape {
5152 num_batches: 1,
5153 rows_per_batch: 1000,
5154 row_size: 4,
5155 },
5156 props,
5157 );
5158
5159 assert_eq!(
5160 &row_group_sizes(builder.metadata()),
5161 &[1000],
5162 "With no limits, all rows should be in a single row group"
5163 );
5164 }
5165
5166 #[test]
5167 fn test_row_group_limit_rows_only() {
5169 let props = WriterProperties::builder()
5170 .set_max_row_group_row_count(Some(300))
5171 .set_max_row_group_bytes(None)
5172 .build();
5173
5174 let builder = write_batches(
5175 WriteBatchesShape {
5176 num_batches: 1,
5177 rows_per_batch: 1000,
5178 row_size: 4,
5179 },
5180 props,
5181 );
5182
5183 assert_eq!(
5184 &row_group_sizes(builder.metadata()),
5185 &[300, 300, 300, 100],
5186 "Row groups should be split by row count"
5187 );
5188 }
5189
5190 #[test]
5191 #[cfg_attr(miri, ignore)] fn test_row_group_limit_rows_only_many_splits() {
5195 let props = WriterProperties::builder()
5196 .set_max_row_group_row_count(Some(1))
5197 .set_max_row_group_bytes(None)
5198 .build();
5199
5200 let rows = 50_000;
5201 let builder = write_batches(
5202 WriteBatchesShape {
5203 num_batches: 1,
5204 rows_per_batch: rows,
5205 row_size: 4,
5206 },
5207 props,
5208 );
5209
5210 let sizes = row_group_sizes(builder.metadata());
5211 assert_eq!(sizes.len(), rows, "Every row should get its own row group");
5212 assert_eq!(
5213 sizes.iter().sum::<i64>(),
5214 rows as i64,
5215 "Total rows should be preserved"
5216 );
5217 }
5218
5219 #[test]
5220 fn test_row_group_limit_bytes_only() {
5222 let props = WriterProperties::builder()
5223 .set_max_row_group_row_count(None)
5224 .set_max_row_group_bytes(Some(3500))
5226 .build();
5227
5228 let builder = write_batches(
5229 WriteBatchesShape {
5230 num_batches: 10,
5231 rows_per_batch: 10,
5232 row_size: 100,
5233 },
5234 props,
5235 );
5236
5237 let sizes = row_group_sizes(builder.metadata());
5238
5239 assert!(
5240 sizes.len() > 1,
5241 "Should have multiple row groups due to byte limit, got {sizes:?}",
5242 );
5243
5244 let total_rows: i64 = sizes.iter().sum();
5245 assert_eq!(total_rows, 100, "Total rows should be preserved");
5246 }
5247
5248 #[test]
5249 fn test_row_group_limit_bytes_flushes_when_current_group_already_too_large() {
5251 let schema = Arc::new(Schema::new(vec![Field::new(
5252 "str",
5253 ArrowDataType::Utf8,
5254 false,
5255 )]));
5256 let file = tempfile::tempfile().unwrap();
5257
5258 let props = WriterProperties::builder()
5260 .set_max_row_group_row_count(None)
5261 .set_max_row_group_bytes(None)
5262 .build();
5263 let mut writer =
5264 ArrowWriter::try_new(file.try_clone().unwrap(), schema.clone(), Some(props)).unwrap();
5265
5266 let first_array = StringArray::from(
5267 (0..10)
5268 .map(|i| format!("{i:0>100}"))
5269 .collect::<Vec<String>>(),
5270 );
5271 let first_batch =
5272 RecordBatch::try_new(schema.clone(), vec![Arc::new(first_array)]).unwrap();
5273 writer.write(&first_batch).unwrap();
5274 assert_eq!(writer.in_progress_rows(), 10);
5275
5276 writer.max_row_group_bytes = Some(1);
5279
5280 let second_array = StringArray::from(vec!["x".to_string()]);
5281 let second_batch =
5282 RecordBatch::try_new(schema.clone(), vec![Arc::new(second_array)]).unwrap();
5283 writer.write(&second_batch).unwrap();
5284 writer.close().unwrap();
5285 let builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
5286
5287 assert_eq!(
5288 &row_group_sizes(builder.metadata()),
5289 &[10, 1],
5290 "The second write should flush an oversized in-progress row group first",
5291 );
5292 }
5293
5294 #[test]
5295 fn test_row_group_limit_both_row_wins_single_batch() {
5297 let props = WriterProperties::builder()
5298 .set_max_row_group_row_count(Some(200)) .set_max_row_group_bytes(Some(1024 * 1024)) .build();
5301
5302 let builder = write_batches(
5303 WriteBatchesShape {
5304 num_batches: 1,
5305 row_size: 4,
5306 rows_per_batch: 1000,
5307 },
5308 props,
5309 );
5310
5311 assert_eq!(
5312 &row_group_sizes(builder.metadata()),
5313 &[200, 200, 200, 200, 200],
5314 "Row limit should trigger before byte limit"
5315 );
5316 }
5317
5318 #[test]
5319 fn test_row_group_limit_both_row_wins_multiple_batches() {
5321 let props = WriterProperties::builder()
5322 .set_max_row_group_row_count(Some(5)) .set_max_row_group_bytes(Some(9999)) .build();
5325
5326 let builder = write_batches(
5327 WriteBatchesShape {
5328 num_batches: 10,
5329 rows_per_batch: 10,
5330 row_size: 100,
5331 },
5332 props,
5333 );
5334
5335 assert_eq!(
5336 &row_group_sizes(builder.metadata()),
5337 &[5; 20],
5338 "Row limit should trigger before byte limit"
5339 );
5340 }
5341
5342 #[test]
5343 fn test_row_group_limit_both_bytes_wins() {
5345 let props = WriterProperties::builder()
5346 .set_max_row_group_row_count(Some(1000)) .set_max_row_group_bytes(Some(3500)) .build();
5349
5350 let builder = write_batches(
5351 WriteBatchesShape {
5352 num_batches: 10,
5353 rows_per_batch: 10,
5354 row_size: 100,
5355 },
5356 props,
5357 );
5358
5359 let sizes = row_group_sizes(builder.metadata());
5360
5361 assert!(
5362 sizes.len() > 1,
5363 "Byte limit should trigger before row limit, got {sizes:?}",
5364 );
5365
5366 assert!(
5367 sizes.iter().all(|&s| s < 1000),
5368 "No row group should hit the row limit"
5369 );
5370
5371 let total_rows: i64 = sizes.iter().sum();
5372 assert_eq!(total_rows, 100, "Total rows should be preserved");
5373 }
5374
5375 #[test]
5376 fn test_row_group_limit_both_apply_to_same_batch() {
5379 let props = WriterProperties::builder()
5380 .set_max_row_group_row_count(Some(15))
5381 .set_max_row_group_bytes(Some(1500))
5382 .build();
5383
5384 let builder = write_batches(
5385 WriteBatchesShape {
5386 num_batches: 2,
5387 rows_per_batch: 10,
5388 row_size: 100,
5389 },
5390 props,
5391 );
5392
5393 assert_eq!(
5394 &row_group_sizes(builder.metadata()),
5395 &[14, 6],
5396 "Byte limit should still apply to a batch the row limit already split"
5397 );
5398 }
5399
5400 #[test]
5401 fn test_row_group_limit_bytes_splits_first_batch() {
5404 let props = WriterProperties::builder()
5405 .set_max_row_group_row_count(None)
5406 .set_max_row_group_bytes(Some(3500))
5407 .build();
5408
5409 let builder = write_batches(
5410 WriteBatchesShape {
5411 num_batches: 3,
5412 rows_per_batch: 100,
5413 row_size: 100,
5414 },
5415 props,
5416 );
5417
5418 let sizes = row_group_sizes(builder.metadata());
5419 assert!(
5420 sizes.iter().all(|&s| s < 100),
5421 "No row group should hold a whole batch, got {sizes:?}",
5422 );
5423 assert_eq!(
5424 sizes.iter().sum::<i64>(),
5425 300,
5426 "Total rows should be preserved"
5427 );
5428 for row_group in builder.metadata().row_groups() {
5429 assert!(
5430 row_group.total_byte_size() <= 3500 + 100,
5431 "Row groups should stay within one row of the byte limit, got {}",
5432 row_group.total_byte_size(),
5433 );
5434 }
5435 }
5436
5437 #[test]
5438 fn arrow_column_chunk_close_mut_drops_column_index() {
5439 use crate::arrow::ArrowSchemaConverter;
5440 use crate::file::writer::SerializedFileWriter;
5441
5442 let schema = Arc::new(Schema::new(vec![Field::new("i", DataType::Int32, false)]));
5443 let props = Arc::new(
5444 WriterProperties::builder()
5445 .set_statistics_enabled(EnabledStatistics::Page)
5446 .build(),
5447 );
5448 let parquet_schema = ArrowSchemaConverter::new()
5449 .with_coerce_types(props.coerce_types())
5450 .convert(&schema)
5451 .unwrap();
5452
5453 let mut buf = Vec::with_capacity(1024);
5454 let mut writer =
5455 SerializedFileWriter::new(&mut buf, parquet_schema.root_schema_ptr(), props.clone())
5456 .unwrap();
5457
5458 let factory = ArrowRowGroupWriterFactory::new(&writer, Arc::clone(&schema));
5459 let mut col_writers = factory.create_column_writers(0).unwrap();
5460 let arr: ArrayRef = Arc::new(Int32Array::from_iter_values(0..64));
5461 for leaves in compute_leaves(schema.field(0), &arr).unwrap() {
5462 col_writers[0].write(&leaves).unwrap();
5463 }
5464 let mut chunk = col_writers.pop().unwrap().close().unwrap();
5465
5466 assert!(
5468 chunk.close().column_index.is_some(),
5469 "EnabledStatistics::Page should produce a column_index"
5470 );
5471
5472 chunk.close_mut().column_index = None;
5474 assert!(chunk.close().column_index.is_none());
5475
5476 let mut rg = writer.next_row_group().unwrap();
5477 chunk.append_to_row_group(&mut rg).unwrap();
5478 rg.close().unwrap();
5479 let file_meta = writer.close().unwrap();
5480
5481 let cc = file_meta.row_group(0).column(0);
5484 assert!(cc.column_index_range().is_none());
5485 }
5486
5487 #[test]
5488 #[cfg_attr(miri, ignore)] fn test_number_distinct_values_exact_count() {
5490 let cardinality = 50u32;
5493 let array: ArrayRef = Arc::new(Int32Array::from_iter((0..100_000u32).map(|i| {
5494 if i % 7 == 0 {
5495 None
5496 } else {
5497 Some((i % cardinality) as i32)
5498 }
5499 })));
5500 let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int32, true)]));
5501 let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
5502
5503 let props = WriterProperties::builder()
5504 .set_write_row_group_number_distinct_values(true)
5505 .build();
5506 let mut buf = Vec::new();
5507 let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap();
5508 writer.write(&batch).unwrap();
5509 let metadata = writer.close().unwrap();
5510
5511 let count = metadata
5512 .row_group(0)
5513 .column(0)
5514 .statistics()
5515 .and_then(|s| s.distinct_count_opt())
5516 .expect("distinct_count should be set");
5517 assert_eq!(count, cardinality as u64);
5519 }
5520
5521 #[test]
5522 fn test_number_distinct_values_view_types() {
5523 let cardinality = 5u32;
5526 let distinct_strings = ["alpha", "beta", "gamma", "delta", "epsilon"];
5527
5528 let string_view_col: ArrayRef = Arc::new(StringViewArray::from_iter((0..30u32).map(|i| {
5529 if i % 4 == 0 {
5530 None
5531 } else {
5532 Some(distinct_strings[(i % cardinality) as usize])
5533 }
5534 })));
5535
5536 let schema = Arc::new(Schema::new(vec![Field::new(
5537 "string_view_col",
5538 DataType::Utf8View,
5539 true,
5540 )]));
5541 let batch = RecordBatch::try_new(schema, vec![string_view_col]).unwrap();
5542
5543 let props = WriterProperties::builder()
5544 .set_write_row_group_number_distinct_values(true)
5545 .build();
5546 let mut parquet_bytes = Vec::new();
5547 let mut writer =
5548 ArrowWriter::try_new(&mut parquet_bytes, batch.schema(), Some(props)).unwrap();
5549 writer.write(&batch).unwrap();
5550 let metadata = writer.close().unwrap();
5551
5552 let distinct_count = metadata
5553 .row_group(0)
5554 .column(0)
5555 .statistics()
5556 .and_then(|s| s.distinct_count_opt())
5557 .expect("distinct_count should be set for Utf8View column");
5558 assert_eq!(distinct_count, cardinality as u64);
5559 }
5560
5561 #[test]
5562 fn test_number_distinct_values_not_written_by_default() {
5563 let array: ArrayRef = Arc::new(Int32Array::from_iter_values(0..100));
5564 let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int32, false)]));
5565 let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
5566
5567 let mut buf = Vec::new();
5568 let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), None).unwrap();
5569 writer.write(&batch).unwrap();
5570 let metadata = writer.close().unwrap();
5571
5572 let count = metadata
5573 .row_group(0)
5574 .column(0)
5575 .statistics()
5576 .and_then(|s| s.distinct_count_opt());
5577 assert!(count.is_none());
5578 }
5579
5580 #[test]
5581 fn test_dictionary_ndv_single_batch() {
5582 let keys = Int32Array::from(vec![0, 1, 2, 0, 1, 2, 0, 1, 2]);
5586 let values: ArrayRef = Arc::new(StringArray::from(vec!["cat", "dog", "bird"]));
5587 let dict: ArrayRef = Arc::new(DictionaryArray::<Int32Type>::try_new(keys, values).unwrap());
5588
5589 let schema = Arc::new(Schema::new(vec![Field::new(
5590 "x",
5591 DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
5592 false,
5593 )]));
5594 let batch = RecordBatch::try_new(schema, vec![dict]).unwrap();
5595
5596 let props = WriterProperties::builder()
5597 .set_write_row_group_number_distinct_values(true)
5598 .build();
5599 let mut buf = Vec::new();
5600 let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap();
5601 writer.write(&batch).unwrap();
5602 let metadata = writer.close().unwrap();
5603
5604 let count = metadata
5605 .row_group(0)
5606 .column(0)
5607 .statistics()
5608 .and_then(|s| s.distinct_count_opt())
5609 .expect("distinct_count should be set");
5610 assert_eq!(count, 3);
5611 }
5612
5613 #[test]
5614 fn test_dictionary_ndv_excludes_unreferenced_values() {
5615 let keys = Int32Array::from(vec![0, 1, 0, 1]);
5618 let values: ArrayRef = Arc::new(StringArray::from(vec!["cat", "dog", "unreferenced"]));
5619 let dict: ArrayRef = Arc::new(DictionaryArray::<Int32Type>::try_new(keys, values).unwrap());
5620
5621 let schema = Arc::new(Schema::new(vec![Field::new(
5622 "x",
5623 DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
5624 false,
5625 )]));
5626 let batch = RecordBatch::try_new(schema, vec![dict]).unwrap();
5627
5628 let props = WriterProperties::builder()
5629 .set_write_row_group_number_distinct_values(true)
5630 .build();
5631 let mut buf = Vec::new();
5632 let mut writer = ArrowWriter::try_new(&mut buf, batch.schema(), Some(props)).unwrap();
5633 writer.write(&batch).unwrap();
5634 let metadata = writer.close().unwrap();
5635
5636 let count = metadata
5637 .row_group(0)
5638 .column(0)
5639 .statistics()
5640 .and_then(|s| s.distinct_count_opt())
5641 .expect("distinct_count should be set");
5642 assert_eq!(
5643 count, 2,
5644 "unreferenced dictionary values must not count toward NDV"
5645 );
5646 }
5647
5648 #[test]
5649 fn test_dictionary_ndv_across_batches_regression() {
5650 let make_dict_batch = |a: &str, b: &str| -> RecordBatch {
5652 let keys = Int32Array::from(vec![0, 1, 0, 1]);
5653 let values: ArrayRef = Arc::new(StringArray::from(vec![a, b]));
5654 let dict: ArrayRef =
5655 Arc::new(DictionaryArray::<Int32Type>::try_new(keys, values).unwrap());
5656 let schema = Arc::new(Schema::new(vec![Field::new(
5657 "x",
5658 DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8)),
5659 false,
5660 )]));
5661 RecordBatch::try_new(schema, vec![dict]).unwrap()
5662 };
5663
5664 let batch1 = make_dict_batch("cat", "dog");
5667 let batch2 = make_dict_batch("fish", "cat");
5668
5669 let props = WriterProperties::builder()
5670 .set_write_row_group_number_distinct_values(true)
5671 .build();
5672 let mut buf = Vec::new();
5673 let mut writer = ArrowWriter::try_new(&mut buf, batch1.schema(), Some(props)).unwrap();
5674 writer.write(&batch1).unwrap();
5675 writer.write(&batch2).unwrap();
5676 let metadata = writer.close().unwrap();
5677
5678 let count = metadata
5679 .row_group(0)
5680 .column(0)
5681 .statistics()
5682 .and_then(|s| s.distinct_count_opt())
5683 .expect("distinct_count should be set");
5684 assert_eq!(
5685 count, 3,
5686 "NDV should count distinct values, not distinct key indices"
5687 );
5688 }
5689
5690 #[test]
5691 fn ree_struct_with_ree_child() {
5692 let run_ends = Int32Array::from(vec![2i32, 3, 5]);
5695
5696 let col_a: ArrayRef = Arc::new(
5697 RunArray::try_new(
5698 &run_ends,
5699 &StringArray::from(vec![Some("foo"), None, Some("bar")]),
5700 )
5701 .unwrap(),
5702 );
5703 let col_b: ArrayRef = Arc::new(
5704 RunArray::try_new(&run_ends, &Int32Array::from(vec![Some(1), None, Some(2)])).unwrap(),
5705 );
5706
5707 let struct_array: ArrayRef = Arc::new(StructArray::new(
5708 Fields::from(vec![
5709 Field::new("a", col_a.data_type().clone(), true),
5710 Field::new("b", col_b.data_type().clone(), true),
5711 ]),
5712 vec![col_a, col_b],
5713 None,
5714 ));
5715
5716 let schema = Arc::new(Schema::new(vec![Field::new(
5717 "row",
5718 struct_array.data_type().clone(),
5719 true,
5720 )]));
5721 let batch = RecordBatch::try_new(schema.clone(), vec![struct_array]).unwrap();
5722
5723 let mut buf = Vec::new();
5724 let mut writer = ArrowWriter::try_new(&mut buf, schema, None).unwrap();
5725 writer.write(&batch).unwrap();
5726 let metadata = writer.close().unwrap();
5727
5728 let parquet_schema = metadata.file_metadata().schema_descr();
5729 assert_eq!(parquet_schema.num_columns(), 2);
5730 assert_eq!(
5731 parquet_schema.column(0).physical_type(),
5732 crate::basic::Type::BYTE_ARRAY
5733 );
5734 assert_eq!(parquet_schema.column(0).path().string(), "row.a");
5735 assert_eq!(
5736 parquet_schema.column(1).physical_type(),
5737 crate::basic::Type::INT32
5738 );
5739 assert_eq!(parquet_schema.column(1).path().string(), "row.b");
5740 }
5741
5742 #[test]
5749 fn test_decimal_byte_array_statistics_signed_comparison() {
5750 use crate::basic::{LogicalType, Repetition, Type as PhysicalType};
5751 use crate::data_type::{ByteArray, ByteArrayType};
5752 use crate::schema::types::Type as SchemaType;
5753
5754 const VALUES: [&[u8]; 3] = [&[0xffu8], &[0x00], &[0x01]];
5756
5757 fn new_file(physical: PhysicalType) -> SerializedFileWriter<Vec<u8>> {
5758 let mut field = SchemaType::primitive_type_builder("value", physical)
5759 .with_repetition(Repetition::REQUIRED)
5760 .with_logical_type(Some(LogicalType::decimal(0, 2)))
5761 .with_precision(2)
5762 .with_scale(0);
5763 if physical == PhysicalType::FIXED_LEN_BYTE_ARRAY {
5764 field = field.with_length(1);
5765 }
5766 let root = SchemaType::group_type_builder("root")
5767 .with_fields(vec![Arc::new(field.build().unwrap())])
5768 .build()
5769 .unwrap();
5770 SerializedFileWriter::new(Vec::new(), Arc::new(root), Default::default()).unwrap()
5771 }
5772
5773 fn read_minmax(file: Vec<u8>) -> (i8, i8) {
5774 let reader = SerializedFileReader::new(Bytes::from(file)).unwrap();
5775 let metadata = reader.metadata();
5776 assert_eq!(metadata.file_metadata().num_rows(), 3);
5777 let stats = metadata
5778 .row_group(0)
5779 .column(0)
5780 .statistics()
5781 .expect("missing statistics");
5782 (
5783 stats.min_bytes_opt().unwrap()[0] as i8,
5784 stats.max_bytes_opt().unwrap()[0] as i8,
5785 )
5786 }
5787
5788 fn direct_parquet() -> Vec<u8> {
5789 let mut file = new_file(PhysicalType::BYTE_ARRAY);
5790 let mut group = file.next_row_group().unwrap();
5791 let mut column = group.next_column().unwrap().unwrap();
5792 let values = VALUES
5793 .iter()
5794 .map(|v| ByteArray::from(v.to_vec()))
5795 .collect::<Vec<_>>();
5796 column
5797 .typed::<ByteArrayType>()
5798 .write_batch(&values, None, None)
5799 .unwrap();
5800 column.close().unwrap();
5801 group.close().unwrap();
5802 file.into_inner().unwrap()
5803 }
5804
5805 fn arrow_bounds(mut file: SerializedFileWriter<Vec<u8>>, array: ArrayRef) -> Vec<u8> {
5806 let field = Field::new("value", array.data_type().clone(), false);
5807 let schema = Arc::new(Schema::new(vec![field.clone()]));
5808 let factory = ArrowRowGroupWriterFactory::new(&file, schema);
5809 let mut writer: ArrowColumnWriter = factory.create_column_writers(0).unwrap().remove(0);
5810 let leaf = compute_leaves(&field, &array).unwrap().remove(0);
5811 writer.write(&leaf).unwrap();
5812 let mut group = file.next_row_group().unwrap();
5813 writer
5814 .close()
5815 .unwrap()
5816 .append_to_row_group(&mut group)
5817 .unwrap();
5818 group.close().unwrap();
5819 file.into_inner().unwrap()
5820 }
5821
5822 fn arrow_binary() -> Vec<u8> {
5823 let file = new_file(PhysicalType::BYTE_ARRAY);
5824 let array: ArrayRef = Arc::new(BinaryArray::from_vec(VALUES.to_vec()));
5825 arrow_bounds(file, array)
5826 }
5827
5828 fn arrow_fixed_size_binary() -> Vec<u8> {
5829 let file = new_file(PhysicalType::FIXED_LEN_BYTE_ARRAY);
5830 let array: ArrayRef =
5831 Arc::new(FixedSizeBinaryArray::try_from_iter(VALUES.into_iter()).unwrap());
5832 arrow_bounds(file, array)
5833 }
5834
5835 let direct = read_minmax(direct_parquet());
5836 let fixed = read_minmax(arrow_fixed_size_binary());
5837 let binary = read_minmax(arrow_binary());
5838
5839 assert_eq!(direct, (-1, 1), "incorrect direct Parquet stats");
5840 assert_eq!(fixed, (-1, 1), "incorrect Arrow FixedSizeBinaryArray stats");
5841 assert_eq!(binary, (-1, 1), "incorrect Arrow BinaryArray stats");
5842 }
5843}