fraiseql_db/traits/adapter_types.rs
1//! Supporting types for the `DatabaseAdapter` trait family.
2//!
3//! Extracted from the main `traits` module to keep the trait definition file
4//! focused on method signatures.
5
6use std::{pin::Pin, sync::Arc};
7
8use fraiseql_error::Result;
9use futures::Stream;
10
11use super::DatabaseAdapter;
12use crate::{
13 types::{
14 ColumnValue, JsonbValue,
15 sql_hints::{OrderByClause, SqlProjectionHint},
16 },
17 where_clause::WhereClause,
18};
19
20/// Result from a relay pagination query, containing rows and an optional total count.
21#[derive(Debug, Clone)]
22#[non_exhaustive]
23pub struct RelayPageResult {
24 /// The page of JSONB rows (already trimmed to the requested page size).
25 pub rows: Vec<JsonbValue>,
26 /// Total count of matching rows (only populated when requested via `include_total_count`).
27 pub total_count: Option<u64>,
28}
29
30impl RelayPageResult {
31 /// Creates a new `RelayPageResult`.
32 #[must_use]
33 pub const fn new(rows: Vec<JsonbValue>, total_count: Option<u64>) -> Self {
34 Self { rows, total_count }
35 }
36
37 /// Returns a reference to the page of JSONB rows.
38 #[must_use]
39 pub fn rows(&self) -> &[JsonbValue] {
40 &self.rows
41 }
42
43 /// Consumes the result and returns the rows.
44 #[must_use]
45 pub fn into_rows(self) -> Vec<JsonbValue> {
46 self.rows
47 }
48
49 /// Returns the total count of matching rows, if requested.
50 #[must_use]
51 pub const fn total_count(&self) -> Option<u64> {
52 self.total_count
53 }
54}
55
56/// A snapshot of an adapter's query-result cache, for the operator surface.
57///
58/// Exists because the admin API could see only the Arrow Flight query cache, and so
59/// answered "Cache is not configured" for a server whose sibling endpoint reported the
60/// query result cache active in the same breath (#941). An adapter that caches nothing
61/// returns `None` from
62/// [`DatabaseAdapter::result_cache_stats`](super::DatabaseAdapter::result_cache_stats)
63/// — "no such cache" and "a cache holding zero entries" are different answers and the
64/// endpoint must not conflate them.
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
66pub struct ResultCacheStats {
67 /// Entries currently held.
68 pub entries: usize,
69 /// Lookups served from cache.
70 pub hits: u64,
71 /// Lookups that reached the database.
72 pub misses: u64,
73 /// Invalidations performed (mutation sweeps, entity evictions, admin clears).
74 pub invalidations: u64,
75 /// Estimated memory held, in bytes.
76 pub memory_bytes: usize,
77 /// Configured default entry TTL, in seconds.
78 pub ttl_seconds: u64,
79 /// Configured entry ceiling.
80 pub max_entries: usize,
81}
82
83/// A typed cursor value for keyset (relay) pagination.
84///
85/// The cursor type is determined at compile time by `QueryDefinition::relay_cursor_type`
86/// and used at runtime to choose the correct SQL comparison and cursor
87/// encoding/decoding path.
88#[derive(Debug, Clone, PartialEq, Eq)]
89#[non_exhaustive]
90pub enum CursorValue {
91 /// BIGINT primary key cursor (default, backward-compatible).
92 Int64(i64),
93 /// UUID cursor — bound as text and cast to `uuid` in SQL.
94 Uuid(String),
95}
96
97/// Parameters for an `execute_with_projection_arc` call (F043).
98///
99/// Consolidates the six positional parameters of the projection-execution path
100/// into a single borrowed struct so adapters and callers cannot reorder them
101/// by mistake. All fields borrow from the caller; the struct is constructed
102/// per-request on the stack and discarded after the call.
103///
104/// # Field ordering
105///
106/// The field order mirrors a SQL `SELECT … FROM view WHERE … ORDER BY … LIMIT
107/// … OFFSET …` clause, top-to-bottom, so that reading the struct mirrors the
108/// query it produces.
109///
110/// Intentionally **not** `#[non_exhaustive]`: the struct is the *call shape*
111/// of the trait method and any field addition is a breaking trait change
112/// regardless. Callers construct it with a struct literal so that omitting a
113/// field is a hard compile error.
114#[derive(Debug, Clone, Copy)]
115pub struct ProjectionRequest<'a> {
116 /// View or table name (e.g. `"v_user"`).
117 pub view: &'a str,
118 /// Projection hint (`SELECT` shape). `None` falls back to `SELECT data`.
119 pub projection: Option<&'a SqlProjectionHint>,
120 /// WHERE clause AST. `None` means no filter.
121 pub where_clause: Option<&'a WhereClause>,
122 /// ORDER BY clauses. Empty slice (or `None`) means unordered.
123 pub order_by: Option<&'a [OrderByClause]>,
124 /// Row limit. `None` means no limit.
125 pub limit: Option<u32>,
126 /// Row offset. `None` means no offset.
127 pub offset: Option<u32>,
128}
129
130impl<'a> ProjectionRequest<'a> {
131 /// Construct a `ProjectionRequest` from a view name with no filters,
132 /// pagination or projection. Useful for tests and simple table scans.
133 #[must_use]
134 pub const fn new(view: &'a str) -> Self {
135 Self {
136 view,
137 projection: None,
138 where_clause: None,
139 order_by: None,
140 limit: None,
141 offset: None,
142 }
143 }
144}
145
146/// JSONB rows delivered as the database produces them, rather than collected
147/// into a `Vec` first (#958).
148///
149/// Returned by
150/// [`DatabaseAdapter::stream_with_projection`](super::DatabaseAdapter::stream_with_projection).
151/// Boxed because the trait is used through `dyn DatabaseAdapter`, and `Send`
152/// because a response body is polled from whichever task the transport runs on.
153///
154/// # What the caller is holding
155///
156/// A stream is not a `Vec` that arrives late. On PostgreSQL it is an open portal
157/// inside an open transaction, which means the read holds a pooled connection for
158/// as long as the stream is alive. Consume it promptly, and drop it as soon as the
159/// response is finished — dropping is what closes the transaction and returns the
160/// connection.
161pub type JsonbRowStream = Pin<Box<dyn Stream<Item = Result<JsonbValue>> + Send>>;
162
163/// The column-shaped counterpart of [`JsonbRowStream`], for transports that
164/// decode typed columns rather than a JSONB document (gRPC).
165///
166/// Returned by
167/// [`DatabaseAdapter::stream_row_query`](super::DatabaseAdapter::stream_row_query).
168/// The same connection-lifetime caveat applies.
169pub type ColumnRowStream = Pin<Box<dyn Stream<Item = Result<Vec<ColumnValue>>> + Send>>;
170
171/// Type alias for boxed dynamic database adapters.
172///
173/// Used to store database adapters without generic type parameters in collections
174/// or struct fields. The adapter type is determined at runtime.
175///
176/// # Example
177///
178/// ```ignore
179/// let adapter: BoxDatabaseAdapter = Box::new(postgres_adapter);
180/// ```
181pub type BoxDatabaseAdapter = Box<dyn DatabaseAdapter>;
182
183/// Type alias for arc-wrapped dynamic database adapters.
184///
185/// Used for thread-safe, reference-counted storage of adapters in shared state.
186///
187/// # Example
188///
189/// ```ignore
190/// let adapter: ArcDatabaseAdapter = Arc::new(postgres_adapter);
191/// ```
192pub type ArcDatabaseAdapter = Arc<dyn DatabaseAdapter>;
193
194/// One operator-supplied statement, and every bound the database is asked to
195/// enforce on it (#962).
196///
197/// This is the call shape of
198/// [`DatabaseAdapter::execute_admin_sql`](super::DatabaseAdapter::execute_admin_sql),
199/// the only path on which FraiseQL runs SQL it did not generate. Every field is a
200/// containment control, and each is enforced *by PostgreSQL* rather than by
201/// inspecting the statement text:
202///
203/// * `read_only` becomes the transaction's own mode, so a write is refused with SQLSTATE `25006` no
204/// matter how it is spelled. A parser deciding whether a string "is a SELECT" is a guess about a
205/// dialect it does not own; a read-only transaction is not.
206/// * `commit` is the *only* thing that makes any effect outlast the request. The transaction is
207/// rolled back otherwise, which is what makes the default a preview.
208/// * `statement_timeout_ms` is `SET LOCAL statement_timeout`, so the server cancels the backend.
209/// * `session_vars` are the same transaction-local settings the executor applies for a real query,
210/// which is what lets an operator preview a row under a tenant's RLS rather than as the pool's
211/// role.
212///
213/// Not `#[non_exhaustive]`: it is a trait method's parameter list, so adding a
214/// field is a breaking change either way, and a struct literal makes an omitted
215/// bound a compile error rather than a default someone did not choose.
216#[derive(Debug, Clone)]
217pub struct AdminSqlRequest {
218 /// The statement to run, verbatim. Never parsed, rewritten or inspected.
219 pub sql: String,
220 /// Start the transaction `READ ONLY`.
221 pub read_only: bool,
222 /// Commit instead of rolling back. `false` is a preview.
223 pub commit: bool,
224 /// `SET LOCAL statement_timeout` for the transaction, in milliseconds.
225 pub statement_timeout_ms: u32,
226 /// Stop reading after this many rows and report the result as truncated.
227 pub max_rows: usize,
228 /// Transaction-local settings applied before the statement, as
229 /// `set_config(name, value, true)` — the RLS-preview identity.
230 pub session_vars: Vec<(String, String)>,
231}
232
233/// What one [`AdminSqlRequest`] did.
234///
235/// `committed` is reported rather than assumed: it is the difference between a
236/// preview and a change, and an operator reading the response should not have to
237/// re-derive it from what they asked for.
238#[derive(Debug, Clone)]
239#[non_exhaustive]
240pub struct AdminSqlOutcome {
241 /// Column names in result order. Empty when the statement returned no rows.
242 pub columns: Vec<String>,
243 /// Rows, in result order, each aligned to `columns`.
244 pub rows: Vec<Vec<serde_json::Value>>,
245 /// `true` when `max_rows` cut the result short — the statement produced more.
246 pub truncated: bool,
247 /// Rows the statement reported affected, when PostgreSQL reported a count.
248 ///
249 /// `None` when the read stopped early, because a count over rows nobody
250 /// consumed is not a count of anything.
251 pub rows_affected: Option<u64>,
252 /// Whether the transaction was committed. `false` means every effect was
253 /// rolled back.
254 pub committed: bool,
255}