Skip to main content

sqry_core/query/builder/
condition.rs

1//! Condition builder for individual field conditions
2
3use std::borrow::Cow;
4
5use crate::query::{Condition, Field, FieldRegistry, Operator, Span, Value};
6
7/// Builder for individual field conditions.
8///
9/// Uses `Cow<'static, str>` for field names to avoid allocations when
10/// using string literals (common case), while still supporting dynamic strings.
11///
12/// This is typically constructed internally by `QueryBuilder` methods rather
13/// than directly by users.
14#[derive(Clone, Debug)]
15pub struct ConditionBuilder {
16    /// Field name (may be alias - resolved at build time)
17    /// Uses Cow to avoid allocation for static string literals
18    field: Cow<'static, str>,
19    /// Comparison operator
20    operator: Operator,
21    /// Value to compare against
22    value: Value,
23}
24
25impl ConditionBuilder {
26    /// Create a new condition with a static field name (no allocation)
27    ///
28    /// This is used by core field methods like `kind()`, `name()`, etc.
29    /// where the field name is a compile-time constant.
30    #[must_use]
31    pub fn new_static(field: &'static str, operator: Operator, value: Value) -> Self {
32        Self {
33            field: Cow::Borrowed(field),
34            operator,
35            value,
36        }
37    }
38
39    /// Create a new condition with a dynamic field name (allocates)
40    ///
41    /// This is used by generic field methods like `field()` where the
42    /// field name is provided at runtime (e.g., for plugin fields).
43    pub fn new(field: impl Into<String>, operator: Operator, value: Value) -> Self {
44        Self {
45            field: Cow::Owned(field.into()),
46            operator,
47            value,
48        }
49    }
50
51    /// Get the field name as a string slice
52    #[must_use]
53    pub fn field(&self) -> &str {
54        &self.field
55    }
56
57    /// Get the operator
58    #[must_use]
59    pub fn operator(&self) -> &Operator {
60        &self.operator
61    }
62
63    /// Get the value
64    #[must_use]
65    pub fn value(&self) -> &Value {
66        &self.value
67    }
68
69    /// Convert to Condition, resolving field alias via registry
70    ///
71    /// The registry is used to resolve field aliases (e.g., "file" -> "path",
72    /// "language" -> "lang") to their canonical names.
73    #[must_use]
74    pub fn into_condition(self, registry: &FieldRegistry) -> Condition {
75        // Resolve alias to canonical name (e.g., "file" -> "path", "language" -> "lang")
76        let field = self.field;
77        let canonical_name = match registry.resolve_canonical(field.as_ref()) {
78            Some(canonical) => canonical.to_string(),
79            None => field.into_owned(),
80        };
81
82        Condition {
83            field: Field::new(canonical_name),
84            operator: self.operator,
85            value: self.value,
86            span: Span::synthetic(),
87        }
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::query::FieldRegistry;
95
96    #[test]
97    fn test_new_static() {
98        let cond = ConditionBuilder::new_static(
99            "kind",
100            Operator::Equal,
101            Value::String("function".to_string()),
102        );
103        assert_eq!(cond.field(), "kind");
104        assert_eq!(cond.operator(), &Operator::Equal);
105        assert!(matches!(cond.value(), Value::String(s) if s == "function"));
106    }
107
108    #[test]
109    fn test_new_dynamic() {
110        let field_name = String::from("custom_field");
111        let cond = ConditionBuilder::new(
112            field_name,
113            Operator::Regex,
114            Value::String("pattern".to_string()),
115        );
116        assert_eq!(cond.field(), "custom_field");
117        assert_eq!(cond.operator(), &Operator::Regex);
118    }
119
120    #[test]
121    fn test_into_condition_canonical() {
122        let registry = FieldRegistry::with_core_fields();
123        let cond = ConditionBuilder::new_static(
124            "kind",
125            Operator::Equal,
126            Value::String("function".to_string()),
127        );
128        let condition = cond.into_condition(&registry);
129        assert_eq!(condition.field.as_str(), "kind");
130        assert!(condition.span.is_synthetic());
131    }
132
133    #[test]
134    fn test_into_condition_alias_resolution() {
135        let registry = FieldRegistry::with_core_fields();
136
137        // "file" should resolve to "path"
138        let cond = ConditionBuilder::new_static(
139            "file",
140            Operator::Equal,
141            Value::String("src/main.rs".to_string()),
142        );
143        let condition = cond.into_condition(&registry);
144        assert_eq!(condition.field.as_str(), "path");
145
146        // "language" should resolve to "lang"
147        let cond = ConditionBuilder::new_static(
148            "language",
149            Operator::Equal,
150            Value::String("rust".to_string()),
151        );
152        let condition = cond.into_condition(&registry);
153        assert_eq!(condition.field.as_str(), "lang");
154    }
155
156    #[test]
157    fn test_into_condition_unknown_field_kept() {
158        let registry = FieldRegistry::with_core_fields();
159
160        // Unknown field names are kept as-is (validation happens separately)
161        let cond = ConditionBuilder::new(
162            "unknown_field",
163            Operator::Equal,
164            Value::String("value".to_string()),
165        );
166        let condition = cond.into_condition(&registry);
167        assert_eq!(condition.field.as_str(), "unknown_field");
168    }
169
170    #[test]
171    fn test_clone() {
172        let cond = ConditionBuilder::new_static(
173            "name",
174            Operator::Regex,
175            Value::String("test.*".to_string()),
176        );
177        let cloned = cond.clone();
178        assert_eq!(cloned.field(), "name");
179        assert_eq!(cloned.operator(), &Operator::Regex);
180    }
181}