Skip to content

Commit a2961e6

Browse files
author
Beinan Wang
committed
test: add coverage for AND/OR/NOT IN, type coercion, multi-field, and empty snapshots
Co-Authored-By: Beinan Wang <beinanwang@microsoft.com>
1 parent 784b9e1 commit a2961e6

1 file changed

Lines changed: 165 additions & 0 deletions

File tree

rust/lance/src/dataset/mem_wal/scanner/shard_pruning.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,4 +371,169 @@ mod tests {
371371
assert!(pruned.contains(&shard_a));
372372
assert!(pruned.contains(&shard_b)); // kept because no field values
373373
}
374+
375+
#[test]
376+
fn test_and_conjunction_extracts_sharding_column() {
377+
let spec = bucket_spec("id", 8);
378+
let shard_a = Uuid::new_v4();
379+
let shard_b = Uuid::new_v4();
380+
381+
let bucket_for_1 = hash_scalar_to_bucket(&ScalarValue::Int32(Some(1)), 8).unwrap();
382+
let other_bucket = (0..8).find(|b| *b != bucket_for_1).unwrap();
383+
384+
let snapshots = vec![
385+
snapshot_with_bucket(shard_a, "bucket", bucket_for_1),
386+
snapshot_with_bucket(shard_b, "bucket", other_bucket),
387+
];
388+
389+
// Filter: id = 1 AND name = "foo"
390+
// Only id = 1 should be extracted; name is not the sharding column.
391+
let filter = col("id").eq(lit(1i32)).and(col("name").eq(lit("foo")));
392+
let result = prune_shards(&filter, &spec, &snapshots, &HashMap::new(), None);
393+
394+
let pruned = result.expect("should prune using id = 1 from AND");
395+
assert!(pruned.contains(&shard_a));
396+
assert!(!pruned.contains(&shard_b));
397+
}
398+
399+
#[test]
400+
fn test_or_disjunction_returns_none() {
401+
let spec = bucket_spec("id", 8);
402+
let shard_a = Uuid::new_v4();
403+
404+
let bucket_for_1 = hash_scalar_to_bucket(&ScalarValue::Int32(Some(1)), 8).unwrap();
405+
let snapshots = vec![snapshot_with_bucket(shard_a, "bucket", bucket_for_1)];
406+
407+
// Filter: id = 1 OR id = 2 -- OR is not handled, should return None.
408+
let filter = col("id").eq(lit(1i32)).or(col("id").eq(lit(2i32)));
409+
let result = prune_shards(&filter, &spec, &snapshots, &HashMap::new(), None);
410+
411+
assert!(result.is_none(), "OR filters should not be prunable");
412+
}
413+
414+
#[test]
415+
fn test_not_in_returns_none() {
416+
let spec = bucket_spec("id", 8);
417+
let shard_a = Uuid::new_v4();
418+
419+
let bucket_for_1 = hash_scalar_to_bucket(&ScalarValue::Int32(Some(1)), 8).unwrap();
420+
let snapshots = vec![snapshot_with_bucket(shard_a, "bucket", bucket_for_1)];
421+
422+
// Filter: id NOT IN (1, 2) -- negated InList should return None.
423+
let filter = col("id").in_list(vec![lit(1i32), lit(2i32)], true);
424+
let result = prune_shards(&filter, &spec, &snapshots, &HashMap::new(), None);
425+
426+
assert!(result.is_none(), "NOT IN should not be prunable");
427+
}
428+
429+
#[test]
430+
fn test_type_coercion_int64_to_int32() {
431+
use std::sync::Arc;
432+
433+
use arrow_schema::{DataType, Field, Schema as ArrowSchema};
434+
435+
let num_buckets = 8;
436+
let spec = bucket_spec("id", num_buckets);
437+
let shard_a = Uuid::new_v4();
438+
let shard_b = Uuid::new_v4();
439+
440+
// Snapshots store bucket values computed from Int32(1) -- matches write path.
441+
let bucket_for_int32 =
442+
hash_scalar_to_bucket(&ScalarValue::Int32(Some(1)), num_buckets).unwrap();
443+
let bucket_for_int64 =
444+
hash_scalar_to_bucket(&ScalarValue::Int64(Some(1)), num_buckets).unwrap();
445+
// Precondition: Int32 and Int64 must hash to different buckets for this
446+
// test to be meaningful. Murmur3 uses hash_int vs hash_long code paths.
447+
assert_ne!(
448+
bucket_for_int32, bucket_for_int64,
449+
"Int32 and Int64 should hash to different buckets for value 1"
450+
);
451+
452+
let snapshots = vec![
453+
snapshot_with_bucket(shard_a, "bucket", bucket_for_int32),
454+
snapshot_with_bucket(shard_b, "bucket", bucket_for_int64),
455+
];
456+
457+
// Filter uses Int64 literal (as SQL parsing typically produces).
458+
let filter = col("id").eq(lit(1i64));
459+
460+
// WITH base_schema: Int64 coerced to Int32 before hashing -> matches shard_a.
461+
let schema: SchemaRef = Arc::new(ArrowSchema::new(vec![Field::new(
462+
"id",
463+
DataType::Int32,
464+
false,
465+
)]));
466+
let with_schema = prune_shards(&filter, &spec, &snapshots, &HashMap::new(), Some(&schema));
467+
let pruned = with_schema.expect("coercion should enable pruning");
468+
assert!(
469+
pruned.contains(&shard_a),
470+
"with coercion, Int64 -> Int32 should match the Int32-hashed shard"
471+
);
472+
assert!(
473+
!pruned.contains(&shard_b),
474+
"with coercion, should not match the Int64-hashed shard"
475+
);
476+
477+
// WITHOUT base_schema: Int64 stays Int64, hashes differently -> misses shard_a.
478+
let without_schema = prune_shards(&filter, &spec, &snapshots, &HashMap::new(), None);
479+
let pruned_raw = without_schema.expect("should still return Some (literals extracted)");
480+
assert!(
481+
!pruned_raw.contains(&shard_a),
482+
"without coercion, Int64 hash differs from Int32 -- should miss shard_a"
483+
);
484+
assert!(
485+
pruned_raw.contains(&shard_b),
486+
"without coercion, Int64 hash matches shard_b (stored with Int64 bucket)"
487+
);
488+
}
489+
490+
#[test]
491+
fn test_multi_field_spec_uses_first_field() {
492+
let spec = ShardingSpec {
493+
spec_id: 1,
494+
fields: vec![
495+
ShardingField {
496+
field_id: "bucket".to_string(),
497+
source_ids: vec![],
498+
transform: Some("bucket".to_string()),
499+
expression: None,
500+
result_type: "int32".to_string(),
501+
parameters: HashMap::from([
502+
("num_buckets".to_string(), "4".to_string()),
503+
("column".to_string(), "id".to_string()),
504+
]),
505+
},
506+
ShardingField {
507+
field_id: "second_field".to_string(),
508+
source_ids: vec![],
509+
transform: Some("bucket".to_string()),
510+
expression: None,
511+
result_type: "int32".to_string(),
512+
parameters: HashMap::from([
513+
("num_buckets".to_string(), "4".to_string()),
514+
("column".to_string(), "region".to_string()),
515+
]),
516+
},
517+
],
518+
};
519+
520+
let shard_a = Uuid::new_v4();
521+
let bucket_for_1 = hash_scalar_to_bucket(&ScalarValue::Int32(Some(1)), 4).unwrap();
522+
let snapshots = vec![snapshot_with_bucket(shard_a, "bucket", bucket_for_1)];
523+
524+
// Filter on the first field's column -- should work without panic.
525+
let filter = col("id").eq(lit(1i32));
526+
let result = prune_shards(&filter, &spec, &snapshots, &HashMap::new(), None);
527+
let pruned = result.expect("should prune using first field");
528+
assert!(pruned.contains(&shard_a));
529+
}
530+
531+
#[test]
532+
fn test_empty_snapshots_returns_empty_set() {
533+
let spec = bucket_spec("id", 4);
534+
let filter = col("id").eq(lit(1i32));
535+
let result = prune_shards(&filter, &spec, &[], &HashMap::new(), None);
536+
let pruned = result.expect("should return Some even with empty snapshots");
537+
assert!(pruned.is_empty(), "empty snapshots should yield empty set");
538+
}
374539
}

0 commit comments

Comments
 (0)