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("ed_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}