Skip to main content

dbrest_core/query/
mod.rs

1//! Query builder module — transforms typed plans into parameterized SQL.
2//!
3//! This module sits between the plan layer (`crate::plan`) and the database
4//! executor. It consumes `ActionPlan` / `CrudPlan` values and produces
5//! parameterized SQL strings ready for execution via sqlx.
6//!
7//! # Pipeline
8//!
9//! ```text
10//! ActionPlan ──▶ main_query() ──▶ MainQuery { tx_vars, pre_req, main }
11//!                                    │
12//!                                    ├─ tx_var_query()     → SET search_path, role, …
13//!                                    ├─ pre_req_query()    → SELECT pre_request()
14//!                                    └─ main_read/write/call → CTE wrapper query
15//! ```
16//!
17//! # SQL Example
18//!
19//! A simple GET /users produces:
20//!
21//! ```sql
22//! -- tx_vars:
23//! SELECT set_config('search_path', '"test_api"', true),
24//!        set_config('role', 'web_anon', true), …
25//!
26//! -- main:
27//! WITH dbrst_source AS (
28//!   SELECT "test_api"."users"."id", "test_api"."users"."name"
29//!   FROM "test_api"."users"
30//! )
31//! SELECT NULL AS total_result_set,
32//!        pg_catalog.count(_dbrst_t) AS page_total,
33//!        coalesce(json_agg(_dbrst_t), '[]')::text AS body,
34//!        …
35//! FROM (SELECT * FROM dbrst_source) AS _dbrst_t
36//! ```
37
38pub mod builder;
39pub mod fragment;
40pub mod pre_query;
41pub mod sql_builder;
42pub mod statements;
43
44// Re-export key types
45pub use sql_builder::{SqlBuilder, SqlParam};
46
47use crate::backend::SqlDialect;
48use crate::config::AppConfig;
49use crate::plan::{ActionPlan, CrudPlan, DbActionPlan};
50
51// ==========================================================================
52// MainQuery — the final query bundle
53// ==========================================================================
54
55/// A bundle of SQL queries to execute for a single API request.
56///
57/// Created by [`main_query`]. The executor runs these queries in order:
58///
59/// 1. `tx_vars` — sets session variables (search_path, role, request context)
60/// 2. `pre_req` — calls the pre-request function (if configured)
61/// 3. `main` — the actual data query (read / mutate / call)
62///
63/// Each field is `Option<SqlBuilder>` because not every request needs all three
64/// queries (e.g., info requests have no main query).
65#[derive(Debug)]
66pub struct MainQuery {
67    /// Session variable setup query (`SELECT set_config(…)`).
68    pub tx_vars: Option<SqlBuilder>,
69    /// Pre-request function call (`SELECT schema.pre_request()`).
70    pub pre_req: Option<SqlBuilder>,
71    /// Mutation statement (INSERT/UPDATE/DELETE with RETURNING).
72    ///
73    /// Only set for backends that don't support DML in CTEs (e.g. SQLite).
74    /// When set, `main` contains only the aggregation SELECT over a temp table
75    /// populated by this mutation.
76    pub mutation: Option<SqlBuilder>,
77    /// Main data query (CTE-wrapped SELECT / INSERT / UPDATE / DELETE / CALL).
78    pub main: Option<SqlBuilder>,
79}
80
81impl MainQuery {
82    /// Create an empty query bundle.
83    pub fn empty() -> Self {
84        Self {
85            tx_vars: None,
86            pre_req: None,
87            mutation: None,
88            main: None,
89        }
90    }
91}
92
93// ==========================================================================
94// main_query — entry point
95// ==========================================================================
96
97/// Build all queries for an API request.
98///
99/// Routes the `ActionPlan` to the appropriate statement builder and assembles
100/// the full query bundle including session variables and pre-request calls.
101///
102/// # Behaviour
103///
104/// - `ActionPlan::Db(DbCrud { .. })` routes to `main_read`, `main_write`, or
105///   `main_call` depending on the `CrudPlan` variant
106/// - `ActionPlan::Db(MayUseDb(InspectPlan))` generates a schema inspection
107///   query (not yet implemented — placeholder for future phases)
108/// - `ActionPlan::NoDb(InfoPlan)` generates no SQL (handled at the HTTP layer)
109///
110/// # Returns
111///
112/// A [`MainQuery`] with the session setup, pre-request, and main query SQL.
113#[allow(clippy::too_many_arguments)]
114pub fn main_query(
115    action_plan: &ActionPlan,
116    config: &AppConfig,
117    dialect: &dyn SqlDialect,
118    method: &str,
119    path: &str,
120    role: Option<&str>,
121    headers_json: Option<&str>,
122    cookies_json: Option<&str>,
123    claims_json: Option<&str>,
124) -> MainQuery {
125    // Session variables
126    let tx_vars = Some(pre_query::tx_var_query(
127        config,
128        dialect,
129        method,
130        path,
131        role,
132        headers_json,
133        cookies_json,
134        claims_json,
135    ));
136
137    // Pre-request function
138    let pre_req = config.db_pre_request.as_ref().map(pre_query::pre_req_query);
139
140    // Main query based on action plan type
141    let (mutation, main) = match action_plan {
142        ActionPlan::Db(db_plan) => match db_plan {
143            DbActionPlan::DbCrud { plan, .. } => {
144                let (m, q) = build_crud_query(plan, config, dialect);
145                (m, Some(q))
146            }
147            DbActionPlan::MayUseDb(_inspect) => {
148                // Schema inspection — placeholder for future phases
149                (None, None)
150            }
151        },
152        ActionPlan::NoDb(_) => {
153            // Info plans (OPTIONS) are handled at the HTTP layer, no SQL needed
154            (None, None)
155        }
156    };
157
158    MainQuery {
159        tx_vars,
160        pre_req,
161        mutation,
162        main,
163    }
164}
165
166/// Build the main SQL query for a CRUD plan.
167///
168/// Dispatches to the appropriate CTE-wrapping statement builder based on the
169/// plan variant:
170///
171/// | Variant           | Builder function       | Query shape              |
172/// |-------------------|------------------------|--------------------------|
173/// | `WrappedReadPlan` | `statements::main_read`  | CTE SELECT with aggregation |
174/// | `MutateReadPlan`  | `statements::main_write` | CTE INSERT/UPDATE/DELETE |
175/// | `CallReadPlan`    | `statements::main_call`  | CTE function call        |
176///
177/// Returns the assembled `SqlBuilder` ready for execution.
178fn build_crud_query(
179    plan: &CrudPlan,
180    config: &AppConfig,
181    dialect: &dyn SqlDialect,
182) -> (Option<SqlBuilder>, SqlBuilder) {
183    match plan {
184        CrudPlan::WrappedReadPlan {
185            read_plan,
186            headers_only,
187            handler,
188            ..
189        } => (
190            None,
191            statements::main_read(
192                read_plan,
193                None,
194                config.db_max_rows,
195                *headers_only,
196                Some(&handler.0),
197                dialect,
198            ),
199        ),
200        CrudPlan::MutateReadPlan {
201            read_plan,
202            mutate_plan,
203            handler,
204            ..
205        } => {
206            let return_representation = !mutate_plan.returning().is_empty();
207            if dialect.supports_dml_cte() {
208                (
209                    None,
210                    statements::main_write(
211                        mutate_plan,
212                        read_plan,
213                        return_representation,
214                        Some(&handler.0),
215                        dialect,
216                    ),
217                )
218            } else {
219                let (mutation, agg) = statements::main_write_split(
220                    mutate_plan,
221                    read_plan,
222                    return_representation,
223                    Some(&handler.0),
224                    dialect,
225                );
226                (Some(mutation), agg)
227            }
228        }
229        CrudPlan::CallReadPlan {
230            call_plan, handler, ..
231        } => (
232            None,
233            statements::main_call(
234                call_plan,
235                None,
236                config.db_max_rows,
237                Some(&handler.0),
238                dialect,
239            ),
240        ),
241    }
242}
243
244// ==========================================================================
245// Tests
246// ==========================================================================
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use crate::api_request::types::{InvokeMethod, Mutation, Payload};
252    use crate::plan::TxMode;
253    use crate::plan::call_plan::{CallArgs, CallParams, CallPlan};
254    use crate::plan::mutate_plan::{InsertPlan, MutatePlan};
255    use crate::plan::read_plan::{ReadPlan, ReadPlanTree};
256    use crate::plan::types::*;
257    use crate::schema_cache::media_handler::{MediaHandler, ResolvedHandler};
258    use crate::test_helpers::TestPgDialect;
259    use crate::types::identifiers::QualifiedIdentifier;
260    use crate::types::media::MediaType;
261    use bytes::Bytes;
262    use smallvec::SmallVec;
263
264    fn dialect() -> &'static dyn SqlDialect {
265        &TestPgDialect
266    }
267
268    fn test_qi() -> QualifiedIdentifier {
269        QualifiedIdentifier::new("test_api", "users")
270    }
271
272    fn test_config() -> AppConfig {
273        let mut config = AppConfig::default();
274        config.db_schemas = vec!["test_api".to_string()];
275        config.db_anon_role = Some("web_anon".to_string());
276        config
277    }
278
279    fn select_field(name: &str) -> CoercibleSelectField {
280        CoercibleSelectField {
281            field: CoercibleField::unknown(name.into(), SmallVec::new()),
282            agg_function: None,
283            agg_cast: None,
284            cast: None,
285            alias: None,
286        }
287    }
288
289    fn default_handler() -> ResolvedHandler {
290        (MediaHandler::BuiltinOvAggJson, MediaType::ApplicationJson)
291    }
292
293    fn make_read_plan() -> ActionPlan {
294        let mut rp = ReadPlan::root(test_qi());
295        rp.select = vec![select_field("id"), select_field("name")];
296        let tree = ReadPlanTree::leaf(rp);
297
298        ActionPlan::Db(DbActionPlan::DbCrud {
299            is_explain: false,
300            plan: CrudPlan::WrappedReadPlan {
301                read_plan: tree,
302                tx_mode: TxMode::default_mode(),
303                handler: default_handler(),
304                media: MediaType::ApplicationJson,
305                headers_only: false,
306                qi: test_qi(),
307            },
308        })
309    }
310
311    fn make_mutate_plan() -> ActionPlan {
312        let rp = ReadPlan::root(test_qi());
313        let tree = ReadPlanTree::leaf(rp);
314
315        let mutate = MutatePlan::Insert(InsertPlan {
316            into: test_qi(),
317            columns: vec![CoercibleField::from_column(
318                "name".into(),
319                SmallVec::new(),
320                "text".into(),
321            )],
322            body: Payload::RawJSON(Bytes::from(r#"[{"name":"Alice"}]"#)),
323            on_conflict: None,
324            where_: vec![],
325            returning: vec![select_field("id"), select_field("name")],
326            pk_cols: vec!["id".into()],
327            apply_defaults: false,
328        });
329
330        ActionPlan::Db(DbActionPlan::DbCrud {
331            is_explain: false,
332            plan: CrudPlan::MutateReadPlan {
333                read_plan: tree,
334                mutate_plan: mutate,
335                tx_mode: TxMode::default_mode(),
336                handler: default_handler(),
337                media: MediaType::ApplicationJson,
338                mutation: Mutation::MutationCreate,
339                qi: test_qi(),
340            },
341        })
342    }
343
344    fn make_call_plan() -> ActionPlan {
345        use crate::schema_cache::routine::{ReturnType, Routine, Volatility};
346        use smallvec::smallvec;
347
348        let rp = ReadPlan::root(test_qi());
349        let tree = ReadPlanTree::leaf(rp);
350
351        let call = CallPlan {
352            qi: QualifiedIdentifier::new("test_api", "get_time"),
353            params: CallParams::KeyParams(vec![]),
354            args: CallArgs::JsonArgs(None),
355            scalar: true,
356            set_of_scalar: false,
357            filter_fields: vec![],
358            returning: vec![],
359        };
360
361        ActionPlan::Db(DbActionPlan::DbCrud {
362            is_explain: false,
363            plan: CrudPlan::CallReadPlan {
364                read_plan: tree,
365                call_plan: call,
366                tx_mode: TxMode::default_mode(),
367                proc: Routine {
368                    schema: "test_api".into(),
369                    name: "get_time".into(),
370                    params: smallvec![],
371                    return_type: ReturnType::Single(crate::schema_cache::routine::PgType::Scalar(
372                        QualifiedIdentifier::new("pg_catalog", "timestamptz"),
373                    )),
374                    is_variadic: false,
375                    volatility: Volatility::Stable,
376                    description: None,
377                    executable: true,
378                },
379                handler: default_handler(),
380                media: MediaType::ApplicationJson,
381                inv_method: InvokeMethod::InvRead(false),
382                qi: QualifiedIdentifier::new("test_api", "get_time"),
383            },
384        })
385    }
386
387    // ------------------------------------------------------------------
388    // main_query tests
389    // ------------------------------------------------------------------
390
391    #[test]
392    fn test_main_query_read() {
393        let plan = make_read_plan();
394        let config = test_config();
395
396        let mq = main_query(
397            &plan,
398            &config,
399            dialect(),
400            "GET",
401            "/users",
402            None,
403            None,
404            None,
405            None,
406        );
407
408        assert!(mq.tx_vars.is_some());
409        assert!(mq.pre_req.is_none()); // No pre-request configured
410        assert!(mq.main.is_some());
411
412        let main_sql = mq.main.unwrap().sql().to_string();
413        assert!(main_sql.contains("dbrst_source"));
414        assert!(main_sql.contains("\"users\""));
415    }
416
417    #[test]
418    fn test_main_query_mutate() {
419        let plan = make_mutate_plan();
420        let config = test_config();
421
422        let mq = main_query(
423            &plan,
424            &config,
425            dialect(),
426            "POST",
427            "/users",
428            None,
429            None,
430            None,
431            None,
432        );
433
434        assert!(mq.main.is_some());
435        let main_sql = mq.main.unwrap().sql().to_string();
436        assert!(main_sql.contains("INSERT INTO"));
437    }
438
439    #[test]
440    fn test_main_query_call() {
441        let plan = make_call_plan();
442        let config = test_config();
443
444        let mq = main_query(
445            &plan,
446            &config,
447            dialect(),
448            "POST",
449            "/rpc/get_time",
450            None,
451            None,
452            None,
453            None,
454        );
455
456        assert!(mq.main.is_some());
457        let main_sql = mq.main.unwrap().sql().to_string();
458        assert!(main_sql.contains("get_time"));
459    }
460
461    #[test]
462    fn test_main_query_with_pre_request() {
463        let plan = make_read_plan();
464        let mut config = test_config();
465        config.db_pre_request = Some(QualifiedIdentifier::new("test_api", "check_request"));
466
467        let mq = main_query(
468            &plan,
469            &config,
470            dialect(),
471            "GET",
472            "/users",
473            None,
474            None,
475            None,
476            None,
477        );
478
479        assert!(mq.pre_req.is_some());
480        let pre_sql = mq.pre_req.unwrap().sql().to_string();
481        assert!(pre_sql.contains("check_request"));
482    }
483
484    #[test]
485    fn test_main_query_info_plan() {
486        let plan = ActionPlan::NoDb(crate::plan::InfoPlan::SchemaInfoPlan);
487        let config = test_config();
488
489        let mq = main_query(
490            &plan,
491            &config,
492            dialect(),
493            "OPTIONS",
494            "/",
495            None,
496            None,
497            None,
498            None,
499        );
500
501        // Info plans have no main SQL
502        assert!(mq.main.is_none());
503        // But still set tx vars
504        assert!(mq.tx_vars.is_some());
505    }
506
507    #[test]
508    fn test_main_query_empty() {
509        let mq = MainQuery::empty();
510        assert!(mq.tx_vars.is_none());
511        assert!(mq.pre_req.is_none());
512        assert!(mq.main.is_none());
513    }
514}