uqa_sql/semantics/
source_filters.rs1use crate::{plan::source_projection::QualifierFilters, SQLError, ScalarExpr};
10use uqa_core::Value;
11
12pub fn checked_integer_value<T>(value: T, label: &str) -> Result<Value, SQLError>
13where
14 T: Copy + std::fmt::Display,
15 i64: TryFrom<T>,
16{
17 i64::try_from(value).map(Value::Int).map_err(|_| {
18 SQLError::TypeMismatch(format!("{label} {value} exceeds the SQL BIGINT range"))
19 })
20}
21
22pub fn qualifier_for(qualifier: &str, alias: Option<&str>) -> String {
23 alias.unwrap_or(qualifier).to_string()
24}
25
26pub fn has_filters_for_qualifier(filters: Option<&QualifierFilters>, qual: &str) -> bool {
27 filters
28 .and_then(|filters| filters.get(qual))
29 .is_some_and(|filters| !filters.is_empty())
30}
31
32pub fn combine_filters(filters: impl IntoIterator<Item = ScalarExpr>) -> Option<ScalarExpr> {
33 let mut filters: Vec<ScalarExpr> = filters.into_iter().collect();
34 if filters.len() == 1 {
35 filters.pop()
36 } else if filters.is_empty() {
37 None
38 } else {
39 Some(ScalarExpr::And(filters))
40 }
41}
42
43pub fn qualifier_filter(filters: Option<&QualifierFilters>, qualifier: &str) -> Option<ScalarExpr> {
44 filters
45 .and_then(|filters| filters.get(qualifier))
46 .filter(|filters| !filters.is_empty())
47 .and_then(|filters| combine_filters(filters.iter().cloned()))
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53 use crate::semantics::doc_id_value;
54
55 #[test]
56 fn combine_filters_handles_empty_and_single_inputs_without_panicking() {
57 assert!(combine_filters(Vec::<ScalarExpr>::new()).is_none());
58 let combined = combine_filters([ScalarExpr::Literal(Value::Bool(true))]);
59 assert!(matches!(
60 combined,
61 Some(ScalarExpr::Literal(Value::Bool(true)))
62 ));
63 }
64
65 #[test]
66 fn document_ids_outside_bigint_are_rejected_at_the_sql_boundary() {
67 assert!(doc_id_value(i64::MAX as u64).is_ok());
68 assert!(doc_id_value(i64::MAX as u64 + 1).is_err());
69 }
70}