rusticx-core 1.0.0

Core traits, types, and abstractions for the Rusticx multi-database ORM
Documentation
use crate::{
    error::Result,
    model::TableSchema,
    query::QueryBuilder,
    value::{Row, Value},
};
use async_trait::async_trait;

/// Async database adapter — the interface every backend must implement.
///
/// All methods are `async`. Use [`SyncAdapter`] to run them in a blocking
/// context without managing a Tokio runtime yourself.
///
/// You typically interact with adapters indirectly through [`Repository`],
/// but you can call adapter methods directly for raw queries or schema ops
/// that the repository doesn't expose.
///
/// [`Repository`]: crate::repository::Repository
#[async_trait]
pub trait DatabaseAdapter: Send + Sync + 'static {
    /// Human-readable backend name, e.g. `"postgres"`, `"mongo"`.
    fn name(&self) -> &'static str;

    /// Ping the server — cheapest possible health check.
    async fn ping(&self) -> Result<()>;

    /// Close all connections and release pool resources.
    async fn close(&self) -> Result<()>;

    // ── Schema ───────────────────────────────────────────────────────────

    /// Create the table / collection described by `schema` if it does not exist.
    ///
    /// For SQL backends this emits `CREATE TABLE IF NOT EXISTS` plus any
    /// index statements. For MongoDB it calls `createCollection` then
    /// creates declared indexes.
    async fn create_table(&self, schema: &TableSchema) -> Result<()>;

    /// Drop the table / collection. **Irreversible.**
    async fn drop_table(&self, table: &str) -> Result<()>;

    /// Return `true` if the table / collection exists.
    async fn table_exists(&self, table: &str) -> Result<bool>;

    // ── CRUD ─────────────────────────────────────────────────────────────

    /// Insert one row and return it with any database-generated fields populated
    /// (e.g. auto-increment IDs, `DEFAULT` expressions).
    ///
    /// Postgres uses `RETURNING *`. MySQL re-fetches via `LAST_INSERT_ID()`.
    async fn insert(&self, table: &str, row: Row) -> Result<Row>;

    /// Insert multiple rows in a single transaction. Returns the count inserted.
    async fn insert_many(&self, table: &str, rows: Vec<Row>) -> Result<u64>;

    /// Execute the SELECT described by `query` and return all matching rows.
    async fn find(&self, query: &QueryBuilder) -> Result<Vec<Row>>;

    /// Execute the SELECT described by `query` and return the first row if any.
    async fn find_one(&self, query: &QueryBuilder) -> Result<Option<Row>>;

    /// Execute the UPDATE described by `query`. Returns the number of rows affected.
    async fn update(&self, query: &QueryBuilder) -> Result<u64>;

    /// Execute the DELETE described by `query`. Returns the number of rows deleted.
    async fn delete(&self, query: &QueryBuilder) -> Result<u64>;

    /// Count rows matching `query`.
    async fn count(&self, query: &QueryBuilder) -> Result<u64>;

    // ── Raw ──────────────────────────────────────────────────────────────

    /// Execute a raw SQL string (or JSON command for MongoDB) with positional
    /// parameter bindings. Returns the number of rows affected.
    ///
    /// Use this when the query builder cannot express what you need.
    async fn execute_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<u64>;

    /// Execute a raw SQL string with positional bindings and return the result rows.
    async fn query_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<Vec<Row>>;
}

/// Blocking wrapper around any [`DatabaseAdapter`].
///
/// `SyncAdapter` owns a dedicated multi-threaded Tokio runtime and exposes
/// every async adapter method as a synchronous blocking call. This lets you
/// use Rusticx in non-async code (CLI tools, scripts, test harnesses) without
/// managing a runtime yourself.
///
/// # Example
///
/// ```rust,ignore
/// use rusticx::prelude::*;
///
/// let rt = tokio::runtime::Runtime::new().unwrap();
/// let adapter = rt.block_on(PostgresAdapter::connect_url("postgres://localhost/mydb"))?;
/// let sync = SyncAdapter::new(adapter)?;
///
/// let schema = TableSchema::from_model::<User>();
/// sync.create_table(&schema)?;
///
/// let rows = sync.find(&QueryBuilder::table("users"))?;
/// ```
pub struct SyncAdapter<A: DatabaseAdapter> {
    inner: A,
    rt: tokio::runtime::Runtime,
}

impl<A: DatabaseAdapter> SyncAdapter<A> {
    pub fn new(adapter: A) -> Result<Self> {
        let rt = tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .build()
            .map_err(|e: std::io::Error| crate::error::RusticxError::Unknown(e.to_string()))?;
        Ok(Self { inner: adapter, rt })
    }

    pub fn ping(&self) -> Result<()> {
        self.rt.block_on(self.inner.ping())
    }

    pub fn create_table(&self, schema: &TableSchema) -> Result<()> {
        self.rt.block_on(self.inner.create_table(schema))
    }

    pub fn insert(&self, table: &str, row: Row) -> Result<Row> {
        self.rt.block_on(self.inner.insert(table, row))
    }

    pub fn insert_many(&self, table: &str, rows: Vec<Row>) -> Result<u64> {
        self.rt.block_on(self.inner.insert_many(table, rows))
    }

    pub fn find(&self, query: &QueryBuilder) -> Result<Vec<Row>> {
        self.rt.block_on(self.inner.find(query))
    }

    pub fn find_one(&self, query: &QueryBuilder) -> Result<Option<Row>> {
        self.rt.block_on(self.inner.find_one(query))
    }

    pub fn update(&self, query: &QueryBuilder) -> Result<u64> {
        self.rt.block_on(self.inner.update(query))
    }

    pub fn delete(&self, query: &QueryBuilder) -> Result<u64> {
        self.rt.block_on(self.inner.delete(query))
    }

    pub fn count(&self, query: &QueryBuilder) -> Result<u64> {
        self.rt.block_on(self.inner.count(query))
    }

    pub fn execute_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<u64> {
        self.rt.block_on(self.inner.execute_raw(sql, bindings))
    }

    pub fn query_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<Vec<Row>> {
        self.rt.block_on(self.inner.query_raw(sql, bindings))
    }
}