fraiseql_db/traits/relay.rs
1//! Relay cursor pagination trait.
2//!
3//! [`RelayDatabaseAdapter`] extends [`DatabaseAdapter`](super::DatabaseAdapter)
4//! with keyset-based pagination for Relay-style GraphQL connections.
5
6use std::future::Future;
7
8use fraiseql_error::Result;
9
10use super::{CursorValue, DatabaseAdapter, RelayPageResult};
11use crate::{
12 types::{ReadRouting, sql_hints::OrderByClause},
13 where_clause::WhereClause,
14};
15
16/// Database adapter supertrait for adapters that implement Relay cursor pagination.
17///
18/// Only adapters that genuinely support keyset pagination need to implement this trait.
19/// Non-implementing adapters carry no relay code at all — no stubs, no flags.
20///
21/// # Implementors
22///
23/// - `PostgresAdapter` — full keyset pagination
24/// - `CachedDatabaseAdapter<A>` — delegates to inner `A`
25///
26/// `MySqlAdapter` was listed here until #374 removed the non-PostgreSQL backends.
27///
28/// # Usage
29///
30/// Construct an `Executor` with `Executor::new_with_relay` to enable relay
31/// query execution. The bound `A: RelayDatabaseAdapter` is enforced at that call site.
32pub trait RelayDatabaseAdapter: DatabaseAdapter {
33 /// Execute keyset (cursor-based) pagination against a JSONB view.
34 ///
35 /// # Arguments
36 ///
37 /// * `view` — SQL view name (will be quoted before use)
38 /// * `cursor_column` — column used as the pagination key (e.g. `pk_user`, `id`)
39 /// * `after` — forward cursor: return rows where `cursor_column > after`
40 /// * `before` — backward cursor: return rows where `cursor_column < before`
41 /// * `limit` — row fetch count (pass `page_size + 1` to detect `hasNextPage`)
42 /// * `forward` — `true` → ASC order; `false` → DESC (re-sorted ASC via subquery)
43 /// * `where_clause` — optional user-supplied filter applied after the cursor condition
44 /// * `order_by` — optional custom sort; cursor column appended as tiebreaker
45 /// * `include_total_count` — when `true`, compute the matching row count before LIMIT
46 ///
47 /// # Errors
48 ///
49 /// Returns `FraiseQLError::Database` on SQL execution failure.
50 fn execute_relay_page<'a>(
51 &'a self,
52 view: &'a str,
53 cursor_column: &'a str,
54 after: Option<CursorValue>,
55 before: Option<CursorValue>,
56 limit: u32,
57 forward: bool,
58 where_clause: Option<&'a WhereClause>,
59 order_by: Option<&'a [OrderByClause]>,
60 include_total_count: bool,
61 ) -> impl Future<Output = Result<RelayPageResult>> + Send + 'a;
62
63 /// Connection-affine variant of [`execute_relay_page`](Self::execute_relay_page).
64 ///
65 /// Applies `session_vars` transaction-locally on the same connection that
66 /// runs the page (and total-count) queries, so RLS-protected relay
67 /// pagination over views reading `current_setting()` returns the correct
68 /// tenant's rows (fixes #329 for the cursor-pagination path).
69 ///
70 /// Adapters that do not support session variables inherit the default,
71 /// which drops `session_vars` and delegates to `execute_relay_page`.
72 ///
73 /// # Errors
74 ///
75 /// Same errors as [`execute_relay_page`](Self::execute_relay_page);
76 /// additionally returns `FraiseQLError::Database` if `set_config` fails.
77 #[allow(clippy::too_many_arguments)] // Reason: relay pagination requires all cursor/filter/sort/count arguments plus session vars; no natural grouping
78 fn execute_relay_page_with_session<'a>(
79 &'a self,
80 view: &'a str,
81 cursor_column: &'a str,
82 after: Option<CursorValue>,
83 before: Option<CursorValue>,
84 limit: u32,
85 forward: bool,
86 where_clause: Option<&'a WhereClause>,
87 order_by: Option<&'a [OrderByClause]>,
88 include_total_count: bool,
89 _session_vars: &'a [(&'a str, &'a str)],
90 _routing: ReadRouting,
91 ) -> impl Future<Output = Result<RelayPageResult>> + Send + 'a {
92 self.execute_relay_page(
93 view,
94 cursor_column,
95 after,
96 before,
97 limit,
98 forward,
99 where_clause,
100 order_by,
101 include_total_count,
102 )
103 }
104}