Skip to main content

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}