use std::sync::Arc;
use crate::{
catalog::{Catalog, projection::ProjectionCache},
error::Result,
store::{
proto::{
ColumnValue, DataFileValue, DeleteFileValue, FileColumnStatsValue, GcFileValue,
MacroValue, MappingValue, PartitionValue, SchemaValue, SnapshotValue, SortValue,
TableColumnStatsValue, TableStatsValue, TableValue, ViewValue,
},
read::{
EntityRecord, read_head, scan_current_entities, scan_history_entities, scan_snapshots,
},
},
};
async fn session_head(session: &crate::store::handle::ReadSession) -> Result<Option<u64>> {
Ok(read_head(session.handle()).await?.map(|h| h.snapshot_id))
}
fn projections_read(catalog: &Catalog) -> std::sync::RwLockReadGuard<'_, ProjectionCache> {
catalog
.projections()
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn projections_write(catalog: &Catalog) -> std::sync::RwLockWriteGuard<'_, ProjectionCache> {
catalog
.projections()
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[doc(hidden)]
pub mod index;
#[doc(hidden)]
pub mod inline;
#[doc(hidden)]
pub mod staged;
#[doc(hidden)]
pub use crate::store::proto::SnapshotValue as SnapshotRecord;
async fn all_entities(catalog: &Catalog) -> Result<Arc<Vec<EntityRecord>>> {
let session = catalog.begin_read().await?;
let head = session_head(&session).await?;
let cache_at = match (catalog.maintains_projections(), head) {
(true, Some(head)) => {
if let Some(records) = projections_read(catalog).entities_at(head) {
session.finish();
return Ok(records);
}
Some(head)
}
_ => None,
};
let current = scan_current_entities(session.handle()).await;
let history = scan_history_entities(session.handle()).await;
session.finish();
let mut records = current?;
records.extend(history?);
let records = Arc::new(records);
if let Some(head) = cache_at {
projections_write(catalog).install_entities(head, records.as_ref().clone());
}
Ok(records)
}
async fn dump_entities<T>(
catalog: &Catalog,
extract: impl Fn(EntityRecord) -> Option<T>,
) -> Result<Vec<T>> {
let records = all_entities(catalog).await?;
Ok(records.iter().cloned().filter_map(extract).collect())
}
async fn dump_current_entities<T>(
catalog: &Catalog,
extract: impl Fn(EntityRecord) -> Option<T>,
) -> Result<Vec<T>> {
if catalog.maintains_projections() {
return dump_entities(catalog, extract).await;
}
let session = catalog.begin_read().await?;
let current = scan_current_entities(session.handle()).await;
session.finish();
Ok(current?.into_iter().filter_map(extract).collect())
}
#[doc(hidden)]
pub async fn dump_schemas(catalog: &Catalog) -> Result<Vec<SchemaValue>> {
dump_entities(catalog, |r| match r {
EntityRecord::Schema(v) => Some(v),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_tables(catalog: &Catalog) -> Result<Vec<TableValue>> {
dump_entities(catalog, |r| match r {
EntityRecord::Table(v) => Some(v),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_views(catalog: &Catalog) -> Result<Vec<ViewValue>> {
dump_entities(catalog, |r| match r {
EntityRecord::View(v) => Some(v),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_macros(catalog: &Catalog) -> Result<Vec<MacroValue>> {
dump_entities(catalog, |r| match r {
EntityRecord::Macro(m) => Some(m),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_mappings(catalog: &Catalog) -> Result<Vec<MappingValue>> {
dump_current_entities(catalog, |r| match r {
EntityRecord::Mapping(m) => Some(m),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_columns(catalog: &Catalog) -> Result<Vec<ColumnValue>> {
dump_entities(catalog, |r| match r {
EntityRecord::Column(v) => Some(v),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_data_files(catalog: &Catalog) -> Result<Vec<DataFileValue>> {
dump_entities(catalog, |r| match r {
EntityRecord::File(v) => Some(v),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_delete_files(catalog: &Catalog) -> Result<Vec<DeleteFileValue>> {
dump_entities(catalog, |r| match r {
EntityRecord::DeleteFile(v) => Some(v),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_partition_info(catalog: &Catalog) -> Result<Vec<PartitionValue>> {
dump_entities(catalog, |r| match r {
EntityRecord::Partition(v) => Some(v),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_sort_info(catalog: &Catalog) -> Result<Vec<SortValue>> {
dump_entities(catalog, |r| match r {
EntityRecord::Sort(v) => Some(v),
_ => None,
})
.await
}
async fn dump_projected_current<T: Clone>(
catalog: &Catalog,
read: impl Fn(&ProjectionCache, u64) -> Option<Vec<T>>,
install: impl Fn(&mut ProjectionCache, u64, Vec<T>),
extract: impl Fn(EntityRecord) -> Option<T>,
) -> Result<Vec<T>> {
let session = catalog.begin_read().await?;
let head = session_head(&session).await?;
let cache_at = match (catalog.maintains_projections(), head) {
(true, Some(head)) => {
if let Some(rows) = read(&projections_read(catalog), head) {
session.finish();
return Ok(rows);
}
Some(head)
}
_ => None,
};
let current = scan_current_entities(session.handle()).await;
session.finish();
let rows: Vec<T> = current?.into_iter().filter_map(extract).collect();
if let Some(head) = cache_at {
install(&mut projections_write(catalog), head, rows.clone());
}
Ok(rows)
}
#[doc(hidden)]
pub async fn dump_table_stats(catalog: &Catalog) -> Result<Vec<TableStatsValue>> {
dump_projected_current(
catalog,
ProjectionCache::table_stats_at,
ProjectionCache::install_table_stats,
|r| match r {
EntityRecord::TableStats(v) => Some(v),
_ => None,
},
)
.await
}
#[doc(hidden)]
pub async fn dump_table_column_stats(catalog: &Catalog) -> Result<Vec<TableColumnStatsValue>> {
dump_projected_current(
catalog,
ProjectionCache::table_column_stats_at,
ProjectionCache::install_table_column_stats,
|r| match r {
EntityRecord::TableColumnStats(v) => Some(v),
_ => None,
},
)
.await
}
#[doc(hidden)]
pub async fn dump_file_column_stats(catalog: &Catalog) -> Result<Vec<FileColumnStatsValue>> {
dump_current_entities(catalog, |r| match r {
EntityRecord::FileColumnStats(v) => Some(v),
_ => None,
})
.await
}
#[doc(hidden)]
pub async fn dump_snapshots(catalog: &Catalog) -> Result<Vec<SnapshotValue>> {
let session = catalog.begin_read().await?;
let head = session_head(&session).await?;
if let (true, Some(head)) = (catalog.maintains_projections(), head) {
if let Some(rows) = projections_read(catalog).snapshots_at(head) {
session.finish();
return Ok(rows);
}
let result = scan_snapshots(session.handle()).await;
session.finish();
let rows = result?;
projections_write(catalog).install_snapshots(head, rows.clone());
return Ok(rows);
}
let result = scan_snapshots(session.handle()).await;
session.finish();
result
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SchemaVersionRow {
pub begin_snapshot: u64,
pub schema_version: u64,
pub table_id: u64,
}
#[doc(hidden)]
pub async fn dump_schema_versions(catalog: &Catalog) -> Result<Vec<SchemaVersionRow>> {
let snapshots = dump_snapshots(catalog).await?;
Ok(snapshots
.into_iter()
.flat_map(|snapshot| {
let begin_snapshot = snapshot.snapshot_id;
let schema_version = snapshot.schema_version;
snapshot
.schema_changed_table_ids
.into_iter()
.map(move |table_id| SchemaVersionRow {
begin_snapshot,
schema_version,
table_id,
})
})
.collect())
}
#[doc(hidden)]
pub async fn dump_scheduled_deletions(catalog: &Catalog) -> Result<Vec<GcFileValue>> {
dump_current_entities(catalog, |r| match r {
EntityRecord::GcFile(v) => Some(v),
_ => None,
})
.await
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagRow {
pub object_id: u64,
pub begin_snapshot: u64,
pub end_snapshot: Option<u64>,
pub key: String,
pub value: String,
}
#[doc(hidden)]
pub async fn dump_tags(catalog: &Catalog) -> Result<Vec<TagRow>> {
let containers = dump_current_entities(catalog, |r| match r {
EntityRecord::Tag(v) => Some(v),
_ => None,
})
.await?;
Ok(containers
.into_iter()
.flat_map(|container| {
let object_id = container.object_id;
container.entries.into_iter().map(move |e| TagRow {
object_id,
begin_snapshot: e.begin_snapshot,
end_snapshot: e.end_snapshot,
key: e.key,
value: e.value,
})
})
.collect())
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ColumnTagRow {
pub table_id: u64,
pub column_id: u64,
pub begin_snapshot: u64,
pub end_snapshot: Option<u64>,
pub key: String,
pub value: String,
}
#[doc(hidden)]
pub async fn dump_column_tags(catalog: &Catalog) -> Result<Vec<ColumnTagRow>> {
let columns = dump_columns(catalog).await?;
let mut latest: std::collections::BTreeMap<(u64, u64), &ColumnValue> =
std::collections::BTreeMap::new();
for column in &columns {
let entry = latest
.entry((column.table_id, column.column_id))
.or_insert(column);
let newer = match (column.end_snapshot, entry.end_snapshot) {
(None, _) => true,
(Some(_), None) => false,
(Some(a), Some(b)) => a > b,
};
if newer {
*entry = column;
}
}
Ok(latest
.into_values()
.flat_map(|column| {
column.tags.iter().map(|t| ColumnTagRow {
table_id: column.table_id,
column_id: column.column_id,
begin_snapshot: t.begin_snapshot,
end_snapshot: t.end_snapshot,
key: t.key.clone(),
value: t.value.clone(),
})
})
.collect())
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MacroImplRow {
pub macro_id: u64,
pub impl_id: u64,
pub dialect: String,
pub sql: String,
pub macro_type: String,
}
#[doc(hidden)]
pub async fn dump_macro_impl_rows(catalog: &Catalog) -> Result<Vec<MacroImplRow>> {
Ok(dump_macros(catalog)
.await?
.into_iter()
.flat_map(|value| {
let macro_id = value.macro_id;
value
.implementations
.into_iter()
.map(move |implementation| MacroImplRow {
macro_id,
impl_id: implementation.impl_id,
dialect: implementation.dialect,
sql: implementation.sql,
macro_type: implementation.macro_type,
})
})
.collect())
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MacroParameterRow {
pub macro_id: u64,
pub impl_id: u64,
pub column_id: u64,
pub parameter_name: String,
pub parameter_type: String,
pub default_value: Option<String>,
pub default_value_type: String,
}
#[doc(hidden)]
pub async fn dump_macro_parameter_rows(catalog: &Catalog) -> Result<Vec<MacroParameterRow>> {
Ok(dump_macros(catalog)
.await?
.into_iter()
.flat_map(|value| {
let macro_id = value.macro_id;
value.implementations.into_iter().flat_map(move |i| {
let impl_id = i.impl_id;
i.parameters.into_iter().map(move |p| MacroParameterRow {
macro_id,
impl_id,
column_id: p.column_id,
parameter_name: p.parameter_name,
parameter_type: p.parameter_type,
default_value: p.default_value,
default_value_type: p.default_value_type,
})
})
})
.collect())
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NameMappingRow {
pub mapping_id: u64,
pub column_id: u64,
pub source_name: String,
pub target_field_id: u64,
pub parent_column: Option<u64>,
pub is_partition: bool,
}
#[doc(hidden)]
pub async fn dump_name_mapping_rows(catalog: &Catalog) -> Result<Vec<NameMappingRow>> {
Ok(dump_mappings(catalog)
.await?
.into_iter()
.flat_map(|value| {
let mapping_id = value.mapping_id;
value
.name_mappings
.into_iter()
.map(move |row| NameMappingRow {
mapping_id,
column_id: row.column_id,
source_name: row.source_name,
target_field_id: row.target_field_id,
parent_column: row.parent_column,
is_partition: row.is_partition,
})
})
.collect())
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PartitionColumnRow {
pub partition_id: u64,
pub table_id: u64,
pub partition_key_index: u64,
pub column_id: u64,
pub transform: String,
}
#[doc(hidden)]
pub async fn dump_partition_column_rows(catalog: &Catalog) -> Result<Vec<PartitionColumnRow>> {
Ok(dump_partition_info(catalog)
.await?
.into_iter()
.flat_map(|spec| {
let (partition_id, table_id) = (spec.partition_id, spec.table_id);
spec.columns
.into_iter()
.map(move |column| PartitionColumnRow {
partition_id,
table_id,
partition_key_index: column.partition_key_index,
column_id: column.column_id,
transform: column.transform,
})
})
.collect())
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilePartitionValueRow {
pub data_file_id: u64,
pub table_id: u64,
pub partition_key_index: u64,
pub partition_value: String,
}
#[doc(hidden)]
pub async fn dump_file_partition_value_rows(
catalog: &Catalog,
) -> Result<Vec<FilePartitionValueRow>> {
Ok(dump_data_files(catalog)
.await?
.into_iter()
.flat_map(|file| {
let (data_file_id, table_id) = (file.data_file_id, file.table_id);
file.partition_values
.into_iter()
.map(move |value| FilePartitionValueRow {
data_file_id,
table_id,
partition_key_index: value.partition_key_index,
partition_value: value.partition_value,
})
})
.collect())
}
#[doc(hidden)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SortExpressionRow {
pub sort_id: u64,
pub table_id: u64,
pub sort_key_index: u64,
pub expression: String,
pub dialect: String,
pub sort_direction: String,
pub null_order: String,
}
#[doc(hidden)]
pub async fn dump_sort_expression_rows(catalog: &Catalog) -> Result<Vec<SortExpressionRow>> {
Ok(dump_sort_info(catalog)
.await?
.into_iter()
.flat_map(|spec| {
let (sort_id, table_id) = (spec.sort_id, spec.table_id);
spec.expressions
.into_iter()
.map(move |expression| SortExpressionRow {
sort_id,
table_id,
sort_key_index: expression.sort_key_index,
expression: expression.expression,
dialect: expression.dialect,
sort_direction: expression.sort_direction,
null_order: expression.null_order,
})
})
.collect())
}
#[cfg(test)]
mod tests {
use object_store::memory::InMemory;
use super::*;
use crate::catalog::{
CatalogOptions, ColumnDef, ColumnStats, DataFile, DeleteFile, FileColumnStats,
MacroImplementationDef, MacroParameterDef,
};
async fn seed() -> Catalog {
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default())
.await
.unwrap();
catalog
.commit(|tx| {
let schema = tx.create_schema("sales")?;
let table = tx.create_table(
schema,
"orders",
&[
ColumnDef {
name: "id".into(),
column_type: "BIGINT".into(),
nulls_allowed: false,
default_value: None,
},
ColumnDef {
name: "amount".into(),
column_type: "DOUBLE".into(),
nulls_allowed: true,
default_value: None,
},
],
)?;
let column = tx.columns_of(table)[0].id;
let file = tx.register_data_file(
table,
DataFile {
path: "orders/data-1.parquet".into(),
path_is_relative: true,
file_format: "parquet".into(),
record_count: 10,
file_size_bytes: 1024,
footer_size: 64,
encryption_key: None,
column_stats: vec![FileColumnStats {
column_id: column,
column_size_bytes: 100,
value_count: 10,
null_count: 0,
min_value: Some("1".into()),
max_value: Some("10".into()),
contains_nan: None,
extra_stats: None,
}],
},
&[],
)?;
tx.register_delete_file(
table,
DeleteFile {
data_file_id: file,
path: "orders/delete-1.parquet".into(),
path_is_relative: true,
format: "parquet".into(),
delete_count: 2,
file_size_bytes: 128,
footer_size: 32,
encryption_key: None,
},
&[],
)?;
tx.update_column_stats(
table,
column,
ColumnStats {
contains_null: Some(false),
contains_nan: None,
min_value: Some("1".into()),
max_value: Some("10".into()),
extra_stats: None,
},
)?;
tx.create_view(schema, "orders_v", "duckdb", "select * from orders")?;
Ok(())
})
.await
.unwrap();
catalog
.commit(|tx| {
let table = tx.tables_in(tx.schemas()[1].id)[0].id;
tx.rename_table(table, "orders2")
})
.await
.unwrap();
catalog
}
#[tokio::test]
async fn unversioned_dumps_serve_live_rows_on_a_history_bearing_catalog() {
let catalog = seed().await;
let stats = dump_table_stats(&catalog).await.unwrap();
assert_eq!(stats.len(), 1);
assert_eq!(stats[0].record_count, 10);
let column_stats = dump_table_column_stats(&catalog).await.unwrap();
assert_eq!(column_stats.len(), 1);
assert_eq!(column_stats[0].min_value.as_deref(), Some("1"));
let file_stats = dump_file_column_stats(&catalog).await.unwrap();
assert_eq!(file_stats.len(), 1);
let deletions = dump_scheduled_deletions(&catalog).await.unwrap();
assert!(deletions.is_empty());
}
#[tokio::test]
async fn dump_schemas_returns_bootstrap_and_seeded_schemas_with_no_history() {
let catalog = seed().await;
let rows = dump_schemas(&catalog).await.unwrap();
assert_eq!(rows.len(), 2);
assert!(rows.iter().all(|r| r.end_snapshot.is_none()));
let names: Vec<&str> = rows.iter().map(|r| r.schema_name.as_str()).collect();
assert_eq!(names, vec!["main", "sales"]);
}
#[tokio::test]
async fn dump_tables_returns_both_versions_of_a_renamed_table() {
let catalog = seed().await;
let rows = dump_tables(&catalog).await.unwrap();
assert_eq!(
rows.len(),
2,
"rename must yield exactly one current + one history row"
);
let ended = rows.iter().find(|r| r.end_snapshot.is_some()).unwrap();
let live = rows.iter().find(|r| r.end_snapshot.is_none()).unwrap();
assert_eq!(ended.table_name, "orders");
assert_eq!(live.table_name, "orders2");
assert_eq!(ended.table_id, live.table_id);
assert_eq!(ended.table_uuid, live.table_uuid);
assert_eq!(ended.end_snapshot, Some(live.begin_snapshot));
assert!(live.begin_snapshot > ended.begin_snapshot);
}
#[tokio::test]
async fn dump_columns_and_views_are_row_faithful() {
let catalog = seed().await;
let columns = dump_columns(&catalog).await.unwrap();
assert_eq!(columns.len(), 2);
assert!(columns.iter().all(|c| c.end_snapshot.is_none()));
let mut names: Vec<&str> = columns.iter().map(|c| c.column_name.as_str()).collect();
names.sort_unstable();
assert_eq!(names, vec!["amount", "id"]);
let views = dump_views(&catalog).await.unwrap();
assert_eq!(views.len(), 1);
assert_eq!(views[0].sql, "select * from orders");
assert!(views[0].end_snapshot.is_none());
}
#[tokio::test]
async fn dump_mappings_serves_embedded_rows() {
use crate::transaction::staged::{Cell, RowOperation, StagedTransaction, TableKind};
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default())
.await
.unwrap();
let db_tx = catalog.begin_write_tx().await.unwrap();
let mut tx = StagedTransaction::begin_detached(db_tx);
tx.stage(RowOperation::Insert {
table: TableKind::ColumnMapping,
cells: vec![Cell::U64(21), Cell::U64(1), Cell::Str("map_by_name".into())],
});
tx.stage(RowOperation::Insert {
table: TableKind::NameMapping,
cells: vec![
Cell::U64(21),
Cell::U64(0),
Cell::Str("id".into()),
Cell::U64(1),
Cell::Null,
Cell::Bool(false),
],
});
tx.stage(RowOperation::Insert {
table: TableKind::Snapshot,
cells: vec![
Cell::U64(1),
Cell::I64(1),
Cell::U64(1),
Cell::U64(11),
Cell::U64(22),
],
});
tx.stage(RowOperation::Insert {
table: TableKind::SnapshotChanges,
cells: vec![
Cell::U64(1),
Cell::Str("inserted_into_table:1".into()),
Cell::Null,
Cell::Null,
Cell::Null,
],
});
tx.commit().await.unwrap();
let mappings = dump_mappings(&catalog).await.unwrap();
assert_eq!(mappings.len(), 1);
assert_eq!(mappings[0].mapping_id, 21);
assert_eq!(mappings[0].table_id, 1);
assert_eq!(mappings[0].map_type, "map_by_name");
assert_eq!(mappings[0].name_mappings.len(), 1);
assert_eq!(mappings[0].name_mappings[0].source_name, "id");
}
#[tokio::test]
async fn dump_macros_serves_children_current_and_history() {
let catalog = Catalog::open(Arc::new(InMemory::new()), CatalogOptions::default())
.await
.unwrap();
catalog
.commit(|tx| {
let schema = tx.create_schema("s")?;
tx.create_macro(
schema,
"add",
&[
MacroImplementationDef {
dialect: "duckdb".into(),
sql: "(a + 1)".into(),
macro_type: "scalar".into(),
parameters: vec![MacroParameterDef {
name: "a".into(),
parameter_type: "unknown".into(),
default_value: None,
default_value_type: "unknown".into(),
}],
},
MacroImplementationDef {
dialect: "duckdb".into(),
sql: "(a + b)".into(),
macro_type: "scalar".into(),
parameters: vec![
MacroParameterDef {
name: "a".into(),
parameter_type: "unknown".into(),
default_value: None,
default_value_type: "unknown".into(),
},
MacroParameterDef {
name: "b".into(),
parameter_type: "unknown".into(),
default_value: Some("5".into()),
default_value_type: "int32".into(),
},
],
},
],
)?;
Ok(())
})
.await
.unwrap();
let head = catalog.snapshot().await.unwrap();
let schema = head.schema_by_name("s").unwrap();
let created = head.macro_by_name(schema.id, "add").unwrap();
catalog
.commit(move |tx| tx.drop_macro(created.id))
.await
.unwrap();
let macros = dump_macros(&catalog).await.unwrap();
assert_eq!(macros.len(), 1);
let ended = ¯os[0];
assert!(ended.end_snapshot.is_some());
assert_eq!(ended.implementations.len(), 2);
assert_eq!(ended.implementations[0].impl_id, 0);
assert_eq!(ended.implementations[1].parameters[1].parameter_name, "b");
assert_eq!(
ended.implementations[1].parameters[1]
.default_value
.as_deref(),
Some("5")
);
}
#[tokio::test]
async fn dump_data_and_delete_files_carry_registration_values_verbatim() {
let catalog = seed().await;
let files = dump_data_files(&catalog).await.unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].path, "orders/data-1.parquet");
assert_eq!(files[0].record_count, 10);
assert_eq!(files[0].row_id_start, Some(0));
assert!(files[0].end_snapshot.is_none());
let deletes = dump_delete_files(&catalog).await.unwrap();
assert_eq!(deletes.len(), 1);
assert_eq!(deletes[0].path, "orders/delete-1.parquet");
assert_eq!(deletes[0].data_file_id, files[0].data_file_id);
assert_eq!(deletes[0].delete_count, 2);
}
#[tokio::test]
async fn dump_statistics_kinds_are_unversioned_single_rows() {
let catalog = seed().await;
let table_rows = dump_table_stats(&catalog).await.unwrap();
assert_eq!(table_rows.len(), 1);
assert_eq!(table_rows[0].record_count, 10);
let table_col_rows = dump_table_column_stats(&catalog).await.unwrap();
assert_eq!(table_col_rows.len(), 1);
assert_eq!(table_col_rows[0].contains_null, Some(false));
let file_col_rows = dump_file_column_stats(&catalog).await.unwrap();
assert_eq!(file_col_rows.len(), 1);
assert_eq!(file_col_rows[0].min_value.as_deref(), Some("1"));
assert_eq!(file_col_rows[0].max_value.as_deref(), Some("10"));
}
#[tokio::test]
async fn dump_snapshots_returns_every_committed_snapshot_in_order() {
let catalog = seed().await;
let rows = dump_snapshots(&catalog).await.unwrap();
assert_eq!(rows.len(), 3);
let ids: Vec<u64> = rows.iter().map(|r| r.snapshot_id).collect();
assert_eq!(ids, vec![0, 1, 2]);
assert_eq!(rows[0].changes_made, "created_schema:\"main\"");
assert!(!rows[1].changes_made.is_empty());
assert!(!rows[2].changes_made.is_empty());
}
}