1use crate::error::BackendCapabilityError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum BackendKind {
13 Postgres,
15 Sqlite,
17 MySql,
19 MongoDb,
21 Redis,
23}
24
25impl BackendKind {
26 pub fn system_name(self) -> &'static str {
28 match self {
29 Self::Postgres => "postgresql",
30 Self::Sqlite => "sqlite",
31 Self::MySql => "mysql",
32 Self::MongoDb => "mongodb",
33 Self::Redis => "redis",
34 }
35 }
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
40pub enum TransactionSupport {
41 None,
43 Flat,
45 Savepoints,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
51pub enum RowLocking {
52 None,
54 Basic,
56 Full,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub enum IsolationLevel {
65 ReadCommitted,
67 RepeatableRead,
69 Serializable,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
75#[non_exhaustive]
76pub enum Feature {
77 Joins,
79 Returning,
81 RowLocking,
83 LockModifiers,
85 WindowFunctions,
87 Regex,
89 Arrays,
91 DistinctOn,
93 CaseInsensitiveLike,
95 StatisticalAggregates,
97 Savepoints,
99 Isolation(IsolationLevel),
101 Subqueries,
103 SetOperations,
105 RawSql,
107 KeysetPagination,
109 NullableKeyset,
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub struct BackendCapabilities {
117 pub kind: BackendKind,
119 pub transactions: TransactionSupport,
121 pub row_locking: RowLocking,
123 pub joins: bool,
125 pub returning: bool,
127 pub window_functions: bool,
129 pub regex: bool,
131 pub arrays: bool,
133 pub distinct_on: bool,
135 pub ilike: bool,
137 pub statistical_aggregates: bool,
139 pub max_params: usize,
142 pub isolation_levels: &'static [IsolationLevel],
144}
145
146impl BackendCapabilities {
147 pub const fn postgres() -> Self {
149 Self {
150 kind: BackendKind::Postgres,
151 transactions: TransactionSupport::Savepoints,
152 row_locking: RowLocking::Full,
153 joins: true,
154 returning: true,
155 window_functions: true,
156 regex: true,
157 arrays: true,
158 distinct_on: true,
159 ilike: true,
160 statistical_aggregates: true,
161 max_params: 65_535,
162 isolation_levels: &[
163 IsolationLevel::ReadCommitted,
164 IsolationLevel::RepeatableRead,
165 IsolationLevel::Serializable,
166 ],
167 }
168 }
169
170 pub const fn sqlite() -> Self {
176 Self {
177 kind: BackendKind::Sqlite,
178 transactions: TransactionSupport::Savepoints,
179 row_locking: RowLocking::None,
180 joins: true,
181 returning: true,
182 window_functions: true,
183 regex: false,
184 arrays: false,
185 distinct_on: false,
186 ilike: false,
187 statistical_aggregates: false,
188 max_params: 32_766,
189 isolation_levels: &[IsolationLevel::Serializable],
190 }
191 }
192
193 pub const fn mysql() -> Self {
203 Self {
204 kind: BackendKind::MySql,
205 transactions: TransactionSupport::Savepoints,
206 row_locking: RowLocking::Full,
207 joins: true,
208 returning: true,
209 window_functions: true,
210 regex: true,
211 arrays: false,
212 distinct_on: false,
213 ilike: false,
214 statistical_aggregates: true,
215 max_params: 65_535,
216 isolation_levels: &[
217 IsolationLevel::ReadCommitted,
218 IsolationLevel::RepeatableRead,
219 IsolationLevel::Serializable,
220 ],
221 }
222 }
223
224 pub const fn mongodb() -> Self {
232 Self {
233 kind: BackendKind::MongoDb,
234 transactions: TransactionSupport::Flat,
235 row_locking: RowLocking::None,
236 joins: false,
237 returning: true,
238 window_functions: false,
239 regex: true,
240 arrays: false,
241 distinct_on: false,
242 ilike: false,
243 statistical_aggregates: true,
244 max_params: 50_000,
245 isolation_levels: &[],
246 }
247 }
248
249 pub const fn redis() -> Self {
258 Self {
259 kind: BackendKind::Redis,
260 transactions: TransactionSupport::Flat,
261 row_locking: RowLocking::None,
262 joins: false,
263 returning: false,
264 window_functions: false,
265 regex: false,
266 arrays: false,
267 distinct_on: false,
268 ilike: false,
269 statistical_aggregates: false,
270 max_params: 0,
271 isolation_levels: &[],
272 }
273 }
274
275 pub fn supports(&self, feature: Feature) -> bool {
277 match feature {
278 Feature::Joins => self.joins,
279 Feature::Returning => self.returning,
280 Feature::RowLocking => self.row_locking >= RowLocking::Basic,
281 Feature::LockModifiers => self.row_locking >= RowLocking::Full,
282 Feature::WindowFunctions => self.window_functions,
283 Feature::Regex => self.regex,
284 Feature::Arrays => self.arrays,
285 Feature::StatisticalAggregates => self.statistical_aggregates,
286 Feature::DistinctOn => self.distinct_on,
287 Feature::CaseInsensitiveLike => {
291 self.ilike
292 || matches!(
293 self.kind,
294 BackendKind::Postgres
295 | BackendKind::Sqlite
296 | BackendKind::MySql
297 | BackendKind::MongoDb
298 )
299 }
300 Feature::Savepoints => self.transactions >= TransactionSupport::Savepoints,
301 Feature::Isolation(level) => self.isolation_levels.contains(&level),
302 Feature::Subqueries | Feature::SetOperations | Feature::RawSql => {
304 !matches!(self.kind, BackendKind::MongoDb | BackendKind::Redis)
305 }
306 Feature::KeysetPagination => self.kind != BackendKind::Redis,
307 Feature::NullableKeyset => {
310 !matches!(self.kind, BackendKind::MongoDb | BackendKind::Redis)
311 }
312 }
313 }
314
315 pub fn require(&self, feature: Feature) -> Result<(), BackendCapabilityError> {
317 if self.supports(feature) {
318 Ok(())
319 } else {
320 Err(BackendCapabilityError::from_feature(self.kind, feature))
321 }
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn sqlite_rejects_row_locking() {
331 let err = BackendCapabilities::sqlite().require(Feature::RowLocking);
332 assert!(matches!(
333 err,
334 Err(BackendCapabilityError::RowLockingUnsupported {
335 backend: BackendKind::Sqlite
336 })
337 ));
338 }
339
340 #[test]
341 fn postgres_supports_everything_modelled() {
342 let caps = BackendCapabilities::postgres();
343 for f in [
344 Feature::Joins,
345 Feature::Regex,
346 Feature::LockModifiers,
347 Feature::DistinctOn,
348 Feature::CaseInsensitiveLike,
349 ] {
350 assert!(caps.supports(f), "{f:?}");
351 }
352 assert!(BackendCapabilities::sqlite().supports(Feature::CaseInsensitiveLike));
353 }
354
355 #[test]
356 fn redis_rejects_relational_features_without_io() {
357 let caps = BackendCapabilities::redis();
358 assert_eq!(caps.kind, BackendKind::Redis);
359 assert_eq!(caps.transactions, TransactionSupport::Flat);
360 assert_eq!(caps.max_params, 0);
361 assert!(matches!(
362 caps.require(Feature::RowLocking),
363 Err(BackendCapabilityError::RowLockingUnsupported {
364 backend: BackendKind::Redis
365 })
366 ));
367 for feature in [
368 Feature::Joins,
369 Feature::Returning,
370 Feature::WindowFunctions,
371 Feature::Regex,
372 Feature::Arrays,
373 Feature::DistinctOn,
374 Feature::StatisticalAggregates,
375 Feature::Savepoints,
376 Feature::CaseInsensitiveLike,
377 Feature::Isolation(IsolationLevel::ReadCommitted),
378 ] {
379 assert!(
380 matches!(
381 caps.require(feature),
382 Err(BackendCapabilityError::Unsupported {
383 backend: BackendKind::Redis,
384 ..
385 })
386 ),
387 "{feature:?}"
388 );
389 }
390 }
391
392 #[test]
393 fn mongodb_declares_its_subset() {
394 let caps = BackendCapabilities::mongodb();
395 for f in [
396 Feature::Joins,
397 Feature::RowLocking,
398 Feature::WindowFunctions,
399 Feature::Arrays,
400 Feature::DistinctOn,
401 Feature::Savepoints,
402 Feature::Subqueries,
403 Feature::SetOperations,
404 Feature::RawSql,
405 Feature::Isolation(IsolationLevel::Serializable),
406 ] {
407 assert!(!caps.supports(f), "{f:?}");
408 }
409 for f in [
410 Feature::Returning,
411 Feature::Regex,
412 Feature::StatisticalAggregates,
413 Feature::CaseInsensitiveLike,
414 ] {
415 assert!(caps.supports(f), "{f:?}");
416 }
417 assert!(BackendCapabilities::sqlite().supports(Feature::Subqueries));
418 assert!(BackendCapabilities::postgres().supports(Feature::RawSql));
419 }
420
421 #[test]
422 fn keyset_pagination_is_listed_per_backend() {
423 for caps in [
424 BackendCapabilities::postgres(),
425 BackendCapabilities::sqlite(),
426 BackendCapabilities::mysql(),
427 ] {
428 assert!(caps.supports(Feature::KeysetPagination));
429 assert!(caps.supports(Feature::NullableKeyset));
430 }
431 let mongo = BackendCapabilities::mongodb();
432 assert!(mongo.supports(Feature::KeysetPagination));
433 assert!(!mongo.supports(Feature::NullableKeyset));
434 let redis = BackendCapabilities::redis();
435 assert!(!redis.supports(Feature::KeysetPagination));
436 assert!(!redis.supports(Feature::NullableKeyset));
437 }
438
439 #[test]
440 fn mysql_rejects_distinct_on_and_arrays() {
441 let caps = BackendCapabilities::mysql();
442 assert!(caps.require(Feature::DistinctOn).is_err());
443 assert!(caps.require(Feature::Arrays).is_err());
444 assert!(caps.require(Feature::LockModifiers).is_ok());
445 assert!(
446 caps.require(Feature::Isolation(IsolationLevel::Serializable))
447 .is_ok()
448 );
449 }
450}