use std::collections::HashMap;
use std::sync::Arc;
use arrow_schema::{DataType, Field, Fields, Schema, SchemaRef, TimeUnit};
use serde_json::{Map, Value, json};
use crate::error::{WriteError, WriteResult};
use crate::rows::UTC;
use crate::{Chunk, Contract};
pub const FIELD_ID_KEY: &str = "PARQUET:field_id";
const PREVIOUS_VERSIONS_MAX: usize = 100;
#[derive(Clone, Debug, PartialEq)]
pub struct Metadata(Value);
fn corrupt(what: &str) -> WriteError {
WriteError::Corrupt(format!("table metadata: {what}"))
}
impl Metadata {
pub fn parse(bytes: &[u8]) -> WriteResult<Self> {
let value: Value = serde_json::from_slice(bytes).map_err(|e| corrupt(&e.to_string()))?;
if value.get("format-version").and_then(Value::as_u64) != Some(2) {
return Err(WriteError::invalid("only Iceberg format version 2 tables can be written"));
}
Ok(Self(value))
}
pub fn to_json(&self) -> String {
self.0.to_string()
}
pub fn location(&self) -> &str {
self.0.get("location").and_then(Value::as_str).unwrap_or_default()
}
pub fn properties(&self) -> HashMap<String, String> {
let Some(Value::Object(p)) = self.0.get("properties") else { return HashMap::new() };
p.iter().filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string()))).collect()
}
pub fn contract(&self) -> WriteResult<Contract> {
Ok(Contract::from_properties(&self.properties())?)
}
pub fn current_snapshot_id(&self) -> Option<i64> {
self.0.get("current-snapshot-id").and_then(Value::as_i64).filter(|&id| id >= 0)
}
fn snapshots(&self) -> &[Value] {
self.0.get("snapshots").and_then(Value::as_array).map_or(&[], Vec::as_slice)
}
fn snapshot(&self, id: i64) -> Option<&Value> {
self.snapshots().iter().find(|s| s.get("snapshot-id").and_then(Value::as_i64) == Some(id))
}
pub fn has_snapshot(&self, id: i64) -> bool {
self.snapshot(id).is_some()
}
pub fn summary(&self) -> HashMap<String, String> {
let summary = self.current_snapshot_id().and_then(|id| self.snapshot(id)?.get("summary"));
let Some(Value::Object(s)) = summary else { return HashMap::new() };
s.iter().filter_map(|(k, v)| Some((k.clone(), v.as_str()?.to_string()))).collect()
}
pub fn history_has_batch(&self, batch_id: &str) -> bool {
let mut id = self.current_snapshot_id();
while let Some(s) = id.and_then(|id| self.snapshot(id)) {
if s.pointer("/summary/timelake.batch-id").and_then(Value::as_str) == Some(batch_id) {
return true;
}
id = s.get("parent-snapshot-id").and_then(Value::as_i64);
}
false
}
pub fn manifest_list(&self) -> Option<&str> {
self.current_snapshot_id().and_then(|id| self.snapshot(id)?.get("manifest-list")?.as_str())
}
pub fn next_sequence_number(&self) -> i64 {
self.0.get("last-sequence-number").and_then(Value::as_i64).unwrap_or(0) + 1
}
pub fn schema_id(&self) -> i32 {
self.0.get("current-schema-id").and_then(Value::as_i64).and_then(|v| i32::try_from(v).ok()).unwrap_or(0)
}
pub fn schema_json(&self) -> WriteResult<&Value> {
let id = i64::from(self.schema_id());
self.0
.get("schemas")
.and_then(Value::as_array)
.and_then(|s| s.iter().find(|s| s.get("schema-id").and_then(Value::as_i64) == Some(id)))
.ok_or_else(|| corrupt("no current schema"))
}
pub fn arrow_schema(&self) -> WriteResult<SchemaRef> {
let fields = struct_fields(self.schema_json()?)?;
Ok(Arc::new(Schema::new(fields)))
}
pub fn partition_spec(&self) -> WriteResult<(i32, &Value)> {
let id = self.0.get("default-spec-id").and_then(Value::as_i64).unwrap_or(0);
let spec = self
.0
.get("partition-specs")
.and_then(Value::as_array)
.and_then(|s| s.iter().find(|s| s.get("spec-id").and_then(Value::as_i64) == Some(id)))
.ok_or_else(|| corrupt("no default partition spec"))?;
let fields = spec.get("fields").ok_or_else(|| corrupt("partition spec without fields"))?;
Ok((i32::try_from(id).map_err(|_| corrupt("spec id"))?, fields))
}
pub fn partition_field(&self) -> WriteResult<Option<(String, Chunk)>> {
let (_, fields) = self.partition_spec()?;
let Some(field) = fields.as_array().and_then(|f| f.first()) else { return Ok(None) };
let chunk = match field.get("transform").and_then(Value::as_str) {
Some("hour") => Chunk::Hour,
Some("day") => Chunk::Day,
Some("month") => Chunk::Month,
other => return Err(WriteError::invalid(format!("unsupported partition transform {other:?}"))),
};
let name = field.get("name").and_then(Value::as_str).ok_or_else(|| corrupt("partition field name"))?;
Ok(Some((name.to_string(), chunk)))
}
pub fn sort_order_id(&self) -> i32 {
self.0.get("default-sort-order-id").and_then(Value::as_i64).and_then(|v| i32::try_from(v).ok()).unwrap_or(0)
}
pub fn last_updated_ms(&self) -> i64 {
self.0.get("last-updated-ms").and_then(Value::as_i64).unwrap_or(0)
}
pub fn snapshot_list(&self) -> Vec<SnapshotRef> {
self.snapshots()
.iter()
.filter_map(|s| {
Some(SnapshotRef {
id: s.get("snapshot-id")?.as_i64()?,
timestamp_ms: s.get("timestamp-ms").and_then(Value::as_i64).unwrap_or(0),
manifest_list: s.get("manifest-list")?.as_str()?.to_string(),
last_value_index: s
.pointer("/summary/timelake.last-value-index")
.and_then(Value::as_str)
.map(str::to_string),
})
})
.collect()
}
pub fn metadata_log_len(&self) -> usize {
self.0.get("metadata-log").and_then(Value::as_array).map_or(0, Vec::len)
}
pub fn without_snapshots(&self, ids: &[i64], previous: &str, now_ms: i64) -> WriteResult<Self> {
let mut m = self.0.clone();
let obj = m.as_object_mut().ok_or_else(|| corrupt("not an object"))?;
let removed = |v: &Value| v.get("snapshot-id").and_then(Value::as_i64).is_some_and(|id| ids.contains(&id));
if let Some(Value::Array(snapshots)) = obj.get_mut("snapshots") {
snapshots.retain(|s| !removed(s));
}
if let Some(Value::Object(refs)) = obj.get_mut("refs") {
refs.retain(|_, r| !removed(r));
}
if let Some(Value::Array(log)) = obj.get_mut("snapshot-log") {
let mut kept = vec![];
for entry in log.drain(..) {
if removed(&entry) {
kept.clear();
} else {
kept.push(entry);
}
}
*log = kept;
}
push(obj, "metadata-log", json!({"metadata-file": previous, "timestamp-ms": self.last_updated_ms()}));
let keep = self
.properties()
.get("write.metadata.previous-versions-max")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(PREVIOUS_VERSIONS_MAX)
.max(1);
if let Some(Value::Array(log)) = obj.get_mut("metadata-log")
&& log.len() > keep
{
log.drain(..log.len() - keep);
}
obj.insert("last-updated-ms".into(), json!(now_ms));
Ok(Self(m))
}
pub fn with_snapshot(&self, snapshot: &NewSnapshot, previous: &str, now_ms: i64) -> WriteResult<Self> {
let mut m = self.0.clone();
let obj = m.as_object_mut().ok_or_else(|| corrupt("not an object"))?;
let mut s = json!({
"snapshot-id": snapshot.snapshot_id,
"sequence-number": snapshot.sequence_number,
"timestamp-ms": snapshot.timestamp_ms,
"manifest-list": snapshot.manifest_list,
"summary": snapshot.summary_json(),
"schema-id": self.schema_id(),
});
if let (Some(parent), Some(s)) = (self.current_snapshot_id(), s.as_object_mut()) {
s.insert("parent-snapshot-id".into(), json!(parent));
}
push(obj, "snapshots", s);
push(obj, "snapshot-log", json!({"snapshot-id": snapshot.snapshot_id, "timestamp-ms": snapshot.timestamp_ms}));
push(obj, "metadata-log", json!({"metadata-file": previous, "timestamp-ms": self.last_updated_ms()}));
let keep = self
.properties()
.get("write.metadata.previous-versions-max")
.and_then(|v| v.parse::<usize>().ok())
.unwrap_or(PREVIOUS_VERSIONS_MAX)
.max(1);
if let Some(Value::Array(log)) = obj.get_mut("metadata-log")
&& log.len() > keep
{
log.drain(..log.len() - keep);
}
obj.insert("current-snapshot-id".into(), json!(snapshot.snapshot_id));
obj.insert("last-sequence-number".into(), json!(snapshot.sequence_number));
obj.insert("last-updated-ms".into(), json!(now_ms));
let refs = obj.entry("refs").or_insert_with(|| Value::Object(Map::new()));
if let Some(refs) = refs.as_object_mut() {
refs.insert("main".into(), json!({"snapshot-id": snapshot.snapshot_id, "type": "branch"}));
}
Ok(Self(m))
}
}
fn push(obj: &mut Map<String, Value>, key: &str, value: Value) {
match obj.get_mut(key) {
Some(Value::Array(a)) => a.push(value),
_ => {
obj.insert(key.into(), Value::Array(vec![value]));
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SnapshotRef {
pub id: i64,
pub timestamp_ms: i64,
pub manifest_list: String,
pub last_value_index: Option<String>,
}
#[derive(Clone, Debug)]
pub struct NewSnapshot {
pub snapshot_id: i64,
pub sequence_number: i64,
pub timestamp_ms: i64,
pub manifest_list: String,
pub operation: String,
pub summary: HashMap<String, String>,
}
impl NewSnapshot {
fn summary_json(&self) -> Value {
let mut s: Map<String, Value> = self.summary.iter().map(|(k, v)| (k.clone(), json!(v))).collect();
s.insert("operation".into(), json!(self.operation));
Value::Object(s)
}
}
fn struct_fields(schema: &Value) -> WriteResult<Vec<Field>> {
let fields = schema.get("fields").and_then(Value::as_array).ok_or_else(|| corrupt("schema without fields"))?;
fields.iter().map(field).collect()
}
fn field(f: &Value) -> WriteResult<Field> {
let name = f.get("name").and_then(Value::as_str).ok_or_else(|| corrupt("field without a name"))?;
let id = f.get("id").and_then(Value::as_i64).ok_or_else(|| corrupt("field without an id"))?;
let required = f.get("required").and_then(Value::as_bool).unwrap_or(false);
let ty = data_type(f.get("type").ok_or_else(|| corrupt("field without a type"))?)?;
Ok(Field::new(name, ty, !required).with_metadata(HashMap::from([(FIELD_ID_KEY.into(), id.to_string())])))
}
fn data_type(t: &Value) -> WriteResult<DataType> {
if let Some(name) = t.as_str() {
return primitive(name);
}
match t.get("type").and_then(Value::as_str) {
Some("struct") => Ok(DataType::Struct(Fields::from(struct_fields(t)?))),
Some("list") => {
let id = t.get("element-id").and_then(Value::as_i64).ok_or_else(|| corrupt("list without element-id"))?;
let required = t.get("element-required").and_then(Value::as_bool).unwrap_or(false);
let element = data_type(t.get("element").ok_or_else(|| corrupt("list without element"))?)?;
let field = Field::new("element", element, !required)
.with_metadata(HashMap::from([(FIELD_ID_KEY.into(), id.to_string())]));
Ok(DataType::List(Arc::new(field)))
}
Some("map") => {
let id = |k: &str| t.get(k).and_then(Value::as_i64).ok_or_else(|| corrupt("map without ids"));
let meta = |id: i64| HashMap::from([(FIELD_ID_KEY.to_string(), id.to_string())]);
let key = data_type(t.get("key").ok_or_else(|| corrupt("map without key"))?)?;
let value = data_type(t.get("value").ok_or_else(|| corrupt("map without value"))?)?;
let value_required = t.get("value-required").and_then(Value::as_bool).unwrap_or(false);
let entries = Field::new(
"key_value",
DataType::Struct(Fields::from(vec![
Field::new("key", key, false).with_metadata(meta(id("key-id")?)),
Field::new("value", value, !value_required).with_metadata(meta(id("value-id")?)),
])),
false,
);
Ok(DataType::Map(Arc::new(entries), false))
}
_ => Err(corrupt("unknown type")),
}
}
fn primitive(name: &str) -> WriteResult<DataType> {
Ok(match name {
"boolean" => DataType::Boolean,
"int" => DataType::Int32,
"long" => DataType::Int64,
"float" => DataType::Float32,
"double" => DataType::Float64,
"date" => DataType::Date32,
"time" => DataType::Time64(TimeUnit::Microsecond),
"timestamp" => DataType::Timestamp(TimeUnit::Microsecond, None),
"timestamptz" => DataType::Timestamp(TimeUnit::Microsecond, Some(UTC.into())),
"timestamp_ns" => DataType::Timestamp(TimeUnit::Nanosecond, None),
"timestamptz_ns" => DataType::Timestamp(TimeUnit::Nanosecond, Some(UTC.into())),
"string" => DataType::Utf8,
"uuid" => DataType::FixedSizeBinary(16),
"binary" => DataType::LargeBinary,
other => {
if let Some(n) = other.strip_prefix("fixed[").and_then(|s| s.strip_suffix(']')) {
DataType::FixedSizeBinary(n.parse().map_err(|_| corrupt("fixed length"))?)
} else if let Some(ps) = other.strip_prefix("decimal(").and_then(|s| s.strip_suffix(')')) {
let (p, s) = ps.split_once(',').ok_or_else(|| corrupt("decimal"))?;
let p: u8 = p.trim().parse().map_err(|_| corrupt("decimal precision"))?;
let s: i8 = s.trim().parse().map_err(|_| corrupt("decimal scale"))?;
DataType::Decimal128(p, s)
} else {
return Err(corrupt(&format!("unknown type {other}")));
}
}
})
}