1use std::marker::PhantomData;
2
3use sqlx::{
4 QueryBuilder,
5 types::{
6 Uuid,
7 time::{Date, OffsetDateTime, PrimitiveDateTime, Time},
8 },
9};
10
11pub trait GatekeepSqlxBackend: Clone + Copy + core::fmt::Debug + Send + Sync + 'static {
13 type Database: sqlx::Database;
15
16 const DRIVER: SqlxDriver;
18
19 const NAME: &'static str;
21
22 fn push_placeholder(sql: &mut String, index: usize);
24
25 fn push_bind(builder: &mut QueryBuilder<Self::Database>, value: &SqlxValue);
27
28 const MIN_FUNCTION: &'static str;
30
31 const MAX_FUNCTION: &'static str;
33
34 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#[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#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
154#[non_exhaustive]
155pub enum SqlxDriver {
156 Postgres,
158 Sqlite,
160 MySql,
162}
163
164impl SqlxDriver {
165 #[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 #[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#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
188#[non_exhaustive]
189pub enum SqlxDriverError {
190 #[error("unsupported SQLx database URL scheme {scheme:?}")]
192 UnsupportedUrlScheme {
193 scheme: Option<String>,
195 },
196
197 #[error("SQLx driver {driver} is not enabled for gatekeep-sqlx")]
199 DriverNotEnabled {
200 driver: &'static str,
202 },
203
204 #[error("SQLx backend mismatch: expected {expected}, found {actual}")]
206 BackendMismatch {
207 expected: &'static str,
209 actual: &'static str,
211 },
212}
213
214pub 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
231pub 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#[derive(Clone, Debug, PartialEq, Eq)]
273#[non_exhaustive]
274pub enum SqlxValue {
275 Bool(bool),
277 I16(i16),
279 I32(i32),
281 I64(i64),
283 Text(String),
285 Bytes(Vec<u8>),
287 Uuid(Uuid),
289 Date(Date),
291 Time(Time),
293 Timestamp(PrimitiveDateTime),
295 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#[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 #[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 #[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 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 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 #[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 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
495pub type PgValue = SqlxValue;
497
498#[cfg(feature = "postgres")]
500pub type PgFragment = SqlxFragment<PostgresBackend>;
501
502#[cfg(feature = "postgres")]
503impl SqlxFragment<PostgresBackend> {
504 #[must_use]
506 pub fn to_postgres_sql(&self) -> String {
507 self.to_sql()
508 }
509}