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#[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#[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}