use std::sync::Arc;
use parquet::basic::Compression;
use parquet::column::writer::ColumnWriter;
use parquet::data_type::{ByteArray, FixedLenByteArray};
use parquet::file::metadata::KeyValue;
use parquet::file::properties::{EnabledStatistics, WriterProperties};
use parquet::file::writer::SerializedFileWriter;
use parquet::schema::parser::parse_message_type;
use parquet::schema::types::ColumnPath;
use uuid::Uuid;
use super::Metadata;
use crate::ids::VersionId;
pub const SCHEMA: &str = "1";
pub const SCHEMA_KEY: &str = "vcon-server.schema";
pub const LEVEL_KEY: &str = "vcon-server.metadata";
const FIXED: &str = "
required fixed_len_byte_array(16) version (UUID);
required int64 stored_at (TIMESTAMP(NANOS, true));
required binary sealed;";
const COUNTS: &str = "
optional binary form (STRING);
optional boolean gzip;
optional int64 size;
optional int64 created_at (TIMESTAMP(NANOS, true));
optional int64 updated_at (TIMESTAMP(NANOS, true));
optional int32 party_count;
optional int32 dialog_count;";
const FULL: &str = "
optional fixed_len_byte_array(16) uuid (UUID);
optional binary subject (STRING);
optional binary parties (JSON);";
const WITH_STATISTICS: [&str; 6] = [
"stored_at",
"size",
"created_at",
"updated_at",
"party_count",
"dialog_count",
];
pub fn schema(level: Metadata) -> String {
let more = match level {
Metadata::None => String::new(),
Metadata::Counts => COUNTS.to_owned(),
Metadata::Full => format!("{COUNTS}{FULL}"),
};
format!("message vcon_export {{{FIXED}{more}\n}}")
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
pub version: VersionId,
pub stored_at: i64,
pub sealed: Vec<u8>,
pub about: About,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct About {
pub form: Option<String>,
pub gzip: Option<bool>,
pub size: Option<i64>,
pub created_at: Option<i64>,
pub updated_at: Option<i64>,
pub party_count: Option<i32>,
pub dialog_count: Option<i32>,
pub uuid: Option<Uuid>,
pub subject: Option<String>,
pub parties: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Place {
pub group: u32,
pub row: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Sizes {
pub row_group: usize,
pub file: usize,
}
pub const SIZES: Sizes = Sizes {
row_group: 64 << 20,
file: 128 << 20,
};
#[derive(Debug)]
pub struct WriteError(pub parquet::errors::ParquetError);
impl From<parquet::errors::ParquetError> for WriteError {
fn from(e: parquet::errors::ParquetError) -> WriteError {
WriteError(e)
}
}
pub struct File {
writer: SerializedFileWriter<Vec<u8>>,
level: Metadata,
pending: Vec<Row>,
pending_bytes: usize,
sizes: Sizes,
rows: u64,
groups: u32,
}
impl std::fmt::Debug for File {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("File")
.field("level", &self.level)
.field("rows", &self.rows)
.finish_non_exhaustive()
}
}
impl File {
pub fn new(level: Metadata, sizes: Sizes) -> Result<File, WriteError> {
let schema = Arc::new(parse_message_type(&schema(level))?);
let mut properties = WriterProperties::builder()
.set_compression(Compression::UNCOMPRESSED)
.set_dictionary_enabled(false)
.set_statistics_enabled(EnabledStatistics::None)
.set_data_page_row_count_limit(1)
.set_key_value_metadata(Some(vec![
KeyValue::new(SCHEMA_KEY.to_owned(), SCHEMA.to_owned()),
KeyValue::new(LEVEL_KEY.to_owned(), level.as_str().to_owned()),
]));
for name in WITH_STATISTICS {
properties = properties
.set_column_statistics_enabled(ColumnPath::from(name), EnabledStatistics::Page);
}
let writer = SerializedFileWriter::new(Vec::new(), schema, Arc::new(properties.build()))?;
Ok(File {
writer,
level,
pending: Vec::new(),
pending_bytes: 0,
sizes,
rows: 0,
groups: 0,
})
}
pub fn push(&mut self, row: Row) -> Result<Place, WriteError> {
let too_many = || {
WriteError(parquet::errors::ParquetError::General(
"too many rows for one file".to_owned(),
))
};
let place = Place {
group: self.groups,
row: u32::try_from(self.pending.len()).map_err(|_| too_many())?,
};
self.pending_bytes += row.sealed.len();
self.pending.push(row);
self.rows += 1;
if self.pending_bytes >= self.sizes.row_group {
self.flush()?;
}
Ok(place)
}
fn flush(&mut self) -> Result<(), WriteError> {
if self.pending.is_empty() {
return Ok(());
}
let rows = std::mem::take(&mut self.pending);
self.pending_bytes = 0;
let mut group = self.writer.next_row_group()?;
while let Some(mut column) = group.next_column()? {
let name = column_name(&mut column);
write_column(&name, &rows, column.untyped())?;
column.close()?;
}
group.close()?;
self.groups += 1;
Ok(())
}
pub fn size(&self) -> usize {
self.writer.bytes_written() + self.pending_bytes
}
pub fn full(&self) -> bool {
self.size() >= self.sizes.file
}
pub fn rows(&self) -> u64 {
self.rows
}
pub fn finish(mut self) -> Result<Vec<u8>, WriteError> {
self.flush()?;
Ok(self.writer.into_inner()?)
}
}
fn column_name(column: &mut parquet::file::writer::SerializedColumnWriter<'_>) -> String {
let descr = match column.untyped() {
ColumnWriter::BoolColumnWriter(w) => w.get_descriptor().clone(),
ColumnWriter::Int32ColumnWriter(w) => w.get_descriptor().clone(),
ColumnWriter::Int64ColumnWriter(w) => w.get_descriptor().clone(),
ColumnWriter::Int96ColumnWriter(w) => w.get_descriptor().clone(),
ColumnWriter::FloatColumnWriter(w) => w.get_descriptor().clone(),
ColumnWriter::DoubleColumnWriter(w) => w.get_descriptor().clone(),
ColumnWriter::ByteArrayColumnWriter(w) => w.get_descriptor().clone(),
ColumnWriter::FixedLenByteArrayColumnWriter(w) => w.get_descriptor().clone(),
};
descr.name().to_owned()
}
fn write_column(name: &str, rows: &[Row], writer: &mut ColumnWriter<'_>) -> Result<(), WriteError> {
fn put<T: parquet::data_type::DataType>(
w: &mut parquet::column::writer::ColumnWriterImpl<'_, T>,
optional: bool,
values: impl Iterator<Item = Option<T::T>>,
) -> Result<(), WriteError>
where
T::T: Clone,
{
for value in values {
match (value, optional) {
(Some(v), false) => w.write_batch(&[v], None, None)?,
(Some(v), true) => w.write_batch(&[v], Some(&[1]), None)?,
(None, true) => w.write_batch(&[], Some(&[0]), None)?,
(None, false) => {
return Err(WriteError(parquet::errors::ParquetError::General(
"a required column has no value".to_owned(),
)));
}
};
}
Ok(())
}
let fixed = |b: &[u8]| FixedLenByteArray::from(b.to_vec());
let bytes = |b: &[u8]| ByteArray::from(b.to_vec());
fn a(row: &Row) -> &About {
&row.about
}
match (name, writer) {
("version", ColumnWriter::FixedLenByteArrayColumnWriter(w)) => {
put(w, false, rows.iter().map(|r| Some(fixed(&r.version.0))))
}
("stored_at", ColumnWriter::Int64ColumnWriter(w)) => {
put(w, false, rows.iter().map(|r| Some(r.stored_at)))
}
("sealed", ColumnWriter::ByteArrayColumnWriter(w)) => {
put(w, false, rows.iter().map(|r| Some(bytes(&r.sealed))))
}
("form", ColumnWriter::ByteArrayColumnWriter(w)) => put(
w,
true,
rows.iter()
.map(|r| a(r).form.as_deref().map(|t| bytes(t.as_bytes()))),
),
("gzip", ColumnWriter::BoolColumnWriter(w)) => put(w, true, rows.iter().map(|r| a(r).gzip)),
("size", ColumnWriter::Int64ColumnWriter(w)) => {
put(w, true, rows.iter().map(|r| a(r).size))
}
("created_at", ColumnWriter::Int64ColumnWriter(w)) => {
put(w, true, rows.iter().map(|r| a(r).created_at))
}
("updated_at", ColumnWriter::Int64ColumnWriter(w)) => {
put(w, true, rows.iter().map(|r| a(r).updated_at))
}
("party_count", ColumnWriter::Int32ColumnWriter(w)) => {
put(w, true, rows.iter().map(|r| a(r).party_count))
}
("dialog_count", ColumnWriter::Int32ColumnWriter(w)) => {
put(w, true, rows.iter().map(|r| a(r).dialog_count))
}
("uuid", ColumnWriter::FixedLenByteArrayColumnWriter(w)) => put(
w,
true,
rows.iter().map(|r| a(r).uuid.map(|u| fixed(u.as_bytes()))),
),
("subject", ColumnWriter::ByteArrayColumnWriter(w)) => put(
w,
true,
rows.iter()
.map(|r| a(r).subject.as_deref().map(|t| bytes(t.as_bytes()))),
),
("parties", ColumnWriter::ByteArrayColumnWriter(w)) => put(
w,
true,
rows.iter()
.map(|r| a(r).parties.as_deref().map(|t| bytes(t.as_bytes()))),
),
(other, _) => Err(WriteError(parquet::errors::ParquetError::General(format!(
"no value for column {other}"
)))),
}
}
#[cfg(test)]
pub(crate) mod tests {
use parquet::file::reader::{FileReader, SerializedFileReader};
use parquet::record::Field;
use super::*;
pub(crate) fn read(bytes: &[u8]) -> Vec<Vec<(String, Field)>> {
let mut file = tempfile::tempfile().expect("a temporary file");
std::io::Write::write_all(&mut file, bytes).expect("writes it");
let reader = SerializedFileReader::new(file).expect("a Parquet file");
reader
.get_row_iter(None)
.expect("its rows")
.map(|row| {
row.expect("a row")
.get_column_iter()
.map(|(name, value)| (name.clone(), value.clone()))
.collect()
})
.collect()
}
pub(crate) fn key_values(bytes: &[u8]) -> Vec<(String, Option<String>)> {
let mut file = tempfile::tempfile().expect("a temporary file");
std::io::Write::write_all(&mut file, bytes).expect("writes it");
let reader = SerializedFileReader::new(file).expect("a Parquet file");
reader
.metadata()
.file_metadata()
.key_value_metadata()
.into_iter()
.flatten()
.map(|kv| (kv.key.clone(), kv.value.clone()))
.collect()
}
pub(crate) fn written(level: Metadata, rows: &[Row], sizes: Sizes) -> Vec<u8> {
let mut file = File::new(level, sizes).expect("a new file");
for row in rows {
file.push(row.clone()).expect("adds a row");
}
file.finish().expect("the file")
}
fn row(n: u8) -> Row {
let mut sealed = vec![0x1f, 0x8b, 0x00, 0xff];
sealed.extend(std::iter::repeat_n(n, usize::from(n) * 7));
Row {
version: VersionId([n; 16]),
stored_at: 1_762_300_000_000_000_000 + i64::from(n),
sealed,
about: About {
form: Some("unsigned".to_owned()),
gzip: Some(n.is_multiple_of(2)),
size: Some(i64::from(n) * 100),
created_at: (!n.is_multiple_of(3)).then_some(1_700_000_000_000_000_000),
updated_at: None,
party_count: Some(2),
dialog_count: Some(i32::from(n)),
uuid: Some(Uuid::from_bytes([0xa0 | (n & 0x0f); 16])),
subject: Some(format!("SUBJECT-{n}")),
parties: Some(format!(
r#"[{{"tel":"+1555555{:04}"}}]"#,
100 + u16::from(n)
)),
},
}
}
fn names(rows: &[Vec<(String, Field)>]) -> Vec<String> {
rows[0].iter().map(|(name, _)| name.clone()).collect()
}
const BIG: Sizes = SIZES;
#[test]
fn each_level_writes_its_columns() {
let rows = [row(1)];
let fixed = ["version", "stored_at", "sealed"];
let counts = [
"form",
"gzip",
"size",
"created_at",
"updated_at",
"party_count",
"dialog_count",
];
let full = ["uuid", "subject", "parties"];
for (level, expected) in [
(Metadata::None, fixed.to_vec()),
(Metadata::Counts, [&fixed[..], &counts].concat()),
(Metadata::Full, [&fixed[..], &counts, &full].concat()),
] {
let bytes = written(level, &rows, BIG);
assert_eq!(names(&read(&bytes)), expected, "{level:?}");
let kv = key_values(&bytes);
assert!(
kv.contains(&(LEVEL_KEY.to_owned(), Some(level.as_str().to_owned()))),
"{level:?}: the level is recorded: {kv:?}"
);
assert!(
kv.contains(&(SCHEMA_KEY.to_owned(), Some(SCHEMA.to_owned()))),
"{level:?}: the schema version is recorded: {kv:?}"
);
}
}
#[test]
fn every_value_reads_back_as_written() {
let rows: Vec<Row> = (1..=9).map(row).collect();
let small = Sizes {
row_group: 40,
file: usize::MAX,
};
let back = read(&written(Metadata::Full, &rows, small));
assert_eq!(back.len(), rows.len(), "one row each");
for (row, got) in rows.iter().zip(&back) {
let value = |name: &str| {
got.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| v.clone())
.unwrap_or_else(|| panic!("no column {name}"))
};
let bytes = |name: &str| match value(name) {
Field::Bytes(b) => b.data().to_vec(),
Field::Str(s) => s.into_bytes(),
other => panic!("{name}: {other:?}"),
};
assert_eq!(bytes("sealed"), row.sealed, "sealed, byte for byte");
assert_eq!(
value("stored_at"),
Field::Long(row.stored_at),
"store time in nanoseconds"
);
assert_eq!(bytes("version"), row.version.0, "the version id's 16 bytes");
let about = &row.about;
let time = |t: Option<i64>| t.map_or(Field::Null, Field::Long);
assert_eq!(value("created_at"), time(about.created_at), "created_at");
assert_eq!(value("updated_at"), Field::Null, "a NULL time");
assert_eq!(value("gzip"), Field::Bool(about.gzip.expect("set")), "gzip");
assert_eq!(value("size"), Field::Long(about.size.expect("set")), "size");
assert_eq!(value("party_count"), Field::Int(2), "party_count");
assert_eq!(
value("dialog_count"),
Field::Int(about.dialog_count.expect("set")),
"dialog_count"
);
assert_eq!(bytes("form"), b"unsigned", "form");
assert_eq!(
bytes("uuid"),
about.uuid.expect("set").as_bytes(),
"the uuid's 16 bytes"
);
assert_eq!(
bytes("subject"),
about.subject.as_deref().expect("set").as_bytes(),
"subject"
);
assert_eq!(
bytes("parties"),
about.parties.as_deref().expect("set").as_bytes(),
"parties"
);
}
}
#[test]
fn a_file_with_no_rows_reads_as_empty() {
assert!(
read(&written(Metadata::None, &[], BIG)).is_empty(),
"no row"
);
}
#[test]
fn a_file_is_full_at_its_size() {
let sizes = Sizes {
row_group: 64,
file: 300,
};
let mut file = File::new(Metadata::None, sizes).expect("a new file");
assert!(!file.full(), "empty");
let mut pushed = 0;
while !file.full() {
file.push(row(9)).expect("adds a row");
pushed += 1;
assert!(pushed < 100, "a file of {} bytes never fills", sizes.file);
}
assert!(file.size() >= sizes.file, "full at its size");
assert_eq!(file.rows(), pushed, "every pushed row is counted");
assert_eq!(
read(&file.finish().expect("the file")).len(),
pushed as usize,
"every pushed row is in the file"
);
}
#[test]
fn no_byte_array_value_is_copied_into_the_file() {
let marker = b"SEALED-MARKER-SEALED-MARKER";
let mut rows: Vec<Row> = (1..=4).map(row).collect();
for row in &mut rows {
row.sealed = marker.to_vec();
row.about.subject = Some("SUBJECT-SAME".to_owned());
}
let bytes = written(Metadata::Full, &rows, BIG);
let count = |needle: &[u8]| bytes.windows(needle.len()).filter(|w| *w == needle).count();
assert_eq!(count(marker), rows.len(), "once per row, no more");
assert_eq!(count(b"SUBJECT-SAME"), rows.len(), "once per row, no more");
for row in &rows {
let uuid = row.about.uuid.expect("set");
assert_eq!(count(uuid.as_bytes()), 1, "a uuid once, in its row");
}
}
#[test]
fn each_sealed_value_is_a_page_of_its_own() {
use parquet::file::serialized_reader::ReadOptionsBuilder;
let rows: Vec<Row> = (1..=5).map(row).collect();
let bytes = written(Metadata::None, &rows, BIG);
let mut file = tempfile::tempfile().expect("a temporary file");
std::io::Write::write_all(&mut file, &bytes).expect("writes it");
let options = ReadOptionsBuilder::new().with_page_index().build();
let reader = SerializedFileReader::new_with_options(file, options).expect("reads");
let index = reader.metadata().page_index_for_row_group(0);
let firsts: Vec<i64> = index
.page_locations(2)
.expect("an offset index for sealed")
.iter()
.map(|p| p.first_row_index)
.collect();
assert_eq!(firsts, [0, 1, 2, 3, 4], "page i starts at row i");
}
}