Skip to main content

fraiseql_wire/operators/
field.rs

1//! Field and value type definitions for operators
2//!
3//! Provides type-safe representations of database fields and values
4//! to prevent SQL injection and improve API ergonomics.
5
6use std::fmt;
7
8/// Represents a field reference in a WHERE clause or ORDER BY
9///
10/// Supports both JSONB payload fields and direct database columns,
11/// with automatic type casting and proper SQL generation.
12///
13/// # Examples
14///
15/// ```rust
16/// use fraiseql_wire::operators::Field;
17///
18/// // JSONB field: (data->>'name')
19/// let _ = Field::JsonbField("name".to_string());
20///
21/// // Direct column: created_at
22/// let _ = Field::DirectColumn("created_at".to_string());
23///
24/// // Nested JSONB: (data->'user'->>'name')
25/// let _ = Field::JsonbPath(vec!["user".to_string(), "name".to_string()]);
26/// ```
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum Field {
29    /// A field extracted from the JSONB `data` column with text extraction (->>)
30    ///
31    /// The value is extracted as text and wrapped in parentheses.
32    ///
33    /// Generated SQL: `(data->>'field_name')`
34    JsonbField(String),
35
36    /// A direct database column (not from JSONB)
37    ///
38    /// Uses the native type stored in the database.
39    ///
40    /// Generated SQL: `column_name`
41    DirectColumn(String),
42
43    /// A nested path within the JSONB `data` column
44    ///
45    /// The path is traversed left-to-right, with intermediate steps using `->` (JSON navigation)
46    /// and the final step using `->>` (text extraction).
47    ///
48    /// All extracted values are text and wrapped in parentheses.
49    ///
50    /// Generated SQL: `(data->'path[0]'->...->>'path[n]')`
51    JsonbPath(Vec<String>),
52}
53
54impl Field {
55    /// Validate field name to prevent SQL injection
56    ///
57    /// Allows: alphanumeric, underscore
58    /// Disallows: quotes, brackets, dashes, special characters
59    ///
60    /// # Errors
61    ///
62    /// Returns a descriptive error string if any field name contains invalid characters.
63    pub fn validate(&self) -> Result<(), String> {
64        let name = match self {
65            Field::JsonbField(n) => n,
66            Field::DirectColumn(n) => n,
67            Field::JsonbPath(path) => {
68                for segment in path {
69                    if !is_valid_field_name(segment) {
70                        return Err(format!("Invalid field name in path: {}", segment));
71                    }
72                }
73                return Ok(());
74            }
75        };
76
77        if !is_valid_field_name(name) {
78            return Err(format!("Invalid field name: {}", name));
79        }
80
81        Ok(())
82    }
83
84    /// Generate SQL for this field
85    pub fn to_sql(&self) -> String {
86        match self {
87            Field::JsonbField(name) => format!("(data->'{}')", name),
88            Field::DirectColumn(name) => name.clone(),
89            Field::JsonbPath(path) => {
90                if path.is_empty() {
91                    return "data".to_string();
92                }
93
94                let mut sql = String::from("(data");
95                for (i, segment) in path.iter().enumerate() {
96                    if i == path.len() - 1 {
97                        // Last segment: use ->> for text extraction
98                        sql.push_str(&format!("->>'{}\'", segment));
99                    } else {
100                        // Intermediate segments: use -> for JSON objects
101                        sql.push_str(&format!("->'{}\'", segment));
102                    }
103                }
104                sql.push(')');
105                sql
106            }
107        }
108    }
109}
110
111impl fmt::Display for Field {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            Field::JsonbField(name) => write!(f, "data->'{}'", name),
115            Field::DirectColumn(name) => write!(f, "{}", name),
116            Field::JsonbPath(path) => {
117                write!(f, "data")?;
118                for (i, segment) in path.iter().enumerate() {
119                    if i == path.len() - 1 {
120                        write!(f, "->>{}", segment)?;
121                    } else {
122                        write!(f, "->{}", segment)?;
123                    }
124                }
125                Ok(())
126            }
127        }
128    }
129}
130
131/// Represents a value to bind in a WHERE clause
132///
133/// # Examples
134///
135/// ```rust
136/// use fraiseql_wire::operators::Value;
137///
138/// let _ = Value::String("John".to_string());
139/// let _ = Value::Number(42.0);
140/// let _ = Value::Bool(true);
141/// let _ = Value::Null;
142/// let _ = Value::Array(vec![Value::String("a".to_string()), Value::String("b".to_string())]);
143/// ```
144#[derive(Debug, Clone)]
145pub enum Value {
146    /// String value
147    String(String),
148
149    /// Numeric value (f64 can represent i64, u64, f32 with precision)
150    Number(f64),
151
152    /// Boolean value
153    Bool(bool),
154
155    /// NULL
156    Null,
157
158    /// Array of values (for IN operators)
159    Array(Vec<Value>),
160
161    /// Vector of floats (for pgvector distance operators)
162    FloatArray(Vec<f32>),
163
164    /// Raw SQL expression (use with caution!)
165    ///
166    /// This should only be used for trusted SQL fragments,
167    /// never for user input.
168    RawSql(String),
169}
170
171impl Value {
172    /// Check if value is NULL
173    pub const fn is_null(&self) -> bool {
174        matches!(self, Value::Null)
175    }
176
177    /// Convert value to SQL literal
178    ///
179    /// For parameterized queries, prefer using parameter placeholders ($1, $2, etc.)
180    /// This is primarily for documentation and debugging.
181    pub fn to_sql_literal(&self) -> String {
182        match self {
183            Value::String(s) => format!("'{}'", s.replace('\'', "''")),
184            Value::Number(n) => n.to_string(),
185            Value::Bool(b) => b.to_string(),
186            Value::Null => "NULL".to_string(),
187            Value::Array(arr) => {
188                let items: Vec<String> = arr.iter().map(|v| v.to_sql_literal()).collect();
189                format!("ARRAY[{}]", items.join(", "))
190            }
191            Value::FloatArray(arr) => {
192                let items: Vec<String> = arr.iter().map(|f| f.to_string()).collect();
193                format!("[{}]", items.join(", "))
194            }
195            Value::RawSql(sql) => sql.clone(),
196        }
197    }
198}
199
200impl fmt::Display for Value {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        write!(f, "{}", self.to_sql_literal())
203    }
204}
205
206/// Check if a field name is valid (alphanumeric + underscore)
207fn is_valid_field_name(name: &str) -> bool {
208    if name.is_empty() {
209        return false;
210    }
211
212    // First character must be alphabetic or underscore
213    let first = name
214        .chars()
215        .next()
216        .expect("empty name already returned false above");
217    if !first.is_alphabetic() && first != '_' {
218        return false;
219    }
220
221    // Remaining characters must be alphanumeric or underscore
222    name.chars().all(|c| c.is_alphanumeric() || c == '_')
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_valid_field_names() {
231        assert!(is_valid_field_name("name"));
232        assert!(is_valid_field_name("_private"));
233        assert!(is_valid_field_name("field_123"));
234        assert!(is_valid_field_name("a"));
235    }
236
237    #[test]
238    fn test_invalid_field_names() {
239        assert!(!is_valid_field_name(""));
240        assert!(!is_valid_field_name("123field")); // starts with digit
241        assert!(!is_valid_field_name("field-name")); // contains dash
242        assert!(!is_valid_field_name("field.name")); // contains dot
243        assert!(!is_valid_field_name("field'name")); // contains quote
244    }
245
246    #[test]
247    fn test_field_validation() {
248        assert!(Field::JsonbField("name".to_string()).validate().is_ok());
249        assert!(Field::JsonbField("name-invalid".to_string())
250            .validate()
251            .is_err());
252        assert!(
253            Field::JsonbPath(vec!["user".to_string(), "name".to_string()])
254                .validate()
255                .is_ok()
256        );
257    }
258
259    #[test]
260    fn test_field_to_sql_jsonb() {
261        let field = Field::JsonbField("name".to_string());
262        assert_eq!(field.to_sql(), "(data->'name')");
263    }
264
265    #[test]
266    fn test_field_to_sql_direct() {
267        let field = Field::DirectColumn("created_at".to_string());
268        assert_eq!(field.to_sql(), "created_at");
269    }
270
271    #[test]
272    fn test_field_to_sql_path() {
273        let field = Field::JsonbPath(vec!["user".to_string(), "name".to_string()]);
274        assert_eq!(field.to_sql(), "(data->'user'->>'name')");
275    }
276
277    #[test]
278    fn test_value_to_sql_literal() {
279        assert_eq!(Value::String("test".to_string()).to_sql_literal(), "'test'");
280        assert_eq!(Value::Number(42.0).to_sql_literal(), "42");
281        assert_eq!(Value::Bool(true).to_sql_literal(), "true");
282        assert_eq!(Value::Null.to_sql_literal(), "NULL");
283    }
284
285    #[test]
286    fn test_value_string_escaping() {
287        let val = Value::String("O'Brien".to_string());
288        assert_eq!(val.to_sql_literal(), "'O''Brien'");
289    }
290}