rust-ef 1.6.0

Rust Entity Framework - An EFCore-inspired ORM for Rust
Documentation
//! Provider traits: `ISqlGenerator`, `IAsyncConnection`, `IDatabaseProvider`.

use crate::error::EFResult;
use async_trait::async_trait;

use super::db_value::DbValue;

/// Represents a SQL dialect with specific syntax for common operations.
pub trait ISqlGenerator: Send + Sync {
    /// Generates a SELECT statement.
    fn select(&self, table: &str, columns: &[&str]) -> String;
    /// Generates an INSERT statement.
    fn insert(&self, table: &str, columns: &[&str], returning: bool) -> String;
    /// Generates a multi-row INSERT statement with `row_count` value groups
    /// (`INSERT INTO t (c1, c2) VALUES (?, ?), (?, ?), ...`). Placeholders
    /// follow the dialect's numbering (`?` for SQLite/MySQL, `$n` for PG).
    fn insert_batch(&self, table: &str, columns: &[&str], row_count: usize) -> String {
        let _ = (table, columns, row_count);
        String::new()
    }
    /// Generates an UPDATE statement.
    fn update(&self, table: &str, set_columns: &[&str], where_clause: &str) -> String;
    /// Generates a batch UPDATE using `CASE pk_col WHEN ? THEN ?` for
    /// `row_count` rows, reducing N round trips to 1.
    ///
    /// The SET clause uses `2 * set_columns.len() * row_count` placeholders
    /// (numbered from 1). The caller-built `where_clause` must number its
    /// placeholders starting from `2 * set_columns.len() * row_count + 1`.
    ///
    /// Parameter layout (caller must arrange params in this order):
    /// - For each set column, for each row: `[pk_value, col_value]`
    /// - Then the `where_clause` params (PK IN-list + optional filter)
    fn update_batch(
        &self,
        table: &str,
        set_columns: &[&str],
        pk_col: &str,
        row_count: usize,
        where_clause: &str,
    ) -> String {
        let mut idx = 1usize;
        let sets: Vec<String> = set_columns
            .iter()
            .map(|col| {
                let whens: Vec<String> = (0..row_count)
                    .map(|_| {
                        let pk_ph = self.parameter_placeholder(idx);
                        idx += 1;
                        let val_ph = self.parameter_placeholder(idx);
                        idx += 1;
                        format!("WHEN {} THEN {}", pk_ph, val_ph)
                    })
                    .collect();
                format!(
                    "{} = CASE {} {} END",
                    self.quote_identifier(col),
                    self.quote_identifier(pk_col),
                    whens.join(" ")
                )
            })
            .collect();
        format!(
            "UPDATE {} SET {} WHERE {}",
            self.quote_identifier(table),
            sets.join(", "),
            where_clause
        )
    }
    /// Generates a DELETE statement.
    fn delete(&self, table: &str, where_clause: &str) -> String;
    /// Generates a CREATE TABLE statement.
    fn create_table(&self, table: &str, columns: &[(String, String)]) -> String;
    /// Generates a DROP TABLE statement.
    fn drop_table(&self, table: &str) -> String;
    /// Generates a pagination clause.
    fn pagination(&self, skip: Option<usize>, take: Option<usize>) -> String;
    /// Returns the parameter placeholder (e.g., `$1` for PG, `?` for MySQL).
    fn parameter_placeholder(&self, index: usize) -> String;
    /// Returns the identifier quoting character (e.g., `"` for PG, `` ` `` for MySQL).
    fn quote_identifier(&self, identifier: &str) -> String;
    /// Returns the dialect-specific auto-increment syntax.
    fn auto_increment_syntax(&self) -> &'static str;

    /// Whether `insert_batch` includes a `RETURNING *` clause (PostgreSQL).
    /// When true, `execute_inserts` uses `query()` to read back generated PKs
    /// directly from the INSERT result set.
    fn supports_returning(&self) -> bool {
        false
    }

    /// SQL that retrieves the auto-increment key generated by the most recent
    /// batch INSERT. Returns `None` when the dialect uses `RETURNING` instead.
    /// - SQLite: `SELECT last_insert_rowid()` (returns the LAST rowid)
    /// - MySQL: `SELECT LAST_INSERT_ID()` (returns the FIRST generated ID)
    fn last_insert_id_sql(&self) -> Option<&'static str> {
        None
    }

    /// Whether `last_insert_id_sql()` returns the FIRST (MySQL) or LAST
    /// (SQLite) generated ID in a batch INSERT. The executor uses this to
    /// compute the full key sequence: `first_id..first_id+N` or
    /// `last_id-N+1..last_id`.
    fn last_insert_id_returns_first(&self) -> bool {
        true
    }

    /// Generates a batch UPSERT statement (`row_count` value groups).
    ///
    /// - SQLite/PostgreSQL: `INSERT INTO t (cols) VALUES (...) ON CONFLICT(conflict_cols) DO UPDATE SET ...`
    /// - MySQL: `INSERT INTO t (cols) VALUES (...) ON DUPLICATE KEY UPDATE ...`
    ///
    /// `columns` are the INSERT columns (excluding auto-increment).
    /// `conflict_cols` are the PK (or unique constraint) column names used as
    /// the conflict target. The UPDATE SET clause is generated for all
    /// `columns` that are NOT in `conflict_cols`.
    fn upsert_batch(
        &self,
        table: &str,
        columns: &[&str],
        conflict_cols: &[&str],
        row_count: usize,
    ) -> String {
        let _ = (table, columns, conflict_cols, row_count);
        String::new()
    }
}

/// ANSI SQL transaction isolation levels.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsolationLevel {
    ReadUncommitted,
    ReadCommitted,
    RepeatableRead,
    Serializable,
}

/// Trait for async database connections.
#[async_trait]
pub trait IAsyncConnection: Send + Sync {
    /// Executes a query with parameters and returns the number of affected rows.
    async fn execute(&mut self, sql: &str, params: &[DbValue]) -> EFResult<u64>;
    /// Executes a query with parameters and returns rows.
    async fn query(&mut self, sql: &str, params: &[DbValue]) -> EFResult<Vec<Vec<DbValue>>>;
    /// Begins a transaction.
    async fn begin_transaction(&mut self) -> EFResult<()>;
    /// Commits the current transaction.
    async fn commit_transaction(&mut self) -> EFResult<()>;
    /// Rolls back the current transaction.
    async fn rollback_transaction(&mut self) -> EFResult<()>;
    /// Creates a savepoint within the current transaction.
    async fn create_savepoint(&mut self, name: &str) -> EFResult<()>;
    /// Releases (commits) a previously created savepoint, discarding its rollback point.
    async fn release_savepoint(&mut self, name: &str) -> EFResult<()>;
    /// Rolls back to the named savepoint, preserving the outer transaction.
    async fn rollback_to_savepoint(&mut self, name: &str) -> EFResult<()>;
    /// Sets the isolation level of the current transaction.
    /// Must be called after `begin_transaction` and before any query.
    async fn set_transaction_isolation(&mut self, level: IsolationLevel) -> EFResult<()>;

    /// Sets the slow query threshold for this connection.
    ///
    /// Only available when the `tracing` feature is enabled on the core
    /// crate. Default implementation is a no-op; provider connections
    /// override to store the threshold for `QueryGuard` comparison.
    #[cfg(feature = "tracing")]
    fn set_slow_query_threshold(&mut self, _threshold: std::time::Duration) {}
}

/// The database provider abstraction.
/// Corresponds to EFCore's provider model.
#[async_trait]
pub trait IDatabaseProvider: Send + Sync {
    /// Returns the SQL dialect generator for this provider.
    ///
    /// Implementations are stateless, so a `&'static` reference is returned —
    /// no heap allocation per call.
    fn sql_generator(&self) -> &'static dyn ISqlGenerator;

    /// Gets an async database connection from the pool.
    async fn get_connection(&self) -> EFResult<Box<dyn IAsyncConnection>>;

    /// Executes a migration command (DDL).
    async fn execute_migration_command(&self, sql: &str) -> EFResult<()>;

    /// Returns the provider name (e.g., "PostgreSQL", "MySQL").
    fn name(&self) -> &str;

    /// Returns the migration dialect for this provider.
    fn migration_dialect(&self) -> crate::migration::MigrationDialect;

    /// Sets the slow query threshold for all connections from this provider.
    ///
    /// Only available when the `tracing` feature is enabled on the core
    /// crate. Default implementation is a no-op; providers override to
    /// store the threshold and pass it to connections on acquisition.
    #[cfg(feature = "tracing")]
    fn set_slow_query_threshold(&self, _threshold: std::time::Duration) {}
}