dbrest_core/backend/mod.rs
1//! Database backend abstraction layer.
2//!
3//! This module defines the core traits that decouple dbrest from any
4//! specific database engine. A concrete backend (e.g. PostgreSQL, MySQL,
5//! SQLite) implements these traits to plug into the rest of the system.
6//!
7//! # Architecture
8//!
9//! ```text
10//! +-----------------------+
11//! | dbrest-core |
12//! | (backend traits) |
13//! +-----------+-----------+
14//! |
15//! +--------+--------+
16//! | |
17//! +--v--+ +---v---+
18//! | PG | | MySQL | (future)
19//! +-----+ +-------+
20//! ```
21
22use std::fmt;
23
24use async_trait::async_trait;
25
26use crate::error::Error;
27use crate::query::sql_builder::{SqlBuilder, SqlParam};
28use crate::schema_cache::db::DbIntrospector;
29
30// ==========================================================================
31// DbVersion — database-agnostic version info
32// ==========================================================================
33
34/// Database server version information.
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct DbVersion {
37 pub major: u32,
38 pub minor: u32,
39 pub patch: u32,
40 /// Human-readable name of the database engine (e.g. "PostgreSQL", "MySQL").
41 pub engine: String,
42}
43
44impl fmt::Display for DbVersion {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(
47 f,
48 "{} {}.{}.{}",
49 self.engine, self.major, self.minor, self.patch
50 )
51 }
52}
53
54// ==========================================================================
55// StatementResult — database-agnostic query result
56// ==========================================================================
57
58/// Parsed result from a CTE-wrapped statement.
59///
60/// This is the uniform shape that every backend must produce from the
61/// main query execution. The handler layer reads these fields to build
62/// the HTTP response.
63#[derive(Debug, Clone)]
64pub struct StatementResult {
65 pub total: Option<i64>,
66 pub page_total: i64,
67 pub body: String,
68 pub response_headers: Option<serde_json::Value>,
69 pub response_status: Option<i32>,
70}
71
72impl StatementResult {
73 pub fn empty() -> Self {
74 Self {
75 total: None,
76 page_total: 0,
77 body: "[]".to_string(),
78 response_headers: None,
79 response_status: None,
80 }
81 }
82}
83
84// ==========================================================================
85// PoolStatus — connection pool metrics
86// ==========================================================================
87
88/// Connection pool status for metrics reporting.
89#[derive(Debug, Clone)]
90pub struct PoolStatus {
91 /// Number of connections currently in use.
92 pub active: u32,
93 /// Number of idle connections in the pool.
94 pub idle: u32,
95 /// Maximum pool size.
96 pub max_size: u32,
97}
98
99// ==========================================================================
100// DatabaseBackend — the main abstraction trait
101// ==========================================================================
102
103/// Core trait that a database backend must implement.
104///
105/// Covers connection management, query execution, introspection,
106/// version detection, error mapping, and change notification.
107#[async_trait]
108pub trait DatabaseBackend: Send + Sync + 'static {
109 /// Connect to the database and return a backend instance.
110 ///
111 /// The implementation should create a connection pool internally.
112 async fn connect(
113 uri: &str,
114 pool_size: u32,
115 acquire_timeout_secs: u64,
116 max_lifetime_secs: u64,
117 idle_timeout_secs: u64,
118 ) -> Result<Self, Error>
119 where
120 Self: Sized;
121
122 /// Query the database server version.
123 async fn version(&self) -> Result<DbVersion, Error>;
124
125 /// Return the minimum supported version for this backend.
126 fn min_version(&self) -> (u32, u32);
127
128 /// Execute a raw SQL statement (no result set expected).
129 ///
130 /// Used for session variable setup and pre-request function calls.
131 async fn exec_raw(&self, sql: &str, params: &[SqlParam]) -> Result<(), Error>;
132
133 /// Execute a CTE-wrapped statement and parse the standard result set.
134 ///
135 /// The query is expected to return columns:
136 /// `total_result_set`, `page_total`, `body`, `response_headers`, `response_status`
137 async fn exec_statement(
138 &self,
139 sql: &str,
140 params: &[SqlParam],
141 ) -> Result<StatementResult, Error>;
142
143 /// Begin a transaction and execute multiple statements within it.
144 ///
145 /// Runs tx_vars, pre_req, mutation, and main query in order within a single
146 /// transaction. Returns the result from the main query.
147 ///
148 /// `mutation` is only set for backends that don't support DML in CTEs.
149 /// When set, the executor must run the mutation, capture RETURNING rows
150 /// into a temp table `_dbrst_mut`, then run `main` which aggregates from it.
151 async fn exec_in_transaction(
152 &self,
153 tx_vars: Option<&SqlBuilder>,
154 pre_req: Option<&SqlBuilder>,
155 mutation: Option<&SqlBuilder>,
156 main: Option<&SqlBuilder>,
157 ) -> Result<StatementResult, Error>;
158
159 /// Get a database introspector for schema cache loading.
160 fn introspector(&self) -> Box<dyn DbIntrospector + '_>;
161
162 /// Start a background change listener (e.g. PostgreSQL NOTIFY).
163 ///
164 /// Returns `None` if the backend does not support change notifications.
165 /// The listener should call `on_schema_reload` when a schema reload is
166 /// requested and `on_config_reload` when a config reload is requested.
167 async fn start_listener(
168 &self,
169 channel: &str,
170 cancel: tokio::sync::watch::Receiver<bool>,
171 on_event: std::sync::Arc<dyn Fn(String) + Send + Sync>,
172 ) -> Result<(), Error>;
173
174 /// Map a backend-specific database error into our Error type.
175 fn map_error(&self, err: Box<dyn std::error::Error + Send + Sync>) -> Error;
176
177 /// Return current connection pool status for metrics reporting.
178 ///
179 /// Returns `None` if the backend does not track pool statistics.
180 fn pool_status(&self) -> Option<PoolStatus> {
181 None
182 }
183}
184
185// ==========================================================================
186// SqlDialect — SQL syntax abstraction
187// ==========================================================================
188
189/// Trait abstracting database-specific SQL syntax.
190///
191/// Each backend provides an implementation that generates the correct SQL
192/// for its engine. The query module calls these methods instead of
193/// hardcoding PostgreSQL-specific functions.
194pub trait SqlDialect: Send + Sync {
195 // -- Aggregation --
196
197 /// JSON array aggregation expression.
198 ///
199 /// `columns` is optionally provided for backends that cannot aggregate
200 /// a whole row alias (e.g. SQLite needs explicit column names).
201 /// PostgreSQL ignores `columns` and uses `json_agg(alias)`.
202 ///
203 /// PostgreSQL: `coalesce(json_agg(_dbrst_t), '[]')::text`
204 /// SQLite: `COALESCE(json_group_array(json_object('col', "col", ...)), '[]')`
205 fn json_agg(&self, b: &mut SqlBuilder, alias: &str) {
206 self.json_agg_with_columns(b, alias, &[]);
207 }
208
209 /// JSON array aggregation with explicit column names.
210 ///
211 /// Default implementation ignores columns and delegates to the alias-based form.
212 fn json_agg_with_columns(&self, b: &mut SqlBuilder, alias: &str, columns: &[&str]);
213
214 /// Single-row JSON expression.
215 ///
216 /// PostgreSQL: `row_to_json(_dbrst_t)::text`
217 /// MySQL: `JSON_OBJECT(...)`
218 fn row_to_json(&self, b: &mut SqlBuilder, alias: &str) {
219 self.row_to_json_with_columns(b, alias, &[]);
220 }
221
222 /// Single-row JSON with explicit column names.
223 fn row_to_json_with_columns(&self, b: &mut SqlBuilder, alias: &str, columns: &[&str]);
224
225 // -- Counting --
226
227 /// COUNT function with schema qualification.
228 ///
229 /// PostgreSQL: `pg_catalog.count(expr)`
230 /// MySQL: `COUNT(expr)`
231 fn count_expr(&self, b: &mut SqlBuilder, expr: &str);
232
233 /// COUNT(*) for total counts.
234 ///
235 /// PostgreSQL: `SELECT COUNT(*) AS "dbrst_filtered_count"`
236 fn count_star(&self, b: &mut SqlBuilder);
237
238 // -- Session variables --
239
240 /// Set a session/transaction-local variable.
241 ///
242 /// The generated expression must be usable as a SELECT column expression.
243 ///
244 /// PostgreSQL: `set_config('key', 'value', true)`
245 /// MySQL: `SET @key = 'value'`
246 fn set_session_var(&self, b: &mut SqlBuilder, key: &str, value: &str);
247
248 /// Read a session variable in a SELECT expression.
249 ///
250 /// PostgreSQL: `nullif(current_setting('key', true), '')`
251 /// MySQL: `@key`
252 fn get_session_var(&self, b: &mut SqlBuilder, key: &str, column_alias: &str);
253
254 /// Whether session variable setup uses SELECT-based expressions.
255 ///
256 /// If true (default), `tx_var_query` wraps calls in `SELECT expr1, expr2, ...`.
257 /// If false, `set_session_var` is not called; instead `build_tx_vars_statement`
258 /// is used to produce a single statement for all variables at once.
259 fn session_vars_are_select_exprs(&self) -> bool {
260 true
261 }
262
263 /// Build a single statement that sets all session/transaction variables.
264 ///
265 /// Only called when `session_vars_are_select_exprs()` returns false.
266 /// The default implementation panics — backends that return false must override.
267 fn build_tx_vars_statement(&self, _b: &mut SqlBuilder, _vars: &[(&str, &str)]) {
268 unimplemented!(
269 "backends with session_vars_are_select_exprs() == false must implement build_tx_vars_statement"
270 )
271 }
272
273 // -- Type casting --
274
275 /// Cast an expression to a type.
276 ///
277 /// PostgreSQL: `expr::type`
278 /// MySQL: `CAST(expr AS type)`
279 fn type_cast(&self, b: &mut SqlBuilder, expr: &str, ty: &str);
280
281 // -- JSON body unpacking --
282
283 /// FROM clause for unpacking a JSON body into rows.
284 ///
285 /// PostgreSQL: `json_to_recordset($1) AS _("col1" type1, "col2" type2)`
286 /// MySQL: `JSON_TABLE($1, '$[*]' COLUMNS(...))`
287 #[allow(clippy::wrong_self_convention)]
288 fn from_json_body(
289 &self,
290 b: &mut SqlBuilder,
291 columns: &[crate::plan::types::CoercibleField],
292 json_bytes: &[u8],
293 );
294
295 // -- Type cast suffix --
296
297 /// Append a type cast suffix to the builder.
298 ///
299 /// PostgreSQL: `::type`
300 /// MySQL: (no-op, or uses CAST wrapping at a higher level)
301 fn push_type_cast_suffix(&self, b: &mut SqlBuilder, ty: &str);
302
303 /// Append an array type cast suffix to the builder.
304 ///
305 /// PostgreSQL: `::type[]`
306 /// MySQL: (no-op)
307 fn push_array_type_cast_suffix(&self, b: &mut SqlBuilder, ty: &str);
308
309 // -- Quoting --
310
311 /// Quote an identifier (table, column, schema name).
312 ///
313 /// PostgreSQL: `"identifier"`
314 /// MySQL: `` `identifier` ``
315 fn quote_ident(&self, ident: &str) -> String;
316
317 /// Quote a string literal.
318 ///
319 /// PostgreSQL: `'literal'`
320 fn quote_literal(&self, lit: &str) -> String;
321
322 // -- Full-text search --
323
324 /// Full-text search predicate.
325 ///
326 /// PostgreSQL: `to_tsvector('config', col) @@ to_tsquery('config', $1)`
327 /// Returns false if the backend doesn't support FTS.
328 fn supports_fts(&self) -> bool;
329
330 /// Generate FTS predicate SQL.
331 fn fts_predicate(&self, b: &mut SqlBuilder, config: Option<&str>, column: &str, operator: &str);
332
333 // -- Scalar row-to-json --
334
335 /// Convert an entire CTE row to JSON text (for scalar function calls).
336 ///
337 /// PostgreSQL: `row_to_json(dbrst_source.*)::text`
338 /// SQLite: `json_object(...)` (requires column list — override if needed)
339 fn row_to_json_star(&self, b: &mut SqlBuilder, source: &str) {
340 // Default implementation uses PG-style syntax.
341 // Override for databases that don't support `source.*`.
342 b.push("row_to_json(");
343 b.push(source);
344 b.push(".*)::text");
345 }
346
347 /// COUNT(*) subquery for exact count from a CTE source.
348 ///
349 /// PostgreSQL: `(SELECT pg_catalog.count(*) FROM dbrst_source)`
350 /// SQLite: `(SELECT COUNT(*) FROM dbrst_source)`
351 fn count_star_from(&self, b: &mut SqlBuilder, source: &str) {
352 b.push("(SELECT pg_catalog.count(*) FROM ");
353 b.push(source);
354 b.push(")");
355 }
356
357 // -- Literal escaping --
358
359 /// Push a single-quoted SQL literal with proper escaping.
360 ///
361 /// PostgreSQL uses `E'...'` for backslash escapes. SQLite uses plain `'...'`.
362 /// The default implementation uses PostgreSQL E-string syntax.
363 fn push_literal(&self, b: &mut SqlBuilder, s: &str) {
364 let has_backslash = s.contains('\\');
365 if has_backslash {
366 b.push("E");
367 }
368 b.push("'");
369 for ch in s.chars() {
370 if ch == '\'' {
371 b.push("'");
372 }
373 b.push_char(ch);
374 }
375 b.push("'");
376 }
377
378 // -- Lateral joins --
379
380 /// Whether the backend supports LATERAL joins.
381 ///
382 /// If false, the query builder must use correlated subqueries instead.
383 fn supports_lateral_join(&self) -> bool;
384
385 // -- Named parameters in function calls --
386
387 /// Named parameter assignment syntax for function calls.
388 ///
389 /// PostgreSQL: `"param" := $1`
390 /// Others may use different syntax or not support this.
391 fn named_param_assign(&self) -> &str {
392 " := "
393 }
394
395 /// Whether the backend supports DML (INSERT/UPDATE/DELETE) inside CTEs.
396 ///
397 /// PostgreSQL supports `WITH cte AS (INSERT ... RETURNING ...) SELECT ... FROM cte`.
398 /// SQLite does NOT — DML is only allowed as the top-level statement.
399 ///
400 /// When false, write queries are split into a mutation statement and a
401 /// separate aggregation SELECT, executed sequentially in the same transaction.
402 fn supports_dml_cte(&self) -> bool {
403 true
404 }
405}