Skip to main content

rusticx_core/
adapter.rs

1use crate::{
2    error::Result,
3    model::TableSchema,
4    query::QueryBuilder,
5    value::{Row, Value},
6};
7use async_trait::async_trait;
8
9/// Async database adapter — the interface every backend must implement.
10///
11/// All methods are `async`. Use [`SyncAdapter`] to run them in a blocking
12/// context without managing a Tokio runtime yourself.
13///
14/// You typically interact with adapters indirectly through [`Repository`],
15/// but you can call adapter methods directly for raw queries or schema ops
16/// that the repository doesn't expose.
17///
18/// [`Repository`]: crate::repository::Repository
19#[async_trait]
20pub trait DatabaseAdapter: Send + Sync + 'static {
21    /// Human-readable backend name, e.g. `"postgres"`, `"mongo"`.
22    fn name(&self) -> &'static str;
23
24    /// Ping the server — cheapest possible health check.
25    async fn ping(&self) -> Result<()>;
26
27    /// Close all connections and release pool resources.
28    async fn close(&self) -> Result<()>;
29
30    // ── Schema ───────────────────────────────────────────────────────────
31
32    /// Create the table / collection described by `schema` if it does not exist.
33    ///
34    /// For SQL backends this emits `CREATE TABLE IF NOT EXISTS` plus any
35    /// index statements. For MongoDB it calls `createCollection` then
36    /// creates declared indexes.
37    async fn create_table(&self, schema: &TableSchema) -> Result<()>;
38
39    /// Drop the table / collection. **Irreversible.**
40    async fn drop_table(&self, table: &str) -> Result<()>;
41
42    /// Return `true` if the table / collection exists.
43    async fn table_exists(&self, table: &str) -> Result<bool>;
44
45    // ── CRUD ─────────────────────────────────────────────────────────────
46
47    /// Insert one row and return it with any database-generated fields populated
48    /// (e.g. auto-increment IDs, `DEFAULT` expressions).
49    ///
50    /// Postgres uses `RETURNING *`. MySQL re-fetches via `LAST_INSERT_ID()`.
51    async fn insert(&self, table: &str, row: Row) -> Result<Row>;
52
53    /// Insert multiple rows in a single transaction. Returns the count inserted.
54    async fn insert_many(&self, table: &str, rows: Vec<Row>) -> Result<u64>;
55
56    /// Execute the SELECT described by `query` and return all matching rows.
57    async fn find(&self, query: &QueryBuilder) -> Result<Vec<Row>>;
58
59    /// Execute the SELECT described by `query` and return the first row if any.
60    async fn find_one(&self, query: &QueryBuilder) -> Result<Option<Row>>;
61
62    /// Execute the UPDATE described by `query`. Returns the number of rows affected.
63    async fn update(&self, query: &QueryBuilder) -> Result<u64>;
64
65    /// Execute the DELETE described by `query`. Returns the number of rows deleted.
66    async fn delete(&self, query: &QueryBuilder) -> Result<u64>;
67
68    /// Count rows matching `query`.
69    async fn count(&self, query: &QueryBuilder) -> Result<u64>;
70
71    // ── Raw ──────────────────────────────────────────────────────────────
72
73    /// Execute a raw SQL string (or JSON command for MongoDB) with positional
74    /// parameter bindings. Returns the number of rows affected.
75    ///
76    /// Use this when the query builder cannot express what you need.
77    async fn execute_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<u64>;
78
79    /// Execute a raw SQL string with positional bindings and return the result rows.
80    async fn query_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<Vec<Row>>;
81}
82
83/// Blocking wrapper around any [`DatabaseAdapter`].
84///
85/// `SyncAdapter` owns a dedicated multi-threaded Tokio runtime and exposes
86/// every async adapter method as a synchronous blocking call. This lets you
87/// use Rusticx in non-async code (CLI tools, scripts, test harnesses) without
88/// managing a runtime yourself.
89///
90/// # Example
91///
92/// ```rust,ignore
93/// use rusticx::prelude::*;
94///
95/// let rt = tokio::runtime::Runtime::new().unwrap();
96/// let adapter = rt.block_on(PostgresAdapter::connect_url("postgres://localhost/mydb"))?;
97/// let sync = SyncAdapter::new(adapter)?;
98///
99/// let schema = TableSchema::from_model::<User>();
100/// sync.create_table(&schema)?;
101///
102/// let rows = sync.find(&QueryBuilder::table("users"))?;
103/// ```
104pub struct SyncAdapter<A: DatabaseAdapter> {
105    inner: A,
106    rt: tokio::runtime::Runtime,
107}
108
109impl<A: DatabaseAdapter> SyncAdapter<A> {
110    pub fn new(adapter: A) -> Result<Self> {
111        let rt = tokio::runtime::Builder::new_multi_thread()
112            .enable_all()
113            .build()
114            .map_err(|e: std::io::Error| crate::error::RusticxError::Unknown(e.to_string()))?;
115        Ok(Self { inner: adapter, rt })
116    }
117
118    pub fn ping(&self) -> Result<()> {
119        self.rt.block_on(self.inner.ping())
120    }
121
122    pub fn create_table(&self, schema: &TableSchema) -> Result<()> {
123        self.rt.block_on(self.inner.create_table(schema))
124    }
125
126    pub fn insert(&self, table: &str, row: Row) -> Result<Row> {
127        self.rt.block_on(self.inner.insert(table, row))
128    }
129
130    pub fn insert_many(&self, table: &str, rows: Vec<Row>) -> Result<u64> {
131        self.rt.block_on(self.inner.insert_many(table, rows))
132    }
133
134    pub fn find(&self, query: &QueryBuilder) -> Result<Vec<Row>> {
135        self.rt.block_on(self.inner.find(query))
136    }
137
138    pub fn find_one(&self, query: &QueryBuilder) -> Result<Option<Row>> {
139        self.rt.block_on(self.inner.find_one(query))
140    }
141
142    pub fn update(&self, query: &QueryBuilder) -> Result<u64> {
143        self.rt.block_on(self.inner.update(query))
144    }
145
146    pub fn delete(&self, query: &QueryBuilder) -> Result<u64> {
147        self.rt.block_on(self.inner.delete(query))
148    }
149
150    pub fn count(&self, query: &QueryBuilder) -> Result<u64> {
151        self.rt.block_on(self.inner.count(query))
152    }
153
154    pub fn execute_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<u64> {
155        self.rt.block_on(self.inner.execute_raw(sql, bindings))
156    }
157
158    pub fn query_raw(&self, sql: &str, bindings: Vec<Value>) -> Result<Vec<Row>> {
159        self.rt.block_on(self.inner.query_raw(sql, bindings))
160    }
161}