Skip to main content

rust_ef/provider/
traits.rs

1//! Provider traits: `ISqlGenerator`, `IAsyncConnection`, `IDatabaseProvider`.
2
3use crate::error::EFResult;
4use async_trait::async_trait;
5
6use super::db_value::DbValue;
7
8/// Represents a SQL dialect with specific syntax for common operations.
9pub trait ISqlGenerator: Send + Sync {
10    /// Generates a SELECT statement.
11    fn select(&self, table: &str, columns: &[&str]) -> String;
12    /// Generates an INSERT statement.
13    fn insert(&self, table: &str, columns: &[&str], returning: bool) -> String;
14    /// Generates a multi-row INSERT statement with `row_count` value groups
15    /// (`INSERT INTO t (c1, c2) VALUES (?, ?), (?, ?), ...`). Placeholders
16    /// follow the dialect's numbering (`?` for SQLite/MySQL, `$n` for PG).
17    fn insert_batch(&self, table: &str, columns: &[&str], row_count: usize) -> String {
18        let _ = (table, columns, row_count);
19        String::new()
20    }
21    /// Generates an UPDATE statement.
22    fn update(&self, table: &str, set_columns: &[&str], where_clause: &str) -> String;
23    /// Generates a batch UPDATE using `CASE pk_col WHEN ? THEN ?` for
24    /// `row_count` rows, reducing N round trips to 1.
25    ///
26    /// The SET clause uses `2 * set_columns.len() * row_count` placeholders
27    /// (numbered from 1). The caller-built `where_clause` must number its
28    /// placeholders starting from `2 * set_columns.len() * row_count + 1`.
29    ///
30    /// Parameter layout (caller must arrange params in this order):
31    /// - For each set column, for each row: `[pk_value, col_value]`
32    /// - Then the `where_clause` params (PK IN-list + optional filter)
33    fn update_batch(
34        &self,
35        table: &str,
36        set_columns: &[&str],
37        pk_col: &str,
38        row_count: usize,
39        where_clause: &str,
40    ) -> String {
41        let mut idx = 1usize;
42        let mut sql = String::with_capacity(256 + set_columns.len() * row_count * 20);
43        sql.push_str("UPDATE ");
44        sql.push_str(&self.quote_identifier(table));
45        sql.push_str(" SET ");
46        let quoted_pk = self.quote_identifier(pk_col);
47        for (i, col) in set_columns.iter().enumerate() {
48            if i > 0 {
49                sql.push_str(", ");
50            }
51            sql.push_str(&self.quote_identifier(col));
52            sql.push_str(" = CASE ");
53            sql.push_str(&quoted_pk);
54            sql.push(' ');
55            for _ in 0..row_count {
56                let pk_ph = self.parameter_placeholder(idx);
57                idx += 1;
58                let val_ph = self.parameter_placeholder(idx);
59                idx += 1;
60                sql.push_str("WHEN ");
61                sql.push_str(&pk_ph);
62                sql.push_str(" THEN ");
63                sql.push_str(&val_ph);
64                sql.push(' ');
65            }
66            sql.push_str("END");
67        }
68        sql.push_str(" WHERE ");
69        sql.push_str(where_clause);
70        sql
71    }
72    /// Generates a DELETE statement.
73    fn delete(&self, table: &str, where_clause: &str) -> String;
74    /// Generates a CREATE TABLE statement.
75    fn create_table(&self, table: &str, columns: &[(String, String)]) -> String;
76    /// Generates a DROP TABLE statement.
77    fn drop_table(&self, table: &str) -> String;
78    /// Generates a pagination clause.
79    fn pagination(&self, skip: Option<usize>, take: Option<usize>) -> String;
80    /// Returns the parameter placeholder (e.g., `$1` for PG, `?` for MySQL).
81    fn parameter_placeholder(&self, index: usize) -> String;
82    /// Returns the identifier quoting character (e.g., `"` for PG, `` ` `` for MySQL).
83    fn quote_identifier(&self, identifier: &str) -> String;
84    /// Returns the dialect-specific auto-increment syntax.
85    fn auto_increment_syntax(&self) -> &'static str;
86
87    /// Whether the dialect uses numbered placeholders (e.g. PostgreSQL `$1`,
88    /// `$2`) where the index depends on position within the full statement.
89    ///
90    /// When `false` (SQLite/MySQL `?`), compiled filter SQL fragments are
91    /// index-independent and can be cached across batches. When `true`, each
92    /// batch must recompile the filter because placeholder numbering shifts.
93    fn uses_numbered_placeholders(&self) -> bool {
94        false
95    }
96
97    /// Whether `insert_batch` includes a `RETURNING *` clause (PostgreSQL).
98    /// When true, `execute_inserts` uses `query()` to read back generated PKs
99    /// directly from the INSERT result set.
100    fn supports_returning(&self) -> bool {
101        false
102    }
103
104    /// SQL that retrieves the auto-increment key generated by the most recent
105    /// batch INSERT. Returns `None` when the dialect uses `RETURNING` instead.
106    /// - SQLite: `SELECT last_insert_rowid()` (returns the LAST rowid)
107    /// - MySQL: `SELECT LAST_INSERT_ID()` (returns the FIRST generated ID)
108    fn last_insert_id_sql(&self) -> Option<&'static str> {
109        None
110    }
111
112    /// Whether `last_insert_id_sql()` returns the FIRST (MySQL) or LAST
113    /// (SQLite) generated ID in a batch INSERT. The executor uses this to
114    /// compute the full key sequence: `first_id..first_id+N` or
115    /// `last_id-N+1..last_id`.
116    fn last_insert_id_returns_first(&self) -> bool {
117        true
118    }
119
120    /// Generates a batch UPSERT statement (`row_count` value groups).
121    ///
122    /// - SQLite/PostgreSQL: `INSERT INTO t (cols) VALUES (...) ON CONFLICT(conflict_cols) DO UPDATE SET ...`
123    /// - MySQL: `INSERT INTO t (cols) VALUES (...) ON DUPLICATE KEY UPDATE ...`
124    ///
125    /// `columns` are the INSERT columns (excluding auto-increment).
126    /// `conflict_cols` are the PK (or unique constraint) column names used as
127    /// the conflict target. The UPDATE SET clause is generated for all
128    /// `columns` that are NOT in `conflict_cols`.
129    fn upsert_batch(
130        &self,
131        table: &str,
132        columns: &[&str],
133        conflict_cols: &[&str],
134        row_count: usize,
135    ) -> String {
136        let _ = (table, columns, conflict_cols, row_count);
137        String::new()
138    }
139}
140
141/// ANSI SQL transaction isolation levels.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum IsolationLevel {
144    ReadUncommitted,
145    ReadCommitted,
146    RepeatableRead,
147    Serializable,
148}
149
150/// Trait for async database connections.
151#[async_trait]
152pub trait IAsyncConnection: Send + Sync {
153    /// Executes a query with parameters and returns the number of affected rows.
154    async fn execute(&mut self, sql: &str, params: &[DbValue]) -> EFResult<u64>;
155    /// Executes a query with parameters and returns rows.
156    async fn query(&mut self, sql: &str, params: &[DbValue]) -> EFResult<Vec<Vec<DbValue>>>;
157    /// Begins a transaction.
158    async fn begin_transaction(&mut self) -> EFResult<()>;
159    /// Begins a transaction with the requested isolation level.
160    ///
161    /// The default implementation begins the transaction and then calls
162    /// [`set_transaction_isolation`](Self::set_transaction_isolation). Providers
163    /// whose database requires the isolation level *before* the transaction
164    /// starts (MySQL) override this method.
165    async fn begin_transaction_with(&mut self, level: IsolationLevel) -> EFResult<()> {
166        self.begin_transaction().await?;
167        if let Err(e) = self.set_transaction_isolation(level).await {
168            let _ = self.rollback_transaction().await;
169            return Err(e);
170        }
171        Ok(())
172    }
173    /// Commits the current transaction.
174    async fn commit_transaction(&mut self) -> EFResult<()>;
175    /// Rolls back the current transaction.
176    async fn rollback_transaction(&mut self) -> EFResult<()>;
177    /// Creates a savepoint within the current transaction.
178    async fn create_savepoint(&mut self, name: &str) -> EFResult<()>;
179    /// Releases (commits) a previously created savepoint, discarding its rollback point.
180    async fn release_savepoint(&mut self, name: &str) -> EFResult<()>;
181    /// Rolls back to the named savepoint, preserving the outer transaction.
182    async fn rollback_to_savepoint(&mut self, name: &str) -> EFResult<()>;
183    /// Sets the isolation level of the current transaction.
184    ///
185    /// Provider-dependent: PostgreSQL accepts this after `BEGIN` and before the
186    /// first query; MySQL requires the level at `START TRANSACTION` time and
187    /// rejects this call. Prefer [`begin_transaction_with`](Self::begin_transaction_with).
188    async fn set_transaction_isolation(&mut self, level: IsolationLevel) -> EFResult<()>;
189
190    /// Synchronously discards the underlying connection without returning it
191    /// to the pool.
192    ///
193    /// Used when a transaction is dropped unfinished or a commit/rollback
194    /// fails: closing the connection lets the database roll back any open
195    /// transaction, and prevents a poisoned connection from being reused.
196    fn discard(self: Box<Self>);
197
198    /// Sets the slow query threshold for this connection.
199    ///
200    /// Only available when the `tracing` feature is enabled on the core
201    /// crate. Default implementation is a no-op; provider connections
202    /// override to store the threshold for `QueryGuard` comparison.
203    #[cfg(feature = "tracing")]
204    fn set_slow_query_threshold(&mut self, _threshold: std::time::Duration) {}
205}
206
207/// The database provider abstraction.
208/// Corresponds to EFCore's provider model.
209#[async_trait]
210pub trait IDatabaseProvider: Send + Sync {
211    /// Returns the SQL dialect generator for this provider.
212    ///
213    /// Implementations are stateless, so a `&'static` reference is returned —
214    /// no heap allocation per call.
215    fn sql_generator(&self) -> &'static dyn ISqlGenerator;
216
217    /// Gets an async database connection from the pool.
218    async fn get_connection(&self) -> EFResult<Box<dyn IAsyncConnection>>;
219
220    /// Executes a migration command (DDL).
221    async fn execute_migration_command(&self, sql: &str) -> EFResult<()>;
222
223    /// Returns the provider name (e.g., "PostgreSQL", "MySQL").
224    fn name(&self) -> &str;
225
226    /// Returns the migration dialect for this provider.
227    fn migration_dialect(&self) -> crate::migration::MigrationDialect;
228
229    /// Sets the slow query threshold for all connections from this provider.
230    ///
231    /// Only available when the `tracing` feature is enabled on the core
232    /// crate. Default implementation is a no-op; providers override to
233    /// store the threshold and pass it to connections on acquisition.
234    #[cfg(feature = "tracing")]
235    fn set_slow_query_threshold(&self, _threshold: std::time::Duration) {}
236}