#[doc(hidden)]
pub use crate::catalog::inline::InlineScanKind;
use crate::{
catalog::{
Catalog,
inline::{InlineRow, materialize_inline_rows},
},
error::{Error, Result},
store::{inline as store_inline, key::InlineOperation},
};
#[doc(hidden)]
pub struct InlineRowRecord {
pub row_id: u64,
pub schema_version: u64,
pub begin_snapshot: u64,
pub end_snapshot: Option<u64>,
pub chunk_index: u64,
pub offset_in_chunk: u64,
}
#[doc(hidden)]
pub struct InlineScanRecord {
pub rows: Vec<InlineRowRecord>,
pub chunk_bodies: Vec<Vec<u8>>,
}
#[doc(hidden)]
pub async fn scan_inline(
catalog: &Catalog,
table_id: u64,
kind: InlineScanKind,
snapshot: u64,
start: u64,
) -> Result<InlineScanRecord> {
let session = catalog.begin_read().await?;
let chunks = store_inline::scan_inline_chunks(session.handle(), table_id).await;
let inline_deletes = store_inline::scan_inline_inline_deletes(session.handle(), table_id).await;
session.finish();
let chunks = chunks?;
let inline_deletes = inline_deletes?;
let rows: Vec<InlineRow> = materialize_inline_rows(&chunks, &inline_deletes);
let mut chunk_indexes: std::collections::HashMap<usize, u64> = std::collections::HashMap::new();
let mut chunk_bodies: Vec<Vec<u8>> = Vec::new();
let selected = kind
.select(&rows, snapshot, start)
.into_iter()
.map(|row| {
let (operation, chunk) = &chunks[row.chunk];
let InlineOperation::Insert { schema_version, .. } = operation else {
return Err(Error::Corruption(format!(
"inline row {} references a non-insert chunk key: {operation:?}",
row.row_id
)));
};
let chunk_index = *chunk_indexes.entry(row.chunk).or_insert_with(|| {
chunk_bodies.push(chunk.body.clone());
chunk_bodies.len() as u64 - 1
});
Ok(InlineRowRecord {
row_id: row.row_id,
schema_version: *schema_version,
begin_snapshot: row.begin_snapshot,
end_snapshot: row.end_snapshot,
chunk_index,
offset_in_chunk: row.offset_in_chunk,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(InlineScanRecord {
rows: selected,
chunk_bodies,
})
}
#[doc(hidden)]
pub async fn inline_schemas(catalog: &Catalog, table_id: u64) -> Result<Vec<(u64, Vec<u8>)>> {
let session = catalog.begin_read().await?;
let schemas = store_inline::scan_inline_schemas(session.handle(), table_id).await;
session.finish();
Ok(schemas?
.into_iter()
.map(|(schema_version, value)| (schema_version, value.arrow_schema))
.collect())
}
#[doc(hidden)]
pub async fn inline_registered_tables(catalog: &Catalog) -> Result<Vec<(u64, u64)>> {
let session = catalog.begin_read().await?;
let schemas = store_inline::scan_all_inline_schemas(session.handle()).await;
session.finish();
Ok(schemas?
.into_iter()
.map(|(table_id, schema_version, _)| (table_id, schema_version))
.collect())
}
#[doc(hidden)]
pub async fn inline_file_delete_table_exists(catalog: &Catalog, table_id: u64) -> Result<bool> {
let session = catalog.begin_read().await?;
let marked = store_inline::read_inline_file_delete_table(session.handle(), table_id).await;
let exists = match marked {
Ok(true) => Ok(true),
Ok(false) => store_inline::scan_inline_file_deletes(session.handle(), table_id)
.await
.map(|records| !records.is_empty()),
Err(error) => Err(error),
};
session.finish();
exists
}
#[doc(hidden)]
pub async fn inline_file_deletes(catalog: &Catalog, table_id: u64) -> Result<Vec<(u64, u64, u64)>> {
let session = catalog.begin_read().await?;
let file_deletes = store_inline::scan_inline_file_deletes(session.handle(), table_id).await;
session.finish();
Ok(file_deletes?
.into_iter()
.map(|(data_file_id, row_id, value)| (data_file_id, row_id, value.begin_snapshot))
.collect())
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use object_store::memory::InMemory;
use super::*;
use crate::{
catalog::CatalogOptions,
transaction::staged::{RowOperation, StagedTransaction, TableKind},
};
fn snapshot_row(id: u64) -> Vec<crate::transaction::staged::Cell> {
use crate::transaction::staged::Cell;
vec![
Cell::U64(id),
Cell::I64(1),
Cell::U64(0),
Cell::U64(1),
Cell::U64(0),
]
}
fn snapshot_changes_row(id: u64) -> Vec<crate::transaction::staged::Cell> {
use crate::transaction::staged::Cell;
vec![
Cell::U64(id),
Cell::Str("inlined_insert:1".to_string()),
Cell::Null,
Cell::Null,
Cell::Null,
]
}
async fn open() -> Catalog {
Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default())
.await
.unwrap()
}
#[tokio::test]
async fn scan_inline_materializes_rows_with_chunk_bodies() {
let catalog = open().await;
let db_tx = catalog.begin_write_tx().await.unwrap();
let mut tx = StagedTransaction::begin_detached(db_tx);
tx.stage(RowOperation::InlineSchema {
table_id: 1,
schema_version: 0,
arrow_schema: b"schema".to_vec(),
});
tx.stage(RowOperation::InlineInsert {
table_id: 1,
schema_version: 0,
begin_snapshot: 1,
row_id_start: 0,
row_count: 2,
arrow_body: b"chunk-a".to_vec(),
});
tx.stage(RowOperation::InlineInsert {
table_id: 1,
schema_version: 0,
begin_snapshot: 1,
row_id_start: 2,
row_count: 1,
arrow_body: b"chunk-b".to_vec(),
});
tx.stage(RowOperation::Insert {
table: TableKind::Snapshot,
cells: snapshot_row(1),
});
tx.stage(RowOperation::Insert {
table: TableKind::SnapshotChanges,
cells: snapshot_changes_row(1),
});
tx.commit().await.unwrap();
let db_tx2 = catalog.begin_write_tx().await.unwrap();
let mut inline_delete = StagedTransaction::begin_detached(db_tx2);
inline_delete.stage(RowOperation::InlineInlineDelete {
table_id: 1,
row_id: 1,
end_snapshot: 2,
});
inline_delete.stage(RowOperation::Insert {
table: TableKind::Snapshot,
cells: snapshot_row(2),
});
inline_delete.stage(RowOperation::Insert {
table: TableKind::SnapshotChanges,
cells: snapshot_changes_row(2),
});
inline_delete.commit().await.unwrap();
let record = scan_inline(&catalog, 1, InlineScanKind::Table, 2, 0)
.await
.unwrap();
let mut by_id: Vec<(u64, Vec<u8>, u64)> = record
.rows
.iter()
.map(|r| {
(
r.row_id,
record.chunk_bodies[usize::try_from(r.chunk_index).unwrap()].clone(),
r.offset_in_chunk,
)
})
.collect();
by_id.sort_by_key(|(id, ..)| *id);
assert_eq!(
by_id,
vec![(0, b"chunk-a".to_vec(), 0), (2, b"chunk-b".to_vec(), 0)]
);
assert_eq!(record.chunk_bodies.len(), 2);
let schemas = inline_schemas(&catalog, 1).await.unwrap();
assert_eq!(schemas, vec![(0, b"schema".to_vec())]);
let registered = inline_registered_tables(&catalog).await.unwrap();
assert_eq!(registered, vec![(1, 0)]);
}
#[tokio::test]
async fn inline_file_deletes_read_back_in_key_order() {
let catalog = open().await;
let db_tx = catalog.begin_write_tx().await.unwrap();
let mut tx = StagedTransaction::begin_detached(db_tx);
tx.stage(RowOperation::InlineFileDelete {
table_id: 1,
data_file_id: 7,
row_id: 2,
begin_snapshot: 6,
});
tx.stage(RowOperation::InlineFileDelete {
table_id: 1,
data_file_id: 7,
row_id: 0,
begin_snapshot: 6,
});
tx.stage(RowOperation::Insert {
table: TableKind::Snapshot,
cells: snapshot_row(6),
});
tx.stage(RowOperation::Insert {
table: TableKind::SnapshotChanges,
cells: snapshot_changes_row(6),
});
tx.commit().await.unwrap();
let rows = inline_file_deletes(&catalog, 1).await.unwrap();
assert_eq!(rows, vec![(7, 0, 6), (7, 2, 6)]);
assert!(inline_file_delete_table_exists(&catalog, 1).await.unwrap());
assert_eq!(inline_file_deletes(&catalog, 9).await.unwrap(), vec![]);
}
}