Skip to main content

gatekeep_sqlx/
fragment.rs

1use std::marker::PhantomData;
2
3use sqlx::{
4    QueryBuilder,
5    types::{
6        Uuid,
7        time::{Date, OffsetDateTime, PrimitiveDateTime, Time},
8    },
9};
10
11/// `SQLx` backend supported by gatekeep lowering.
12pub trait GatekeepSqlxBackend: Clone + Copy + core::fmt::Debug + Send + Sync + 'static {
13    /// `SQLx` database driver for this backend.
14    type Database: sqlx::Database;
15
16    /// Database driver represented by this backend.
17    const DRIVER: SqlxDriver;
18
19    /// Stable backend name.
20    const NAME: &'static str;
21
22    /// Appends one bind placeholder to rendered SQL.
23    fn push_placeholder(sql: &mut String, index: usize);
24
25    /// Appends one typed bind value to a `SQLx` query builder.
26    fn push_bind(builder: &mut QueryBuilder<Self::Database>, value: &SqlxValue);
27
28    /// Name of the SQL function that returns the lower of two non-null grades.
29    const MIN_FUNCTION: &'static str;
30
31    /// Name of the SQL function that returns the higher of two non-null grades.
32    const MAX_FUNCTION: &'static str;
33
34    /// Whether the backend's grade functions return `NULL` when any input is
35    /// `NULL`.
36    const GRADE_FUNCTION_PROPAGATES_NULL: bool;
37}
38
39macro_rules! push_sqlx_bind {
40    ($builder:expr, $value:expr) => {
41        match $value {
42            SqlxValue::Bool(value) => {
43                $builder.push_bind(*value);
44            }
45            SqlxValue::I16(value) => {
46                $builder.push_bind(*value);
47            }
48            SqlxValue::I32(value) => {
49                $builder.push_bind(*value);
50            }
51            SqlxValue::I64(value) => {
52                $builder.push_bind(*value);
53            }
54            SqlxValue::Text(value) => {
55                $builder.push_bind(value.clone());
56            }
57            SqlxValue::Bytes(value) => {
58                $builder.push_bind(value.clone());
59            }
60            SqlxValue::Uuid(value) => {
61                $builder.push_bind(*value);
62            }
63            SqlxValue::Date(value) => {
64                $builder.push_bind(*value);
65            }
66            SqlxValue::Time(value) => {
67                $builder.push_bind(*value);
68            }
69            SqlxValue::Timestamp(value) => {
70                $builder.push_bind(*value);
71            }
72            SqlxValue::TimestampTz(value) => {
73                $builder.push_bind(*value);
74            }
75        }
76    };
77}
78
79/// Postgres backend marker.
80#[cfg(feature = "postgres")]
81#[derive(Clone, Copy, Debug)]
82pub struct PostgresBackend;
83
84#[cfg(feature = "postgres")]
85impl GatekeepSqlxBackend for PostgresBackend {
86    type Database = sqlx::Postgres;
87
88    const DRIVER: SqlxDriver = SqlxDriver::Postgres;
89    const NAME: &'static str = "postgres";
90    const MIN_FUNCTION: &'static str = "LEAST";
91    const MAX_FUNCTION: &'static str = "GREATEST";
92    const GRADE_FUNCTION_PROPAGATES_NULL: bool = false;
93
94    fn push_placeholder(sql: &mut String, index: usize) {
95        sql.push('$');
96        sql.push_str(&index.to_string());
97    }
98
99    fn push_bind(builder: &mut QueryBuilder<Self::Database>, value: &SqlxValue) {
100        push_sqlx_bind!(builder, value);
101    }
102}
103
104/// `SQLite` backend marker.
105#[cfg(feature = "sqlite")]
106#[derive(Clone, Copy, Debug)]
107pub struct SqliteBackend;
108
109#[cfg(feature = "sqlite")]
110impl GatekeepSqlxBackend for SqliteBackend {
111    type Database = sqlx::Sqlite;
112
113    const DRIVER: SqlxDriver = SqlxDriver::Sqlite;
114    const NAME: &'static str = "sqlite";
115    const MIN_FUNCTION: &'static str = "min";
116    const MAX_FUNCTION: &'static str = "max";
117    const GRADE_FUNCTION_PROPAGATES_NULL: bool = true;
118
119    fn push_placeholder(sql: &mut String, _index: usize) {
120        sql.push('?');
121    }
122
123    fn push_bind(builder: &mut QueryBuilder<Self::Database>, value: &SqlxValue) {
124        push_sqlx_bind!(builder, value);
125    }
126}
127
128/// `MySQL` backend marker.
129#[cfg(feature = "mysql")]
130#[derive(Clone, Copy, Debug)]
131pub struct MySqlBackend;
132
133#[cfg(feature = "mysql")]
134impl GatekeepSqlxBackend for MySqlBackend {
135    type Database = sqlx::MySql;
136
137    const DRIVER: SqlxDriver = SqlxDriver::MySql;
138    const NAME: &'static str = "mysql";
139    const MIN_FUNCTION: &'static str = "LEAST";
140    const MAX_FUNCTION: &'static str = "GREATEST";
141    const GRADE_FUNCTION_PROPAGATES_NULL: bool = true;
142
143    fn push_placeholder(sql: &mut String, _index: usize) {
144        sql.push('?');
145    }
146
147    fn push_bind(builder: &mut QueryBuilder<Self::Database>, value: &SqlxValue) {
148        push_sqlx_bind!(builder, value);
149    }
150}
151
152/// Supported `SQLx` database driver.
153#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154#[non_exhaustive]
155pub enum SqlxDriver {
156    /// Postgres `SQLx` driver.
157    Postgres,
158    /// `SQLite` `SQLx` driver.
159    Sqlite,
160    /// `MySQL` `SQLx` driver.
161    MySql,
162}
163
164impl SqlxDriver {
165    /// Stable driver name.
166    #[must_use]
167    pub const fn name(self) -> &'static str {
168        match self {
169            Self::Postgres => "postgres",
170            Self::Sqlite => "sqlite",
171            Self::MySql => "mysql",
172        }
173    }
174
175    /// Whether this crate was compiled with the matching backend feature.
176    #[must_use]
177    pub const fn is_enabled(self) -> bool {
178        match self {
179            Self::Postgres => cfg!(feature = "postgres"),
180            Self::Sqlite => cfg!(feature = "sqlite"),
181            Self::MySql => cfg!(feature = "mysql"),
182        }
183    }
184}
185
186/// Database driver configuration error.
187#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
188#[non_exhaustive]
189pub enum SqlxDriverError {
190    /// The URL scheme is not recognized as a `SQLx` database driver.
191    #[error("unsupported SQLx database URL scheme {scheme:?}")]
192    UnsupportedUrlScheme {
193        /// URL scheme, if one could be parsed.
194        scheme: Option<String>,
195    },
196
197    /// The URL selects a driver whose feature was not enabled.
198    #[error("SQLx driver {driver} is not enabled for gatekeep-sqlx")]
199    DriverNotEnabled {
200        /// Driver inferred from the URL.
201        driver: &'static str,
202    },
203
204    /// The configured driver does not match the selected backend.
205    #[error("SQLx backend mismatch: expected {expected}, found {actual}")]
206    BackendMismatch {
207        /// Backend expected by the selected lowerer.
208        expected: &'static str,
209        /// Driver inferred from runtime configuration.
210        actual: &'static str,
211    },
212}
213
214/// Infers the `SQLx` driver from a database URL or `SQLx`-style `SQLite` memory URL.
215///
216/// # Errors
217///
218/// Returns [`SqlxDriverError`] when the URL scheme is unsupported or when the
219/// inferred driver was not enabled at compile time.
220pub fn infer_enabled_driver_from_url(database_url: &str) -> Result<SqlxDriver, SqlxDriverError> {
221    let driver = infer_driver_from_url(database_url)?;
222    if driver.is_enabled() {
223        Ok(driver)
224    } else {
225        Err(SqlxDriverError::DriverNotEnabled {
226            driver: driver.name(),
227        })
228    }
229}
230
231/// Validates that a database URL matches a selected backend.
232///
233/// # Errors
234///
235/// Returns [`SqlxDriverError`] when the URL is unsupported, names a disabled
236/// driver, or names a different driver from `B`.
237pub fn validate_database_url_for_backend<B>(database_url: &str) -> Result<(), SqlxDriverError>
238where
239    B: GatekeepSqlxBackend,
240{
241    let actual = infer_enabled_driver_from_url(database_url)?;
242    if actual == B::DRIVER {
243        Ok(())
244    } else {
245        Err(SqlxDriverError::BackendMismatch {
246            expected: B::NAME,
247            actual: actual.name(),
248        })
249    }
250}
251
252fn infer_driver_from_url(database_url: &str) -> Result<SqlxDriver, SqlxDriverError> {
253    if database_url.starts_with("sqlite:") {
254        return Ok(SqlxDriver::Sqlite);
255    }
256
257    let Some((scheme, _rest)) = database_url.split_once(':') else {
258        return Err(SqlxDriverError::UnsupportedUrlScheme { scheme: None });
259    };
260
261    match scheme {
262        "postgres" | "postgresql" => Ok(SqlxDriver::Postgres),
263        "mysql" | "mariadb" => Ok(SqlxDriver::MySql),
264        "sqlite" => Ok(SqlxDriver::Sqlite),
265        other => Err(SqlxDriverError::UnsupportedUrlScheme {
266            scheme: Some(other.to_owned()),
267        }),
268    }
269}
270
271/// Scalar value carried by a lowered SQL fragment.
272#[derive(Clone, Debug, PartialEq, Eq)]
273#[non_exhaustive]
274pub enum SqlxValue {
275    /// Boolean bind value.
276    Bool(bool),
277    /// Signed 16-bit integer bind value.
278    I16(i16),
279    /// Signed 32-bit integer bind value.
280    I32(i32),
281    /// Signed 64-bit integer bind value.
282    I64(i64),
283    /// Text bind value.
284    Text(String),
285    /// Binary bind value.
286    Bytes(Vec<u8>),
287    /// UUID bind value.
288    Uuid(Uuid),
289    /// Date bind value.
290    Date(Date),
291    /// Time bind value.
292    Time(Time),
293    /// Timestamp without time zone bind value.
294    Timestamp(PrimitiveDateTime),
295    /// Timestamp with time zone bind value.
296    TimestampTz(OffsetDateTime),
297}
298
299macro_rules! impl_sqlx_value_from {
300    ($ty:ty, $variant:ident) => {
301        impl From<$ty> for SqlxValue {
302            fn from(value: $ty) -> Self {
303                Self::$variant(value)
304            }
305        }
306    };
307}
308
309impl_sqlx_value_from!(bool, Bool);
310impl_sqlx_value_from!(i16, I16);
311impl_sqlx_value_from!(i32, I32);
312impl_sqlx_value_from!(i64, I64);
313impl_sqlx_value_from!(String, Text);
314impl_sqlx_value_from!(Vec<u8>, Bytes);
315impl_sqlx_value_from!(Uuid, Uuid);
316impl_sqlx_value_from!(Date, Date);
317impl_sqlx_value_from!(Time, Time);
318impl_sqlx_value_from!(PrimitiveDateTime, Timestamp);
319impl_sqlx_value_from!(OffsetDateTime, TimestampTz);
320
321impl From<&str> for SqlxValue {
322    fn from(value: &str) -> Self {
323        Self::Text(value.to_owned())
324    }
325}
326
327impl From<&[u8]> for SqlxValue {
328    fn from(value: &[u8]) -> Self {
329        Self::Bytes(value.to_vec())
330    }
331}
332
333#[derive(Clone, Debug, PartialEq, Eq)]
334enum SqlPart {
335    Text(String),
336    Bind(SqlxValue),
337}
338
339/// Trusted SQL plus ordered bind values for one `SQLx` backend.
340#[derive(Debug, PartialEq, Eq)]
341pub struct SqlxFragment<B> {
342    parts: Vec<SqlPart>,
343    backend: PhantomData<fn() -> B>,
344}
345
346impl<B> Clone for SqlxFragment<B> {
347    fn clone(&self) -> Self {
348        Self {
349            parts: self.parts.clone(),
350            backend: PhantomData,
351        }
352    }
353}
354
355impl<B> Default for SqlxFragment<B> {
356    fn default() -> Self {
357        Self {
358            parts: Vec::new(),
359            backend: PhantomData,
360        }
361    }
362}
363
364impl<B> SqlxFragment<B> {
365    /// Builds a fragment from SQL owned by the application.
366    ///
367    /// Callers must not pass user-supplied text here. Dynamic values belong in
368    /// bind fragments built with [`Self::bind`].
369    #[must_use]
370    pub fn trusted(sql: impl Into<String>) -> Self {
371        let sql = sql.into();
372        if sql.is_empty() {
373            Self::default()
374        } else {
375            Self {
376                parts: vec![SqlPart::Text(sql)],
377                backend: PhantomData,
378            }
379        }
380    }
381
382    /// Builds a bind fragment from a supported `SQLx` scalar value.
383    #[must_use]
384    pub fn bind(value: impl Into<SqlxValue>) -> Self {
385        Self {
386            parts: vec![SqlPart::Bind(value.into())],
387            backend: PhantomData,
388        }
389    }
390
391    /// Returns the ordered bind values.
392    pub fn binds(&self) -> impl Iterator<Item = &SqlxValue> {
393        self.parts.iter().filter_map(|part| match part {
394            SqlPart::Text(_) => None,
395            SqlPart::Bind(value) => Some(value),
396        })
397    }
398
399    /// Appends another fragment to this one.
400    pub fn push_fragment(&mut self, fragment: Self) {
401        self.parts.extend(fragment.parts);
402    }
403
404    pub(crate) fn push_sql(&mut self, sql: impl Into<String>) {
405        let sql = sql.into();
406        if !sql.is_empty() {
407            self.parts.push(SqlPart::Text(sql));
408        }
409    }
410
411    #[must_use]
412    pub(crate) fn wrapped(self) -> Self {
413        let mut fragment = Self::trusted("(");
414        fragment.push_fragment(self);
415        fragment.push_sql(")");
416        fragment
417    }
418
419    #[must_use]
420    pub(crate) fn unary(prefix: &str, inner: Self) -> Self {
421        let mut fragment = Self::trusted(prefix);
422        fragment.push_fragment(inner.wrapped());
423        fragment
424    }
425
426    #[must_use]
427    pub(crate) fn binary(separator: &str, fragments: Vec<Self>) -> Self {
428        let mut iter = fragments.into_iter();
429        let Some(first) = iter.next() else {
430            return Self::trusted("FALSE");
431        };
432
433        let mut fragment = first.wrapped();
434        for next in iter {
435            fragment.push_sql(separator);
436            fragment.push_fragment(next.wrapped());
437        }
438        fragment
439    }
440
441    #[must_use]
442    pub(crate) fn function(name: &str, fragments: Vec<Self>) -> Self {
443        let mut fragment = Self::trusted(name);
444        fragment.push_sql("(");
445
446        let mut iter = fragments.into_iter();
447        if let Some(first) = iter.next() {
448            fragment.push_fragment(first);
449            for next in iter {
450                fragment.push_sql(", ");
451                fragment.push_fragment(next);
452            }
453        }
454
455        fragment.push_sql(")");
456        fragment
457    }
458}
459
460impl<B> SqlxFragment<B>
461where
462    B: GatekeepSqlxBackend,
463{
464    /// Converts the fragment to SQL with this backend's placeholder syntax.
465    #[must_use]
466    pub fn to_sql(&self) -> String {
467        let mut sql = String::new();
468        let mut placeholders = 0usize;
469
470        for part in &self.parts {
471            match part {
472                SqlPart::Text(text) => sql.push_str(text),
473                SqlPart::Bind(_) => {
474                    placeholders += 1;
475                    B::push_placeholder(&mut sql, placeholders);
476                }
477            }
478        }
479        sql
480    }
481
482    /// Appends this fragment to a `SQLx` query builder.
483    pub fn push_to(&self, builder: &mut QueryBuilder<B::Database>) {
484        for part in &self.parts {
485            match part {
486                SqlPart::Text(text) => {
487                    builder.push(text);
488                }
489                SqlPart::Bind(value) => B::push_bind(builder, value),
490            }
491        }
492    }
493}
494
495/// Postgres scalar value carried by a lowered SQL fragment.
496pub type PgValue = SqlxValue;
497
498/// Trusted Postgres SQL plus ordered bind values.
499#[cfg(feature = "postgres")]
500pub type PgFragment = SqlxFragment<PostgresBackend>;
501
502#[cfg(feature = "postgres")]
503impl SqlxFragment<PostgresBackend> {
504    /// Converts the fragment to Postgres placeholders (`$1`, `$2`, ...).
505    #[must_use]
506    pub fn to_postgres_sql(&self) -> String {
507        self.to_sql()
508    }
509}