Skip to main content

saddle_db/
statement.rs

1use std::fmt;
2
3use sqlx::{MySql, query::Query};
4
5use crate::{Result, error::invalid_statement};
6
7pub const MAX_OPERATION_BYTES: usize = 128;
8pub const MAX_SQL_BYTES: usize = 65_536;
9pub const MAX_PARAMETERS: usize = 256;
10pub const MAX_PARAMETER_BYTES: usize = 1_048_576;
11pub const MAX_PARAMETERS_TOTAL_BYTES: usize = 2_097_152;
12
13/// A scalar parameter accepted by the controlled V1 database API.
14///
15/// SQL parameters are never included in framework logs or errors.
16#[derive(Clone, PartialEq)]
17#[non_exhaustive]
18pub enum DbValue {
19    Null,
20    Bool(bool),
21    I64(i64),
22    U64(u64),
23    F64(f64),
24    String(String),
25    Bytes(Vec<u8>),
26}
27
28impl fmt::Debug for DbValue {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        formatter.write_str(match self {
31            Self::Null => "Null",
32            Self::Bool(_) => "Bool(<redacted>)",
33            Self::I64(_) => "I64(<redacted>)",
34            Self::U64(_) => "U64(<redacted>)",
35            Self::F64(_) => "F64(<redacted>)",
36            Self::String(_) => "String(<redacted>)",
37            Self::Bytes(_) => "Bytes(<redacted>)",
38        })
39    }
40}
41
42impl From<bool> for DbValue {
43    fn from(value: bool) -> Self {
44        Self::Bool(value)
45    }
46}
47impl From<i32> for DbValue {
48    fn from(value: i32) -> Self {
49        Self::I64(i64::from(value))
50    }
51}
52impl From<i64> for DbValue {
53    fn from(value: i64) -> Self {
54        Self::I64(value)
55    }
56}
57impl From<u32> for DbValue {
58    fn from(value: u32) -> Self {
59        Self::U64(u64::from(value))
60    }
61}
62impl From<u64> for DbValue {
63    fn from(value: u64) -> Self {
64        Self::U64(value)
65    }
66}
67impl From<f64> for DbValue {
68    fn from(value: f64) -> Self {
69        Self::F64(value)
70    }
71}
72impl From<String> for DbValue {
73    fn from(value: String) -> Self {
74        Self::String(value)
75    }
76}
77impl From<&str> for DbValue {
78    fn from(value: &str) -> Self {
79        Self::String(value.to_owned())
80    }
81}
82impl From<Vec<u8>> for DbValue {
83    fn from(value: Vec<u8>) -> Self {
84        Self::Bytes(value)
85    }
86}
87
88/// A parameterized SQL statement and its operation name.
89///
90/// `operation` is a stable, low-cardinality name used for Trace records. It
91/// must describe the operation (for example `orders.insert`), not contain SQL.
92#[derive(Clone, PartialEq)]
93pub struct Statement {
94    operation: String,
95    sql: String,
96    parameters: Vec<DbValue>,
97}
98
99impl Statement {
100    pub fn new(operation: impl AsRef<str>, sql: impl AsRef<str>) -> Result<Self> {
101        let operation = operation.as_ref();
102        let sql = sql.as_ref();
103        validate_operation(operation)?;
104        if sql.trim().is_empty() || sql.len() > MAX_SQL_BYTES {
105            return Err(invalid_statement(
106                "SQL must be non-empty and within the V1 size limit",
107            ));
108        }
109        Ok(Self {
110            operation: operation.to_owned(),
111            sql: sql.to_owned(),
112            parameters: Vec::new(),
113        })
114    }
115
116    pub fn bind(mut self, value: impl Into<DbValue>) -> Result<Self> {
117        let value = value.into();
118        self.ensure_parameter_fits(&value)?;
119        self.parameters.push(value);
120        Ok(self)
121    }
122
123    pub fn bind_null(self) -> Result<Self> {
124        self.bind(DbValue::Null)
125    }
126
127    pub fn operation(&self) -> &str {
128        &self.operation
129    }
130    pub fn parameter_count(&self) -> usize {
131        self.parameters.len()
132    }
133
134    pub(crate) fn validate(&self) -> Result<()> {
135        validate_operation(&self.operation)?;
136        if self.sql.trim().is_empty() || self.sql.len() > MAX_SQL_BYTES {
137            return Err(invalid_statement(
138                "SQL must be non-empty and within the V1 size limit",
139            ));
140        }
141        if self.parameters.len() > MAX_PARAMETERS {
142            return Err(invalid_statement("statement has too many parameters"));
143        }
144        let mut total_bytes = 0_usize;
145        for parameter in &self.parameters {
146            let bytes = parameter.encoded_size();
147            if bytes > MAX_PARAMETER_BYTES {
148                return Err(invalid_statement(
149                    "statement parameter exceeds the V1 byte limit",
150                ));
151            }
152            total_bytes = total_bytes.saturating_add(bytes);
153            if total_bytes > MAX_PARAMETERS_TOTAL_BYTES {
154                return Err(invalid_statement(
155                    "statement parameters exceed the V1 total byte limit",
156                ));
157            }
158        }
159        Ok(())
160    }
161
162    fn ensure_parameter_fits(&self, value: &DbValue) -> Result<()> {
163        if self.parameters.len() == MAX_PARAMETERS {
164            return Err(invalid_statement("statement has too many parameters"));
165        }
166        let bytes = value.encoded_size();
167        if bytes > MAX_PARAMETER_BYTES {
168            return Err(invalid_statement(
169                "statement parameter exceeds the V1 byte limit",
170            ));
171        }
172        let current_bytes = self
173            .parameters
174            .iter()
175            .map(DbValue::encoded_size)
176            .fold(0_usize, usize::saturating_add);
177        if current_bytes.saturating_add(bytes) > MAX_PARAMETERS_TOTAL_BYTES {
178            return Err(invalid_statement(
179                "statement parameters exceed the V1 total byte limit",
180            ));
181        }
182        Ok(())
183    }
184
185    pub(crate) fn query(&self) -> Query<'_, MySql, sqlx::mysql::MySqlArguments> {
186        let mut query = sqlx::query(&self.sql);
187        for value in &self.parameters {
188            query = match value {
189                DbValue::Null => query.bind(Option::<String>::None),
190                DbValue::Bool(value) => query.bind(*value),
191                DbValue::I64(value) => query.bind(*value),
192                DbValue::U64(value) => query.bind(*value),
193                DbValue::F64(value) => query.bind(*value),
194                DbValue::String(value) => query.bind(value.as_str()),
195                DbValue::Bytes(value) => query.bind(value.as_slice()),
196            };
197        }
198        query
199    }
200}
201
202impl DbValue {
203    fn encoded_size(&self) -> usize {
204        match self {
205            Self::Null => 0,
206            Self::Bool(_) => 1,
207            Self::I64(_) | Self::U64(_) | Self::F64(_) => 8,
208            Self::String(value) => value.len(),
209            Self::Bytes(value) => value.len(),
210        }
211    }
212}
213
214pub(crate) fn validate_operation(operation: &str) -> Result<()> {
215    if operation.is_empty()
216        || operation.len() > MAX_OPERATION_BYTES
217        || !operation
218            .bytes()
219            .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'))
220    {
221        return Err(invalid_statement(
222            "operation must be a bounded stable identifier",
223        ));
224    }
225    Ok(())
226}
227
228impl fmt::Debug for Statement {
229    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
230        formatter
231            .debug_struct("Statement")
232            .field("operation", &self.operation)
233            .field("sql_bytes", &self.sql.len())
234            .field("parameter_count", &self.parameters.len())
235            .finish()
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn debug_and_validation_do_not_expose_sql_or_parameters() {
245        let statement = Statement::new("orders.insert", "INSERT secret")
246            .unwrap()
247            .bind("password")
248            .unwrap();
249        statement.validate().unwrap();
250        let output = format!("{statement:?}");
251        assert!(!output.contains("secret"));
252        assert!(!output.contains("password"));
253        assert!(!format!("{:?}", DbValue::from("password")).contains("password"));
254        assert!(Statement::new("raw SQL", "SELECT 1").is_err());
255        assert!(Statement::new("orders.query", "x".repeat(MAX_SQL_BYTES + 1)).is_err());
256        assert!(
257            Statement::new("orders.insert", "INSERT")
258                .unwrap()
259                .bind(vec![0_u8; MAX_PARAMETER_BYTES + 1])
260                .is_err()
261        );
262        let mut statement = Statement::new("orders.insert", "INSERT").unwrap();
263        for _ in 0..MAX_PARAMETERS {
264            statement = statement.bind_null().unwrap();
265        }
266        assert!(statement.bind_null().is_err());
267        assert!(
268            Statement::new("orders.insert", "INSERT")
269                .unwrap()
270                .bind(vec![0_u8; MAX_PARAMETER_BYTES])
271                .unwrap()
272                .bind(vec![0_u8; MAX_PARAMETER_BYTES])
273                .unwrap()
274                .bind(1_u64)
275                .is_err()
276        );
277    }
278}