1mod cli;
39
40use clap::Parser;
41use parquet::data_type::ByteArray;
42use parquet::errors::{ParquetError, Result};
43use parquet::file::page_index::column_index::{
44 ByteArrayColumnIndex, ColumnIndexMetaData, PrimitiveColumnIndex,
45};
46use parquet::file::page_index::offset_index::{OffsetIndexMetaData, PageLocation};
47use parquet::file::reader::{FileReader, SerializedFileReader};
48use parquet::file::serialized_reader::ReadOptionsBuilder;
49use std::fs::File;
50
51#[derive(Debug, Parser)]
52#[clap(author, version, about("Prints the page index of a parquet file"), long_about = None)]
53struct Args {
54 #[clap(help("Path to a parquet file"))]
55 file: String,
56
57 #[clap(help(
58 "Dot-separated column path to get index for. Literal dots can be escaped as `\\.`"
59 ))]
60 column: String,
61}
62
63impl Args {
64 fn run(&self) -> Result<()> {
65 let file = File::open(&self.file)?;
66 let options = ReadOptionsBuilder::new().with_page_index().build();
67 let reader = SerializedFileReader::new_with_options(file, options)?;
68
69 let schema = reader.metadata().file_metadata().schema_descr();
70 let column_path = cli::parse_column_path(&self.column).map_err(ParquetError::General)?;
71 let column_idx = schema
72 .columns()
73 .iter()
74 .position(|x| x.path().parts() == column_path.as_slice())
75 .ok_or_else(|| {
76 ParquetError::General(format!("Failed to find column {}", self.column))
77 })?;
78
79 let page_index = reader
80 .metadata()
81 .page_index()
82 .ok_or_else(|| ParquetError::General("Page index not found".to_string()))?;
83
84 for row_group_idx in 0..reader.metadata().num_row_groups() {
86 let row_group = reader.metadata().row_group(row_group_idx);
87 println!("Row Group: {row_group_idx}");
88 let offset_index = page_index
89 .offset_index(row_group_idx, column_idx)
90 .ok_or_else(|| {
91 ParquetError::General(format!(
92 "No offset index for row group {row_group_idx} column chunk {column_idx}"
93 ))
94 })?;
95
96 let row_counts =
97 compute_row_counts(offset_index.page_locations(), row_group.num_rows());
98 match page_index.column_index(row_group_idx, column_idx) {
99 None => println!("NO INDEX"),
100 Some(ColumnIndexMetaData::BOOLEAN(v)) => {
101 print_index::<bool>(v, offset_index, &row_counts)?
102 }
103 Some(ColumnIndexMetaData::INT32(v)) => print_index(v, offset_index, &row_counts)?,
104 Some(ColumnIndexMetaData::INT64(v)) => print_index(v, offset_index, &row_counts)?,
105 Some(ColumnIndexMetaData::INT96(v)) => print_index(v, offset_index, &row_counts)?,
106 Some(ColumnIndexMetaData::FLOAT(v)) => print_index(v, offset_index, &row_counts)?,
107 Some(ColumnIndexMetaData::DOUBLE(v)) => print_index(v, offset_index, &row_counts)?,
108 Some(ColumnIndexMetaData::BYTE_ARRAY(v)) => {
109 print_bytes_index(v, offset_index, &row_counts)?
110 }
111 Some(ColumnIndexMetaData::FIXED_LEN_BYTE_ARRAY(v)) => {
112 print_bytes_index(v, offset_index, &row_counts)?
113 }
114 }
115 }
116 Ok(())
117 }
118}
119
120fn compute_row_counts(offset_index: &[PageLocation], rows: i64) -> Vec<i64> {
122 if offset_index.is_empty() {
123 return vec![];
124 }
125
126 let mut last = offset_index[0].first_row_index;
127 let mut out = Vec::with_capacity(offset_index.len());
128 for o in offset_index.iter().skip(1) {
129 out.push(o.first_row_index - last);
130 last = o.first_row_index;
131 }
132 out.push(rows - last);
133 out
134}
135
136fn print_index<T: std::fmt::Display>(
138 column_index: &PrimitiveColumnIndex<T>,
139 offset_index: &OffsetIndexMetaData,
140 row_counts: &[i64],
141) -> Result<()> {
142 if column_index.num_pages() as usize != offset_index.page_locations().len() {
143 return Err(ParquetError::General(format!(
144 "Index length mismatch, got {} and {}",
145 column_index.num_pages(),
146 offset_index.page_locations().len()
147 )));
148 }
149
150 for (idx, (((min, max), o), row_count)) in column_index
151 .min_values_iter()
152 .zip(column_index.max_values_iter())
153 .zip(offset_index.page_locations())
154 .zip(row_counts)
155 .enumerate()
156 {
157 print!(
158 "Page {:>5} at offset {:>10} with length {:>10} and row count {:>10}",
159 idx, o.offset, o.compressed_page_size, row_count
160 );
161 match min {
162 Some(m) => print!(", min {m:>10}"),
163 None => print!(", min {:>10}", "NONE"),
164 }
165
166 match max {
167 Some(m) => print!(", max {m:>10}"),
168 None => print!(", max {:>10}", "NONE"),
169 }
170 println!()
171 }
172
173 Ok(())
174}
175
176fn print_bytes_index(
177 column_index: &ByteArrayColumnIndex,
178 offset_index: &OffsetIndexMetaData,
179 row_counts: &[i64],
180) -> Result<()> {
181 if column_index.num_pages() as usize != offset_index.page_locations().len() {
182 return Err(ParquetError::General(format!(
183 "Index length mismatch, got {} and {}",
184 column_index.num_pages(),
185 offset_index.page_locations().len()
186 )));
187 }
188
189 for (idx, (((min, max), o), row_count)) in column_index
190 .min_values_iter()
191 .zip(column_index.max_values_iter())
192 .zip(offset_index.page_locations())
193 .zip(row_counts)
194 .enumerate()
195 {
196 print!(
197 "Page {:>5} at offset {:>10} with length {:>10} and row count {:>10}",
198 idx, o.offset, o.compressed_page_size, row_count
199 );
200 match min {
201 Some(m) => match String::from_utf8(m.to_vec()) {
202 Ok(s) => print!(", min {s:>10}"),
203 Err(_) => print!(", min {:>10}", ByteArray::from(m)),
204 },
205 None => print!(", min {:>10}", "NONE"),
206 }
207
208 match max {
209 Some(m) => match String::from_utf8(m.to_vec()) {
210 Ok(s) => print!(", max {s:>10}"),
211 Err(_) => print!(", min {:>10}", ByteArray::from(m)),
212 },
213 None => print!(", max {:>10}", "NONE"),
214 }
215 println!()
216 }
217
218 Ok(())
219}
220
221fn main() -> Result<()> {
222 Args::parse().run()
223}