use uni_common::core::schema::{LabelMeta, Schema, SchemaElementState};
use uni_cypher::ast::{BinaryOp, CypherLiteral, Expr};
use uni_query::query::pushdown::{IndexAwareAnalyzer, LanceFilterGenerator, PredicateAnalyzer};
#[test]
fn test_lance_filter_generation() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"name".to_string(),
)),
op: BinaryOp::Contains,
right: Box::new(Expr::Literal(CypherLiteral::String("foo".to_string()))),
};
let filter = LanceFilterGenerator::generate(&[expr], "n", None).unwrap();
assert_eq!(filter, "name LIKE '%foo%'");
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"title".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("Intro".to_string()))),
};
let filter = LanceFilterGenerator::generate(&[expr], "n", None).unwrap();
assert_eq!(filter, "title LIKE 'Intro%'");
}
#[test]
fn test_or_to_in_conversion() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"status".to_string(),
)),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(CypherLiteral::String("a".to_string()))),
}),
op: BinaryOp::Or,
right: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"status".to_string(),
)),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(CypherLiteral::String("b".to_string()))),
}),
};
let analyzer = PredicateAnalyzer::new();
let analysis = analyzer.analyze(&expr, "n");
assert_eq!(analysis.pushable.len(), 1);
assert!(analysis.residual.is_empty());
let pushed = &analysis.pushable[0];
if let Expr::In { list, .. } = pushed {
if let Expr::List(items) = list.as_ref() {
assert_eq!(items.len(), 2);
} else {
panic!("Expected list on RHS of IN");
}
} else {
panic!("Expected IN expression, got {:?}", pushed);
}
let sql = LanceFilterGenerator::generate(&analysis.pushable, "n", None).unwrap();
assert!(sql == "status IN ('a', 'b')" || sql == "status IN ('b', 'a')");
}
#[test]
fn test_is_null_pushdown() {
let expr = Expr::IsNull(Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"email".to_string(),
)));
let analyzer = PredicateAnalyzer::new();
let analysis = analyzer.analyze(&expr, "n");
assert_eq!(analysis.pushable.len(), 1);
assert!(analysis.residual.is_empty());
let sql = LanceFilterGenerator::generate(&analysis.pushable, "n", None).unwrap();
assert_eq!(sql, "email IS NULL");
}
#[test]
fn test_is_not_null_pushdown() {
let expr = Expr::IsNotNull(Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"email".to_string(),
)));
let analyzer = PredicateAnalyzer::new();
let analysis = analyzer.analyze(&expr, "n");
assert_eq!(analysis.pushable.len(), 1);
let sql = LanceFilterGenerator::generate(&analysis.pushable, "n", None).unwrap();
assert_eq!(sql, "email IS NOT NULL");
}
#[test]
fn test_predicate_flattening() {
let expr = Expr::BinaryOp {
left: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"a".to_string(),
)),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(CypherLiteral::Integer(1))),
}),
op: BinaryOp::And,
right: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"b".to_string(),
)),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(CypherLiteral::Integer(2))),
}),
};
let analyzer = PredicateAnalyzer::new();
let analysis = analyzer.analyze(&expr, "n");
assert_eq!(analysis.pushable.len(), 2);
let sql = LanceFilterGenerator::generate(&analysis.pushable, "n", None).unwrap();
assert_eq!(sql, "a = 1 AND b = 2");
}
fn create_test_schema_with_label(label: &str, label_id: u16) -> Schema {
let mut schema = Schema::default();
schema.labels.insert(
label.to_string(),
LabelMeta {
id: label_id,
created_at: chrono::Utc::now(),
state: SchemaElementState::Active,
},
);
schema
}
#[test]
fn test_index_aware_uid_extraction() {
let schema = create_test_schema_with_label("Person", 1);
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"_uid".to_string(),
)),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(CypherLiteral::String(
"not-a-valid-uid".to_string(),
))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert!(strategy.uid_lookup.is_none());
assert!(!strategy.residual.is_empty() || !strategy.lance_predicates.is_empty());
}
#[test]
fn test_index_aware_jsonpath_extraction() {
let schema = create_test_schema_with_label("Doc", 2);
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"title".to_string(),
)),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(CypherLiteral::String("Hello".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 2);
assert!(strategy.json_fts_predicates.is_empty());
assert_eq!(strategy.lance_predicates.len(), 1);
}
#[test]
fn test_index_aware_non_indexed_property() {
let schema = create_test_schema_with_label("Doc", 2);
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"author".to_string(),
)),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(CypherLiteral::String("John".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 2);
assert!(strategy.json_fts_predicates.is_empty());
assert_eq!(strategy.lance_predicates.len(), 1);
}
#[test]
fn test_index_aware_combined_predicates() {
let schema = create_test_schema_with_label("Doc", 2);
let expr = Expr::BinaryOp {
left: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"title".to_string(),
)),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(CypherLiteral::String("Hello".to_string()))),
}),
op: BinaryOp::And,
right: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"author".to_string(),
)),
op: BinaryOp::Eq,
right: Box::new(Expr::Literal(CypherLiteral::String("John".to_string()))),
}),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 2);
assert!(strategy.json_fts_predicates.is_empty());
assert_eq!(strategy.lance_predicates.len(), 2);
}
use uni_common::core::schema::{IndexDefinition, IndexStatus, ScalarIndexConfig, ScalarIndexType};
fn create_test_schema_with_btree_index(label: &str, label_id: u16, index_property: &str) -> Schema {
let mut schema = Schema::default();
schema.labels.insert(
label.to_string(),
LabelMeta {
id: label_id,
created_at: chrono::Utc::now(),
state: SchemaElementState::Active,
},
);
schema
.indexes
.push(IndexDefinition::Scalar(ScalarIndexConfig {
name: format!("idx_{}_{}", label, index_property),
label: label.to_string(),
properties: vec![index_property.to_string()],
index_type: ScalarIndexType::BTree,
where_clause: None,
metadata: Default::default(),
}));
schema
}
#[test]
fn test_btree_starts_with_extraction() {
let schema = create_test_schema_with_btree_index("Person", 1, "name");
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"name".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("John".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert_eq!(strategy.btree_prefix_scans.len(), 1);
assert_eq!(strategy.btree_prefix_scans[0].0, "name");
assert_eq!(strategy.btree_prefix_scans[0].1, "John");
assert_eq!(strategy.btree_prefix_scans[0].2, "Joho");
assert!(strategy.lance_predicates.is_empty());
}
#[test]
fn test_btree_starts_with_non_indexed_property() {
let schema = create_test_schema_with_btree_index("Person", 1, "name");
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"email".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("john@".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert!(strategy.btree_prefix_scans.is_empty());
assert_eq!(strategy.lance_predicates.len(), 1);
}
#[test]
fn test_btree_starts_with_empty_prefix() {
let schema = create_test_schema_with_btree_index("Person", 1, "name");
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"name".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert!(strategy.btree_prefix_scans.is_empty());
assert_eq!(strategy.lance_predicates.len(), 1);
}
#[test]
fn test_btree_starts_with_hash_index_not_used() {
let mut schema = create_test_schema_with_label("Person", 1);
schema
.indexes
.push(IndexDefinition::Scalar(ScalarIndexConfig {
name: "idx_person_name".to_string(),
label: "Person".to_string(),
properties: vec!["name".to_string()],
index_type: ScalarIndexType::Hash, where_clause: None,
metadata: Default::default(),
}));
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"name".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("John".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert!(strategy.btree_prefix_scans.is_empty());
assert_eq!(strategy.lance_predicates.len(), 1);
}
#[test]
fn test_btree_prefix_increment_logic() {
let schema = create_test_schema_with_btree_index("Person", 1, "name");
let test_cases = vec![
("A", "B"), ("Z", "["), ("abc", "abd"), ("test", "tesu"), ("123", "124"), ];
for (prefix, expected_upper) in test_cases {
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"name".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String(prefix.to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert_eq!(
strategy.btree_prefix_scans.len(),
1,
"Failed for prefix: {}",
prefix
);
assert_eq!(strategy.btree_prefix_scans[0].1, prefix);
assert_eq!(
strategy.btree_prefix_scans[0].2, expected_upper,
"Upper bound mismatch for prefix: {}",
prefix
);
}
}
#[test]
fn test_btree_starts_with_special_characters() {
let schema = create_test_schema_with_btree_index("Person", 1, "name");
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"name".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("O'Brien".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert_eq!(strategy.btree_prefix_scans.len(), 1);
assert_eq!(strategy.btree_prefix_scans[0].1, "O'Brien");
}
#[test]
fn test_btree_starts_with_unicode() {
let schema = create_test_schema_with_btree_index("Person", 1, "name");
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"name".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("日本".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert_eq!(strategy.btree_prefix_scans.len(), 1);
assert_eq!(strategy.btree_prefix_scans[0].1, "日本");
}
#[test]
fn test_btree_starts_with_multiple_indexed_properties() {
let mut schema = create_test_schema_with_label("Person", 1);
schema
.indexes
.push(IndexDefinition::Scalar(ScalarIndexConfig {
name: "idx_person_name_email".to_string(),
label: "Person".to_string(),
properties: vec!["name".to_string(), "email".to_string()],
index_type: ScalarIndexType::BTree,
where_clause: None,
metadata: Default::default(),
}));
let expr1 = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"name".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("John".to_string()))),
};
let expr2 = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"email".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("john@".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy1 = analyzer.analyze(&expr1, "n", 1);
assert_eq!(strategy1.btree_prefix_scans.len(), 1);
let strategy2 = analyzer.analyze(&expr2, "n", 1);
assert_eq!(strategy2.btree_prefix_scans.len(), 1);
}
#[test]
fn test_btree_prefix_scan_skips_non_online_index() {
let mut schema = create_test_schema_with_btree_index("Person", 1, "name");
if let IndexDefinition::Scalar(cfg) = &mut schema.indexes[0] {
cfg.metadata.status = IndexStatus::Building;
}
let expr = Expr::BinaryOp {
left: Box::new(Expr::Property(
Box::new(Expr::Variable("n".to_string())),
"name".to_string(),
)),
op: BinaryOp::StartsWith,
right: Box::new(Expr::Literal(CypherLiteral::String("John".to_string()))),
};
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert!(strategy.btree_prefix_scans.is_empty());
assert_eq!(strategy.lance_predicates.len(), 1);
if let IndexDefinition::Scalar(cfg) = &mut schema.indexes[0] {
cfg.metadata.status = IndexStatus::Online;
}
let analyzer = IndexAwareAnalyzer::new(&schema);
let strategy = analyzer.analyze(&expr, "n", 1);
assert_eq!(strategy.btree_prefix_scans.len(), 1);
assert_eq!(strategy.btree_prefix_scans[0].0, "name");
assert!(strategy.lance_predicates.is_empty());
}