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}