Skip to main content

fraiseql_core/federation/
sql_utils.rs

1//! SQL utility functions for federation query building.
2//!
3//! Shared utilities for SQL generation across federation modules.
4
5use serde_json::Value;
6
7use crate::error::{FraiseQLError, Result};
8
9/// Convert a JSON value to a SQL literal representation.
10///
11/// Handles all JSON types and applies proper SQL escaping:
12/// - Strings: wrapped in quotes with single quotes doubled (PostgreSQL style)
13/// - Numbers: converted to string without quotes
14/// - Booleans: converted to "true" or "false"
15/// - Null: converted to "NULL"
16/// - Arrays/Objects: returns error
17///
18/// # Examples
19///
20/// ```ignore
21/// value_to_sql_literal(&json!("test")) // produces "'test'"
22/// value_to_sql_literal(&json!("O'Brien")) // produces "'O''Brien'"
23/// value_to_sql_literal(&json!(123)) // produces "123"
24/// value_to_sql_literal(&json!(null)) // produces "NULL"
25/// ```
26pub fn value_to_sql_literal(value: &Value) -> Result<String> {
27    match value {
28        Value::String(s) => {
29            let escaped = escape_sql_string(s);
30            Ok(format!("'{}'", escaped))
31        },
32        Value::Number(n) => Ok(n.to_string()),
33        Value::Bool(b) => Ok(if *b { "true" } else { "false" }.to_string()),
34        Value::Null => Ok("NULL".to_string()),
35        _ => Err(FraiseQLError::Validation {
36            message: format!("Cannot convert {} to SQL literal", value.type_str()),
37            path:    None,
38        }),
39    }
40}
41
42/// Convert a JSON value to its string representation for use in SQL.
43///
44/// This is used for extracting key values before they are escaped and quoted.
45///
46/// # Examples
47///
48/// ```ignore
49/// value_to_string(&json!("test")) // produces "test"
50/// value_to_string(&json!(123)) // produces "123"
51/// ```
52pub fn value_to_string(value: &Value) -> Result<String> {
53    match value {
54        Value::String(s) => Ok(s.clone()),
55        Value::Number(n) => Ok(n.to_string()),
56        Value::Bool(b) => Ok(b.to_string()),
57        Value::Null => Ok("null".to_string()),
58        _ => Err(FraiseQLError::Validation {
59            message: format!("Cannot convert {} to string for WHERE clause", value.type_str()),
60            path:    None,
61        }),
62    }
63}
64
65/// Escape single quotes in SQL string values to prevent SQL injection.
66///
67/// Uses PostgreSQL/SQL Server style escaping where single quotes are doubled.
68///
69/// # Examples
70///
71/// ```
72/// # use fraiseql_core::federation::sql_utils::escape_sql_string;
73/// assert_eq!(escape_sql_string("O'Brien"), "O''Brien");
74/// assert_eq!(escape_sql_string("test"), "test");
75/// ```
76pub fn escape_sql_string(value: &str) -> String {
77    value.replace("'", "''")
78}
79
80/// Helper trait to get string representation of JSON value type for error messages.
81pub trait JsonTypeStr {
82    fn type_str(&self) -> &'static str;
83}
84
85impl JsonTypeStr for Value {
86    fn type_str(&self) -> &'static str {
87        match self {
88            Value::Null => "null",
89            Value::Bool(_) => "bool",
90            Value::Number(_) => "number",
91            Value::String(_) => "string",
92            Value::Array(_) => "array",
93            Value::Object(_) => "object",
94        }
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use serde_json::json;
101
102    use super::*;
103
104    #[test]
105    fn test_value_to_sql_literal_string() {
106        let result = value_to_sql_literal(&Value::String("John".to_string())).unwrap();
107        assert_eq!(result, "'John'");
108    }
109
110    #[test]
111    fn test_value_to_sql_literal_string_with_quotes() {
112        let result = value_to_sql_literal(&Value::String("O'Brien".to_string())).unwrap();
113        assert_eq!(result, "'O''Brien'");
114    }
115
116    #[test]
117    fn test_value_to_sql_literal_number() {
118        let result = value_to_sql_literal(&json!(123)).unwrap();
119        assert_eq!(result, "123");
120
121        let result = value_to_sql_literal(&json!(99.99)).unwrap();
122        assert_eq!(result, "99.99");
123    }
124
125    #[test]
126    fn test_value_to_sql_literal_bool() {
127        let result = value_to_sql_literal(&Value::Bool(true)).unwrap();
128        assert_eq!(result, "true");
129
130        let result = value_to_sql_literal(&Value::Bool(false)).unwrap();
131        assert_eq!(result, "false");
132    }
133
134    #[test]
135    fn test_value_to_sql_literal_null() {
136        let result = value_to_sql_literal(&Value::Null).unwrap();
137        assert_eq!(result, "NULL");
138    }
139
140    #[test]
141    fn test_value_to_sql_literal_array_error() {
142        let result = value_to_sql_literal(&Value::Array(vec![]));
143        assert!(result.is_err());
144    }
145
146    #[test]
147    fn test_value_to_string() {
148        assert_eq!(value_to_string(&Value::String("test".to_string())).unwrap(), "test");
149        assert_eq!(value_to_string(&Value::Number(789.into())).unwrap(), "789");
150        assert_eq!(value_to_string(&Value::Bool(true)).unwrap(), "true");
151        assert_eq!(value_to_string(&Value::Null).unwrap(), "null");
152    }
153
154    #[test]
155    fn test_escape_sql_string() {
156        assert_eq!(escape_sql_string("O'Brien"), "O''Brien");
157        assert_eq!(escape_sql_string("test"), "test");
158        assert_eq!(escape_sql_string("test''; DROP--"), "test''''; DROP--");
159    }
160}