Parquet
Parquet files for data lakes, DuckDB, Spark and pandas: rows written with their column types found, and read back as rows.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use parquet.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use parquet_write, parquet_read from parquet (Quillo 0.6).
parquet_write(path: str, rows: list, columns: list) → intor an errorWrites rows (maps) to a new Parquet file at path (folders made), with columns in that order (empty: the first row's keys, sorted), Snappy-compressed; gives the number of rows. Each column's type comes from its values: booleans, ints (INT64), floats (DOUBLE, also when ints and floats mix), else text (UTF-8; lists and maps as JSON). Any value may be null.
parquet_read(path: str, limit: int) → listor an errorThe rows of a Parquet file as maps (at most limit, up to a million; 0 for the most), from any writer (pandas, Spark, DuckDB): uncompressed, Snappy or gzip. Dates and timestamps come as text, decimals as numbers, nested columns as lists and maps.
parquet_info(path: str) → anyor an errorWhat is in a Parquet file without reading its rows: { rows, row_groups, created_by, columns }, each column { name, type } (its physical type, and its logical one when it has one).
Get it
$ quillo install parquet # parquet.qwer into this folder$ quillo install parquet --example # and its example, parquet.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/parquet.qwer$ curl -O https://quillolang.org/plugins/files/parquet.qweThen, in your program, all of it or only what you call:
use parquet.qweruse parquet_write, parquet_read from parquet // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
parquet = { version = "53", default-features = false, features = ["snap", "flate2", "json"] }Example
// A nightly export of orders as Parquet for the analytics team, and a look inside it.//// quillo run parquet.qwe then open http://localhost:8080/exportuse "parquet.qwer"let ORDERS = [ { id: 1, customer: "Ada Lovelace", total: 120.5, paid: true, placed: "2026-11-02T09:15:00Z", items: ["B7", "C2"] }, { id: 2, customer: "Grace Hopper", total: 99, paid: false, placed: "2026-11-02T10:40:00Z", items: ["A1"] }, { id: 3, customer: "Alan Turing", total: null, paid: true, placed: "2026-11-03T08:05:00Z", items: [] },]route GET /export { let n = parquet_write("data/orders.parquet", ORDERS, ["id", "customer", "total", "paid", "placed", "items"])? { written: n, info: parquet_info("data/orders.parquet")? }}route GET /import { parquet_write("data/orders.parquet", ORDERS, [])? parquet_read("data/orders.parquet", 0)?}$ quillo run parquet.qwe$ curl 'localhost:8080/export'$ curl 'localhost:8080/import'Source
//! Parquet files, the columnar format of data lakes, DuckDB, Spark and pandas: rows written with their column types found, and Parquet files read back as rows.//! version: 1.0.0//!//! [dependencies]//! parquet = { version = "53", default-features = false, features = ["snap", "flate2", "json"] }use parquet::basic::{Compression, ConvertedType, LogicalType, Repetition, Type as Physical};use parquet::column::writer::ColumnWriter;use parquet::data_type::ByteArray;use parquet::file::properties::WriterProperties;use parquet::file::reader::{FileReader, SerializedFileReader};use parquet::file::writer::SerializedFileWriter;use parquet::schema::types::Type;use serde_json::{json, Value};use std::sync::Arc;/// At most a million rows are read at once.const MAX_ROWS: usize = 1_000_000;#[derive(Clone, Copy, PartialEq)]enum Kind { Bool, Int, Float, Text,}fn kind_of(rows: &[std::collections::HashMap<String, Value>], col: &str) -> Kind { let mut k: Option<Kind> = None; for r in rows { let this = match r.get(col) { None | Some(Value::Null) => continue, Some(Value::Bool(_)) => Kind::Bool, Some(Value::Number(n)) if n.is_i64() || n.is_u64() => Kind::Int, Some(Value::Number(_)) => Kind::Float, Some(_) => Kind::Text, }; k = Some(match (k, this) { (None, t) => t, (Some(a), b) if a == b => a, (Some(Kind::Int), Kind::Float) | (Some(Kind::Float), Kind::Int) => Kind::Float, _ => Kind::Text, }); } k.unwrap_or(Kind::Text)}fn text(v: &Value) -> String { match v { Value::String(s) => s.clone(), other => other.to_string(), }}/// Writes `rows` (maps) to a new Parquet file at `path` (folders made), with `columns` in that order (empty: the first/// row's keys, sorted), Snappy-compressed; gives the number of rows. Each column's type comes from its values: booleans,/// ints (INT64), floats (DOUBLE, also when ints and floats mix), else text (UTF-8; lists and maps as JSON). Any value may be null.pub fn parquet_write(path: &str, rows: Vec<std::collections::HashMap<String, serde_json::Value>>, columns: Vec<String>) -> Result<i64, String> { let cols: Vec<String> = if columns.is_empty() { let mut k: Vec<String> = rows.first().map(|r| r.keys().cloned().collect()).unwrap_or_default(); k.sort(); k } else { columns }; if cols.is_empty() { return Err("no columns: give them, or rows with keys".into()); } let kinds: Vec<Kind> = cols.iter().map(|c| kind_of(&rows, c)).collect(); let mut fields = vec![]; for (c, k) in cols.iter().zip(&kinds) { let b = match k { Kind::Bool => Type::primitive_type_builder(c, Physical::BOOLEAN), Kind::Int => Type::primitive_type_builder(c, Physical::INT64), Kind::Float => Type::primitive_type_builder(c, Physical::DOUBLE), Kind::Text => Type::primitive_type_builder(c, Physical::BYTE_ARRAY).with_logical_type(Some(LogicalType::String)).with_converted_type(ConvertedType::UTF8), }; fields.push(Arc::new(b.with_repetition(Repetition::OPTIONAL).build().map_err(|e| format!("column `{}`: {}", c, e))?)); } let schema = Arc::new(Type::group_type_builder("schema").with_fields(fields).build().map_err(|e| e.to_string())?); let props = Arc::new(WriterProperties::builder().set_compression(Compression::SNAPPY).build()); if let Some(dir) = std::path::Path::new(path).parent() { if !dir.as_os_str().is_empty() { std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?; } } let file = std::fs::File::create(path).map_err(|e| format!("{}: {}", path, e))?; let mut w = SerializedFileWriter::new(file, schema, props).map_err(|e| e.to_string())?; // Row groups of 100,000 rows keep memory flat for large exports. for chunk in rows.chunks(100_000) { let mut rg = w.next_row_group().map_err(|e| e.to_string())?; let mut i = 0; while let Some(mut col) = rg.next_column().map_err(|e| e.to_string())? { let (name, k) = (&cols[i], kinds[i]); let vals: Vec<Option<&Value>> = chunk.iter().map(|r| r.get(name).filter(|v| !v.is_null())).collect(); let defs: Vec<i16> = vals.iter().map(|v| v.is_some() as i16).collect(); let present = vals.iter().flatten(); let bad = |what: &str| format!("column `{}`: {}", name, what); let r = match (col.untyped(), k) { (ColumnWriter::BoolColumnWriter(cw), Kind::Bool) => cw.write_batch(&present.map(|v| v.as_bool().unwrap_or(false)).collect::<Vec<_>>(), Some(&defs), None), (ColumnWriter::Int64ColumnWriter(cw), Kind::Int) => cw.write_batch(&present.map(|v| v.as_i64().unwrap_or(0)).collect::<Vec<_>>(), Some(&defs), None), (ColumnWriter::DoubleColumnWriter(cw), Kind::Float) => cw.write_batch(&present.map(|v| v.as_f64().unwrap_or(0.0)).collect::<Vec<_>>(), Some(&defs), None), (ColumnWriter::ByteArrayColumnWriter(cw), Kind::Text) => cw.write_batch(&present.map(|v| ByteArray::from(text(v).as_str())).collect::<Vec<_>>(), Some(&defs), None), _ => return Err(bad("type mix-up")), }; r.map_err(|e| bad(&e.to_string()))?; col.close().map_err(|e| bad(&e.to_string()))?; i += 1; } rg.close().map_err(|e| e.to_string())?; } w.close().map_err(|e| e.to_string())?; Ok(rows.len() as i64)}fn reader(path: &str) -> Result<SerializedFileReader<std::fs::File>, String> { let f = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?; SerializedFileReader::new(f).map_err(|e| format!("{}: not a Parquet file: {}", path, e))}/// The rows of a Parquet file as maps (at most `limit`, up to a million; 0 for the most), from any writer (pandas, Spark,/// DuckDB): uncompressed, Snappy or gzip. Dates and timestamps come as text, decimals as numbers, nested columns as lists and maps.pub fn parquet_read(path: &str, limit: i64) -> Result<Vec<Value>, String> { let max = if limit <= 0 { MAX_ROWS } else { (limit as usize).min(MAX_ROWS) }; let r = reader(path)?; let mut out = vec![]; for row in r.get_row_iter(None).map_err(|e| e.to_string())?.take(max) { out.push(row.map_err(|e| e.to_string())?.to_json_value()); } Ok(out)}/// What is in a Parquet file without reading its rows: `{ rows, row_groups, created_by, columns }`, each column/// `{ name, type }` (its physical type, and its logical one when it has one).pub fn parquet_info(path: &str) -> Result<Value, String> { let r = reader(path)?; let m = r.metadata(); let fm = m.file_metadata(); let cols: Vec<Value> = fm.schema_descr().columns().iter().map(|c| { let t = match c.logical_type() { Some(l) => format!("{} ({:?})", c.physical_type(), l), None => c.physical_type().to_string(), }; json!({ "name": c.path().string(), "type": t }) }).collect(); Ok(json!({ "rows": fm.num_rows(), "row_groups": m.num_row_groups(), "created_by": fm.created_by(), "columns": cols }))}Related
avro.qwer · 1.0.0Apache Avro data files: rows written with a schema, as Kafka, Hadoop and BigQuery use, and read back with the schema they carry.9 views · 7 downloadsFixed width fixed_width.qwer · 1.0.0Fixed-width records read and written from a column spec: bank payment files, payroll exports and mainframe formats.8 views · 9 downloadsBibTeX bibtex.qwer · 1.0.0BibTeX and BibLaTeX bibliographies read, with LaTeX accents and macros resolved; reference lists in APA or IEEE style, CSL-JSON for Zotero and pandoc, and entries written back.7 views · 6 downloadsCSV csv.qwer · 1.0.0CSV in and out: rows as maps keyed by the header, with quotes, commas and line breaks in cells handled.8 views · 9 downloadsINI ini.qwer · 1.0.0INI files read and written, sections and keys: php.ini, setup.cfg, .gitconfig and the settings of older software.8 views · 8 downloadsJSON Lines ndjson.qwer · 1.0.0JSON Lines (NDJSON) read a page at a time from files of any size, searched by field, appended to and written: logs, exports and imports.7 views · 7 downloadsThread 0 posts
On Parquet: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.