1use std::fmt;
2use std::path::{Path, PathBuf};
3
4use a3s_orm::{
5 sql_query, Database, Executor, FromRow, Migrator, Query, SqlQuery, SqliteDialect, SqliteError,
6 SqliteExecutor, SqliteRow, SqliteTransaction, SqliteTransactionError,
7};
8use async_trait::async_trait;
9use chrono::{DateTime, Utc};
10use uuid::Uuid;
11
12use crate::error::{FlowError, Result};
13use crate::model::{
14 ActiveHookSnapshot, FlowEvent, FlowEventEnvelope, HookSnapshot, HookStatus, ScheduledWakeup,
15};
16
17use super::{scheduled_wakeup_from_row, scheduled_wakeup_key, sqlite_migrations, FlowEventStore};
18
19mod retention;
20
21#[derive(Clone)]
27pub struct SqliteEventStore {
28 executor: SqliteExecutor,
29}
30
31impl fmt::Debug for SqliteEventStore {
32 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
33 formatter
34 .debug_struct("SqliteEventStore")
35 .finish_non_exhaustive()
36 }
37}
38
39impl SqliteEventStore {
40 pub async fn connect(database_url: impl AsRef<str>) -> Result<Self> {
46 let database_url = database_url.as_ref().trim();
47 let executor = if matches!(
48 database_url,
49 "sqlite::memory:" | "sqlite://:memory:" | ":memory:"
50 ) {
51 SqliteExecutor::open_in_memory()
52 .await
53 .map_err(sqlite_driver_error)?
54 } else {
55 let path = sqlite_path(database_url)?;
56 ensure_sqlite_parent_dir(&path).await?;
57 SqliteExecutor::open(path)
58 .await
59 .map_err(sqlite_driver_error)?
60 };
61 Self::from_executor(executor).await
62 }
63
64 pub async fn from_executor(executor: SqliteExecutor) -> Result<Self> {
66 Migrator::new(executor.clone())
67 .run(sqlite_migrations())
68 .await
69 .map_err(|error| FlowError::Store(format!("SQLite Flow migration failed: {error}")))?;
70 Ok(Self { executor })
71 }
72
73 pub fn executor(&self) -> &SqliteExecutor {
75 &self.executor
76 }
77
78 async fn append_with_expected_sequence(
79 &self,
80 run_id: &str,
81 expected_sequence: Option<u64>,
82 event: FlowEvent,
83 ) -> Result<FlowEventEnvelope> {
84 let run_id = run_id.to_string();
85 let result = self
86 .executor
87 .transaction(|transaction| {
88 Box::pin(async move {
89 let linked_run_id =
90 retention::required_linked_flow_run_id(&event).map(str::to_string);
91 retention::ensure_sqlite_history_not_tombstoned(transaction, &run_id).await?;
92 if let Some(linked_run_id) = linked_run_id.as_deref() {
93 retention::ensure_sqlite_history_not_tombstoned(transaction, linked_run_id)
94 .await?;
95 if latest_sqlite_sequence(transaction, linked_run_id).await? == 0 {
96 return Err(FlowError::RunNotFound(linked_run_id.to_string()));
97 }
98 }
99 let actual_sequence = latest_sqlite_sequence(transaction, &run_id).await?;
100 if let Some(expected_sequence) = expected_sequence {
101 if actual_sequence != expected_sequence {
102 return Err(FlowError::EventConflict {
103 run_id,
104 expected_sequence,
105 actual_sequence,
106 });
107 }
108 }
109 if let FlowEvent::HookCreated { hook_id, token, .. } = &event {
110 ensure_sqlite_active_hook_available(transaction, &run_id, hook_id, token)
111 .await?;
112 }
113
114 let envelope = FlowEventEnvelope {
115 run_id,
116 sequence: actual_sequence + 1,
117 event_id: Uuid::new_v4(),
118 timestamp: Utc::now(),
119 event,
120 };
121 insert_sqlite_envelope(transaction, &envelope).await?;
122 Ok(envelope)
123 })
124 })
125 .await;
126 map_sqlite_transaction(result)
127 }
128}
129
130#[async_trait]
131impl FlowEventStore for SqliteEventStore {
132 async fn append(&self, run_id: &str, event: FlowEvent) -> Result<FlowEventEnvelope> {
133 self.append_with_expected_sequence(run_id, None, event)
134 .await
135 }
136
137 async fn append_if_sequence(
138 &self,
139 run_id: &str,
140 expected_sequence: u64,
141 event: FlowEvent,
142 ) -> Result<FlowEventEnvelope> {
143 self.append_with_expected_sequence(run_id, Some(expected_sequence), event)
144 .await
145 }
146
147 async fn list(&self, run_id: &str) -> Result<Vec<FlowEventEnvelope>> {
148 let database = Database::new(SqliteDialect, self.executor.clone());
149 let rows = database
150 .fetch_all_as(
151 sql_query::<(String, i64, String, String, String)>(
152 "SELECT run_id, sequence, event_id, timestamp, event_json \
153 FROM flow_events WHERE run_id = ",
154 )
155 .bind(run_id)
156 .append(" ORDER BY sequence ASC"),
157 )
158 .await
159 .map_err(sqlite_orm_error)?
160 .rows;
161 if rows.is_empty() {
162 return Err(FlowError::RunNotFound(run_id.to_string()));
163 }
164 rows.into_iter().map(row_to_envelope).collect()
165 }
166
167 async fn list_run_ids(&self) -> Result<Vec<String>> {
168 let database = Database::new(SqliteDialect, self.executor.clone());
169 Ok(database
170 .fetch_all_as(sql_query::<String>(
171 "SELECT DISTINCT run_id FROM flow_events ORDER BY run_id ASC",
172 ))
173 .await
174 .map_err(sqlite_orm_error)?
175 .rows)
176 }
177
178 async fn list_due_wakeups(&self, now: DateTime<Utc>) -> Result<Vec<ScheduledWakeup>> {
179 let database = Database::new(SqliteDialect, self.executor.clone());
180 database
181 .fetch_all_as(
182 sql_query::<(String, i64, String, String, Option<String>)>(
183 "SELECT wakeup.run_id, wakeup.wakeup_kind, wakeup.subject_id, \
184 wakeup.scheduled_at_key, \
185 json_extract(created.event_json, '$.spec.runtime_build_id') \
186 FROM flow_scheduled_wakeups AS wakeup \
187 JOIN flow_events AS created \
188 ON created.run_id = wakeup.run_id AND created.sequence = 1 \
189 WHERE wakeup.scheduled_at_key <= ",
190 )
191 .bind(scheduled_wakeup_key(now))
192 .append(" ORDER BY wakeup.wakeup_kind, wakeup.run_id, wakeup.subject_id"),
193 )
194 .await
195 .map_err(sqlite_orm_error)?
196 .rows
197 .into_iter()
198 .map(scheduled_wakeup_from_row)
199 .collect()
200 }
201
202 async fn next_scheduled_wakeup(&self) -> Result<Option<ScheduledWakeup>> {
203 let database = Database::new(SqliteDialect, self.executor.clone());
204 database
205 .fetch_all_as(sql_query::<(String, i64, String, String, Option<String>)>(
206 "SELECT wakeup.run_id, wakeup.wakeup_kind, wakeup.subject_id, \
207 wakeup.scheduled_at_key, \
208 json_extract(created.event_json, '$.spec.runtime_build_id') \
209 FROM flow_scheduled_wakeups AS wakeup \
210 JOIN flow_events AS created \
211 ON created.run_id = wakeup.run_id AND created.sequence = 1 \
212 ORDER BY wakeup.scheduled_at_key, wakeup.run_id, \
213 wakeup.wakeup_kind, wakeup.subject_id LIMIT 1",
214 ))
215 .await
216 .map_err(sqlite_orm_error)?
217 .rows
218 .into_iter()
219 .next()
220 .map(scheduled_wakeup_from_row)
221 .transpose()
222 }
223
224 async fn find_active_hooks_by_token(&self, token: &str) -> Result<Vec<ActiveHookSnapshot>> {
225 let database = Database::new(SqliteDialect, self.executor.clone());
226 database
227 .fetch_all_as(
228 sql_query::<(String, String, String, String)>(
229 "SELECT run_id, hook_id, token, metadata_json \
230 FROM flow_active_hooks WHERE token = ",
231 )
232 .bind(token)
233 .append(" ORDER BY run_id, hook_id"),
234 )
235 .await
236 .map_err(sqlite_orm_error)?
237 .rows
238 .into_iter()
239 .map(active_hook_from_row)
240 .collect()
241 }
242
243 async fn list_active_hooks(&self) -> Result<Vec<ActiveHookSnapshot>> {
244 let database = Database::new(SqliteDialect, self.executor.clone());
245 database
246 .fetch_all_as(sql_query::<(String, String, String, String)>(
247 "SELECT run_id, hook_id, token, metadata_json \
248 FROM flow_active_hooks ORDER BY run_id, hook_id",
249 ))
250 .await
251 .map_err(sqlite_orm_error)?
252 .rows
253 .into_iter()
254 .map(active_hook_from_row)
255 .collect()
256 }
257}
258
259pub(super) async fn execute_sqlite<E>(executor: &E, query: SqlQuery<()>) -> Result<u64>
260where
261 E: Executor<Row = SqliteRow, Error = SqliteError>,
262{
263 let query = query.compile(&SqliteDialect).map_err(sqlite_query_error)?;
264 Ok(executor
265 .execute(&query)
266 .await
267 .map_err(sqlite_driver_error)?
268 .rows_affected)
269}
270
271pub(super) async fn fetch_all_sqlite<T, E>(executor: &E, query: SqlQuery<T>) -> Result<Vec<T>>
272where
273 T: FromRow + Send,
274 E: Executor<Row = SqliteRow, Error = SqliteError>,
275{
276 let query = query.compile(&SqliteDialect).map_err(sqlite_query_error)?;
277 executor
278 .fetch_all(&query)
279 .await
280 .map_err(sqlite_driver_error)?
281 .rows
282 .iter()
283 .map(T::from_row)
284 .collect::<std::result::Result<Vec<_>, _>>()
285 .map_err(sqlite_decode_error)
286}
287
288pub(super) async fn fetch_optional_sqlite<T, E>(
289 executor: &E,
290 query: SqlQuery<T>,
291) -> Result<Option<T>>
292where
293 T: FromRow + Send,
294 E: Executor<Row = SqliteRow, Error = SqliteError>,
295{
296 let mut rows = fetch_all_sqlite(executor, query).await?;
297 match rows.len() {
298 0 => Ok(None),
299 1 => Ok(rows.pop()),
300 actual => Err(FlowError::Store(format!(
301 "SQLite Flow query returned {actual} rows where at most one was expected"
302 ))),
303 }
304}
305
306pub(super) async fn latest_sqlite_sequence(
307 transaction: &SqliteTransaction,
308 run_id: &str,
309) -> Result<u64> {
310 let rows = fetch_all_sqlite(
311 transaction,
312 sql_query::<i64>("SELECT COALESCE(MAX(sequence), 0) FROM flow_events WHERE run_id = ")
313 .bind(run_id),
314 )
315 .await?;
316 let sequence = rows
317 .first()
318 .copied()
319 .ok_or_else(|| FlowError::Store("SQLite sequence query returned no row".to_string()))?;
320 u64::try_from(sequence)
321 .map_err(|error| FlowError::Store(format!("invalid SQLite sequence {sequence}: {error}")))
322}
323
324async fn ensure_sqlite_active_hook_available(
325 transaction: &SqliteTransaction,
326 run_id: &str,
327 hook_id: &str,
328 token: &str,
329) -> Result<()> {
330 let owners = fetch_all_sqlite::<(String, String), _>(
331 transaction,
332 sql_query::<(String, String)>(
333 "SELECT run_id, hook_id FROM flow_active_hooks WHERE token = ",
334 )
335 .bind(token),
336 )
337 .await?;
338 if let Some((existing_run_id, existing_hook_id)) = owners.into_iter().next() {
339 if existing_run_id == run_id && existing_hook_id == hook_id {
340 return Ok(());
341 }
342 return Err(FlowError::HookTokenConflict {
343 token: token.to_string(),
344 existing_run_id,
345 existing_hook_id,
346 });
347 }
348
349 let existing_tokens = fetch_all_sqlite::<String, _>(
350 transaction,
351 sql_query::<String>("SELECT token FROM flow_active_hooks WHERE run_id = ")
352 .bind(run_id)
353 .append(" AND hook_id = ")
354 .bind(hook_id),
355 )
356 .await?;
357 if existing_tokens
358 .first()
359 .is_some_and(|existing_token| existing_token != token)
360 {
361 return Err(FlowError::InvalidTransition(format!(
362 "active hook {hook_id} for run {run_id} already uses a different token (value redacted)"
363 )));
364 }
365 Ok(())
366}
367
368async fn insert_sqlite_envelope(
369 transaction: &SqliteTransaction,
370 envelope: &FlowEventEnvelope,
371) -> Result<()> {
372 let sequence = i64::try_from(envelope.sequence).map_err(|error| {
373 FlowError::Store(format!(
374 "event sequence {} exceeds SQLite integer range: {error}",
375 envelope.sequence
376 ))
377 })?;
378 let query = sql_query::<()>(
379 "INSERT INTO flow_events (run_id, sequence, event_id, timestamp, event_json) VALUES (",
380 )
381 .bind(envelope.run_id.clone())
382 .append(", ")
383 .bind(sequence)
384 .append(", ")
385 .bind(envelope.event_id.to_string())
386 .append(", ")
387 .bind(envelope.timestamp.to_rfc3339())
388 .append(", ")
389 .bind(serde_json::to_string(&envelope.event)?)
390 .append(")")
391 .compile(&SqliteDialect)
392 .map_err(sqlite_query_error)?;
393 transaction
394 .execute(&query)
395 .await
396 .map_err(sqlite_driver_error)?;
397 Ok(())
398}
399
400pub(super) fn row_to_envelope(
401 (run_id, sequence, event_id, timestamp, event_json): (String, i64, String, String, String),
402) -> Result<FlowEventEnvelope> {
403 Ok(FlowEventEnvelope {
404 run_id,
405 sequence: u64::try_from(sequence).map_err(|error| {
406 FlowError::Store(format!("invalid SQLite sequence {sequence}: {error}"))
407 })?,
408 event_id: event_id.parse().map_err(|error| {
409 FlowError::Store(format!("invalid SQLite event id {event_id}: {error}"))
410 })?,
411 timestamp: timestamp.parse().map_err(|error| {
412 FlowError::Store(format!(
413 "invalid SQLite event timestamp {timestamp}: {error}"
414 ))
415 })?,
416 event: serde_json::from_str(&event_json)?,
417 })
418}
419
420fn active_hook_from_row(
421 (run_id, hook_id, token, metadata_json): (String, String, String, String),
422) -> Result<ActiveHookSnapshot> {
423 Ok(ActiveHookSnapshot {
424 run_id,
425 hook: HookSnapshot {
426 hook_id,
427 token,
428 status: HookStatus::Active,
429 metadata: serde_json::from_str(&metadata_json)?,
430 payload: None,
431 },
432 })
433}
434
435fn sqlite_path(database_url: &str) -> Result<PathBuf> {
436 let path = database_url
437 .strip_prefix("sqlite://")
438 .or_else(|| database_url.strip_prefix("sqlite:"))
439 .unwrap_or(database_url)
440 .trim();
441 if path.is_empty() {
442 return Err(FlowError::Store(format!(
443 "invalid SQLite database URL: {database_url}"
444 )));
445 }
446 Ok(PathBuf::from(path))
447}
448
449async fn ensure_sqlite_parent_dir(path: &Path) -> Result<()> {
450 let Some(parent) = path
451 .parent()
452 .filter(|parent| !parent.as_os_str().is_empty())
453 else {
454 return Ok(());
455 };
456 tokio::fs::create_dir_all(parent).await?;
457 Ok(())
458}
459
460pub(super) fn map_sqlite_transaction<T>(
461 result: std::result::Result<T, SqliteTransactionError<FlowError>>,
462) -> Result<T> {
463 match result {
464 Ok(value) => Ok(value),
465 Err(SqliteTransactionError::Operation(error)) => Err(error),
466 Err(error) => Err(FlowError::Store(format!(
467 "SQLite Flow transaction failed: {error}"
468 ))),
469 }
470}
471
472fn sqlite_query_error(error: a3s_orm::Error) -> FlowError {
473 FlowError::Store(format!("SQLite Flow query build failed: {error}"))
474}
475
476fn sqlite_driver_error(error: a3s_orm::SqliteError) -> FlowError {
477 FlowError::Store(format!("SQLite Flow storage failed: {error}"))
478}
479
480fn sqlite_decode_error(error: a3s_orm::DecodeError) -> FlowError {
481 FlowError::Store(format!("SQLite Flow row decoding failed: {error}"))
482}
483
484fn sqlite_orm_error(error: a3s_orm::DatabaseError<a3s_orm::SqliteError>) -> FlowError {
485 FlowError::Store(format!("SQLite Flow storage failed: {error}"))
486}