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    /// Commits the current transaction.
160    async fn commit_transaction(&mut self) -> EFResult<()>;
161    /// Rolls back the current transaction.
162    async fn rollback_transaction(&mut self) -> EFResult<()>;
163    /// Creates a savepoint within the current transaction.
164    async fn create_savepoint(&mut self, name: &str) -> EFResult<()>;
165    /// Releases (commits) a previously created savepoint, discarding its rollback point.
166    async fn release_savepoint(&mut self, name: &str) -> EFResult<()>;
167    /// Rolls back to the named savepoint, preserving the outer transaction.
168    async fn rollback_to_savepoint(&mut self, name: &str) -> EFResult<()>;
169    /// Sets the isolation level of the current transaction.
170    /// Must be called after `begin_transaction` and before any query.
171    async fn set_transaction_isolation(&mut self, level: IsolationLevel) -> EFResult<()>;
172
173    /// Sets the slow query threshold for this connection.
174    ///
175    /// Only available when the `tracing` feature is enabled on the core
176    /// crate. Default implementation is a no-op; provider connections
177    /// override to store the threshold for `QueryGuard` comparison.
178    #[cfg(feature = "tracing")]
179    fn set_slow_query_threshold(&mut self, _threshold: std::time::Duration) {}
180}
181
182/// The database provider abstraction.
183/// Corresponds to EFCore's provider model.
184#[async_trait]
185pub trait IDatabaseProvider: Send + Sync {
186    /// Returns the SQL dialect generator for this provider.
187    ///
188    /// Implementations are stateless, so a `&'static` reference is returned —
189    /// no heap allocation per call.
190    fn sql_generator(&self) -> &'static dyn ISqlGenerator;
191
192    /// Gets an async database connection from the pool.
193    async fn get_connection(&self) -> EFResult<Box<dyn IAsyncConnection>>;
194
195    /// Executes a migration command (DDL).
196    async fn execute_migration_command(&self, sql: &str) -> EFResult<()>;
197
198    /// Returns the provider name (e.g., "PostgreSQL", "MySQL").
199    fn name(&self) -> &str;
200
201    /// Returns the migration dialect for this provider.
202    fn migration_dialect(&self) -> crate::migration::MigrationDialect;
203
204    /// Sets the slow query threshold for all connections from this provider.
205    ///
206    /// Only available when the `tracing` feature is enabled on the core
207    /// crate. Default implementation is a no-op; providers override to
208    /// store the threshold and pass it to connections on acquisition.
209    #[cfg(feature = "tracing")]
210    fn set_slow_query_threshold(&self, _threshold: std::time::Duration) {}
211}