use std::sync::Arc;
use rstest::rstest;
use test_utils::{add_commit, assert_result_error_with_message, delta_path_for_version};
use super::{CheckpointActionResolution, LogSegment};
use crate::actions::LastManifestCommit;
use crate::crc::{Crc, FileStats, FileStatsState};
use crate::engine::sync::SyncEngine;
#[cfg(feature = "declarative-plans")]
use crate::engine::test_delegating::DelegatingEngine;
use crate::object_store::memory::InMemory;
use crate::object_store::ObjectStoreExt as _;
use crate::schema::SchemaRef;
use crate::table_features::TableFeature;
use crate::unit_test_utils::{
adaptive_metadata_table_configuration, test_schema_flat_with_column_mapping,
};
use crate::{Engine, Snapshot};
fn one_column_schema() -> SchemaRef {
test_schema_flat_with_column_mapping()
.project(&["id"])
.unwrap()
}
fn checkpoint_commit(version: i64, extra_features: &[TableFeature], schema: SchemaRef) -> String {
let config = adaptive_metadata_table_configuration(schema, extra_features);
serde_json::json!({ "checkpoint": [
{ "checkpointMetadata": { "version": version } },
{ "contentRoot": { "path": "metadata/root.parquet", "sizeInBytes": 1, "version": version } },
{ "protocol": config.protocol() },
{ "metaData": config.metadata() },
] })
.to_string()
}
fn metadata_commit(schema: SchemaRef) -> String {
let config = adaptive_metadata_table_configuration(schema, &[]);
serde_json::json!({ "metaData": config.metadata() }).to_string()
}
fn standalone_pm_commit(schema: SchemaRef) -> String {
let config = adaptive_metadata_table_configuration(schema, &[]);
format!(
"{}\n{}",
serde_json::json!({ "protocol": config.protocol() }),
serde_json::json!({ "metaData": config.metadata() }),
)
}
fn protocol_commit(min_reader_version: i64, min_writer_version: i64) -> String {
serde_json::json!({ "protocol": {
"minReaderVersion": min_reader_version,
"minWriterVersion": min_writer_version,
} })
.to_string()
}
fn non_plan_engine(store: Arc<InMemory>) -> impl Engine {
let engine = SyncEngine::new_with_store(store);
#[cfg(feature = "declarative-plans")]
let engine = DelegatingEngine::new(Arc::new(engine)).without_plan_executor();
engine
}
#[tokio::test]
async fn test_load_resolves_pm_from_manifest_commit_checkpoint_action() {
check_manifest_commit_checkpoint(non_plan_engine).await;
#[cfg(feature = "declarative-plans")]
check_manifest_commit_checkpoint(|store| SyncEngine::new_with_store(store)).await;
}
async fn check_manifest_commit_checkpoint<E: Engine>(make_engine: impl FnOnce(Arc<InMemory>) -> E) {
let store = Arc::new(InMemory::new());
let table_root = url::Url::parse("memory:///").unwrap();
add_commit(
table_root.as_str(),
store.as_ref(),
0,
checkpoint_commit(0, &[], one_column_schema()),
)
.await
.unwrap();
let engine = make_engine(store);
let snapshot = Snapshot::builder_for(table_root).build(&engine).unwrap();
assert_eq!(snapshot.version(), 0);
assert!(snapshot.schema().field("id").is_some());
}
#[rstest]
#[case::newer_checkpoint_beats_older_pm(
format!("{}\n{}", protocol_commit(1, 2), metadata_commit(one_column_schema())),
checkpoint_commit(1, &[], test_schema_flat_with_column_mapping()),
2,
3
)]
#[case::newer_checkpoint_beats_older_metadata(
metadata_commit(one_column_schema()),
checkpoint_commit(1, &[], test_schema_flat_with_column_mapping()),
2,
3
)]
#[case::newer_metadata_beats_older_checkpoint(
checkpoint_commit(0, &[], one_column_schema()),
metadata_commit(test_schema_flat_with_column_mapping()),
2,
3
)]
#[case::newer_checkpoint_protocol_wins(
checkpoint_commit(0, &[], one_column_schema()),
checkpoint_commit(1, &[TableFeature::TimestampWithoutTimezone], one_column_schema()),
1,
4
)]
#[tokio::test]
async fn resolve_pm_newest_action_wins(
#[case] v0: String,
#[case] v1: String,
#[case] expected_fields: usize,
#[case] expected_reader_features: usize,
) {
assert_newest_pm_wins(v0, v1, expected_fields, expected_reader_features).await;
}
async fn assert_newest_pm_wins(
v0: String,
v1: String,
expected_fields: usize,
expected_reader_features: usize,
) {
build_and_check_pm(
&v0,
&v1,
expected_fields,
expected_reader_features,
non_plan_engine,
)
.await;
#[cfg(feature = "declarative-plans")]
build_and_check_pm(
&v0,
&v1,
expected_fields,
expected_reader_features,
|store| SyncEngine::new_with_store(store),
)
.await;
}
async fn build_and_check_pm<E: Engine>(
v0: &str,
v1: &str,
expected_fields: usize,
expected_reader_features: usize,
make_engine: impl FnOnce(Arc<InMemory>) -> E,
) {
let store = Arc::new(InMemory::new());
let table_root = url::Url::parse("memory:///").unwrap();
add_commit(table_root.as_str(), store.as_ref(), 0, v0.to_string())
.await
.unwrap();
add_commit(table_root.as_str(), store.as_ref(), 1, v1.to_string())
.await
.unwrap();
let engine = make_engine(store);
let snapshot = Snapshot::builder_for(table_root).build(&engine).unwrap();
assert_eq!(snapshot.version(), 1);
assert_eq!(
snapshot.schema().num_fields(),
expected_fields,
"resolved metaData should have {expected_fields} column(s)"
);
let protocol = snapshot.table_configuration().protocol();
assert_eq!(
protocol.min_reader_version(),
3,
"AMT protocol is reader v3"
);
assert_eq!(
protocol.reader_features().map_or(0, |f| f.len()),
expected_reader_features,
"resolved protocol should have {expected_reader_features} reader feature(s)"
);
}
#[tokio::test]
async fn test_lagging_checkpoint_ranks_by_checkpoint_version() {
assert_lagging_checkpoint_loses_to_gap_commit(non_plan_engine).await;
#[cfg(feature = "declarative-plans")]
assert_lagging_checkpoint_loses_to_gap_commit(|store| SyncEngine::new_with_store(store)).await;
}
async fn assert_lagging_checkpoint_loses_to_gap_commit<E: Engine>(
make_engine: impl FnOnce(Arc<InMemory>) -> E,
) {
let store = Arc::new(InMemory::new());
let table_root = url::Url::parse("memory:///").unwrap();
add_commit(
table_root.as_str(),
store.as_ref(),
0,
checkpoint_commit(0, &[], one_column_schema()),
)
.await
.unwrap();
add_commit(
table_root.as_str(),
store.as_ref(),
1,
metadata_commit(test_schema_flat_with_column_mapping()),
)
.await
.unwrap();
add_commit(
table_root.as_str(),
store.as_ref(),
2,
checkpoint_commit(0, &[], one_column_schema()),
)
.await
.unwrap();
let engine = make_engine(store);
let snapshot = Snapshot::builder_for(table_root).build(&engine).unwrap();
assert_eq!(snapshot.version(), 2);
let schema = snapshot.schema();
assert!(schema.field("name").is_some());
assert_eq!(schema.num_fields(), 2);
}
#[rstest]
#[case::commit_carries_checkpoint_action(checkpoint_commit(0, &[], one_column_schema()), Some(0))]
#[case::standalone_pm_has_no_checkpoint_action(standalone_pm_commit(one_column_schema()), None)]
#[tokio::test]
async fn replay_captures_latest_checkpoint_action(
#[case] commit: String,
#[case] expected_version: Option<i64>,
) {
assert_latest_checkpoint_action(&commit, expected_version, non_plan_engine).await;
#[cfg(feature = "declarative-plans")]
assert_latest_checkpoint_action(&commit, expected_version, |store| {
SyncEngine::new_with_store(store)
})
.await;
}
async fn assert_latest_checkpoint_action<E: Engine>(
commit: &str,
expected_version: Option<i64>,
make_engine: impl FnOnce(Arc<InMemory>) -> E,
) {
let store = Arc::new(InMemory::new());
let table_root = url::Url::parse("memory:///").unwrap();
add_commit(table_root.as_str(), store.as_ref(), 0, commit.to_string())
.await
.unwrap();
let engine = make_engine(store);
let snapshot = Snapshot::builder_for(table_root).build(&engine).unwrap();
let action = snapshot.latest_checkpoint_action(&engine).unwrap();
assert_eq!(action.map(|action| action.version), expected_version);
}
#[tokio::test]
async fn latest_checkpoint_action_scans_when_resolution_unknown() {
let store = Arc::new(InMemory::new());
let table_root = url::Url::parse("memory:///").unwrap();
add_commit(
table_root.as_str(),
store.as_ref(),
0,
checkpoint_commit(0, &[], one_column_schema()),
)
.await
.unwrap();
let engine = non_plan_engine(store);
let storage = engine.storage_handler();
let log_root = table_root.join("_delta_log/").unwrap();
let log_segment =
LogSegment::for_snapshot_impl(storage.as_ref(), log_root, vec![], None, None, None)
.unwrap();
let table_configuration = adaptive_metadata_table_configuration(one_column_schema(), &[]);
let snapshot = Snapshot::new(log_segment, table_configuration).unwrap();
let action = snapshot
.latest_checkpoint_action(&engine)
.unwrap()
.expect("lazy scan should find the checkpoint action");
assert_eq!(action.version, 0);
assert_eq!(action.content_root.path, "metadata/root.parquet");
}
#[rstest]
#[case::single_checkpoint_commit(vec![checkpoint_commit(0, &[], one_column_schema())], Some(0))]
#[case::checkpoint_action_in_non_first_batch(
vec![
checkpoint_commit(0, &[], one_column_schema()),
metadata_commit(test_schema_flat_with_column_mapping()),
],
Some(0)
)]
#[case::standalone_pm_has_no_checkpoint_action(
vec![standalone_pm_commit(one_column_schema())],
None
)]
#[tokio::test]
async fn replay_resolves_latest_checkpoint_action(
#[case] commits: Vec<String>,
#[case] expected_version: Option<i64>,
) {
assert_replay_resolution(&commits, expected_version, non_plan_engine).await;
#[cfg(feature = "declarative-plans")]
assert_replay_resolution(&commits, expected_version, |store| {
SyncEngine::new_with_store(store)
})
.await;
}
async fn assert_replay_resolution<E: Engine>(
commits: &[String],
expected_version: Option<i64>,
make_engine: impl FnOnce(Arc<InMemory>) -> E,
) {
let store = Arc::new(InMemory::new());
let table_root = url::Url::parse("memory:///").unwrap();
for (version, commit) in commits.iter().enumerate() {
add_commit(
table_root.as_str(),
store.as_ref(),
version as u64,
commit.clone(),
)
.await
.unwrap();
}
let engine = make_engine(store);
let storage = engine.storage_handler();
let log_root = table_root.join("_delta_log/").unwrap();
let log_segment =
LogSegment::for_snapshot_impl(storage.as_ref(), log_root, vec![], None, None, None)
.unwrap();
let resolution = log_segment
.read_protocol_metadata_opt(&engine, None)
.unwrap();
let version = match resolution.checkpoint_action {
CheckpointActionResolution::Captured(action) => Some(action.version),
CheckpointActionResolution::Located(_) | CheckpointActionResolution::Unresolved => None,
};
assert_eq!(version, expected_version);
}
#[rstest]
#[case::new_commit_carries_a_newer_action(checkpoint_commit(1, &[], one_column_schema()), 1)]
#[case::new_commit_carries_no_action(metadata_commit(one_column_schema()), 0)]
#[tokio::test]
async fn incremental_update_resolves_latest_checkpoint_action(
#[case] new_commit: String,
#[case] expected_version: i64,
) {
let store = Arc::new(InMemory::new());
let table_root = url::Url::parse("memory:///").unwrap();
add_commit(
table_root.as_str(),
store.as_ref(),
0,
checkpoint_commit(0, &[], one_column_schema()),
)
.await
.unwrap();
let engine = non_plan_engine(store.clone());
let base = Snapshot::builder_for(table_root.clone())
.at_version(0)
.build(&engine)
.unwrap();
assert_eq!(
base.latest_checkpoint_action(&engine)
.unwrap()
.map(|a| a.version),
Some(0)
);
add_commit(table_root.as_str(), store.as_ref(), 1, new_commit)
.await
.unwrap();
let updated = Snapshot::builder_from(base).build(&engine).unwrap();
assert_eq!(updated.version(), 1);
assert_eq!(
updated
.latest_checkpoint_action(&engine)
.unwrap()
.map(|a| a.version),
Some(expected_version)
);
}
#[rstest]
#[case::pointer_names_latest_manifest_commit(
checkpoint_commit(1, &[], one_column_schema()),
2,
2,
Ok(2)
)]
#[case::pointer_is_read_instead_of_scanning(
checkpoint_commit(1, &[], one_column_schema()),
1,
1,
Ok(1)
)]
#[case::pointer_names_commit_checkpointing_older_version(
checkpoint_commit(0, &[], one_column_schema()),
1,
0,
Ok(0)
)]
#[case::manifest_commit_without_action_fails(
metadata_commit(one_column_schema()),
1,
1,
Err("that commit carries no checkpoint action")
)]
#[case::manifest_commit_with_other_content_root_version_fails(
checkpoint_commit(0, &[], one_column_schema()),
1,
1,
Err("that commit's checkpoint action has content root version 0")
)]
#[tokio::test]
async fn latest_checkpoint_action_reads_commit_named_by_crc_pointer(
#[case] v1: String,
#[case] pointer_version: i64,
#[case] pointer_content_root_version: i64,
#[case] expected: Result<i64, &str>,
) {
let store = Arc::new(InMemory::new());
let table_root = url::Url::parse("memory:///").unwrap();
let commits = [
checkpoint_commit(0, &[], one_column_schema()),
v1,
checkpoint_commit(2, &[], one_column_schema()),
];
for (version, commit) in commits.into_iter().enumerate() {
add_commit(table_root.as_str(), store.as_ref(), version as u64, commit)
.await
.unwrap();
}
let pointer = LastManifestCommit::new(pointer_version, pointer_content_root_version).unwrap();
put_crc_with_pointer(&store, 2, pointer).await;
let engine = non_plan_engine(store);
let snapshot = Snapshot::builder_for(table_root).build(&engine).unwrap();
let result = snapshot.latest_checkpoint_action(&engine);
match expected {
Ok(version) => assert_eq!(result.unwrap().map(|a| a.version), Some(version)),
Err(message) => assert_result_error_with_message(result, message),
}
}
#[rstest]
#[case::below_segment(0)]
#[case::above_segment(5)]
#[tokio::test]
async fn find_last_checkpoint_action_errors_when_manifest_commit_is_not_listed(
#[case] pointer_version: i64,
) {
let store = Arc::new(InMemory::new());
let table_root = url::Url::parse("memory:///").unwrap();
for version in 0..2 {
add_commit(
table_root.as_str(),
store.as_ref(),
version,
checkpoint_commit(version as i64, &[], one_column_schema()),
)
.await
.unwrap();
}
let engine = non_plan_engine(store);
let storage = engine.storage_handler();
let log_root = table_root.join("_delta_log/").unwrap();
let log_segment =
LogSegment::for_snapshot_impl(storage.as_ref(), log_root, vec![], None, None, None)
.unwrap()
.segment_after_version(0);
let pointer = LastManifestCommit::new(pointer_version, pointer_version).unwrap();
let result = log_segment.find_last_checkpoint_action(&engine, Some(&pointer));
assert_result_error_with_message(result, "this log segment does not list that commit");
}
async fn put_crc_with_pointer(store: &InMemory, version: u64, pointer: LastManifestCommit) {
let config = adaptive_metadata_table_configuration(one_column_schema(), &[]);
let crc = Crc {
version,
protocol: config.protocol().clone(),
metadata: config.metadata().clone(),
file_stats_state: FileStatsState::Complete(FileStats::try_new(0, 0, None).unwrap()),
in_commit_timestamp_opt: Some(1000),
last_manifest_commit_opt: Some(pointer),
..Default::default()
};
store
.put(
&delta_path_for_version(version, "crc"),
serde_json::to_vec(&crc).unwrap().into(),
)
.await
.unwrap();
}