use ::test_utils::add_commit;
use super::*;
use crate::actions::{get_commit_schema, TIGHT_BOUNDS};
use crate::arrow::array::{ArrayRef, AsArray, BinaryArray};
use crate::engine::arrow_expression::evaluate_expression::evaluate_expression;
use crate::engine::arrow_expression::opaque::{ArrowOpaqueExpression, ArrowOpaqueExpressionOp};
use crate::expressions::{ParseJsonExpression, ScalarExpressionEvaluator};
use crate::schema::SchemaStructPatchBuilder;
use crate::unit_test_utils::{load_test_table, string_array_to_engine_data};
use crate::{EvaluationHandler, ExpressionEvaluator, PredicateEvaluator};
#[rstest]
#[case::all_struct(StatsOptions::all_struct())]
#[case::struct_columns(StatsOptions::struct_columns(vec![column_name!("id")]))]
fn scan_builder_accepts_variant_min_max_stats_with_struct_stats_without_json_synthesis(
#[case] stats: StatsOptions,
) {
let (_, snapshot, _tempdir) = load_test_table("parsed-stats").unwrap();
snapshot
.scan_builder()
.with_stats(stats.with_variant_min_max_stats(true))
.build()
.unwrap();
}
#[rstest]
#[case::json_only(StatsOptions::json_only(), "requires struct stats output")]
#[case::none(StatsOptions::none(), "requires struct stats output")]
#[case::all(StatsOptions::all(), "cannot be combined with JSON stats synthesis")]
fn scan_builder_rejects_variant_min_max_stats_without_struct_stats_or_with_json_synthesis(
#[case] stats: StatsOptions,
#[case] message: &str,
) {
let (_, snapshot, _tempdir) = load_test_table("parsed-stats").unwrap();
let result = snapshot
.scan_builder()
.with_stats(stats.with_variant_min_max_stats(true))
.build();
assert_result_error_with_message(result, message);
}
#[tokio::test]
async fn scan_metadata_variant_stats_from_commit_json_and_checkpoint_stats_parsed() {
let (engine, table_root) = setup_variant_stats_table().await;
let snapshot = Snapshot::builder_for(&table_root).build(&engine).unwrap();
assert_eq!(snapshot.log_segment().checkpoint_version, Some(1));
let scan = snapshot
.scan_builder()
.with_stats(StatsOptions::all_struct().with_variant_min_max_stats(true))
.build()
.unwrap();
let expected =
|path: &str, bound, n: u8| (path.to_string(), bound, vec![1, 0, 0], vec![0x0c, n]);
assert_eq!(
collect_variant_bounds(&scan, &engine),
vec![
expected("a.parquet", MAX_VALUES, 3),
expected("a.parquet", MIN_VALUES, 1),
expected("b.parquet", MAX_VALUES, 6),
expected("b.parquet", MIN_VALUES, 4),
]
);
}
async fn setup_variant_stats_table() -> (DelegatingEngine, String) {
let table_root = String::from("memory:///");
let store = Arc::new(InMemory::new());
let sync = Arc::new(SyncEngine::new_with_store(store.clone()));
let engine = DelegatingEngine::new(sync.clone()).with_evaluation_handler(Arc::new(
VariantStatsEvaluationHandler {
inner: sync.evaluation_handler(),
},
));
let commit = |actions: &[serde_json::Value]| {
actions
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
};
let table_schema = schema! {
nullable "id": LONG,
nullable "v": (DataType::unshredded_variant()),
};
let protocol = serde_json::json!({"protocol": {
"minReaderVersion": 3,
"minWriterVersion": 7,
"readerFeatures": ["variantType"],
"writerFeatures": ["variantType"],
}});
let metadata = serde_json::json!({"metaData": {
"id": "variant-stats",
"format": {"provider": "parquet", "options": {}},
"schemaString": serde_json::to_string(&table_schema).unwrap(),
"partitionColumns": [],
"configuration": {},
"createdTime": 1,
}});
let stats = |min: u8, max: u8, variant: fn(u8) -> serde_json::Value| {
serde_json::json!({
"numRecords": 3,
"nullCount": {"id": 0, "v": 0},
"minValues": {"id": min, "v": variant(min)},
"maxValues": {"id": max, "v": variant(max)},
"tightBounds": true,
})
};
let connector_variant = |n: u8| serde_json::json!(n);
let add = |path: &str, stats_field: &str, stats: serde_json::Value| {
serde_json::json!({"add": {
"path": path,
"partitionValues": {},
"size": 1,
"modificationTime": 1,
"dataChange": true,
stats_field: stats,
}})
};
add_commit(
&table_root,
store.as_ref(),
0,
commit(&[protocol.clone(), metadata.clone()]),
)
.await
.unwrap();
add_commit(
&table_root,
store.as_ref(),
1,
commit(&[add(
"a.parquet",
"stats",
stats(1, 3, connector_variant).to_string().into(),
)]),
)
.await
.unwrap();
let bounds = schema! {
nullable "id": LONG,
nullable "v": {
not_null "metadata": BINARY,
not_null "value": BINARY,
},
};
let stats_parsed = StructField::nullable(
STATS_PARSED,
schema! {
nullable NUM_RECORDS: LONG,
nullable NULL_COUNT: { nullable "id": LONG, nullable "v": LONG },
nullable MIN_VALUES: (bounds.clone()),
nullable MAX_VALUES: (bounds),
nullable TIGHT_BOUNDS: BOOLEAN,
},
);
let checkpoint_schema = Arc::new(
SchemaStructPatchBuilder::new()
.append_at(["add"], stats_parsed)
.build(get_commit_schema().as_ref())
.unwrap(),
);
let checkpoint_rows = [
protocol,
metadata,
add(
"a.parquet",
STATS_PARSED,
stats(1, 3, physical_variant_int8_json),
),
];
let checkpoint = engine
.json_handler()
.parse_json(
string_array_to_engine_data(StringArray::from_iter_values(
checkpoint_rows.iter().map(ToString::to_string),
)),
checkpoint_schema,
)
.unwrap();
engine
.parquet_handler()
.write_parquet_file(
Url::parse(&table_root)
.unwrap()
.join("_delta_log/00000000000000000001.checkpoint.parquet")
.unwrap(),
Box::new(std::iter::once(Ok(checkpoint))),
)
.unwrap();
add_commit(
&table_root,
store.as_ref(),
2,
commit(&[add(
"b.parquet",
"stats",
stats(4, 6, connector_variant).to_string().into(),
)]),
)
.await
.unwrap();
(engine, table_root)
}
fn collect_variant_bounds(
scan: &Scan,
engine: &dyn Engine,
) -> Vec<(String, &'static str, Vec<u8>, Vec<u8>)> {
let mut actual = Vec::new();
for scan_metadata in scan.scan_metadata(engine).unwrap() {
let (data, selection_vector) = scan_metadata.unwrap().scan_files.into_parts();
let batch: RecordBatch = ArrowEngineData::try_from_engine_data(data).unwrap().into();
let batch = filter_record_batch(&batch, &BooleanArray::from(selection_vector)).unwrap();
let paths = get_column!(batch, "path", StringArray);
let stats_parsed = get_column!(batch, STATS_PARSED, StructArray);
for bound in [MIN_VALUES, MAX_VALUES] {
let bounds = get_column!(stats_parsed, bound, StructArray);
let variant = get_column!(bounds, "v", StructArray);
let metadata = get_column!(variant, "metadata", BinaryArray);
let value = get_column!(variant, "value", BinaryArray);
for row in 0..batch.num_rows() {
actual.push((
paths.value(row).to_string(),
bound,
metadata.value(row).to_vec(),
value.value(row).to_vec(),
));
}
}
}
actual.sort();
actual
}
struct VariantStatsEvaluationHandler {
inner: Arc<dyn EvaluationHandler>,
}
impl EvaluationHandler for VariantStatsEvaluationHandler {
fn new_expression_evaluator(
&self,
input_schema: SchemaRef,
expression: ExpressionRef,
output_type: DataType,
) -> KernelResult<Arc<dyn ExpressionEvaluator>> {
let expression = Arc::new(
DecodeVariantStatsTransform
.transform_expr(&expression)
.into_owned(),
);
self.inner
.new_expression_evaluator(input_schema, expression, output_type)
}
fn new_predicate_evaluator(
&self,
input_schema: SchemaRef,
predicate: PredicateRef,
) -> KernelResult<Arc<dyn PredicateEvaluator>> {
self.inner.new_predicate_evaluator(input_schema, predicate)
}
fn create_many(
&self,
schema: SchemaRef,
rows: Vec<Vec<Scalar>>,
) -> KernelResult<Box<dyn EngineData>> {
self.inner.create_many(schema, rows)
}
}
struct DecodeVariantStatsTransform;
impl<'a> ExpressionTransform<'a> for DecodeVariantStatsTransform {
transform_output_type!(|'a, T| Cow<'a, T>);
fn transform_expr_parse_json(
&mut self,
expr: &'a ParseJsonExpression,
) -> Cow<'a, ParseJsonExpression> {
let json_expr = Expr::arrow_opaque(DecodeVariantStatsOp, [expr.json_expr.as_ref().clone()]);
Cow::Owned(ParseJsonExpression::new(
json_expr,
expr.output_schema.clone(),
))
}
}
#[derive(Debug, PartialEq)]
struct DecodeVariantStatsOp;
impl ArrowOpaqueExpressionOp for DecodeVariantStatsOp {
fn name(&self) -> &str {
"decode_variant_stats"
}
fn eval_expr(
&self,
args: &[Expr],
batch: &RecordBatch,
_result_type: Option<&DataType>,
) -> KernelResult<ArrayRef> {
let [json] = args else {
panic!("expected one argument, got {}", args.len());
};
let json = evaluate_expression(json, batch, Some(&DataType::STRING))?;
let decoded: StringArray = json
.as_string::<i32>()
.iter()
.map(|stats| {
let mut stats: serde_json::Value = serde_json::from_str(stats?).unwrap();
for bound in [MIN_VALUES, MAX_VALUES] {
if let Some(n) = stats[bound]["v"].as_u64() {
stats[bound]["v"] = physical_variant_int8_json(n.try_into().unwrap());
}
}
Some(stats.to_string())
})
.collect();
Ok(Arc::new(decoded))
}
fn eval_expr_scalar(
&self,
_eval_expr: &ScalarExpressionEvaluator<'_>,
_exprs: &[Expr],
) -> KernelResult<Scalar> {
unimplemented!()
}
}
fn physical_variant_int8_json(n: u8) -> serde_json::Value {
serde_json::json!({"metadata": "010000", "value": format!("0c{n:02x}")})
}