Skip to main content

prax_query/
capabilities.rs

1//! Engine capability marker traits.
2//!
3//! Each trait in this module marks a capability that some `QueryEngine`
4//! impls satisfy and others don't. The macro DSL (phase 3+) and the
5//! generated input types (phase 2) carry `where E: SupportsX` bounds on
6//! the methods that produce capability-dependent SQL. Using such a
7//! method against an engine that doesn't impl the trait fails to compile
8//! with a clear diagnostic.
9//!
10//! Engine crates (`prax-postgres`, `prax-mysql`, ...) impl the traits
11//! they satisfy on their concrete engine types. Phase 1 only defines
12//! the traits; engine impls land in phase 2.
13
14use crate::traits::QueryEngine;
15
16/// Engine supports relation filters (`some`/`every`/`none`/`is`/`is_not`)
17/// that lower to correlated EXISTS / NOT EXISTS subqueries (or the
18/// equivalent in non-SQL engines).
19#[diagnostic::on_unimplemented(
20    message = "the engine `{Self}` does not support relation filters (`some` / `every` / `none` / `is` / `is_not`)",
21    note = "ScyllaDB / Cassandra do not support correlated subqueries. Consider flattening the join or restructuring the model."
22)]
23pub trait SupportsRelationFilter: QueryEngine {}
24
25/// Engine supports correlated subqueries in WHERE clauses.
26///
27/// Superset of `SupportsRelationFilter` — used by features that need
28/// arbitrary subqueries (e.g. computed-field WHERE lowering in phase 5.5).
29#[diagnostic::on_unimplemented(
30    message = "the engine `{Self}` does not support correlated subqueries in WHERE clauses"
31)]
32pub trait SupportsCorrelatedSubquery: QueryEngine {}
33
34/// Engine supports JSON-path filter operators (`path_eq`, `path_gt`, etc.).
35#[diagnostic::on_unimplemented(
36    message = "the engine `{Self}` does not support JSON path operators",
37    note = "Postgres / MySQL >= 5.7 / SQLite + JSON1 / MSSQL support JSON paths."
38)]
39pub trait SupportsJsonPath: QueryEngine {}
40
41/// Engine has native case-insensitive comparison (`ILIKE`, `COLLATE NOCASE`,
42/// equivalent). Engines without it fall back to `LOWER(...)` comparisons and
43/// **do not** need to impl this trait.
44#[diagnostic::on_unimplemented(
45    message = "the engine `{Self}` does not advertise native case-insensitive comparison"
46)]
47pub trait SupportsCaseInsensitiveMode: QueryEngine {}
48
49/// Engine supports full-text search predicates.
50#[diagnostic::on_unimplemented(message = "the engine `{Self}` does not support full-text search")]
51pub trait SupportsFullTextSearch: QueryEngine {}
52
53/// Engine supports native array column operators (`contains`, `overlaps`, ...).
54#[diagnostic::on_unimplemented(message = "the engine `{Self}` does not support array operators")]
55pub trait SupportsArrayOps: QueryEngine {}
56
57/// Engine supports DDL for `GENERATED ALWAYS AS (expr) STORED|VIRTUAL`
58/// computed columns.
59#[diagnostic::on_unimplemented(
60    message = "the engine `{Self}` does not support generated columns",
61    note = "Postgres / MySQL / SQLite / MSSQL / DuckDB support GENERATED ALWAYS AS."
62)]
63pub trait SupportsGeneratedColumns: QueryEngine {}
64
65/// Engine supports scalar subqueries in the SELECT list.
66///
67/// Required for relation-aggregate virtual fields (`@count`, `@sum`,
68/// `@avg`, `@min`, `@max`) and Prisma-style `_count`.
69#[diagnostic::on_unimplemented(
70    message = "the engine `{Self}` does not support scalar subqueries in SELECT",
71    note = "All SQL engines satisfy this. MongoDB requires the $lookup-lowering follow-up plan."
72)]
73pub trait SupportsScalarSubqueryInSelect: QueryEngine {}
74
75/// Engine supports Prisma-style nested writes
76/// (`create` / `connect` / `connect_or_create` / `disconnect` / `set`
77/// / `update` / `upsert` / `delete` / `delete_many` inside `data`).
78///
79/// CQL engines (`prax-scylladb`, `prax-cassandra`) deliberately do not
80/// impl this trait — phase 5's `*CreateNestedInput` / `*UpdateNestedInput`
81/// types carry `where E: SupportsNestedWrites` bounds so misuse fails
82/// to compile.
83#[diagnostic::on_unimplemented(
84    message = "the engine `{Self}` does not support nested writes",
85    note = "ScyllaDB / Cassandra batch semantics don't map onto Prisma-style nested writes. Use the engine-native BATCH API or restructure."
86)]
87pub trait SupportsNestedWrites: QueryEngine {}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    // A tiny stub engine for trait-impl smoke tests.
94    #[derive(Clone)]
95    struct StubEngine;
96
97    impl QueryEngine for StubEngine {
98        fn query_many<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
99            &self,
100            _sql: &str,
101            _params: Vec<crate::filter::FilterValue>,
102        ) -> crate::traits::BoxFuture<'_, crate::error::QueryResult<Vec<T>>> {
103            Box::pin(async { Ok(Vec::new()) })
104        }
105        fn query_one<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
106            &self,
107            _sql: &str,
108            _params: Vec<crate::filter::FilterValue>,
109        ) -> crate::traits::BoxFuture<'_, crate::error::QueryResult<T>> {
110            Box::pin(async { Err(crate::error::QueryError::not_found("t")) })
111        }
112        fn query_optional<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
113            &self,
114            _sql: &str,
115            _params: Vec<crate::filter::FilterValue>,
116        ) -> crate::traits::BoxFuture<'_, crate::error::QueryResult<Option<T>>> {
117            Box::pin(async { Ok(None) })
118        }
119        fn execute_insert<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
120            &self,
121            _sql: &str,
122            _params: Vec<crate::filter::FilterValue>,
123        ) -> crate::traits::BoxFuture<'_, crate::error::QueryResult<T>> {
124            Box::pin(async { Err(crate::error::QueryError::not_found("t")) })
125        }
126        fn execute_update<T: crate::traits::Model + crate::row::FromRow + Send + 'static>(
127            &self,
128            _sql: &str,
129            _params: Vec<crate::filter::FilterValue>,
130        ) -> crate::traits::BoxFuture<'_, crate::error::QueryResult<Vec<T>>> {
131            Box::pin(async { Ok(Vec::new()) })
132        }
133        fn execute_delete(
134            &self,
135            _sql: &str,
136            _params: Vec<crate::filter::FilterValue>,
137        ) -> crate::traits::BoxFuture<'_, crate::error::QueryResult<u64>> {
138            Box::pin(async { Ok(0) })
139        }
140        fn execute_raw(
141            &self,
142            _sql: &str,
143            _params: Vec<crate::filter::FilterValue>,
144        ) -> crate::traits::BoxFuture<'_, crate::error::QueryResult<u64>> {
145            Box::pin(async { Ok(0) })
146        }
147        fn count(
148            &self,
149            _sql: &str,
150            _params: Vec<crate::filter::FilterValue>,
151        ) -> crate::traits::BoxFuture<'_, crate::error::QueryResult<u64>> {
152            Box::pin(async { Ok(0) })
153        }
154    }
155
156    impl SupportsRelationFilter for StubEngine {}
157
158    fn needs_relation_filter<E: SupportsRelationFilter>() {}
159
160    #[test]
161    fn marker_trait_dispatch_compiles() {
162        needs_relation_filter::<StubEngine>();
163    }
164}