use std::collections::HashMap;
use std::sync::Arc;
use arrow::datatypes::{DataType, Field, Schema};
use lance::Dataset;
use crate::error::{Error, Result};
use crate::format::check_format;
const COMPRESSION_KEY: &str = "lance-encoding:compression";
const COMPRESSION_LEVEL_KEY: &str = "lance-encoding:compression-level";
const BYTE_STREAM_SPLIT_KEY: &str = "lance-encoding:bss";
const RLE_THRESHOLD_KEY: &str = "lance-encoding:rle-threshold";
const STRUCTURAL_ENCODING_KEY: &str = "lance-encoding:structural-encoding";
const KEYS: [&str; 5] = [
COMPRESSION_KEY,
COMPRESSION_LEVEL_KEY,
BYTE_STREAM_SPLIT_KEY,
RLE_THRESHOLD_KEY,
STRUCTURAL_ENCODING_KEY,
];
pub(crate) const LANCE_ENCODING_PREFIX: &str = "lance-encoding:";
pub(crate) const COLUMN_DEFAULTS_PREFIX: &str = "lance_nd.column_defaults.";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Compression {
None,
Lz4,
Zstd {
level: Option<i32>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StructuralEncoding {
MiniBlock,
FullZip,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ByteStreamSplit {
Off,
On,
Auto,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ColumnOptions {
pub compression: Option<Compression>,
pub byte_stream_split: Option<ByteStreamSplit>,
pub rle_threshold: Option<f64>,
pub structural_encoding: Option<StructuralEncoding>,
}
impl ColumnOptions {
pub fn with_compression(mut self, compression: Compression) -> Self {
self.compression = Some(compression);
self
}
pub fn with_byte_stream_split(mut self, mode: ByteStreamSplit) -> Self {
self.byte_stream_split = Some(mode);
self
}
pub fn with_rle_threshold(mut self, threshold: f64) -> Self {
self.rle_threshold = Some(threshold);
self
}
pub fn with_structural_encoding(mut self, encoding: StructuralEncoding) -> Self {
self.structural_encoding = Some(encoding);
self
}
pub(crate) fn or(&self, defaults: &ColumnOptions) -> ColumnOptions {
ColumnOptions {
compression: self.compression.or(defaults.compression),
byte_stream_split: self.byte_stream_split.or(defaults.byte_stream_split),
rle_threshold: self.rle_threshold.or(defaults.rle_threshold),
structural_encoding: self.structural_encoding.or(defaults.structural_encoding),
}
}
pub(crate) fn leaf_metadata(&self) -> HashMap<String, String> {
let mut metadata = HashMap::new();
match self.compression {
Some(Compression::None) => insert(&mut metadata, COMPRESSION_KEY, "none"),
Some(Compression::Lz4) => insert(&mut metadata, COMPRESSION_KEY, "lz4"),
Some(Compression::Zstd { level }) => {
insert(&mut metadata, COMPRESSION_KEY, "zstd");
if let Some(level) = level {
insert(&mut metadata, COMPRESSION_LEVEL_KEY, &level.to_string());
}
}
None => {}
}
let mode = self.byte_stream_split.map(|mode| match mode {
ByteStreamSplit::Off => "off",
ByteStreamSplit::On => "on",
ByteStreamSplit::Auto => "auto",
});
if let Some(mode) = mode {
insert(&mut metadata, BYTE_STREAM_SPLIT_KEY, mode);
}
if let Some(threshold) = self.rle_threshold {
insert(&mut metadata, RLE_THRESHOLD_KEY, &threshold.to_string());
}
metadata
}
pub(crate) fn root_metadata(&self) -> HashMap<String, String> {
let mut metadata = HashMap::new();
let encoding = self.structural_encoding.map(|encoding| match encoding {
StructuralEncoding::MiniBlock => "miniblock",
StructuralEncoding::FullZip => "fullzip",
});
if let Some(encoding) = encoding {
insert(&mut metadata, STRUCTURAL_ENCODING_KEY, encoding);
}
metadata
}
pub(crate) fn to_table_metadata(&self) -> HashMap<String, String> {
self.leaf_metadata()
.into_iter()
.chain(self.root_metadata())
.map(|(key, value)| (format!("{COLUMN_DEFAULTS_PREFIX}{key}"), value))
.collect()
}
pub(crate) fn from_table_metadata(metadata: &HashMap<String, String>) -> Result<Self> {
let get = |key: &str| metadata.get(&format!("{COLUMN_DEFAULTS_PREFIX}{key}"));
let invalid = |key: &str, value: &str| {
Error::InvalidTable(format!(
"column default {key} has the invalid value '{value}'"
))
};
let level = get(COMPRESSION_LEVEL_KEY)
.map(|v| v.parse().map_err(|_| invalid(COMPRESSION_LEVEL_KEY, v)))
.transpose()?;
let compression = get(COMPRESSION_KEY)
.map(|v| match v.as_str() {
"none" => Ok(Compression::None),
"lz4" => Ok(Compression::Lz4),
"zstd" => Ok(Compression::Zstd { level }),
_ => Err(invalid(COMPRESSION_KEY, v)),
})
.transpose()?;
let byte_stream_split = get(BYTE_STREAM_SPLIT_KEY)
.map(|v| match v.as_str() {
"off" => Ok(ByteStreamSplit::Off),
"on" => Ok(ByteStreamSplit::On),
"auto" => Ok(ByteStreamSplit::Auto),
_ => Err(invalid(BYTE_STREAM_SPLIT_KEY, v)),
})
.transpose()?;
let rle_threshold = get(RLE_THRESHOLD_KEY)
.map(|v| v.parse().map_err(|_| invalid(RLE_THRESHOLD_KEY, v)))
.transpose()?;
let structural_encoding = get(STRUCTURAL_ENCODING_KEY)
.map(|v| match v.as_str() {
"miniblock" => Ok(StructuralEncoding::MiniBlock),
"fullzip" => Ok(StructuralEncoding::FullZip),
_ => Err(invalid(STRUCTURAL_ENCODING_KEY, v)),
})
.transpose()?;
Ok(Self {
compression,
byte_stream_split,
rle_threshold,
structural_encoding,
})
}
}
fn insert(metadata: &mut HashMap<String, String>, key: &str, value: &str) {
metadata.insert(key.to_string(), value.to_string());
}
pub fn column_defaults(dataset: &Dataset) -> Result<ColumnOptions> {
check_format(dataset)?;
ColumnOptions::from_table_metadata(&dataset.schema().metadata)
}
#[tracing::instrument(skip_all, fields(uri = %dataset.uri()), err)]
pub async fn set_column_defaults(dataset: &mut Dataset, defaults: &ColumnOptions) -> Result<()> {
check_format(dataset)?;
let mut values = defaults.to_table_metadata();
let entries: Vec<(String, Option<String>)> = KEYS
.iter()
.map(|key| {
let key = format!("{COLUMN_DEFAULTS_PREFIX}{key}");
let value = values.remove(&key);
(key, value)
})
.collect();
dataset.update_schema_metadata(entries).await?;
Ok(())
}
pub(crate) fn with_column_options(
schema: &Schema,
defaults: &ColumnOptions,
columns: &HashMap<String, ColumnOptions>,
) -> Schema {
let fields: Vec<Field> = schema
.fields()
.iter()
.map(|field| {
let options = match columns.get(field.name()) {
Some(options) => options.or(defaults),
None => defaults.clone(),
};
let leaf = options.leaf_metadata();
let mut field = with_leaf_metadata(field, &leaf);
let mut metadata = field.metadata().clone();
metadata.extend(options.root_metadata());
field.set_metadata(metadata);
field
})
.collect();
Schema::new_with_metadata(fields, schema.metadata().clone())
}
fn with_leaf_metadata(field: &Field, leaf: &HashMap<String, String>) -> Field {
let data_type = match field.data_type() {
DataType::Struct(children) => DataType::Struct(
children
.iter()
.map(|child| with_leaf_metadata(child, leaf))
.collect(),
),
DataType::List(item) => DataType::List(Arc::new(with_leaf_metadata(item, leaf))),
_ => {
let mut metadata = field.metadata().clone();
metadata.extend(leaf.clone());
return field.clone().with_metadata(metadata);
}
};
field.clone().with_data_type(data_type)
}
pub(crate) fn without_encoding_metadata(field: &Field) -> Field {
let data_type = match field.data_type() {
DataType::Struct(children) => DataType::Struct(
children
.iter()
.map(|child| without_encoding_metadata(child))
.collect(),
),
DataType::List(item) => DataType::List(Arc::new(without_encoding_metadata(item))),
data_type => data_type.clone(),
};
let metadata = field
.metadata()
.iter()
.filter(|(key, _)| !key.starts_with(LANCE_ENCODING_PREFIX))
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
field
.clone()
.with_data_type(data_type)
.with_metadata(metadata)
}