veloq-query 0.4.0

DuckDB-backed query helpers shared by VeloQ profile backends.
Documentation
use crate::sql::SqlFragment;
use duckdb::types::Value;
use veloq_core::NameFilterRef;

pub fn glob_like(expr: &str, pattern: &str) -> SqlFragment {
    SqlFragment::new(
        format!(r#"{expr} LIKE ? ESCAPE '\'"#),
        vec![Value::Text(veloq_core::query::glob_sql_like(pattern))],
    )
}

pub fn regex_matches(expr: &str, pattern: &str) -> SqlFragment {
    SqlFragment::new(
        format!("regexp_matches({expr}, ?)"),
        vec![Value::Text(pattern.to_string())],
    )
}

pub fn predicate(expr: &str, filter: NameFilterRef<'_>) -> Option<SqlFragment> {
    match filter {
        NameFilterRef::Any => None,
        NameFilterRef::Glob(pattern) => Some(glob_like(expr, pattern)),
        NameFilterRef::Regex(pattern) => Some(regex_matches(expr, pattern)),
    }
}

pub fn predicate_or_bound_true(expr: &str, filter: NameFilterRef<'_>) -> SqlFragment {
    predicate(expr, filter).unwrap_or_else(|| {
        SqlFragment::new("(? IS NOT NULL OR TRUE)", vec![Value::Text(String::new())])
    })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn glob_like_uses_canonical_like_escape_and_param() {
        let fragment = glob_like("name", "foo*_%");

        assert_eq!(fragment.sql, r#"name LIKE ? ESCAPE '\'"#);
        assert_eq!(fragment.params, vec![Value::Text("foo%\\_\\%".to_string())]);
    }

    #[test]
    fn regex_matches_passes_pattern_verbatim() {
        let fragment = regex_matches("COALESCE(name, '')", "foo|bar");

        assert_eq!(fragment.sql, "regexp_matches(COALESCE(name, ''), ?)");
        assert_eq!(fragment.params, vec![Value::Text("foo|bar".to_string())]);
    }

    #[test]
    fn predicate_returns_none_for_unfiltered_names() -> anyhow::Result<()> {
        let filter = NameFilterRef::from_optional(None, None)?;

        assert!(predicate("name", filter).is_none());
        Ok(())
    }

    #[test]
    fn bound_true_preserves_legacy_single_param_layout() -> anyhow::Result<()> {
        let fragment = predicate_or_bound_true("name", NameFilterRef::from_optional(None, None)?);

        assert_eq!(fragment.sql, "(? IS NOT NULL OR TRUE)");
        assert_eq!(fragment.params, vec![Value::Text(String::new())]);
        Ok(())
    }
}