Skip to main content

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}