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`, `started_at`,
251/// `completed_at`, etc.) are only consulted on the INSERT path. On the UPDATE
252/// path only `request_id`, `response_body`, and `status_code` are used — the
253/// pre-existing row already carries a real `started_at` from `create_realtime`.
254#[derive(Debug, Clone)]
255pub struct PersistCompletedRealtimeInput {
256 /// The request UUID (primary key).
257 pub request_id: Uuid,
258 /// Upstream response body to store.
259 pub response_body: String,
260 /// Upstream HTTP status code.
261 pub status_code: u16,
262 /// Original request body, stored on the synthesized template.
263 pub request_body: String,
264 /// Model identifier.
265 pub model: String,
266 /// Base URL of the target endpoint.
267 pub endpoint: String,
268 /// HTTP method (e.g., "POST").
269 pub method: String,
270 /// API path (e.g., "/v1/responses").
271 pub path: String,
272 /// API key for the request.
273 pub api_key: String,
274 /// User/org ID that owns this request.
275 pub created_by: String,
276 /// Wall-clock instant the request arrived, as recorded by the caller.
277 /// INSERT path only: becomes the synthesized row's `created_at`,
278 /// `claimed_at`, and `started_at`. Ignored on the UPDATE path, where the row
279 /// already carries a real `started_at`.
280 pub started_at: DateTime<Utc>,
281 /// Wall-clock instant the response completed (`started_at` plus the caller's
282 /// measured request duration). INSERT path only: stored as the row's `completed_at` on 2xx
283 /// (so the listing's `duration_ms = completed_at - started_at` reflects the
284 /// true latency instead of zero) or as `failed_at` on non-2xx. Note
285 /// `duration_ms` is derived from the `completed_at` column, so it is NULL for
286 /// failed rows — the completion instant is still recorded there, in
287 /// `failed_at`. Ignored on the UPDATE path.
288 pub completed_at: DateTime<Utc>,
289}
290
291/// Input for creating a flex (async) response that the daemon will process.
292///
293/// Inserts a request template (no parent file) and a request row in `pending`
294/// state with `batch_id = NULL`. The daemon claims and processes it like any
295/// other pending request.
296#[derive(Debug, Clone)]
297pub struct CreateFlexInput {
298 /// Pre-generated request ID. Becomes the request's primary key.
299 pub request_id: Uuid,
300 /// The request body as a JSON string.
301 pub body: String,
302 /// Model identifier.
303 pub model: String,
304 /// Base URL of the target endpoint (e.g., "http://localhost:3001/ai").
305 pub endpoint: String,
306 /// HTTP method (e.g., "POST").
307 pub method: String,
308 /// API path (e.g., "/v1/responses").
309 pub path: String,
310 /// API key for the request.
311 pub api_key: String,
312 /// User/org ID that owns this request.
313 pub created_by: String,
314}
315
316/// Result of a paginated request list query.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct RequestListResult {
319 pub data: Vec<RequestSummary>,
320 /// Best-effort total row count for the full query result.
321 ///
322 /// Returns an exact count when the count query completes within a short
323 /// internal timeout; otherwise falls back to a query-planner row estimate.
324 /// Planner estimates are typically within a few percent when table
325 /// statistics are current, but may diverge more if stats are stale.
326 pub total_count: i64,
327}