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