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}