#![allow(clippy::unwrap_used)]
use serde_json::json;
use super::GenericWhereGenerator;
use crate::{
dialect::PostgresDialect,
where_clause::{WhereClause, WhereOperator},
where_generator::HierarchyContext,
};
fn field(path: &str, op: WhereOperator, val: serde_json::Value) -> WhereClause {
WhereClause::Field {
path: vec![path.to_string()],
operator: op,
value: val,
}
}
#[test]
fn generic_eq_postgres() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("email", WhereOperator::Eq, json!("alice@example.com"));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "data->>'email' = $1");
assert_eq!(params, vec![json!("alice@example.com")]);
}
#[test]
fn generic_and_postgres() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = WhereClause::And(vec![
field("status", WhereOperator::Eq, json!("active")),
field("age", WhereOperator::Gte, json!(18)),
]);
let (sql, params) = gen.generate(&clause).unwrap();
assert!(sql.starts_with("(data->>'status' = $1 AND"));
assert_eq!(params.len(), 2);
}
#[test]
fn generic_empty_and_returns_true() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = WhereClause::And(vec![]);
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "TRUE");
assert!(params.is_empty());
}
#[test]
fn generic_empty_or_returns_false() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = WhereClause::Or(vec![]);
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "FALSE");
assert!(params.is_empty());
}
#[test]
fn generic_not_postgres() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = WhereClause::Not(Box::new(field("deleted", WhereOperator::Eq, json!(true))));
let (sql, _) = gen.generate(&clause).unwrap();
assert!(sql.starts_with("NOT ("));
}
#[test]
fn generate_resets_counter() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("x", WhereOperator::Eq, json!(1));
let (sql1, _) = gen.generate(&clause).unwrap();
let (sql2, _) = gen.generate(&clause).unwrap();
assert_eq!(sql1, sql2);
assert!(sql1.contains("$1"));
assert!(!sql1.contains("$2"));
}
#[test]
fn generate_with_param_offset() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("email", WhereOperator::Eq, json!("a@b.com"));
let (sql, _) = gen.generate_with_param_offset(&clause, 2).unwrap();
assert!(sql.contains("$3"), "Expected $3 (offset 2 + 1), got: {sql}");
}
#[test]
fn generic_icontains_postgres() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("email", WhereOperator::Icontains, json!("example.com"));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "data->>'email' ILIKE '%' || $1 || '%'");
assert_eq!(params, vec![json!("example.com")]);
}
#[test]
fn generic_startswith_postgres() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("name", WhereOperator::Startswith, json!("Al"));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "data->>'name' LIKE $1 || '%'");
assert_eq!(params, vec![json!("Al")]);
}
#[test]
fn generic_endswith_postgres() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("name", WhereOperator::Endswith, json!("son"));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "data->>'name' LIKE '%' || $1");
assert_eq!(params, vec![json!("son")]);
}
#[test]
fn generic_in_postgres() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("status", WhereOperator::In, json!(["active", "pending"]));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "data->>'status' IN ($1, $2)");
assert_eq!(params.len(), 2);
}
#[test]
fn generic_in_empty_returns_false() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("status", WhereOperator::In, json!([]));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "FALSE");
assert!(params.is_empty());
}
#[test]
fn generic_nin_empty_returns_true() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("status", WhereOperator::Nin, json!([]));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "TRUE");
assert!(params.is_empty());
}
#[test]
fn no_value_in_sql_string() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let injection = "'; DROP TABLE users; --";
let clause = field("email", WhereOperator::Eq, json!(injection));
let (sql, params) = gen.generate(&clause).unwrap();
assert!(!sql.contains(injection), "Value must not appear in SQL: {sql}");
assert_eq!(params[0], json!(injection));
}
#[test]
fn generic_pg_cosine_distance_threshold_predicate_exact_sql() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field(
"embedding",
WhereOperator::CosineDistance,
json!({"vector": [0.1, 0.2], "threshold": 0.5}),
);
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(
sql, "((data->>'embedding')::vector <=> $1::text::vector) <= $2::text::float8",
"the predicate must be boolean, the cast parenthesised, the vector parameterised"
);
assert_eq!(params[0], json!("[0.1,0.2]"), "query vector binds as a text literal");
assert_eq!(params[1], json!(0.5));
}
#[test]
fn generic_pg_inner_product_negates_the_bound_threshold() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field(
"embedding",
WhereOperator::InnerProduct,
json!({"vector": [1.0, 0.0], "threshold": 0.8}),
);
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, "((data->>'embedding')::vector <#> $1::text::vector) <= $2::text::float8");
assert_eq!(params[1], json!(-0.8));
}
#[test]
fn generic_pg_vector_operator_refuses_the_old_bare_array_shape() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("embedding", WhereOperator::L2Distance, json!([0.1, 0.2]));
let err = gen.generate(&clause).unwrap_err().to_string();
assert!(
err.contains("threshold"),
"the refusal must teach the {{vector, threshold}} shape: {err}"
);
}
#[test]
fn generic_pg_binary_vector_threshold_predicates_exact_sql() {
let gen = GenericWhereGenerator::new(PostgresDialect);
for (op, sql_op) in [
(WhereOperator::HammingDistance, "<~>"),
(WhereOperator::JaccardDistance, "<%>"),
] {
let clause = field("fingerprint", op, json!({"vector": "1011", "threshold": 2.0}));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(
sql,
format!(
"((data->>'fingerprint')::varbit {sql_op} $1::text::varbit) <= \
$2::text::float8"
)
);
assert_eq!(params[0], json!("1011"), "the query bits bind as a text literal");
assert_eq!(params[1], json!(2.0));
}
}
#[test]
fn generic_pg_binary_vector_operand_rejects_non_bit_characters() {
let gen = GenericWhereGenerator::new(PostgresDialect);
for operand in [
json!({"vector": "10x1", "threshold": 1.0}),
json!({"vector": "", "threshold": 1.0}),
json!({"vector": [1, 0, 1], "threshold": 1.0}),
] {
let clause = field("fingerprint", WhereOperator::HammingDistance, operand.clone());
let err = gen.generate(&clause).unwrap_err().to_string();
assert!(
err.contains("'0' and '1'") || err.contains("binary vector operators take"),
"expected a shape refusal for {operand}: {err}"
);
}
}
#[test]
fn generic_pg_vector_operand_rejects_non_numeric_components() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field(
"embedding",
WhereOperator::CosineDistance,
json!({"vector": [0.1, "0.2); DROP TABLE x;--"], "threshold": 0.5}),
);
let err = gen.generate(&clause).unwrap_err().to_string();
assert!(err.contains("finite numbers"), "got: {err}");
}
#[test]
fn generic_pg_network_ipv4() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("ip", WhereOperator::IsIPv4, json!(true));
let (sql, _) = gen.generate(&clause).unwrap();
assert!(sql.contains("family("), "Expected family() call, got: {sql}");
assert!(sql.contains("= 4"), "Expected = 4, got: {sql}");
}
#[test]
fn generic_pg_ltree_ancestor_of() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("path", WhereOperator::AncestorOf, json!("europe.france"));
let (sql, params) = gen.generate(&clause).unwrap();
assert!(sql.contains("@>") && sql.contains("ltree"), "Got: {sql}");
assert_eq!(params.len(), 1);
}
#[test]
fn escape_like_literal_escapes_percent_and_underscore() {
assert_eq!(super::escape_like_literal("50%"), "50\\%");
assert_eq!(super::escape_like_literal("user_name"), "user\\_name");
assert_eq!(super::escape_like_literal("a%b_c\\d"), "a\\%b\\_c\\\\d");
assert_eq!(super::escape_like_literal("plain"), "plain");
}
#[test]
fn contains_escapes_like_metacharacters() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("name", WhereOperator::Contains, json!("50%off"));
let (_sql, params) = gen.generate(&clause).unwrap();
assert_eq!(params[0], json!("50\\%off"));
}
#[test]
fn startswith_escapes_like_metacharacters() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("name", WhereOperator::Startswith, json!("user_"));
let (_sql, params) = gen.generate(&clause).unwrap();
assert_eq!(params[0], json!("user\\_"));
}
#[test]
fn endswith_escapes_like_metacharacters() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("name", WhereOperator::Endswith, json!("100%"));
let (_sql, params) = gen.generate(&clause).unwrap();
assert_eq!(params[0], json!("100\\%"));
}
#[test]
fn regex_rejects_nested_quantifiers() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("name", WhereOperator::Regex, json!("(a+)+$"));
let err = gen.generate(&clause).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("nested quantifiers"), "Got: {msg}");
}
#[test]
fn regex_rejects_star_star_pattern() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("name", WhereOperator::Regex, json!("(x*)*"));
let err = gen.generate(&clause).unwrap_err();
assert!(err.to_string().contains("nested quantifiers"));
}
#[test]
fn regex_rejects_too_long_pattern() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let long_pattern = "a".repeat(1_001);
let clause = field("name", WhereOperator::Regex, json!(long_pattern));
let err = gen.generate(&clause).unwrap_err();
assert!(err.to_string().contains("maximum length"));
}
#[test]
fn regex_allows_safe_patterns() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("name", WhereOperator::Regex, json!("^[a-z]+$"));
assert!(gen.generate(&clause).is_ok());
}
#[test]
fn iregex_also_validates_pattern() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("name", WhereOperator::Iregex, json!("(a+)+"));
assert!(gen.generate(&clause).is_err());
}
#[test]
fn hierarchy_context_none_is_backward_compatible() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("email", WhereOperator::Eq, json!("test@example.com"));
let (sql, _) = gen.generate(&clause).unwrap();
assert!(sql.contains("= $1"));
}
#[test]
fn hierarchy_context_can_be_constructed() {
let ctx = HierarchyContext {
table: "tb_category".to_string(),
path_column: "category_path".to_string(),
fk_column: None,
};
assert_eq!(ctx.table, "tb_category");
assert_eq!(ctx.path_column, "category_path");
assert!(ctx.fk_column.is_none());
}
#[test]
fn hierarchy_context_cross_table() {
let ctx = HierarchyContext {
table: "tb_location".to_string(),
path_column: "location_path".to_string(),
fk_column: Some("fk_location".to_string()),
};
assert_eq!(ctx.fk_column, Some("fk_location".to_string()));
}
#[test]
fn descendant_of_id_self_referencing() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let ctx = HierarchyContext {
table: "tb_category".to_string(),
path_column: "category_path".to_string(),
fk_column: None,
};
let clause = field(
"category_path",
WhereOperator::DescendantOfId,
json!("550e8400-e29b-41d4-a716-446655440000"),
);
let (sql, params) = gen
.generate(&WhereClause::InHierarchy {
context: ctx,
inner: Box::new(clause),
})
.unwrap();
assert_eq!(
sql,
"(data->>'category_path')::ltree <@ (SELECT \"category_path\" FROM \"tb_category\" WHERE \"id\" = $1::text::uuid)"
);
assert_eq!(params, vec![json!("550e8400-e29b-41d4-a716-446655440000")]);
}
#[test]
fn ancestor_of_id_self_referencing() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let ctx = HierarchyContext {
table: "tb_category".to_string(),
path_column: "category_path".to_string(),
fk_column: None,
};
let clause = field(
"category_path",
WhereOperator::AncestorOfId,
json!("550e8400-e29b-41d4-a716-446655440000"),
);
let (sql, params) = gen
.generate(&WhereClause::InHierarchy {
context: ctx,
inner: Box::new(clause),
})
.unwrap();
assert_eq!(
sql,
"(data->>'category_path')::ltree @> (SELECT \"category_path\" FROM \"tb_category\" WHERE \"id\" = $1::text::uuid)"
);
assert_eq!(params, vec![json!("550e8400-e29b-41d4-a716-446655440000")]);
}
#[test]
fn descendant_of_id_cross_table() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let ctx = HierarchyContext {
table: "tb_location".to_string(),
path_column: "location_path".to_string(),
fk_column: Some("fk_location".to_string()),
};
let clause = field(
"location",
WhereOperator::DescendantOfId,
json!("550e8400-e29b-41d4-a716-446655440000"),
);
let (sql, params) = gen
.generate(&WhereClause::InHierarchy {
context: ctx,
inner: Box::new(clause),
})
.unwrap();
assert_eq!(
sql,
"\"fk_location\" IN (SELECT \"id\" FROM \"tb_location\" WHERE \"location_path\" <@ (SELECT \"location_path\" FROM \"tb_location\" WHERE \"id\" = $1::text::uuid))"
);
assert_eq!(params, vec![json!("550e8400-e29b-41d4-a716-446655440000")]);
}
#[test]
fn ancestor_of_id_cross_table() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let ctx = HierarchyContext {
table: "tb_location".to_string(),
path_column: "location_path".to_string(),
fk_column: Some("fk_location".to_string()),
};
let clause = field(
"location",
WhereOperator::AncestorOfId,
json!("550e8400-e29b-41d4-a716-446655440000"),
);
let (sql, params) = gen
.generate(&WhereClause::InHierarchy {
context: ctx,
inner: Box::new(clause),
})
.unwrap();
assert_eq!(
sql,
"\"fk_location\" IN (SELECT \"id\" FROM \"tb_location\" WHERE \"location_path\" @> (SELECT \"location_path\" FROM \"tb_location\" WHERE \"id\" = $1::text::uuid))"
);
assert_eq!(params, vec![json!("550e8400-e29b-41d4-a716-446655440000")]);
}
#[test]
fn descendant_of_id_without_hierarchy_ctx_errors() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("category_path", WhereOperator::DescendantOfId, json!("some-id"));
let result = gen.generate(&clause);
assert!(result.is_err());
}
#[test]
fn ltree_id_subquery_escapes_adversarial_identifiers() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let ctx = HierarchyContext {
table: r#"evil"table"#.to_string(),
path_column: r#"evil"col"#.to_string(),
fk_column: None,
};
let clause = field("category_path", WhereOperator::DescendantOfId, json!("some-id"));
let (sql, _) = gen
.generate(&WhereClause::InHierarchy {
context: ctx,
inner: Box::new(clause),
})
.unwrap();
assert!(
sql.contains(r#""evil""table""#),
"Table name should have doubled quotes, got: {sql}"
);
assert!(
sql.contains(r#""evil""col""#),
"Column name should have doubled quotes, got: {sql}"
);
}
#[test]
fn every_inet_operator_negates_when_the_value_is_false() {
for op in [
WhereOperator::IsIPv4,
WhereOperator::IsIPv6,
WhereOperator::IsPrivate,
WhereOperator::IsLoopback,
WhereOperator::IsMulticast,
WhereOperator::IsLinkLocal,
WhereOperator::IsDocumentation,
WhereOperator::IsCarrierGrade,
] {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("ip_address", op.clone(), json!(false));
let generated = gen.generate(&clause);
assert!(generated.is_ok(), "{op:?} with value=false must generate SQL: {generated:?}");
let (sql, _params) = generated.unwrap();
assert!(
sql.contains("NOT ") || sql.contains("<> 4") || sql.contains("<> 6"),
"{op:?} with value=false must emit a negated check, got: {sql}"
);
}
}
#[test]
fn every_inet_operator_is_positive_when_the_value_is_true() {
for op in [
WhereOperator::IsIPv4,
WhereOperator::IsIPv6,
WhereOperator::IsPrivate,
WhereOperator::IsLoopback,
WhereOperator::IsMulticast,
WhereOperator::IsLinkLocal,
WhereOperator::IsDocumentation,
WhereOperator::IsCarrierGrade,
] {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = field("ip_address", op.clone(), json!(true));
let generated = gen.generate(&clause);
assert!(generated.is_ok(), "{op:?} with value=true must generate SQL: {generated:?}");
let (sql, _params) = generated.unwrap();
assert!(
!sql.contains("NOT "),
"{op:?} with value=true must emit a positive check, got: {sql}"
);
}
}
#[test]
fn not_ipv4_is_distinct_from_ipv4() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let (positive, _) =
gen.generate(&field("ip_address", WhereOperator::IsIPv4, json!(true))).unwrap();
let (negative, _) =
gen.generate(&field("ip_address", WhereOperator::IsIPv4, json!(false))).unwrap();
assert_ne!(positive, negative, "is_ipv4:true and is_ipv4:false must differ");
}
fn vector_typed(clause: WhereClause) -> WhereClause {
use std::sync::Arc;
use crate::{types::sql_hints::ScalarFieldType, where_clause::FieldTypeMap};
clause.typed(Arc::new(FieldTypeMap::from_pairs([
("embedding", ScalarFieldType::Vector),
("fingerprint", ScalarFieldType::Vector),
("terms", ScalarFieldType::Vector),
("title", ScalarFieldType::Text),
])))
}
#[test]
fn a_declared_vector_field_reads_its_native_column_not_the_jsonb_text() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = vector_typed(field(
"embedding",
WhereOperator::CosineDistance,
json!({"vector": [0.1, 0.2], "threshold": 0.5}),
));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, r#"("embedding" <=> $1::text::vector) <= $2::text::float8"#);
assert_eq!(params[0], json!("[0.1,0.2]"));
assert_eq!(params[1], json!(0.5));
}
#[test]
fn every_dense_metric_reads_the_native_column() {
let gen = GenericWhereGenerator::new(PostgresDialect);
for (op, sql_op, bound) in [
(WhereOperator::CosineDistance, "<=>", json!(0.5)),
(WhereOperator::L2Distance, "<->", json!(0.5)),
(WhereOperator::L1Distance, "<+>", json!(0.5)),
(WhereOperator::InnerProduct, "<#>", json!(-0.5)),
] {
let clause =
vector_typed(field("embedding", op, json!({"vector": [0.1, 0.2], "threshold": 0.5})));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(sql, format!(r#"("embedding" {sql_op} $1::text::vector) <= $2::text::float8"#));
assert_eq!(params[1], bound, "inner product negates its bound; the others do not");
}
}
#[test]
fn the_binary_metrics_read_the_native_bit_column() {
let gen = GenericWhereGenerator::new(PostgresDialect);
for (op, sql_op) in [
(WhereOperator::HammingDistance, "<~>"),
(WhereOperator::JaccardDistance, "<%>"),
] {
let clause =
vector_typed(field("fingerprint", op, json!({"vector": "1011", "threshold": 2.0})));
let (sql, _) = gen.generate(&clause).unwrap();
assert_eq!(
sql,
format!(r#"("fingerprint" {sql_op} $1::text::varbit) <= $2::text::float8"#)
);
}
}
#[test]
fn a_sparse_operand_reads_the_native_column_too() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = vector_typed(field(
"terms",
WhereOperator::L2Distance,
json!({"vector": "{1:0.5,7:0.25}/1000", "threshold": 0.25}),
));
let (sql, _) = gen.generate(&clause).unwrap();
assert_eq!(sql, r#"("terms" <-> $1::text::sparsevec) <= $2::text::float8"#);
}
#[test]
fn an_undeclared_field_keeps_the_jsonb_operand() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = vector_typed(field(
"not_declared",
WhereOperator::CosineDistance,
json!({"vector": [0.1, 0.2], "threshold": 0.5}),
));
let (sql, _) = gen.generate(&clause).unwrap();
assert_eq!(
sql,
"((data->>'not_declared')::vector <=> $1::text::vector) <= $2::text::float8"
);
}
#[test]
fn a_non_vector_field_keeps_the_jsonb_operand() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = vector_typed(field(
"title",
WhereOperator::CosineDistance,
json!({"vector": [0.1, 0.2], "threshold": 0.5}),
));
let (sql, _) = gen.generate(&clause).unwrap();
assert_eq!(sql, "((data->>'title')::vector <=> $1::text::vector) <= $2::text::float8");
}
#[test]
fn a_nested_vector_path_keeps_the_jsonb_operand() {
use std::sync::Arc;
use crate::{types::sql_hints::ScalarFieldType, where_clause::FieldTypeMap};
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = WhereClause::Field {
path: vec!["machine".to_string(), "embedding".to_string()],
operator: WhereOperator::CosineDistance,
value: json!({"vector": [0.1, 0.2], "threshold": 0.5}),
}
.typed(Arc::new(FieldTypeMap::from_pairs([(
"machine.embedding",
ScalarFieldType::Vector,
)])));
let (sql, _) = gen.generate(&clause).unwrap();
assert!(
sql.starts_with("((data->"),
"a nested vector path must stay on the JSONB operand: {sql}"
);
}
#[test]
fn a_non_distance_operator_on_a_vector_field_keeps_the_jsonb_operand() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = vector_typed(field("embedding", WhereOperator::Eq, json!("[1,0,0]")));
let (sql, _) = gen.generate(&clause).unwrap();
assert_eq!(sql, "data->>'embedding' = $1");
}
fn guarded_team_name(inner: WhereClause) -> WhereClause {
WhereClause::Guarded {
under: vec!["team".to_string()],
guard: Box::new(WhereClause::Field {
path: vec!["team".to_string(), "tenant_id".to_string()],
operator: WhereOperator::Eq,
value: json!("A"),
}),
inner: Box::new(inner),
}
}
fn team_name(op: WhereOperator, value: serde_json::Value) -> WhereClause {
WhereClause::Field {
path: vec!["team".to_string(), "name".to_string()],
operator: op,
value,
}
}
#[test]
fn a_guarded_path_reads_its_value_only_where_the_guard_holds() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = guarded_team_name(team_name(WhereOperator::Eq, json!("blue")));
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(
sql,
"(CASE WHEN data->'team'->>'tenant_id' = $1 THEN data->'team'->>'name' END) = $2"
);
assert_eq!(params, [json!("A"), json!("blue")]);
}
#[test]
fn a_guarded_path_under_not_negates_the_guarded_value_not_the_guard() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause =
guarded_team_name(WhereClause::Not(Box::new(team_name(WhereOperator::Eq, json!("blue")))));
let (sql, _) = gen.generate(&clause).unwrap();
assert_eq!(
sql,
"NOT ((CASE WHEN data->'team'->>'tenant_id' = $1 THEN data->'team'->>'name' END) = $2)"
);
}
#[test]
fn a_view_guarded_path_reads_its_value_only_where_the_related_row_is_visible() {
let gen = GenericWhereGenerator::new(PostgresDialect);
let clause = WhereClause::Guarded {
under: vec!["team".to_string()],
guard: Box::new(team_key_in()),
inner: Box::new(team_name(WhereOperator::Eq, json!("blue"))),
};
let (sql, params) = gen.generate(&clause).unwrap();
assert_eq!(
sql,
"(CASE WHEN ((data->>'fk_team')::bigint IN (SELECT (data->>'id')::bigint FROM \
\"app\".\"v_team\" WHERE data->>'tenant_id' = $1)) THEN data->'team'->>'name' END) = $2"
);
assert_eq!(params, [json!("A"), json!("blue")]);
}
fn team_key_in() -> WhereClause {
WhereClause::KeyIn {
path: vec!["fk_team".to_string()],
key_type: crate::types::sql_hints::ScalarFieldType::Integer,
view: "app.v_team".to_string(),
target_key: vec!["id".to_string()],
predicate: Box::new(WhereClause::Field {
path: vec!["tenant_id".to_string()],
operator: WhereOperator::Eq,
value: json!("A"),
}),
}
}
#[test]
fn a_path_outside_the_guarded_relation_is_unchanged() {
let plain = field("id", WhereOperator::Eq, json!(1));
let unguarded = GenericWhereGenerator::new(PostgresDialect).generate(&plain).unwrap();
let guarded = GenericWhereGenerator::new(PostgresDialect)
.generate(&guarded_team_name(plain))
.unwrap();
assert_eq!(guarded, unguarded);
}