Skip to main content

fusillade_core/request/
query.rs

1//! Cross-batch request query types and filters.
2//!
3//! These types support listing and retrieving individual requests across batches,
4//! with server-side filtering, pagination, and sorting.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10/// Default number of rows to return when limit is not specified.
11const DEFAULT_LIMIT: i64 = 50;
12
13/// Derive the service tier from the batch completion window.
14/// "1h" → "flex" (async), everything else → NULL (batch).
15pub fn service_tier_from_completion_window(completion_window: &str) -> Option<&'static str> {
16    match completion_window {
17        "1h" => Some("flex"),
18        "0s" => Some("priority"),
19        _ => None,
20    }
21}
22
23/// Filter on `service_tier`.
24///
25/// `None` in the inner vec represents the batch tier (`service_tier IS NULL`);
26/// named strings match specific tier values such as `"flex"` or `"priority"`.
27///
28/// `Default` is `Any` — no filter applied.
29#[derive(Debug, Clone, Default)]
30pub enum ServiceTierFilter {
31    /// No filter — match all tiers including batch (NULL).
32    #[default]
33    Any,
34    /// Match only rows whose tier is in this set. Empty matches nothing.
35    Include(Vec<Option<String>>),
36    /// Match all tiers except those in this set.
37    Exclude(Vec<Option<String>>),
38}
39
40impl ServiceTierFilter {
41    /// Split a list of `Option<String>` tiers into (named_tiers, includes_null).
42    pub fn split(tiers: &[Option<String>]) -> (Vec<String>, bool) {
43        let mut names = Vec::with_capacity(tiers.len());
44        let mut has_null = false;
45        for t in tiers {
46            match t {
47                Some(s) => names.push(s.clone()),
48                None => has_null = true,
49            }
50        }
51        (names, has_null)
52    }
53}
54
55/// Filter parameters for listing requests across batches.
56#[derive(Debug, Clone)]
57pub struct ListRequestsFilter {
58    /// Filter by request creator (user ID or org ID)
59    pub created_by: Option<String>,
60    /// Filter by request state (pending, claimed, processing, completed, failed, canceled)
61    pub status: Option<String>,
62    /// Filter by model(s) — when multiple, matches any.
63    /// `None` disables model filtering. `Some(vec![])` matches no rows.
64    pub models: Option<Vec<String>>,
65    /// Only return requests created after this timestamp
66    pub created_after: Option<DateTime<Utc>>,
67    /// Only return requests created before this timestamp
68    pub created_before: Option<DateTime<Utc>>,
69    /// Filter by service tier(s). When set, only returns requests whose
70    /// `service_tier` matches one of the provided values (e.g., `["flex", "priority"]`).
71    /// Uses `= ANY($7)` which hits the composite `idx_requests_created_tier` index.
72    /// `None` disables tier filtering. `Some(vec![])` matches no rows.
73    pub service_tiers: Option<Vec<String>>,
74    /// Sort active requests (pending/claimed/processing) first
75    pub active_first: bool,
76    /// Number of rows to skip (offset pagination)
77    pub skip: i64,
78    /// Maximum number of rows to return (defaults to 50)
79    pub limit: i64,
80}
81
82impl Default for ListRequestsFilter {
83    fn default() -> Self {
84        Self {
85            created_by: None,
86            status: None,
87            models: None,
88            created_after: None,
89            created_before: None,
90            service_tiers: None,
91            active_first: false,
92            skip: 0,
93            limit: DEFAULT_LIMIT,
94        }
95    }
96}
97
98/// Summary of an individual request, suitable for list views.
99///
100/// **Row scope: batchless-only.** `list_requests` filters
101/// `WHERE r.created_by IS NOT NULL` so the planner can use the partial
102/// index `idx_requests_user_*_sort`. Batched rows are not returned here.
103/// For batched-row attribution, fetch the row's `batches.created_by`
104/// separately or use `RequestDetail` (which joins batches).
105///
106/// Note: This type does not include user email or token/cost metrics.
107/// Callers should enrich with data from their own tables (users, analytics).
108#[derive(Debug, Clone, Serialize, Deserialize)]
109#[cfg_attr(feature = "sqlx-postgres", derive(sqlx::FromRow))]
110pub struct RequestSummary {
111    pub id: Uuid,
112    pub batch_id: Option<Uuid>,
113    pub model: String,
114    #[cfg_attr(feature = "sqlx-postgres", sqlx(rename = "state"))]
115    pub status: String,
116    pub created_at: DateTime<Utc>,
117    pub completed_at: Option<DateTime<Utc>>,
118    pub failed_at: Option<DateTime<Utc>>,
119    pub duration_ms: Option<f64>,
120    pub response_status: Option<i16>,
121    pub service_tier: Option<String>,
122    /// Creator ID (user or org) for ownership checks and email lookup.
123    /// Always set: `list_requests` only returns rows where
124    /// `requests.created_by IS NOT NULL` (see struct-level docs).
125    pub created_by: String,
126}
127
128/// Internal row shape used previously when `list_requests` computed the total
129/// count via a `COUNT(*) OVER()` window function. Kept for backward
130/// compatibility with the public re-export introduced in v16.1.0; no longer
131/// constructed by this crate.
132#[allow(deprecated)]
133mod deprecated_types {
134    use super::{DateTime, Deserialize, RequestSummary, Serialize, Utc, Uuid};
135
136    #[deprecated(
137        since = "16.1.1",
138        note = "no longer used internally; use RequestSummary and RequestListResult.total_count"
139    )]
140    #[derive(Debug, Clone, Serialize, Deserialize)]
141    #[cfg_attr(feature = "sqlx-postgres", derive(sqlx::FromRow))]
142    pub struct RequestSummaryWithCount {
143        pub id: Uuid,
144        pub batch_id: Uuid,
145        pub model: String,
146        #[cfg_attr(feature = "sqlx-postgres", sqlx(rename = "state"))]
147        pub status: String,
148        pub created_at: DateTime<Utc>,
149        pub completed_at: Option<DateTime<Utc>>,
150        pub failed_at: Option<DateTime<Utc>>,
151        pub duration_ms: Option<f64>,
152        pub response_status: Option<i16>,
153        pub service_tier: Option<String>,
154        pub batch_created_by: String,
155        pub total_count: i64,
156    }
157
158    impl From<RequestSummaryWithCount> for RequestSummary {
159        fn from(r: RequestSummaryWithCount) -> Self {
160            Self {
161                id: r.id,
162                batch_id: Some(r.batch_id),
163                model: r.model,
164                status: r.status,
165                created_at: r.created_at,
166                completed_at: r.completed_at,
167                failed_at: r.failed_at,
168                duration_ms: r.duration_ms,
169                response_status: r.response_status,
170                service_tier: r.service_tier,
171                created_by: r.batch_created_by,
172            }
173        }
174    }
175}
176
177#[allow(deprecated)]
178pub use deprecated_types::RequestSummaryWithCount;
179
180/// Full detail of an individual request, including body and response.
181///
182/// **Row scope: batchless-only.** `get_request_detail` filters
183/// `WHERE r.created_by IS NOT NULL`. Per-row inspection of batched
184/// requests goes through `get_batch_results_stream` / `BatchResultItem`
185/// instead. Used by the dashboard's response-detail page and the Open
186/// Responses API.
187#[derive(Debug, Clone, Serialize, Deserialize)]
188#[cfg_attr(feature = "sqlx-postgres", derive(sqlx::FromRow))]
189pub struct RequestDetail {
190    pub id: Uuid,
191    /// Always `None` — this query is scoped to batchless rows only.
192    /// Per-row inspection of batched requests uses
193    /// `get_batch_results_stream` / `BatchResultItem`.
194    pub batch_id: Option<Uuid>,
195    pub model: String,
196    #[cfg_attr(feature = "sqlx-postgres", sqlx(rename = "state"))]
197    pub status: String,
198    pub created_at: DateTime<Utc>,
199    pub completed_at: Option<DateTime<Utc>>,
200    pub failed_at: Option<DateTime<Utc>>,
201    pub duration_ms: Option<f64>,
202    pub response_status: Option<i16>,
203    /// `None` when the template has been purged (file soft-deleted + orphan purge).
204    pub body: Option<String>,
205    pub response_body: Option<String>,
206    pub error: Option<String>,
207    pub service_tier: Option<String>,
208    /// Creator ID (user or org). Always set: `get_request_detail` only
209    /// returns rows where `requests.created_by IS NOT NULL`.
210    pub created_by: String,
211}
212
213/// Input for creating a realtime response that the proxy is already handling.
214///
215/// Inserts a request template (no parent file) and a request row in
216/// `processing` state with `batch_id = NULL` and `daemon_id = Uuid::nil()`.
217/// The proxy completes/fails the row directly via `complete_request` /
218/// `fail_request`; the daemon never claims it.
219#[derive(Debug, Clone)]
220pub struct CreateRealtimeInput {
221    /// Pre-generated request ID. Becomes the request's primary key.
222    pub request_id: Uuid,
223    /// The request body as a JSON string.
224    pub body: String,
225    /// Model identifier.
226    pub model: String,
227    /// Base URL of the target endpoint (e.g., "http://localhost:3001/ai").
228    pub endpoint: String,
229    /// HTTP method (e.g., "POST").
230    pub method: String,
231    /// API path (e.g., "/v1/responses").
232    pub path: String,
233    /// API key for the request.
234    pub api_key: String,
235    /// User/org ID that owns this request.
236    pub created_by: String,
237}
238
239/// Input for persisting a batch of already-completed realtime responses.
240///
241/// Used by the dwctl responses writer to flush a buffer of finished
242/// realtime calls in one transaction.
243///
244/// Two cases are handled in the same batch:
245///   * Background realtime: the row was pre-created in `processing` state by
246///     `create_realtime`; we UPDATE it to `completed`.
247///   * Non-background realtime: no row exists yet; we INSERT a template and
248///     a request row directly in `completed` state.
249///
250/// All synthesize fields (`request_body`, `model`, `endpoint`, etc.) are
251/// only consulted on the INSERT path. On the UPDATE path only `request_id`,
252/// `response_body`, and `status_code` are used.
253#[derive(Debug, Clone)]
254pub struct PersistCompletedRealtimeInput {
255    /// The request UUID (primary key).
256    pub request_id: Uuid,
257    /// Upstream response body to store.
258    pub response_body: String,
259    /// Upstream HTTP status code.
260    pub status_code: u16,
261    /// Original request body, stored on the synthesized template.
262    pub request_body: String,
263    /// Model identifier.
264    pub model: String,
265    /// Base URL of the target endpoint.
266    pub endpoint: String,
267    /// HTTP method (e.g., "POST").
268    pub method: String,
269    /// API path (e.g., "/v1/responses").
270    pub path: String,
271    /// API key for the request.
272    pub api_key: String,
273    /// User/org ID that owns this request.
274    pub created_by: String,
275}
276
277/// Input for creating a flex (async) response that the daemon will process.
278///
279/// Inserts a request template (no parent file) and a request row in `pending`
280/// state with `batch_id = NULL`. The daemon claims and processes it like any
281/// other pending request.
282#[derive(Debug, Clone)]
283pub struct CreateFlexInput {
284    /// Pre-generated request ID. Becomes the request's primary key.
285    pub request_id: Uuid,
286    /// The request body as a JSON string.
287    pub body: String,
288    /// Model identifier.
289    pub model: String,
290    /// Base URL of the target endpoint (e.g., "http://localhost:3001/ai").
291    pub endpoint: String,
292    /// HTTP method (e.g., "POST").
293    pub method: String,
294    /// API path (e.g., "/v1/responses").
295    pub path: String,
296    /// API key for the request.
297    pub api_key: String,
298    /// User/org ID that owns this request.
299    pub created_by: String,
300}
301
302/// Result of a paginated request list query.
303#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct RequestListResult {
305    pub data: Vec<RequestSummary>,
306    /// Best-effort total row count for the full query result.
307    ///
308    /// Returns an exact count when the count query completes within a short
309    /// internal timeout; otherwise falls back to a query-planner row estimate.
310    /// Planner estimates are typically within a few percent when table
311    /// statistics are current, but may diverge more if stats are stale.
312    pub total_count: i64,
313}