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