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}