Skip to main content

obeli_sk_db_sqlite/
sqlite_dao.rs

1use crate::{histograms::Histograms, sqlite_dao::conversions::to_generic_error};
2use async_trait::async_trait;
3use chrono::{DateTime, Utc};
4use concepts::{
5    ComponentId, ComponentRetryConfig, ComponentType, ContentDigest, ExecutionFailureKind,
6    ExecutionId, FunctionFqn, JoinSetId, StrVariant, SupportedFunctionReturnValue,
7    cas::{Cas, CasError},
8    component_id::{ComponentDigest, Digest},
9    prefixed_ulid::{DelayId, DeploymentId, ExecutionIdDerived, ExecutorId, RunId},
10    storage::{
11        AppendBatchResponse, AppendDelayResponseOutcome, AppendEventsToExecution, AppendRequest,
12        AppendResponse, AppendResponseToExecution, BacktraceFilter, BacktraceInfo, CancelOutcome,
13        ComponentMetadataRecord, ComponentUpgradeOutcome, ComponentUpgradeReason, CreateRequest,
14        DUMMY_CREATED, DUMMY_HISTORY_EVENT, DbConnection, DbErrorGeneric, DbErrorRead,
15        DbErrorReadWithTimeout, DbErrorStubResponse, DbErrorWrite, DbErrorWriteNonRetriable,
16        DbExecutor, DbExternalApi, DbPool, DbPoolCloseable, DeploymentComponentDetail,
17        DeploymentComponentRecord, DeploymentExecutionCounts, DeploymentFileRecord,
18        DeploymentRecord, DeploymentState, DeploymentStatus, EnqueueOutcome, ExecutionEvent,
19        ExecutionListPagination, ExecutionRequest, ExecutionWithState,
20        ExecutionWithStateRequestsResponses, ExpiredDelay, ExpiredLock, ExpiredTimer,
21        HISTORY_EVENT_TYPE_JOIN_NEXT, HistoryEvent, JoinSetRequest, JoinSetResponse,
22        JoinSetResponseEvent, JoinSetResponseEventOuter, LIFECYCLE_ACTIVE, LIFECYCLE_CANCELLING,
23        LIFECYCLE_PAUSED, Lifecycle, ListExecutionEventsResponse, ListExecutionsFilter,
24        ListLogsResponse, ListResponsesResponse, LockPendingResponse, Locked, LockedBy,
25        LockedExecution, LogCursor, LogEntry, LogEntryRow, LogFilter, LogInfoAppendRow, LogLevel,
26        LogStreamType, Pagination, PendingState, PendingStateBlockedByJoinSet,
27        PendingStateFinishedError, PendingStateFinishedResultKind, PendingStateMerged,
28        RESULT_KIND_JSON_ERROR, RESULT_KIND_JSON_OK, ResponseCursor, ResponseWithCursor,
29        STATE_BLOCKED_BY_JOIN_SET, STATE_FINISHED, STATE_LOCKED, STATE_PENDING_AT, TimeoutOutcome,
30        Unlocked, Version, VersionType,
31    },
32};
33use conversions::{JsonWrapper, consistency_db_err, consistency_rusqlite, from_generic_error};
34use db_common::{
35    AppendNotifier, CombinedState, CombinedStateDTO, NotifierExecutionFinished, NotifierPendingAt,
36    PendingFfqnSubscribersHolder, state_filter_to_sql, state_filters_now,
37};
38use hashbrown::HashMap;
39use rusqlite::{
40    CachedStatement, Connection, OpenFlags, OptionalExtension, Row, ToSql, Transaction,
41    TransactionBehavior, named_params,
42    types::{ToSqlOutput, Value},
43};
44use sha2::{Digest as _, Sha256};
45use std::{
46    cmp::max,
47    collections::VecDeque,
48    fmt::Debug,
49    ops::DerefMut,
50    panic::Location,
51    path::Path,
52    sync::{
53        Arc, Mutex,
54        atomic::{AtomicBool, Ordering},
55    },
56    time::{Duration, Instant},
57};
58use std::{fmt::Write as _, pin::Pin, str::FromStr as _};
59use strum::IntoEnumIterator as _;
60use tokio::sync::{mpsc, oneshot};
61use tracing::{Level, Span, debug, error, info, instrument, trace, warn};
62use tracing_error::SpanTrace;
63
64#[derive(Debug, thiserror::Error)]
65#[error("initialization error")]
66pub struct InitializationError;
67
68#[derive(Debug, Clone)]
69struct DelayReq {
70    join_set_id: JoinSetId,
71    delay_id: DelayId,
72    expires_at: DateTime<Utc>,
73    paused: bool,
74}
75/*
76mmap_size = 128MB - Set the global memory map so all processes can share some data
77https://www.sqlite.org/pragma.html#pragma_mmap_size
78https://www.sqlite.org/mmap.html
79
80journal_size_limit = 64 MB - limit on the WAL file to prevent unlimited growth
81https://www.sqlite.org/pragma.html#pragma_journal_size_limit
82
83Inspired by https://github.com/rails/rails/pull/49349
84*/
85
86const PRAGMA: [[&str; 2]; 10] = [
87    ["journal_mode", "wal"],
88    ["synchronous", "FULL"],
89    ["foreign_keys", "true"],
90    ["busy_timeout", "1000"],
91    ["cache_size", "10000"], // number of pages
92    ["temp_store", "MEMORY"],
93    ["page_size", "8192"], // 8 KB
94    ["mmap_size", "134217728"],
95    ["journal_size_limit", "67108864"],
96    ["integrity_check", ""],
97];
98
99mod embedded {
100    refinery::embed_migrations!("migrations");
101}
102
103#[derive(Debug, thiserror::Error, Clone)]
104enum RusqliteError {
105    #[error("not found")]
106    NotFound,
107    #[error("generic: {reason}")]
108    Generic {
109        reason: StrVariant,
110        context: SpanTrace,
111        source: Option<Arc<dyn std::error::Error + Send + Sync>>,
112        loc: &'static Location<'static>,
113    },
114    #[error("close")]
115    Close,
116}
117
118mod conversions {
119
120    use super::RusqliteError;
121    use concepts::{
122        StrVariant,
123        storage::{
124            DbErrorGeneric, DbErrorRead, DbErrorReadWithTimeout, DbErrorStubResponse, DbErrorWrite,
125        },
126    };
127    use rusqlite::{
128        ToSql,
129        types::{FromSql, FromSqlError},
130    };
131    use std::{fmt::Debug, panic::Location, sync::Arc};
132    use tracing::error;
133    use tracing_error::SpanTrace;
134
135    impl From<rusqlite::Error> for RusqliteError {
136        // The LTX returns this, capture inner location.
137        #[track_caller]
138        fn from(err: rusqlite::Error) -> Self {
139            if matches!(err, rusqlite::Error::QueryReturnedNoRows) {
140                RusqliteError::NotFound
141            } else {
142                RusqliteError::Generic {
143                    reason: err.to_string().into(),
144                    context: SpanTrace::capture(),
145                    source: Some(Arc::new(err)),
146                    loc: Location::caller(),
147                }
148            }
149        }
150    }
151
152    // Manual conversion, as this function ignores the NotFound.
153    #[track_caller]
154    pub fn to_generic_error(err: RusqliteError) -> DbErrorGeneric {
155        if let RusqliteError::Close = err {
156            DbErrorGeneric::Close
157        } else {
158            DbErrorGeneric::Uncategorized {
159                reason: err.to_string().into(),
160                context: SpanTrace::capture(),
161                source: Some(Arc::new(err)),
162                loc: Location::caller(),
163            }
164        }
165    }
166
167    /// Convert a `DbErrorGeneric` to `rusqlite::Error` for use in row mapping closures.
168    #[track_caller]
169    pub fn from_generic_error(err: &DbErrorGeneric) -> rusqlite::Error {
170        FromSqlError::other(OtherError {
171            reason: err.to_string().into(),
172            loc: Location::caller(),
173        })
174        .into()
175    }
176
177    impl From<RusqliteError> for DbErrorRead {
178        fn from(err: RusqliteError) -> Self {
179            if matches!(err, RusqliteError::NotFound) {
180                Self::NotFound
181            } else {
182                to_generic_error(err).into()
183            }
184        }
185    }
186    impl From<RusqliteError> for DbErrorReadWithTimeout {
187        fn from(err: RusqliteError) -> Self {
188            Self::from(DbErrorRead::from(err))
189        }
190    }
191    impl From<RusqliteError> for DbErrorWrite {
192        fn from(err: RusqliteError) -> Self {
193            if matches!(err, RusqliteError::NotFound) {
194                Self::NotFound
195            } else {
196                to_generic_error(err).into()
197            }
198        }
199    }
200
201    impl From<RusqliteError> for DbErrorStubResponse {
202        fn from(err: RusqliteError) -> Self {
203            DbErrorStubResponse::Write(DbErrorWrite::from(err))
204        }
205    }
206
207    pub(crate) struct JsonWrapper<T>(pub(crate) T);
208    impl<T: serde::de::DeserializeOwned + 'static + Debug> FromSql for JsonWrapper<T> {
209        fn column_result(
210            value: rusqlite::types::ValueRef<'_>,
211        ) -> rusqlite::types::FromSqlResult<Self> {
212            let value = match value {
213                rusqlite::types::ValueRef::Text(value) | rusqlite::types::ValueRef::Blob(value) => {
214                    Ok(value)
215                }
216                other => {
217                    error!(
218                        backtrace = %std::backtrace::Backtrace::capture(),
219                        "Unexpected type when conveting to JSON - expected Text or Blob, got type `{other:?}`",
220                    );
221                    Err(FromSqlError::InvalidType)
222                }
223            }?;
224            let value = serde_json::from_slice::<T>(value).map_err(|err| {
225                error!(
226                    backtrace = %std::backtrace::Backtrace::capture(),
227                    "Cannot convert JSON value `{value:?}` to type:`{type}` - {err:?}",
228                    r#type = std::any::type_name::<T>()
229                );
230                FromSqlError::InvalidType
231            })?;
232            Ok(Self(value))
233        }
234    }
235    impl<T: serde::ser::Serialize + Debug> ToSql for JsonWrapper<T> {
236        fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
237            let string = serde_json::to_string(&self.0).map_err(|err| {
238                error!(
239                    "Cannot serialize {value:?} of type `{type}` - {err:?}",
240                    value = self.0,
241                    r#type = std::any::type_name::<T>()
242                );
243                rusqlite::Error::ToSqlConversionFailure(Box::new(err))
244            })?;
245            Ok(rusqlite::types::ToSqlOutput::Owned(
246                rusqlite::types::Value::Text(string),
247            ))
248        }
249    }
250
251    // Used as a wrapper for `FromSqlError::Other`
252    #[derive(Debug, thiserror::Error)]
253    #[error("{reason}")]
254    pub(crate) struct OtherError {
255        reason: StrVariant,
256        loc: &'static Location<'static>,
257    }
258
259    #[track_caller]
260    pub(crate) fn consistency_rusqlite(reason: impl Into<StrVariant>) -> rusqlite::Error {
261        FromSqlError::other(OtherError {
262            reason: reason.into(),
263            loc: Location::caller(),
264        })
265        .into()
266    }
267
268    #[track_caller]
269    pub(crate) fn consistency_db_err(reason: impl Into<StrVariant>) -> DbErrorGeneric {
270        DbErrorGeneric::Uncategorized {
271            reason: reason.into(),
272            context: SpanTrace::capture(),
273            source: None,
274            loc: Location::caller(),
275        }
276    }
277}
278
279#[derive(Debug, Copy, Clone, PartialEq, Eq)]
280enum TxType {
281    MultipleWrites, // PhyTx must be rolled back, other LTX restarted
282    Other,          // Read only or a single write LTX. Continue the PhyTx if LTX returns error.
283}
284
285#[derive(Debug, Clone, Copy)]
286enum CancellationFfqnCheck {
287    Required,
288    Skipped,
289}
290
291#[derive(Clone)]
292struct CommitError(RusqliteError);
293
294#[derive(Debug)]
295struct ShouldRollback;
296
297#[derive(derive_more::Debug)]
298struct LogicalTx {
299    #[debug(skip)]
300    #[expect(clippy::type_complexity)]
301    func: Box<dyn FnMut(&mut Transaction) -> Result<(), ShouldRollback> + Send>,
302    sent_at: Instant,
303    func_name: &'static str,
304    #[debug(skip)]
305    // Result of Physical Tx commit / rollback
306    phytx_flush_sender: oneshot::Sender<Result<(), CommitError>>,
307    priority: LtxPriority,
308}
309#[derive(Copy, Clone, Debug, PartialEq, Eq)]
310enum LtxPriority {
311    High,
312    Low,
313}
314
315#[derive(derive_more::Debug)]
316enum ThreadCommand {
317    LogicalTx(LogicalTx),
318    Shutdown,
319}
320
321#[derive(Clone)]
322pub struct SqlitePool(SqlitePoolInner);
323
324/// Notification channel where subscriber blocks waiting for a response. Writer thread
325/// ensures serializability - subscriber ensures there are no missed responses. Senders
326/// notify outside of the writer thread, making ordering not guaranteed. Subscriber must
327/// therefore refetch from its own watermark.
328type ResponseSubscribers =
329    Arc<Mutex<HashMap<ExecutionId, (oneshot::Sender<()>, u64 /* unique_tag */)>>>;
330type PendingSubscribers = Arc<Mutex<PendingFfqnSubscribersHolder>>;
331type ExecutionFinishedSubscribers = Mutex<
332    HashMap<
333        ExecutionId,
334        HashMap<u64 /* unique_tag */, oneshot::Sender<SupportedFunctionReturnValue>>,
335    >,
336>;
337
338#[derive(Clone)]
339struct SqlitePoolInner {
340    shutdown_requested: Arc<AtomicBool>,
341    shutdown_finished: Arc<AtomicBool>,
342    command_tx: tokio::sync::mpsc::Sender<ThreadCommand>,
343    response_subscribers: ResponseSubscribers,
344    pending_subscribers: PendingSubscribers,
345    execution_finished_subscribers: Arc<ExecutionFinishedSubscribers>,
346    join_handle: Option<Arc<std::thread::JoinHandle<()>>>, // always Some, Optional for swapping in drop.
347}
348
349#[async_trait]
350impl DbPoolCloseable for SqlitePool {
351    async fn close(&self) {
352        debug!("Sqlite is closing");
353        self.0.shutdown_requested.store(true, Ordering::Release);
354        // Unblock the thread's blocking_recv. If the capacity is reached, the next processed message will trigger shutdown.
355        let _ = self.0.command_tx.try_send(ThreadCommand::Shutdown);
356        while !self.0.shutdown_finished.load(Ordering::Acquire) {
357            tokio::time::sleep(Duration::from_millis(1)).await;
358        }
359        debug!("Sqlite was closed");
360    }
361}
362
363#[async_trait]
364impl DbPool for SqlitePool {
365    async fn db_exec_conn(&self) -> Result<Box<dyn DbExecutor>, DbErrorGeneric> {
366        if self.0.shutdown_requested.load(Ordering::Acquire) {
367            return Err(DbErrorGeneric::Close);
368        }
369        Ok(Box::new(self.clone()))
370    }
371
372    async fn connection(&self) -> Result<Box<dyn DbConnection>, DbErrorGeneric> {
373        if self.0.shutdown_requested.load(Ordering::Acquire) {
374            return Err(DbErrorGeneric::Close);
375        }
376        Ok(Box::new(self.clone()))
377    }
378    async fn external_api_conn(&self) -> Result<Box<dyn DbExternalApi>, DbErrorGeneric> {
379        if self.0.shutdown_requested.load(Ordering::Acquire) {
380            return Err(DbErrorGeneric::Close);
381        }
382        Ok(Box::new(self.clone()))
383    }
384    async fn cas_conn(&self) -> Result<Box<dyn Cas>, DbErrorGeneric> {
385        if self.0.shutdown_requested.load(Ordering::Acquire) {
386            return Err(DbErrorGeneric::Close);
387        }
388        Ok(Box::new(self.clone()))
389    }
390    #[cfg(feature = "test")]
391    async fn connection_test(
392        &self,
393    ) -> Result<Box<dyn concepts::storage::DbConnectionTest>, DbErrorGeneric> {
394        if self.0.shutdown_requested.load(Ordering::Acquire) {
395            return Err(DbErrorGeneric::Close);
396        }
397        Ok(Box::new(self.clone()))
398    }
399}
400
401impl Drop for SqlitePool {
402    fn drop(&mut self) {
403        let arc = self.0.join_handle.take().expect("join_handle was set");
404        if let Ok(join_handle) = Arc::try_unwrap(arc) {
405            // Last holder
406            if !join_handle.is_finished() {
407                if !self.0.shutdown_finished.load(Ordering::Acquire) {
408                    // Best effort to shut down the sqlite thread.
409                    let backtrace = std::backtrace::Backtrace::capture();
410                    warn!("SqlitePool was not closed properly - {backtrace}");
411                    self.0.shutdown_requested.store(true, Ordering::Release);
412                    // Unblock the thread's blocking_recv. If the capacity is reached, the next processed message will trigger shutdown.
413                    let _ = self.0.command_tx.try_send(ThreadCommand::Shutdown);
414                    // Not joining the thread, drop might be called from async context.
415                    // We are shutting down the server anyway.
416                } else {
417                    // The thread set `shutdown_finished` as its last operation.
418                }
419            }
420        }
421    }
422}
423
424#[derive(Debug, Clone)]
425pub struct SqliteConfig {
426    pub queue_capacity: usize,
427    pub pragma_override: Option<HashMap<String, String>>,
428    pub metrics_threshold: Option<Duration>,
429}
430impl Default for SqliteConfig {
431    fn default() -> Self {
432        Self {
433            queue_capacity: 100,
434            pragma_override: None,
435            metrics_threshold: None,
436        }
437    }
438}
439
440struct ShutdownRequested;
441
442fn deployment_record_from_row(row: &Row<'_>) -> rusqlite::Result<DeploymentRecord> {
443    let deployment_id: DeploymentId = row.get("deployment_id")?;
444    let status_str: String = row.get("status")?;
445    let status = status_str.parse::<DeploymentStatus>().map_err(|_| {
446        rusqlite::Error::InvalidColumnType(3, "status".to_string(), rusqlite::types::Type::Text)
447    })?;
448    Ok(DeploymentRecord {
449        deployment_id,
450        description: row.get("description")?,
451        digest: row.get("digest")?,
452        created_at: row.get("created_at")?,
453        last_active_at: row.get("last_active_at")?,
454        status,
455        deployment_toml: row.get("deployment_toml")?,
456        obelisk_version: row.get("obelisk_version")?,
457        created_by: row.get("created_by")?,
458        files: Vec::new(),
459    })
460}
461
462fn deployment_component_detail_from_row(
463    row: &Row<'_>,
464) -> Result<DeploymentComponentDetail, DbErrorGeneric> {
465    let component_name: String = row
466        .get("component_name")
467        .map_err(|err| consistency_db_err(format!("invalid component_name: {err}")))?;
468    let component_type: String = row
469        .get("component_type")
470        .map_err(|err| consistency_db_err(format!("invalid component_type: {err}")))?;
471    let component_type = component_type
472        .parse::<ComponentType>()
473        .map_err(|err| consistency_db_err(format!("invalid component_type: {err}")))?;
474    let component_digest: ComponentDigest = row
475        .get("component_digest")
476        .map_err(|err| consistency_db_err(format!("invalid component_digest: {err}")))?;
477    let component_id = ComponentId::new(
478        component_type,
479        StrVariant::from(component_name),
480        component_digest,
481    )
482    .map_err(|err| consistency_db_err(err.to_string()))?;
483    let imports: String = row
484        .get("imports_json")
485        .map_err(|err| consistency_db_err(format!("invalid imports_json: {err}")))?;
486    let imports = serde_json::from_str(&imports)
487        .map_err(|err| consistency_db_err(format!("invalid imports_json: {err}")))?;
488    let exports: String = row
489        .get("exports_json")
490        .map_err(|err| consistency_db_err(format!("invalid exports_json: {err}")))?;
491    let exports = serde_json::from_str(&exports)
492        .map_err(|err| consistency_db_err(format!("invalid exports_json: {err}")))?;
493    let wit: String = row
494        .get("wit")
495        .map_err(|err| consistency_db_err(format!("invalid wit: {err}")))?;
496    Ok(DeploymentComponentDetail {
497        component_id,
498        imports,
499        exports,
500        wit,
501    })
502}
503
504struct PendingAfterEventUpdate {
505    scheduled_at: DateTime<Utc>,
506    intermittent_failure: bool,
507    component_input_digest: ComponentDigest,
508}
509
510impl SqlitePool {
511    fn init_thread(
512        path: &Path,
513        mut pragma_override: HashMap<String, String>,
514    ) -> Result<Connection, InitializationError> {
515        fn pragma_update(
516            conn: &Connection,
517            name: &str,
518            value: &str,
519        ) -> Result<(), InitializationError> {
520            if value.is_empty() {
521                debug!("Querying PRAGMA {name}");
522                conn.pragma_query(None, name, |row| {
523                    debug!("{row:?}");
524                    Ok(())
525                })
526                .map_err(|err| {
527                    error!("cannot update pragma `{name}`=`{value}` - {err:?}");
528                    InitializationError
529                })
530            } else {
531                debug!("Setting PRAGMA {name}={value}");
532                conn.pragma_update(None, name, value).map_err(|err| {
533                    error!("cannot update pragma `{name}`=`{value}` - {err:?}");
534                    InitializationError
535                })
536            }
537        }
538
539        let mut conn = Connection::open_with_flags(path, OpenFlags::default()).map_err(|err| {
540            error!("cannot open the connection - {err:?}");
541            InitializationError
542        })?;
543
544        for [pragma_name, default_value] in PRAGMA {
545            let pragma_value = pragma_override
546                .remove(pragma_name)
547                .unwrap_or_else(|| default_value.to_string());
548            pragma_update(&conn, pragma_name, &pragma_value)?;
549        }
550        // drain the rest overrides
551        for (pragma_name, pragma_value) in pragma_override.drain() {
552            pragma_update(&conn, &pragma_name, &pragma_value)?;
553        }
554
555        embedded::migrations::runner()
556            .run(&mut conn)
557            .map_err(|err| {
558                error!("Cannot run migrations - {err:?}");
559                InitializationError
560            })?;
561        Ok(conn)
562    }
563
564    fn connection_rpc(
565        mut conn: Connection,
566        shutdown_requested: &AtomicBool,
567        shutdown_finished: &AtomicBool,
568        mut command_rx: mpsc::Receiver<ThreadCommand>,
569        metrics_threshold: Option<Duration>,
570    ) {
571        let mut histograms = Histograms::new(metrics_threshold);
572        while Self::tick(
573            &mut conn,
574            shutdown_requested,
575            &mut command_rx,
576            &mut histograms,
577        )
578        .is_ok()
579        {
580            // Loop until shutdown is set to true.
581        }
582        debug!("Closing command thread");
583        shutdown_finished.store(true, Ordering::Release);
584    }
585
586    fn tick(
587        conn: &mut Connection,
588        shutdown_requested: &AtomicBool,
589        command_rx: &mut mpsc::Receiver<ThreadCommand>,
590        histograms: &mut Histograms,
591    ) -> Result<(), ShutdownRequested> {
592        #[derive(Clone, Copy, PartialEq, Eq)]
593        enum ApplyOrSkip {
594            Apply,
595            Skip, // LTX apply failed previously
596        }
597        let mut ltx_list: Vec<(LogicalTx, ApplyOrSkip)> = Vec::new();
598        // perf: Wait for first logical tx with high priority.
599        loop {
600            let ltx = match command_rx.blocking_recv() {
601                Some(ThreadCommand::LogicalTx(ltx)) => ltx,
602                Some(ThreadCommand::Shutdown) => {
603                    debug!("Shutdown message received");
604                    return Err(ShutdownRequested);
605                }
606                None => {
607                    debug!("command_rx was closed");
608                    return Err(ShutdownRequested);
609                }
610            };
611            let prio = ltx.priority;
612            ltx_list.push((ltx, ApplyOrSkip::Apply));
613            if prio == LtxPriority::High {
614                break;
615            }
616        }
617        // Exactly one High prio LTX is in `ltx_list`.
618
619        let all_fns_start = std::time::Instant::now();
620
621        // Add all remaining LTXes that were queued after the first High-prio.
622        while let Ok(more) = command_rx.try_recv() {
623            let ltx = match more {
624                ThreadCommand::Shutdown => {
625                    debug!("Shutdown message received");
626                    // ptx gets rolled back on drop
627                    // phytx_flush_receiver drop is converted to `RusqliteError::Close` error
628                    return Err(ShutdownRequested);
629                }
630                ThreadCommand::LogicalTx(ltx) => ltx,
631            };
632            ltx_list.push((ltx, ApplyOrSkip::Apply));
633        }
634
635        struct NeedsRestart;
636        type CommitResult = Result<(), CommitError>;
637        fn try_apply_all(
638            mut ptx: Transaction<'_>,
639            ltx_list: &mut [(LogicalTx, ApplyOrSkip)],
640            histograms: &mut Histograms,
641            all_fns_start: Instant,
642        ) -> Result<CommitResult, NeedsRestart> {
643            for (ltx, former_res) in ltx_list
644                .iter_mut()
645                .filter(|(_, former_res)| *former_res == ApplyOrSkip::Apply)
646            {
647                if let Ok(()) = SqlitePool::ltx_apply_to_phytx(ltx, &mut ptx, histograms) {
648                } else {
649                    *former_res = ApplyOrSkip::Skip; // the problematic ltx will be skipped in the next iteration.
650                    // ptx rollbacks on drop
651                    return Err(NeedsRestart);
652                }
653            }
654            // All LTXes were applied or skipped.
655            histograms.record_all_fns(all_fns_start.elapsed());
656            let now = std::time::Instant::now();
657            let commit_result = ptx.commit().map_err(|err| {
658                warn!("Cannot commit transaction - {err:?}");
659                CommitError(RusqliteError::from(err))
660            });
661            histograms.record_commit(now.elapsed());
662            Ok(commit_result)
663        }
664
665        fn apply_all(
666            conn: &mut Connection,
667            ltx_list: &mut [(LogicalTx, ApplyOrSkip)],
668            histograms: &mut Histograms,
669            all_fns_start: Instant,
670            shutdown_requested: &AtomicBool,
671        ) -> Result<CommitResult, ShutdownRequested> {
672            // TODO: Investigate SAVEPOINT + RELEASE SAVEPOINT, ROLLBACK savepoint instead.
673            loop {
674                match conn.transaction_with_behavior(TransactionBehavior::Immediate) {
675                    Ok(ptx) => {
676                        if let Ok(commit_res) =
677                            try_apply_all(ptx, ltx_list, histograms, all_fns_start)
678                        {
679                            return Ok(commit_res);
680                        }
681                    }
682                    Err(begin_err) => {
683                        error!("Cannot open transaction - {begin_err:?}");
684                        std::thread::sleep(Duration::from_millis(100));
685                        if shutdown_requested.load(Ordering::Acquire) {
686                            return Err(ShutdownRequested);
687                        }
688                    }
689                }
690            }
691        }
692        let ok_or_commit_error = apply_all(
693            conn,
694            &mut ltx_list,
695            histograms,
696            all_fns_start,
697            shutdown_requested,
698        )?;
699
700        for (ltx, apply_or_skip) in ltx_list {
701            let to_send = match apply_or_skip {
702                ApplyOrSkip::Apply => ok_or_commit_error.clone(),
703                ApplyOrSkip::Skip => {
704                    Ok(()) // tx was aborted, caller will receive error from the LTX fn
705                }
706            };
707            // Ignore the sending result: ThreadCommand producer timed out before awaiting the ack.
708            let _ = ltx.phytx_flush_sender.send(to_send);
709        }
710
711        histograms.print_if_elapsed();
712        Ok(())
713    }
714
715    // Returning error means the PhyTx needs to be rolled back, implying LTX contained multiple writes.
716    fn ltx_apply_to_phytx(
717        ltx: &mut LogicalTx,
718        physical_tx: &mut Transaction,
719        histograms: &mut Histograms,
720    ) -> Result<(), ShouldRollback> {
721        let sent_latency = ltx.sent_at.elapsed();
722        let started_at = Instant::now();
723        let res = (ltx.func)(physical_tx);
724        histograms.record_command(sent_latency, ltx.func_name, started_at.elapsed());
725        res
726    }
727
728    #[instrument(skip_all, name = "sqlite_new")]
729    pub async fn new<P: AsRef<Path>>(
730        path: P,
731        config: SqliteConfig,
732    ) -> Result<Self, InitializationError> {
733        let path = path.as_ref().to_owned();
734
735        let shutdown_requested = Arc::new(AtomicBool::new(false));
736        let shutdown_finished = Arc::new(AtomicBool::new(false));
737
738        let (command_tx, command_rx) = tokio::sync::mpsc::channel(config.queue_capacity);
739        info!("Sqlite database location: {path:?}");
740        let join_handle = {
741            // Initialize the `Connection`.
742            let init_task = {
743                tokio::task::spawn_blocking(move || {
744                    Self::init_thread(&path, config.pragma_override.unwrap_or_default())
745                })
746                .await
747            };
748            let conn = match init_task {
749                Ok(res) => res?,
750                Err(join_err) => {
751                    error!("Initialization panic - {join_err:?}");
752                    return Err(InitializationError);
753                }
754            };
755            let shutdown_requested = shutdown_requested.clone();
756            let shutdown_finished = shutdown_finished.clone();
757            // Start the RPC thread.
758            std::thread::spawn(move || {
759                Self::connection_rpc(
760                    conn,
761                    &shutdown_requested,
762                    &shutdown_finished,
763                    command_rx,
764                    config.metrics_threshold,
765                );
766            })
767        };
768        Ok(SqlitePool(SqlitePoolInner {
769            shutdown_requested,
770            shutdown_finished,
771            command_tx,
772            response_subscribers: Arc::default(),
773            pending_subscribers: Arc::default(),
774            join_handle: Some(Arc::new(join_handle)),
775            execution_finished_subscribers: Arc::default(),
776        }))
777    }
778
779    /// Invokes the provided function wrapping a new [`rusqlite::Transaction`] that is committed automatically.
780    async fn transaction<F, T, E>(
781        &self,
782        mut func: F,
783        tx_type: TxType,
784        func_name: &'static str,
785    ) -> Result<T, E>
786    where
787        F: FnMut(&mut rusqlite::Transaction) -> Result<T, E> + Send + 'static,
788        T: Send + 'static,
789        E: From<RusqliteError> + Send + 'static,
790    {
791        let fn_res: Arc<std::sync::Mutex<Option<_>>> = Arc::default();
792        let (phytx_flush_sender, phytx_flush_receiver) = oneshot::channel();
793        let current_span = Span::current();
794        let thread_command_func = {
795            let fn_res = fn_res.clone();
796            ThreadCommand::LogicalTx(LogicalTx {
797                func: Box::new(move |tx| {
798                    let _guard = current_span.enter();
799                    let func_res = func(tx);
800                    let res = if func_res.is_ok() {
801                        Ok(())
802                    } else {
803                        Err(ShouldRollback)
804                    };
805                    // save result to be sent to the caller
806                    *fn_res.lock().unwrap() = Some(func_res);
807                    match tx_type {
808                        TxType::MultipleWrites => res, // Rollback, skipping this on LTX replay.
809                        TxType::Other => Ok(()), // Failed single write should not rollback the PhyTx
810                    }
811                }),
812                sent_at: Instant::now(),
813                func_name,
814                phytx_flush_sender,
815                priority: LtxPriority::High,
816            })
817        };
818        self.0
819            .command_tx
820            .send(thread_command_func)
821            .await
822            .map_err(|_send_err| RusqliteError::Close)?;
823
824        // Wait for commit / rollbeck, then get the retval from the mutex.
825        match phytx_flush_receiver.await {
826            Ok(Ok(())) => {
827                let mut guard = fn_res.lock().unwrap();
828                std::mem::take(guard.deref_mut()).expect("ltx must have been run at least once")
829            }
830            Ok(Err(CommitError(rusqlite_err))) => Err(E::from(rusqlite_err)),
831            Err(_) => Err(E::from(RusqliteError::Close)),
832        }
833    }
834
835    /// Invokes the provided function wrapping a new [`rusqlite::Transaction`] that is committed automatically.
836    async fn transaction_fire_forget<F, T, E>(&self, mut func: F, func_name: &'static str)
837    where
838        F: FnMut(&mut rusqlite::Transaction) -> Result<T, E> + Send + 'static,
839        T: Send + 'static + Default,
840        E: From<RusqliteError> + Send + 'static,
841    {
842        let (commit_ack_sender, _commit_ack_receiver) = oneshot::channel(); // Nobody is interested in receiving the result
843        let current_span = Span::current();
844        let thread_command_func = {
845            ThreadCommand::LogicalTx(LogicalTx {
846                func: Box::new(move |tx| {
847                    let _guard = current_span.enter();
848                    let _ = func(tx);
849                    Ok(()) // Never rollback
850                }),
851                sent_at: Instant::now(),
852                func_name,
853                phytx_flush_sender: commit_ack_sender,
854                priority: LtxPriority::Low,
855            })
856        };
857        let _ = self.0.command_tx.send(thread_command_func).await; // Ignore error when the channel is closed.
858    }
859
860    fn fetch_created_event(
861        conn: &Connection,
862        execution_id: &ExecutionId,
863    ) -> Result<CreateRequest, DbErrorRead> {
864        let mut stmt = conn.prepare(
865            "SELECT created_at, json_value FROM t_execution_log WHERE \
866            execution_id = :execution_id AND version = 0",
867        )?;
868        let (created_at, event) = stmt.query_row(
869            named_params! {
870                ":execution_id": execution_id.to_string(),
871            },
872            |row| {
873                let created_at = row.get("created_at")?;
874                let event = row
875                    .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
876                    .map_err(|serde| {
877                        error!("cannot deserialize `Created` event: {row:?} - `{serde:?}`");
878                        consistency_rusqlite("cannot deserialize `Created` event")
879                    })?;
880                Ok((created_at, event.0))
881            },
882        )?;
883        if let ExecutionRequest::Created {
884            ffqn,
885            params,
886            parent,
887            scheduled_at,
888            component_id,
889            deployment_id,
890            metadata,
891            scheduled_by,
892        } = event
893        {
894            Ok(CreateRequest {
895                created_at,
896                execution_id: execution_id.clone(),
897                ffqn,
898                params,
899                parent,
900                scheduled_at,
901                component_id,
902                deployment_id,
903                metadata,
904                scheduled_by,
905                paused: false,
906            })
907        } else {
908            error!("Row with version=0 must be a `Created` event - {event:?}");
909            Err(consistency_db_err("expected `Created` event").into())
910        }
911    }
912
913    fn check_expected_next_and_appending_version(
914        expected_version: &Version,
915        appending_version: &Version,
916    ) -> Result<(), DbErrorWrite> {
917        if *expected_version != *appending_version {
918            debug!(
919                "Version conflict - expected: {expected_version:?}, appending: {appending_version:?}"
920            );
921            return Err(DbErrorWrite::NonRetriable(
922                DbErrorWriteNonRetriable::VersionConflict {
923                    expected: expected_version.clone(),
924                    requested: appending_version.clone(),
925                },
926            ));
927        }
928        Ok(())
929    }
930
931    #[instrument(level = Level::DEBUG, skip_all, fields(execution_id = %req.execution_id))]
932    fn create_inner(
933        tx: &Transaction,
934        req: CreateRequest,
935    ) -> Result<(AppendResponse, AppendNotifier), DbErrorWrite> {
936        trace!("create_inner");
937
938        let version = Version::default();
939        let execution_id = req.execution_id.clone();
940        let execution_id_str = execution_id.to_string();
941        let ffqn = req.ffqn.clone();
942        let created_at = req.created_at;
943        let scheduled_at = req.scheduled_at;
944        let component_id = req.component_id.clone();
945        let deployment_id = req.deployment_id;
946        let paused = req.paused;
947        let event = ExecutionRequest::from(req);
948        let event_ser = serde_json::to_string(&event).map_err(|err| {
949            error!("Cannot serialize {event:?} - {err:?}");
950            DbErrorWriteNonRetriable::ValidationFailed("parameter serialization error".into())
951        })?;
952        tx.prepare(
953                "INSERT INTO t_execution_log (execution_id, created_at, version, json_value, variant, join_set_id ) \
954                VALUES (:execution_id, :created_at, :version, :json_value, :variant, :join_set_id)")
955        ?
956        .execute(named_params! {
957            ":execution_id": &execution_id_str,
958            ":created_at": created_at,
959            ":version": version.0,
960            ":json_value": event_ser,
961            ":variant": event.variant(),
962            ":join_set_id": event.join_set_id().map(std::string::ToString::to_string),
963        })
964        ?;
965        let pending_at = {
966            debug!("Creating with `Pending(`{scheduled_at:?}`)");
967            tx.prepare(
968                r"
969                INSERT INTO t_state (
970                    execution_id,
971                    is_top_level,
972                    corresponding_version,
973                    pending_expires_finished,
974                    ffqn,
975                    state,
976                    created_at,
977                    component_id_input_digest,
978                    component_type,
979                    deployment_id,
980                    updated_at,
981                    first_scheduled_at,
982                    intermittent_event_count
983                    )
984                VALUES (
985                    :execution_id,
986                    :is_top_level,
987                    :corresponding_version,
988                    :pending_expires_finished,
989                    :ffqn,
990                    :state,
991                    :created_at,
992                    :component_id_input_digest,
993                    :component_type,
994                    :deployment_id,
995                    CURRENT_TIMESTAMP,
996                    :first_scheduled_at,
997                    0
998                    )
999                ",
1000            )? // lifecycle defaults to 'active'
1001            .execute(named_params! {
1002                ":execution_id": execution_id.to_string(),
1003                ":is_top_level": execution_id.is_top_level(),
1004                ":corresponding_version": version.0,
1005                ":pending_expires_finished": scheduled_at,
1006                ":ffqn": ffqn.to_string(),
1007                ":state": STATE_PENDING_AT,
1008                ":created_at": created_at,
1009                ":component_id_input_digest": component_id.component_digest,
1010                ":component_type": component_id.component_type,
1011                ":deployment_id": deployment_id.to_string(),
1012                ":first_scheduled_at": scheduled_at,
1013            })?;
1014            AppendNotifier {
1015                pending_at: if paused {
1016                    None
1017                } else {
1018                    Some(NotifierPendingAt {
1019                        scheduled_at,
1020                        ffqn: ffqn.clone(),
1021                        component_input_digest: component_id.component_digest,
1022                    })
1023                },
1024                execution_finished: None,
1025                response: None,
1026            }
1027        };
1028        let mut next_version = Version::new(version.0 + 1);
1029        if paused {
1030            // Append the Paused event in the same transaction.
1031            let (v, _) = Self::append(
1032                tx,
1033                &execution_id,
1034                AppendRequest {
1035                    created_at,
1036                    event: ExecutionRequest::Paused,
1037                },
1038                next_version,
1039            )?;
1040            next_version = v;
1041        }
1042        Ok((next_version, pending_at))
1043    }
1044
1045    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %scheduled_at))]
1046    fn update_state_pending_after_response_appended(
1047        tx: &Transaction,
1048        execution_id: &ExecutionId,
1049        scheduled_at: DateTime<Utc>, // Changing to state PendingAt
1050        component_input_digest: ComponentDigest,
1051    ) -> Result<AppendNotifier, DbErrorWrite> {
1052        debug!("Setting t_state to Pending(`{scheduled_at:?}`) after response appended");
1053        let mut stmt = tx
1054            .prepare_cached(
1055                r"
1056                UPDATE t_state
1057                SET
1058                    pending_expires_finished = :pending_expires_finished,
1059                    state = :state,
1060                    updated_at = CURRENT_TIMESTAMP,
1061                    max_retries = NULL,
1062                    retry_exp_backoff_millis = NULL,
1063                    last_lock_version = NULL,
1064
1065                    join_set_id = NULL,
1066                    join_set_closing = NULL,
1067
1068                    result_kind = NULL
1069                WHERE execution_id = :execution_id
1070            ",
1071            )
1072            .map_err(|err| DbErrorGeneric::Uncategorized {
1073                reason: err.to_string().into(),
1074                context: SpanTrace::capture(),
1075                source: Some(Arc::new(err)),
1076                loc: Location::caller(),
1077            })?;
1078        let updated = stmt
1079            .execute(named_params! {
1080                ":execution_id": execution_id,
1081                ":pending_expires_finished": scheduled_at,
1082                ":state": STATE_PENDING_AT,
1083            })
1084            .map_err(|err| DbErrorGeneric::Uncategorized {
1085                reason: err.to_string().into(),
1086                context: SpanTrace::capture(),
1087                source: Some(Arc::new(err)),
1088                loc: Location::caller(),
1089            })?;
1090        if updated != 1 {
1091            return Err(DbErrorWrite::NotFound);
1092        }
1093        Ok(AppendNotifier {
1094            pending_at: Some(NotifierPendingAt {
1095                scheduled_at,
1096                ffqn: Self::fetch_created_event(tx, execution_id)?.ffqn,
1097                component_input_digest,
1098            }),
1099            execution_finished: None,
1100            response: None,
1101        })
1102    }
1103
1104    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, scheduled_at = %update.scheduled_at, %appending_version))]
1105    fn update_state_pending_after_event_appended(
1106        tx: &Transaction,
1107        execution_id: &ExecutionId,
1108        appending_version: &Version,
1109        update: PendingAfterEventUpdate,
1110    ) -> Result<(AppendResponse, AppendNotifier), DbErrorWrite> {
1111        let scheduled_at = update.scheduled_at;
1112        debug!("Setting t_state to Pending(`{scheduled_at:?}`) after event appended");
1113        let sql = r"
1114                UPDATE t_state
1115                SET
1116                    corresponding_version = :appending_version,
1117                    pending_expires_finished = :pending_expires_finished,
1118                    state = :state,
1119                    updated_at = CURRENT_TIMESTAMP,
1120                    intermittent_event_count = intermittent_event_count + :intermittent_delta,
1121
1122                    max_retries = NULL,
1123                    retry_exp_backoff_millis = NULL,
1124                    last_lock_version = NULL,
1125
1126                    join_set_id = NULL,
1127                    join_set_closing = NULL,
1128
1129                    result_kind = NULL
1130                WHERE execution_id = :execution_id;
1131            "; // `executor_id` and `run_id` are preserved for lock extension.
1132        let mut stmt = tx.prepare_cached(sql)?;
1133        let updated = stmt.execute(named_params! {
1134            ":execution_id": execution_id.to_string(),
1135            ":appending_version": appending_version.0,
1136            ":pending_expires_finished": scheduled_at,
1137            ":state": STATE_PENDING_AT,
1138            ":intermittent_delta": i32::from(update.intermittent_failure),
1139        })?;
1140        if updated != 1 {
1141            return Err(DbErrorWrite::NotFound);
1142        }
1143        Ok((
1144            appending_version.increment(),
1145            AppendNotifier {
1146                pending_at: Some(NotifierPendingAt {
1147                    scheduled_at,
1148                    ffqn: Self::fetch_created_event(tx, execution_id)?.ffqn,
1149                    component_input_digest: update.component_input_digest,
1150                }),
1151                execution_finished: None,
1152                response: None,
1153            },
1154        ))
1155    }
1156
1157    fn update_state_component_upgrade_finished_success(
1158        tx: &Transaction,
1159        execution_id: &ExecutionId,
1160        component_digest: &ComponentDigest,
1161        deployment_id: DeploymentId,
1162        appending_version: &Version,
1163    ) -> Result<AppendResponse, DbErrorWrite> {
1164        debug!("Updating t_state to component {component_digest}");
1165        let mut stmt = tx.prepare_cached(
1166            r"
1167                UPDATE t_state
1168                SET
1169                    corresponding_version = :appending_version,
1170                    updated_at = CURRENT_TIMESTAMP,
1171                    component_id_input_digest = :component_digest,
1172                    deployment_id = :deployment_id,
1173                    incompatible_digest = NULL
1174                WHERE execution_id = :execution_id;
1175            ",
1176        )?;
1177        let updated = stmt.execute(named_params! {
1178            ":execution_id": execution_id.to_string(),
1179            ":appending_version": appending_version.0,
1180            ":component_digest": component_digest,
1181            ":deployment_id": deployment_id.to_string(),
1182        })?;
1183        if updated != 1 {
1184            return Err(DbErrorWrite::NotFound);
1185        }
1186        Ok(appending_version.increment())
1187    }
1188
1189    fn update_state_component_upgrade_finished_failed(
1190        tx: &Transaction,
1191        execution_id: &ExecutionId,
1192        target_digest: &ComponentDigest,
1193        appending_version: &Version,
1194    ) -> Result<AppendResponse, DbErrorWrite> {
1195        debug!("Marking component {target_digest} incompatible after upgrade failure");
1196        let mut stmt = tx.prepare_cached(
1197            r"
1198                UPDATE t_state
1199                SET
1200                    corresponding_version = :appending_version,
1201                    updated_at = CURRENT_TIMESTAMP,
1202                    incompatible_digest = :target_digest
1203                WHERE execution_id = :execution_id;
1204            ",
1205        )?;
1206        let updated = stmt.execute(named_params! {
1207            ":execution_id": execution_id.to_string(),
1208            ":appending_version": appending_version.0,
1209            ":target_digest": target_digest,
1210        })?;
1211        if updated != 1 {
1212            return Err(DbErrorWrite::NotFound);
1213        }
1214        Ok(appending_version.increment())
1215    }
1216
1217    #[expect(clippy::too_many_arguments)]
1218    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version))]
1219    fn update_state_locked_get_intermittent_event_count(
1220        tx: &Transaction,
1221        execution_id: &ExecutionId,
1222        deployment_id: Option<DeploymentId>,
1223        component_digest: Option<&ComponentDigest>,
1224        executor_id: ExecutorId,
1225        run_id: RunId,
1226        lock_expires_at: DateTime<Utc>,
1227        appending_version: &Version,
1228        retry_config: ComponentRetryConfig,
1229    ) -> Result<u32, DbErrorWrite> {
1230        debug!("Setting t_state to Locked(`{lock_expires_at:?}`)");
1231        let backoff_millis =
1232            i64::try_from(retry_config.retry_exp_backoff.as_millis()).map_err(|err| {
1233                DbErrorGeneric::Uncategorized {
1234                    reason: "backoff too big".into(),
1235                    context: SpanTrace::capture(),
1236                    source: Some(Arc::new(err)),
1237                    loc: Location::caller(),
1238                }
1239            })?; // Keep equal to Postgres' BIGINT = i64
1240        let execution_id_str = execution_id.to_string();
1241        let mut stmt = tx.prepare_cached(
1242            r"
1243                UPDATE t_state
1244                SET
1245                    corresponding_version = :appending_version,
1246                    pending_expires_finished = :pending_expires_finished,
1247                    state = :state,
1248                    updated_at = CURRENT_TIMESTAMP,
1249                    deployment_id = COALESCE(:deployment_id, deployment_id),
1250                    component_id_input_digest = COALESCE(:component_id_input_digest, component_id_input_digest),
1251
1252                    max_retries = :max_retries,
1253                    retry_exp_backoff_millis = :retry_exp_backoff_millis,
1254                    last_lock_version = :appending_version,
1255                    executor_id = :executor_id,
1256                    run_id = :run_id,
1257
1258                    join_set_id = NULL,
1259                    join_set_closing = NULL,
1260
1261                    result_kind = NULL
1262                WHERE execution_id = :execution_id
1263                AND lifecycle = 'active'
1264            ",
1265        )?;
1266        let updated = stmt.execute(named_params! {
1267            ":execution_id": execution_id_str,
1268            ":appending_version": appending_version.0,
1269            ":pending_expires_finished": lock_expires_at,
1270            ":state": STATE_LOCKED,
1271            ":deployment_id": deployment_id, // no change if `None` due to `coalesce`
1272            ":component_id_input_digest": component_digest.cloned(), // no change if `None` due to `coalesce`
1273            ":max_retries": retry_config.max_retries,
1274            ":retry_exp_backoff_millis": backoff_millis,
1275            ":executor_id": executor_id.to_string(),
1276            ":run_id": run_id.to_string(),
1277        })?;
1278        if updated != 1 {
1279            return Err(DbErrorWrite::NotFound);
1280        }
1281
1282        // fetch intermittent event count from the just-inserted row.
1283        let intermittent_event_count = tx
1284            .prepare(
1285                "SELECT intermittent_event_count FROM t_state WHERE execution_id = :execution_id",
1286            )?
1287            .query_row(
1288                named_params! {
1289                    ":execution_id": execution_id_str,
1290                },
1291                |row| {
1292                    let intermittent_event_count = row.get("intermittent_event_count")?;
1293                    Ok(intermittent_event_count)
1294                },
1295            )?;
1296
1297        Ok(intermittent_event_count)
1298    }
1299
1300    /// Appending [`HistoryEvent::JoinNext`].
1301    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version))]
1302    fn update_state_blocked(
1303        tx: &Transaction,
1304        execution_id: &ExecutionId,
1305        appending_version: &Version,
1306        // BlockedByJoinSet fields
1307        join_set_id: &JoinSetId,
1308        lock_expires_at: DateTime<Utc>,
1309        join_set_closing: bool,
1310    ) -> Result<
1311        AppendResponse, // next version
1312        DbErrorWrite,
1313    > {
1314        debug!("Setting t_state to BlockedByJoinSet(`{join_set_id}`)");
1315        let execution_id_str = execution_id.to_string();
1316        let mut stmt = tx.prepare_cached(
1317            r"
1318                UPDATE t_state
1319                SET
1320                    corresponding_version = :appending_version,
1321                    pending_expires_finished = :pending_expires_finished,
1322                    state = :state,
1323                    updated_at = CURRENT_TIMESTAMP,
1324
1325                    max_retries = NULL,
1326                    retry_exp_backoff_millis = NULL,
1327                    last_lock_version = NULL,
1328
1329                    join_set_id = :join_set_id,
1330                    join_set_closing = :join_set_closing,
1331
1332                    result_kind = NULL
1333                WHERE execution_id = :execution_id
1334            ",
1335        )?;
1336        let updated = stmt.execute(named_params! {
1337            ":execution_id": execution_id_str,
1338            ":appending_version": appending_version.0,
1339            ":pending_expires_finished": lock_expires_at,
1340            ":state": STATE_BLOCKED_BY_JOIN_SET,
1341            ":join_set_id": join_set_id,
1342            ":join_set_closing": join_set_closing,
1343        })?;
1344        if updated != 1 {
1345            return Err(DbErrorWrite::NotFound);
1346        }
1347        Ok(appending_version.increment())
1348    }
1349
1350    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version))]
1351    fn update_state_finished(
1352        tx: &Transaction,
1353        execution_id: &ExecutionId,
1354        appending_version: &Version,
1355        // Finished fields
1356        finished_at: DateTime<Utc>,
1357        result_kind: PendingStateFinishedResultKind,
1358    ) -> Result<(), DbErrorWrite> {
1359        debug!("Setting t_state to Finished");
1360        let execution_id_str = execution_id.to_string();
1361        let mut stmt = tx.prepare_cached(
1362            r"
1363                UPDATE t_state
1364                SET
1365                    corresponding_version = :appending_version,
1366                    pending_expires_finished = :pending_expires_finished,
1367                    state = :state,
1368                    updated_at = CURRENT_TIMESTAMP,
1369
1370                    max_retries = NULL,
1371                    retry_exp_backoff_millis = NULL,
1372                    last_lock_version = NULL,
1373                    executor_id = NULL,
1374                    run_id = NULL,
1375
1376                    join_set_id = NULL,
1377                    join_set_closing = NULL,
1378
1379                    lifecycle = 'active',
1380                    result_kind = :result_kind
1381                WHERE execution_id = :execution_id
1382            ",
1383        )?;
1384
1385        let updated = stmt.execute(named_params! {
1386            ":execution_id": execution_id_str,
1387            ":appending_version": appending_version.0,
1388            ":pending_expires_finished": finished_at,
1389            ":state": STATE_FINISHED,
1390            ":result_kind": JsonWrapper(result_kind),
1391        })?;
1392        if updated != 1 {
1393            return Err(DbErrorWrite::NotFound);
1394        }
1395        Ok(())
1396    }
1397
1398    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version, %is_paused))]
1399    fn update_state_paused(
1400        tx: &Transaction,
1401        execution_id: &ExecutionId,
1402        appending_version: &Version,
1403        is_paused: bool,
1404    ) -> Result<AppendResponse, DbErrorWrite> {
1405        debug!(
1406            "Setting t_state to {}",
1407            if is_paused { "paused" } else { "unpaused" }
1408        );
1409        let execution_id_str = execution_id.to_string();
1410        let lifecycle = if is_paused {
1411            LIFECYCLE_PAUSED
1412        } else {
1413            LIFECYCLE_ACTIVE
1414        };
1415        let mut stmt = tx.prepare_cached(
1416            r"
1417                UPDATE t_state
1418                SET
1419                    corresponding_version = :appending_version,
1420                    lifecycle = :lifecycle,
1421                    updated_at = CURRENT_TIMESTAMP
1422                WHERE execution_id = :execution_id
1423            ",
1424        )?;
1425
1426        let updated = stmt.execute(named_params! {
1427            ":execution_id": execution_id_str,
1428            ":appending_version": appending_version.0,
1429            ":lifecycle": lifecycle,
1430        })?;
1431        if updated != 1 {
1432            return Err(DbErrorWrite::NotFound);
1433        }
1434        Ok(appending_version.increment())
1435    }
1436
1437    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version))]
1438    fn update_state_cancelling(
1439        tx: &Transaction,
1440        execution_id: &ExecutionId,
1441        appending_version: &Version,
1442    ) -> Result<AppendResponse, DbErrorWrite> {
1443        debug!("Setting t_state lifecycle to cancelling");
1444        let execution_id_str = execution_id.to_string();
1445        let mut stmt = tx.prepare_cached(
1446            r"
1447                UPDATE t_state
1448                SET
1449                    corresponding_version = :appending_version,
1450                    lifecycle = :lifecycle,
1451                    updated_at = CURRENT_TIMESTAMP
1452                WHERE execution_id = :execution_id
1453            ",
1454        )?;
1455
1456        let updated = stmt.execute(named_params! {
1457            ":execution_id": execution_id_str,
1458            ":appending_version": appending_version.0,
1459            ":lifecycle": LIFECYCLE_CANCELLING,
1460        })?;
1461        if updated != 1 {
1462            return Err(DbErrorWrite::NotFound);
1463        }
1464        Ok(appending_version.increment())
1465    }
1466
1467    // Upon appending new event to t_execution_log, copy the previous t_state with changed appending_version and created_at.
1468    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %appending_version))]
1469    fn bump_state_next_version(
1470        tx: &Transaction,
1471        execution_id: &ExecutionId,
1472        appending_version: &Version,
1473        delay_req: Option<DelayReq>,
1474    ) -> Result<AppendResponse /* next version */, DbErrorWrite> {
1475        debug!("update_index_version");
1476        let execution_id_str = execution_id.to_string();
1477        let mut stmt = tx.prepare_cached(
1478            r"
1479                UPDATE t_state
1480                SET
1481                    corresponding_version = :appending_version,
1482                    updated_at = CURRENT_TIMESTAMP
1483                WHERE execution_id = :execution_id
1484            ",
1485        )?;
1486        let updated = stmt.execute(named_params! {
1487            ":execution_id": execution_id_str,
1488            ":appending_version": appending_version.0,
1489        })?;
1490        if updated != 1 {
1491            return Err(DbErrorWrite::NotFound);
1492        }
1493        if let Some(DelayReq {
1494            join_set_id,
1495            delay_id,
1496            expires_at,
1497            paused,
1498        }) = delay_req
1499        {
1500            debug!("Inserting delay to `t_delay`");
1501            let mut stmt = tx.prepare_cached(
1502                "INSERT INTO t_delay (execution_id, join_set_id, delay_id, expires_at, is_paused) \
1503                VALUES \
1504                (:execution_id, :join_set_id, :delay_id, :expires_at, :is_paused)",
1505            )?;
1506            stmt.execute(named_params! {
1507                ":execution_id": execution_id_str,
1508                ":join_set_id": join_set_id.to_string(),
1509                ":delay_id": delay_id.to_string(),
1510                ":expires_at": expires_at,
1511                ":is_paused": paused,
1512            })?;
1513        }
1514        Ok(appending_version.increment())
1515    }
1516
1517    fn get_combined_state(
1518        tx: &Transaction,
1519        execution_id: &ExecutionId,
1520    ) -> Result<CombinedState, DbErrorRead> {
1521        let mut stmt = tx.prepare(
1522            r"
1523                SELECT
1524                    created_at, first_scheduled_at,
1525                    state, ffqn, component_id_input_digest, component_type, deployment_id,
1526                    corresponding_version, pending_expires_finished,
1527                    last_lock_version, executor_id, run_id,
1528                    join_set_id, join_set_closing,
1529                    result_kind, lifecycle
1530                    FROM t_state
1531                WHERE
1532                    execution_id = :execution_id
1533                ",
1534        )?;
1535        stmt.query_row(
1536            named_params! {
1537                ":execution_id": execution_id.to_string(),
1538            },
1539            |row| {
1540                CombinedState::new(
1541                    CombinedStateDTO {
1542                        execution_id: execution_id.clone(),
1543                        created_at: row.get("created_at")?,
1544                        first_scheduled_at: row.get("first_scheduled_at")?,
1545                        component_digest: row.get("component_id_input_digest")?,
1546                        component_type: row.get("component_type")?,
1547                        deployment_id: row.get("deployment_id")?,
1548                        state: row.get("state")?,
1549                        ffqn: row.get("ffqn")?,
1550                        pending_expires_finished: row
1551                            .get::<_, DateTime<Utc>>("pending_expires_finished")?,
1552                        last_lock_version: row
1553                            .get::<_, Option<VersionType>>("last_lock_version")?
1554                            .map(Version::new),
1555                        executor_id: row.get::<_, Option<ExecutorId>>("executor_id")?,
1556                        run_id: row.get::<_, Option<RunId>>("run_id")?,
1557                        join_set_id: row.get::<_, Option<JoinSetId>>("join_set_id")?,
1558                        join_set_closing: row.get::<_, Option<bool>>("join_set_closing")?,
1559                        result_kind: row
1560                            .get::<_, Option<JsonWrapper<PendingStateFinishedResultKind>>>(
1561                                "result_kind",
1562                            )?
1563                            .map(|wrapper| wrapper.0),
1564                        lifecycle: Lifecycle::from_column(&row.get::<_, String>("lifecycle")?)
1565                            .ok_or_else(|| consistency_rusqlite("invalid t_state.lifecycle"))?,
1566                    },
1567                    Version::new(row.get("corresponding_version")?),
1568                )
1569                .map_err(|e| from_generic_error(&e))
1570            },
1571        )
1572        .map_err(DbErrorRead::from)
1573    }
1574
1575    fn list_executions(
1576        read_tx: &Transaction,
1577        filter: &ListExecutionsFilter,
1578        pagination: &ExecutionListPagination,
1579    ) -> Result<Vec<ExecutionWithState>, RusqliteError> {
1580        #[derive(Debug)]
1581        struct StatementModifier<'a> {
1582            where_vec: Vec<String>,
1583            params: Vec<(&'static str, ToSqlOutput<'a>)>,
1584            limit: u32,
1585            limit_desc: bool,
1586        }
1587
1588        fn paginate<'a, T: Clone + rusqlite::ToSql + 'static>(
1589            pagination: &'a Pagination<Option<T>>,
1590            column: &str,
1591            filter: &ListExecutionsFilter,
1592        ) -> Result<StatementModifier<'a>, RusqliteError> {
1593            let mut where_vec: Vec<String> = vec![];
1594            let mut params: Vec<(&'static str, ToSqlOutput<'a>)> = vec![];
1595            let limit = pagination.length();
1596            let limit_desc = pagination.is_desc();
1597            match pagination {
1598                Pagination::NewerThan {
1599                    cursor: Some(cursor),
1600                    ..
1601                }
1602                | Pagination::OlderThan {
1603                    cursor: Some(cursor),
1604                    ..
1605                } => {
1606                    where_vec.push(format!("{column} {rel} :cursor", rel = pagination.rel()));
1607                    let cursor = cursor.to_sql().map_err(|err| {
1608                        error!("Possible program error - cannot convert cursor to sql - {err:?}");
1609                        RusqliteError::Generic {
1610                            reason: "cannot convert cursor to sql".into(),
1611                            context: SpanTrace::capture(),
1612                            source: Some(Arc::new(err)),
1613                            loc: Location::caller(),
1614                        }
1615                    })?;
1616                    params.push((":cursor", cursor));
1617                }
1618                _ => {}
1619            }
1620            if !filter.show_derived {
1621                where_vec.push("is_top_level=true".to_string());
1622            }
1623            Ok(StatementModifier {
1624                where_vec,
1625                params,
1626                limit: u32::from(limit),
1627                limit_desc,
1628            })
1629        }
1630
1631        let mut statement_mod = match pagination {
1632            ExecutionListPagination::CreatedBy(pagination) => {
1633                paginate(pagination, "created_at", filter)?
1634            }
1635            ExecutionListPagination::ExecutionId(pagination) => {
1636                paginate(pagination, "execution_id", filter)?
1637            }
1638        };
1639        if let Some(function_name_filter) = &filter.function_name_filter {
1640            let ffqn = ToSqlOutput::Owned(Value::from(function_name_filter.like_pattern()));
1641            statement_mod.where_vec.push("ffqn LIKE :ffqn".to_string());
1642            statement_mod.params.push((":ffqn", ffqn));
1643        }
1644        let like = |value: &str| format!("{value}%");
1645
1646        if filter.hide_finished {
1647            statement_mod
1648                .where_vec
1649                .push(format!("state != '{STATE_FINISHED}'"));
1650        }
1651        let prefix_temporary;
1652        if let Some(prefix) = &filter.execution_id_prefix {
1653            statement_mod
1654                .where_vec
1655                .push("execution_id LIKE :prefix".to_string());
1656            prefix_temporary = like(prefix);
1657            statement_mod.params.push((
1658                ":prefix",
1659                prefix_temporary
1660                    .to_sql()
1661                    .expect("string conversion never fails"),
1662            ));
1663        }
1664
1665        let component_digest_temporary;
1666        if let Some(componnet_digest) = &filter.component_digest {
1667            statement_mod
1668                .where_vec
1669                .push("component_id_input_digest = :component_digest".to_string());
1670            component_digest_temporary = componnet_digest.clone();
1671            let component_digest_sql = component_digest_temporary
1672                .to_sql()
1673                .expect("InputContentDigest conversion never fails");
1674            statement_mod
1675                .params
1676                .push((":component_digest", component_digest_sql));
1677        }
1678
1679        let deployment_id_temporary;
1680        if let Some(deployment_id) = filter.deployment_id {
1681            statement_mod
1682                .where_vec
1683                .push("deployment_id = :deployment_id".to_string());
1684            deployment_id_temporary = deployment_id;
1685            let deployment_id = deployment_id_temporary
1686                .to_sql()
1687                .expect("DeploymentId conversion never fails");
1688            statement_mod.params.push((":deployment_id", deployment_id));
1689        }
1690
1691        let state_filter_now_temporary;
1692        if !filter.state_filters.is_empty() {
1693            let conditions: Vec<String> = filter
1694                .state_filters
1695                .iter()
1696                .map(|state_filter| state_filter_to_sql(state_filter, ":state_filter_now", ""))
1697                .collect();
1698            statement_mod
1699                .where_vec
1700                .push(format!("({})", conditions.join(" OR ")));
1701            if let Some(now) = state_filters_now(&filter.state_filters) {
1702                state_filter_now_temporary = now;
1703                statement_mod.params.push((
1704                    ":state_filter_now",
1705                    state_filter_now_temporary
1706                        .to_sql()
1707                        .expect("DateTime conversion never fails"),
1708                ));
1709            }
1710        }
1711
1712        let where_str = if statement_mod.where_vec.is_empty() {
1713            String::new()
1714        } else {
1715            format!("WHERE {}", statement_mod.where_vec.join(" AND "))
1716        };
1717
1718        // Inner query: fetch rows with cursor-based ordering
1719        // Outer query: always return results in descending order
1720        let (inner_order, outer_order) = if statement_mod.limit_desc {
1721            ("DESC", "")
1722        } else {
1723            ("", "DESC")
1724        };
1725
1726        let inner_sql = format!(
1727            r"SELECT created_at, first_scheduled_at, component_id_input_digest, component_type, deployment_id,
1728            state, execution_id, ffqn, corresponding_version, pending_expires_finished,
1729            last_lock_version, executor_id, run_id,
1730            join_set_id, join_set_closing,
1731            result_kind, lifecycle
1732            FROM t_state {where_str} ORDER BY created_at {inner_order} LIMIT {limit}",
1733            limit = statement_mod.limit,
1734        );
1735
1736        let sql = if outer_order.is_empty() {
1737            inner_sql
1738        } else {
1739            format!("SELECT * FROM ({inner_sql}) AS sub ORDER BY created_at {outer_order}")
1740        };
1741        let vec: Vec<_> = read_tx
1742            .prepare(&sql)?
1743            .query_map::<_, &[(&'static str, ToSqlOutput)], _>(
1744                statement_mod
1745                    .params
1746                    .into_iter()
1747                    .collect::<Vec<_>>()
1748                    .as_ref(),
1749                |row| {
1750                    let combined_state = CombinedState::new(
1751                        CombinedStateDTO {
1752                            execution_id: row.get("execution_id")?,
1753                            created_at: row.get("created_at")?,
1754                            first_scheduled_at: row.get("first_scheduled_at")?,
1755                            component_digest: row.get("component_id_input_digest")?,
1756                            component_type: row.get("component_type")?,
1757                            deployment_id: row.get("deployment_id")?,
1758                            state: row.get("state")?,
1759                            ffqn: row.get("ffqn")?,
1760                            pending_expires_finished: row.get("pending_expires_finished")?,
1761                            executor_id: row.get::<_, Option<ExecutorId>>("executor_id")?,
1762
1763                            last_lock_version: row
1764                                .get::<_, Option<VersionType>>("last_lock_version")?
1765                                .map(Version::new),
1766                            run_id: row.get::<_, Option<RunId>>("run_id")?,
1767                            join_set_id: row.get::<_, Option<JoinSetId>>("join_set_id")?,
1768                            join_set_closing: row.get::<_, Option<bool>>("join_set_closing")?,
1769                            result_kind: row
1770                                .get::<_, Option<JsonWrapper<PendingStateFinishedResultKind>>>(
1771                                    "result_kind",
1772                                )?
1773                                .map(|wrapper| wrapper.0),
1774                            lifecycle: Lifecycle::from_column(&row.get::<_, String>("lifecycle")?)
1775                                .ok_or_else(|| consistency_rusqlite("invalid t_state.lifecycle"))?,
1776                        },
1777                        Version::new(row.get("corresponding_version")?),
1778                    )
1779                    .map_err(|e| from_generic_error(&e))?;
1780                    Ok(combined_state.execution_with_state)
1781                },
1782            )?
1783            .collect::<Vec<Result<_, _>>>()
1784            .into_iter()
1785            .filter_map(|row| match row {
1786                Ok(row) => Some(row),
1787                Err(err) => {
1788                    warn!("Skipping row - {err:?}");
1789                    None
1790                }
1791            })
1792            .collect();
1793
1794        Ok(vec)
1795    }
1796
1797    fn list_responses(
1798        tx: &Transaction,
1799        execution_id: &ExecutionId,
1800        pagination: Option<Pagination<u32>>,
1801    ) -> Result<Vec<ResponseWithCursor>, DbErrorRead> {
1802        // TODO: Add test
1803        let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![];
1804        let mut sql = "SELECT \
1805            r.id, r.seq, r.created_at, r.join_set_id,  r.delay_id, r.delay_success, r.child_execution_id, r.finished_version, l.json_value \
1806            FROM t_join_set_response r LEFT OUTER JOIN t_execution_log l ON r.child_execution_id = l.execution_id \
1807            WHERE \
1808            r.execution_id = :execution_id \
1809            AND ( r.finished_version = l.version OR r.child_execution_id IS NULL ) \
1810            "
1811        .to_string();
1812        let limit = match &pagination {
1813            Some(
1814                pagination @ (Pagination::NewerThan { cursor, .. }
1815                | Pagination::OlderThan { cursor, .. }),
1816            ) => {
1817                params.push((":cursor", Box::new(cursor)));
1818                write!(sql, " AND r.seq {rel} :cursor", rel = pagination.rel()).unwrap();
1819                Some(pagination.length())
1820            }
1821            None => None,
1822        };
1823        sql.push_str(" ORDER BY seq");
1824        let is_desc = pagination.as_ref().is_some_and(Pagination::is_desc);
1825        if is_desc {
1826            sql.push_str(" DESC");
1827        }
1828        if let Some(limit) = limit {
1829            write!(sql, " LIMIT {limit}").unwrap();
1830        }
1831        // Re-order to ascending for consistent oldest-to-newest results
1832        if is_desc {
1833            sql = format!("SELECT * FROM ({sql}) ORDER BY seq ASC");
1834        }
1835        params.push((":execution_id", Box::new(execution_id.to_string())));
1836        tx.prepare(&sql)?
1837            .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
1838                params
1839                    .iter()
1840                    .map(|(key, value)| (*key, value.as_ref()))
1841                    .collect::<Vec<_>>()
1842                    .as_ref(),
1843                Self::parse_response_with_cursor,
1844            )?
1845            .collect::<Result<Vec<_>, rusqlite::Error>>()
1846            .map_err(DbErrorRead::from)
1847    }
1848
1849    fn parse_response_with_cursor(
1850        row: &rusqlite::Row<'_>,
1851    ) -> Result<ResponseWithCursor, rusqlite::Error> {
1852        let id: i64 = row.get("id")?;
1853        let seq: u32 = row.get("seq")?;
1854        let created_at: DateTime<Utc> = row.get("created_at")?;
1855        let join_set_id = row.get::<_, JoinSetId>("join_set_id")?;
1856        let event = match (
1857            row.get::<_, Option<DelayId>>("delay_id")?,
1858            row.get::<_, Option<bool>>("delay_success")?,
1859            row.get::<_, Option<ExecutionIdDerived>>("child_execution_id")?,
1860            row.get::<_, Option<VersionType>>("finished_version")?,
1861            row.get::<_, Option<JsonWrapper<ExecutionRequest>>>("json_value")?,
1862        ) {
1863            (Some(delay_id), Some(delay_success), None, None, None) => {
1864                JoinSetResponse::DelayFinished {
1865                    delay_id,
1866                    result: delay_success.then_some(()).ok_or(()),
1867                }
1868            }
1869            (
1870                None,
1871                None,
1872                Some(child_execution_id),
1873                Some(finished_version),
1874                Some(JsonWrapper(ExecutionRequest::Finished { retval: result, .. })),
1875            ) => JoinSetResponse::ChildExecutionFinished {
1876                child_execution_id,
1877                finished_version: Version(finished_version),
1878                result,
1879            },
1880            (delay, delay_success, child, finished, result) => {
1881                error!(
1882                    "Invalid row in t_join_set_response {id} - {delay:?} {delay_success:?} {child:?} {finished:?} {:?}",
1883                    result.map(|it| it.0)
1884                );
1885                return Err(consistency_rusqlite("invalid row in t_join_set_response"));
1886            }
1887        };
1888        Ok(ResponseWithCursor {
1889            cursor: ResponseCursor(seq),
1890            event: JoinSetResponseEventOuter {
1891                event: JoinSetResponseEvent { join_set_id, event },
1892                created_at,
1893            },
1894        })
1895    }
1896
1897    /// `component_id` is used to construct the `Locked` event.
1898    /// If `update_component_digest_and_deployment_id` is set, execution state (`t_state`) will be updated to the component's digest and deployment id.
1899    /// This should be true in all cases except for `lock_pending_by_ffqns_auto`, where
1900    /// the digest in `t_state` is only updated after a successful execution upgrade.
1901    #[instrument(level = Level::TRACE, skip(tx))]
1902    #[expect(clippy::too_many_arguments)]
1903    fn lock_single_execution(
1904        tx: &Transaction,
1905        created_at: DateTime<Utc>,
1906        component_id: &ComponentId,
1907        update_component_digest_and_deployment_id: bool,
1908        deployment_id: DeploymentId,
1909        execution_id: &ExecutionId,
1910        run_id: RunId,
1911        appending_version: &Version,
1912        executor_id: ExecutorId,
1913        lock_expires_at: DateTime<Utc>,
1914        retry_config: ComponentRetryConfig,
1915    ) -> Result<LockedExecution, DbErrorWrite> {
1916        trace!("lock_single_execution");
1917        let combined_state = Self::get_combined_state(tx, execution_id)?;
1918        let context_component_digest = if update_component_digest_and_deployment_id {
1919            component_id.component_digest.clone()
1920        } else {
1921            combined_state.execution_with_state.component_digest.clone()
1922        };
1923        combined_state
1924            .execution_with_state
1925            .pending_state
1926            .can_append_lock(created_at, executor_id, run_id, lock_expires_at)?;
1927        let expected_version = combined_state.get_next_version_assert_not_finished();
1928        Self::check_expected_next_and_appending_version(&expected_version, appending_version)?;
1929
1930        // Append to `execution_log` table.
1931        let locked_event = Locked {
1932            component_id: component_id.clone(),
1933            deployment_id,
1934            executor_id,
1935            lock_expires_at,
1936            run_id,
1937            retry_config,
1938        };
1939        let event = ExecutionRequest::Locked(locked_event.clone());
1940        let event_ser = serde_json::to_string(&event).map_err(|err| {
1941            warn!("Cannot serialize {event:?} - {err:?}");
1942            DbErrorWriteNonRetriable::ValidationFailed("parameter serialization error".into())
1943        })?;
1944        let mut stmt = tx
1945            .prepare_cached(
1946                "INSERT INTO t_execution_log \
1947            (execution_id, created_at, json_value, version, variant) \
1948            VALUES \
1949            (:execution_id, :created_at, :json_value, :version, :variant)",
1950            )
1951            .map_err(|err| DbErrorGeneric::Uncategorized {
1952                reason: err.to_string().into(),
1953                context: SpanTrace::capture(),
1954                source: Some(Arc::new(err)),
1955                loc: Location::caller(),
1956            })?;
1957        stmt.execute(named_params! {
1958            ":execution_id": execution_id.to_string(),
1959            ":created_at": created_at,
1960            ":json_value": event_ser,
1961            ":version": appending_version.0,
1962            ":variant": event.variant(),
1963        })
1964        .map_err(|err| {
1965            DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::IllegalState {
1966                reason: "cannot lock".into(),
1967                context: SpanTrace::capture(),
1968                source: Some(Arc::new(err)),
1969                loc: Location::caller(),
1970            })
1971        })?;
1972
1973        let responses = Self::list_responses(tx, execution_id, None)?;
1974        trace!("Responses: {responses:?}");
1975
1976        // Update `t_state`
1977        let intermittent_event_count = Self::update_state_locked_get_intermittent_event_count(
1978            tx,
1979            execution_id,
1980            update_component_digest_and_deployment_id.then_some(deployment_id),
1981            update_component_digest_and_deployment_id.then_some(&component_id.component_digest),
1982            executor_id,
1983            run_id,
1984            lock_expires_at,
1985            appending_version,
1986            retry_config,
1987        )?;
1988        // Fetch event_history and `Created` event to construct the response.
1989        let mut events = tx
1990            .prepare(
1991                "SELECT json_value, version FROM t_execution_log WHERE \
1992                execution_id = :execution_id AND (variant = :variant1 OR variant = :variant2) \
1993                ORDER BY version",
1994            )?
1995            .query_map(
1996                named_params! {
1997                    ":execution_id": execution_id.to_string(),
1998                    ":variant1": DUMMY_CREATED.variant(),
1999                    ":variant2": DUMMY_HISTORY_EVENT.variant(),
2000                },
2001                |row| {
2002                    let created_at_fake = DateTime::from_timestamp_nanos(0); // not used, only the inner event and version
2003                    let event = row
2004                        .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2005                        .map_err(|serde| {
2006                            error!("Cannot deserialize {row:?} - {serde:?}");
2007                            consistency_rusqlite("cannot deserialize event")
2008                        })?
2009                        .0;
2010                    let version = Version(row.get("version")?);
2011
2012                    Ok(ExecutionEvent {
2013                        created_at: created_at_fake,
2014                        event,
2015                        backtrace_id: None,
2016                        version,
2017                    })
2018                },
2019            )?
2020            .collect::<Result<Vec<_>, _>>()?
2021            .into_iter()
2022            .collect::<VecDeque<_>>();
2023        let Some(ExecutionRequest::Created {
2024            ffqn,
2025            params,
2026            parent,
2027            metadata,
2028            ..
2029        }) = events.pop_front().map(|outer| outer.event)
2030        else {
2031            return Err(consistency_db_err("execution log must contain `Created` event").into());
2032        };
2033
2034        let event_history = events
2035            .into_iter()
2036            .map(|ExecutionEvent { event, version, .. }| {
2037                if let ExecutionRequest::HistoryEvent { event } = event {
2038                    Ok((event, version))
2039                } else {
2040                    Err(consistency_db_err(
2041                        "rows can only contain `Created` and `HistoryEvent` event kinds",
2042                    ))
2043                }
2044            })
2045            .collect::<Result<Vec<_>, _>>()?;
2046
2047        Ok(LockedExecution {
2048            execution_id: execution_id.clone(),
2049            metadata,
2050            component_digest: context_component_digest,
2051            next_version: appending_version.increment(),
2052            ffqn,
2053            params,
2054            event_history,
2055            responses,
2056            parent,
2057            intermittent_event_count,
2058            locked_event,
2059        })
2060    }
2061
2062    fn count_join_next(
2063        tx: &Transaction,
2064        execution_id: &ExecutionId,
2065        join_set_id: &JoinSetId,
2066    ) -> Result<u32, DbErrorRead> {
2067        let mut stmt = tx.prepare(
2068            "SELECT COUNT(*) as count FROM t_execution_log WHERE execution_id = :execution_id AND join_set_id = :join_set_id \
2069            AND history_event_type = :join_next",
2070        )?;
2071        Ok(stmt.query_row(
2072            named_params! {
2073                ":execution_id": execution_id.to_string(),
2074                ":join_set_id": join_set_id.to_string(),
2075                ":join_next": HISTORY_EVENT_TYPE_JOIN_NEXT,
2076            },
2077            |row| row.get::<_, u32>("count"),
2078        )?)
2079    }
2080
2081    fn nth_response(
2082        tx: &Transaction,
2083        execution_id: &ExecutionId,
2084        join_set_id: &JoinSetId,
2085        skip_rows: u32,
2086    ) -> Result<Option<ResponseWithCursor>, DbErrorRead> {
2087        // TODO: Add test
2088        tx
2089            .prepare(
2090                "SELECT r.id, r.seq, r.created_at, r.join_set_id, \
2091                    r.delay_id, r.delay_success, \
2092                    r.child_execution_id, r.finished_version, l.json_value \
2093                    FROM t_join_set_response r LEFT OUTER JOIN t_execution_log l ON r.child_execution_id = l.execution_id \
2094                    WHERE \
2095                    r.execution_id = :execution_id AND r.join_set_id = :join_set_id AND \
2096                    (
2097                    r.finished_version = l.version \
2098                    OR \
2099                    r.child_execution_id IS NULL \
2100                    ) \
2101                    ORDER BY seq \
2102                    LIMIT 1 OFFSET :offset",
2103            )
2104            ?
2105            .query_row(
2106                named_params! {
2107                    ":execution_id": execution_id.to_string(),
2108                    ":join_set_id": join_set_id.to_string(),
2109                    ":offset": skip_rows,
2110                },
2111                Self::parse_response_with_cursor,
2112            )
2113            .optional()
2114            .map_err(DbErrorRead::from)
2115    }
2116
2117    #[instrument(level = Level::TRACE, skip_all, fields(%execution_id, %appending_version))]
2118    #[expect(clippy::needless_return)]
2119    fn append(
2120        tx: &Transaction,
2121        execution_id: &ExecutionId,
2122        req: AppendRequest,
2123        appending_version: Version,
2124    ) -> Result<(AppendResponse, AppendNotifier), DbErrorWrite> {
2125        if matches!(req.event, ExecutionRequest::Created { .. }) {
2126            return Err(DbErrorWrite::NonRetriable(
2127                DbErrorWriteNonRetriable::ValidationFailed(
2128                    "cannot append `Created` event - use `create` instead".into(),
2129                ),
2130            ));
2131        }
2132        if let AppendRequest {
2133            event:
2134                ExecutionRequest::Locked(Locked {
2135                    component_id,
2136                    deployment_id,
2137                    executor_id,
2138                    run_id,
2139                    lock_expires_at,
2140                    retry_config,
2141                }),
2142            created_at,
2143        } = req
2144        {
2145            return Self::lock_single_execution(
2146                tx,
2147                created_at,
2148                &component_id,
2149                true,
2150                deployment_id,
2151                execution_id,
2152                run_id,
2153                &appending_version,
2154                executor_id,
2155                lock_expires_at,
2156                retry_config,
2157            )
2158            .map(|locked_execution| (locked_execution.next_version, AppendNotifier::default()));
2159        }
2160
2161        let combined_state = Self::get_combined_state(tx, execution_id)?;
2162        if combined_state
2163            .execution_with_state
2164            .pending_state
2165            .is_finished()
2166        {
2167            debug!("Execution is already finished");
2168            return Err(DbErrorWrite::NonRetriable(
2169                DbErrorWriteNonRetriable::AlreadyFinished,
2170            ));
2171        }
2172
2173        Self::check_expected_next_and_appending_version(
2174            &combined_state.get_next_version_assert_not_finished(),
2175            &appending_version,
2176        )?;
2177        let event_ser = serde_json::to_string(&req.event).map_err(|err| {
2178            error!("Cannot serialize {:?} - {err:?}", req.event);
2179            DbErrorWriteNonRetriable::ValidationFailed("parameter serialization error".into())
2180        })?;
2181
2182        let mut stmt = tx.prepare(
2183                    "INSERT INTO t_execution_log (execution_id, created_at, json_value, version, variant, join_set_id) \
2184                    VALUES (:execution_id, :created_at, :json_value, :version, :variant, :join_set_id)")
2185                    ?;
2186        stmt.execute(named_params! {
2187            ":execution_id": execution_id.to_string(),
2188            ":created_at": req.created_at,
2189            ":json_value": event_ser,
2190            ":version": appending_version.0,
2191            ":variant": req.event.variant(),
2192            ":join_set_id": req.event.join_set_id().map(std::string::ToString::to_string),
2193        })?;
2194        // Calculate current pending state
2195
2196        match &req.event {
2197            ExecutionRequest::Created { .. } => {
2198                unreachable!("handled in the caller")
2199            }
2200
2201            ExecutionRequest::Locked { .. } => {
2202                unreachable!("handled above")
2203            }
2204
2205            ExecutionRequest::TemporarilyFailed {
2206                backoff_expires_at, ..
2207            }
2208            | ExecutionRequest::TemporarilyTimedOut {
2209                backoff_expires_at, ..
2210            } => {
2211                let (next_version, notifier) = Self::update_state_pending_after_event_appended(
2212                    tx,
2213                    execution_id,
2214                    &appending_version,
2215                    PendingAfterEventUpdate {
2216                        scheduled_at: *backoff_expires_at,
2217                        intermittent_failure: true,
2218                        component_input_digest: combined_state
2219                            .execution_with_state
2220                            .component_digest,
2221                    },
2222                )?;
2223                return Ok((next_version, notifier));
2224            }
2225
2226            ExecutionRequest::Unlocked(unlocked) => {
2227                match &combined_state.execution_with_state.pending_state {
2228                    PendingState::PendingAt(_) => {
2229                        return Err(DbErrorWrite::NonRetriable(
2230                            DbErrorWriteNonRetriable::UnlockedCannotBeAppended("pending"),
2231                        ));
2232                    }
2233                    PendingState::Locked(_) => {
2234                        let (next_version, notifier) =
2235                            Self::update_state_pending_after_event_appended(
2236                                tx,
2237                                execution_id,
2238                                &appending_version,
2239                                PendingAfterEventUpdate {
2240                                    scheduled_at: unlocked.unlocked_at,
2241                                    intermittent_failure: false,
2242                                    component_input_digest: combined_state
2243                                        .execution_with_state
2244                                        .component_digest,
2245                                },
2246                            )?;
2247                        return Ok((next_version, notifier));
2248                    }
2249                    PendingState::BlockedByJoinSet(_) => {
2250                        return Err(DbErrorWrite::NonRetriable(
2251                            DbErrorWriteNonRetriable::UnlockedCannotBeAppended("blocked"),
2252                        ));
2253                    }
2254                    PendingState::Paused(_) => {
2255                        return Err(DbErrorWrite::NonRetriable(
2256                            DbErrorWriteNonRetriable::UnlockedCannotBeAppended("paused"),
2257                        ));
2258                    }
2259                    PendingState::Cancelling(_) => {
2260                        return Err(DbErrorWrite::NonRetriable(
2261                            DbErrorWriteNonRetriable::UnlockedCannotBeAppended("cancelling"),
2262                        ));
2263                    }
2264                    PendingState::Finished(_) => {
2265                        unreachable!("handled above");
2266                    }
2267                }
2268            }
2269
2270            ExecutionRequest::ComponentUpgradeFinished {
2271                component_digest,
2272                deployment_id,
2273                outcome,
2274            } => {
2275                let next_version = match outcome {
2276                    ComponentUpgradeOutcome::Success { .. } => {
2277                        Self::update_state_component_upgrade_finished_success(
2278                            tx,
2279                            execution_id,
2280                            component_digest,
2281                            *deployment_id,
2282                            &appending_version,
2283                        )?
2284                    }
2285                    ComponentUpgradeOutcome::Failed { .. } => {
2286                        Self::update_state_component_upgrade_finished_failed(
2287                            tx,
2288                            execution_id,
2289                            component_digest,
2290                            &appending_version,
2291                        )?
2292                    }
2293                };
2294                return Ok((next_version, AppendNotifier::default()));
2295            }
2296
2297            ExecutionRequest::Paused => {
2298                match &combined_state.execution_with_state.pending_state {
2299                    PendingState::Finished { .. } => {
2300                        unreachable!("handled above");
2301                    }
2302                    PendingState::Locked(..) => {
2303                        return Err(DbErrorWriteNonRetriable::IllegalState {
2304                            reason:
2305                                "cannot append Paused event when execution is locked; use pause_execution"
2306                                    .into(),
2307                            context: SpanTrace::capture(),
2308                            source: None,
2309                            loc: Location::caller(),
2310                        }
2311                        .into());
2312                    }
2313                    PendingState::Paused(..) => {
2314                        return Err(DbErrorWriteNonRetriable::IllegalState {
2315                            reason: "cannot pause, execution is already paused".into(),
2316                            context: SpanTrace::capture(),
2317                            source: None,
2318                            loc: Location::caller(),
2319                        }
2320                        .into());
2321                    }
2322                    PendingState::Cancelling(..) => {
2323                        return Err(DbErrorWriteNonRetriable::IllegalState {
2324                            reason: "cannot pause, execution is cancelling".into(),
2325                            context: SpanTrace::capture(),
2326                            source: None,
2327                            loc: Location::caller(),
2328                        }
2329                        .into());
2330                    }
2331                    _ => {}
2332                }
2333                let next_version =
2334                    Self::update_state_paused(tx, execution_id, &appending_version, true)?;
2335                return Ok((next_version, AppendNotifier::default()));
2336            }
2337
2338            ExecutionRequest::Unpaused => {
2339                if !combined_state
2340                    .execution_with_state
2341                    .pending_state
2342                    .is_paused()
2343                {
2344                    return Err(DbErrorWriteNonRetriable::IllegalState {
2345                        reason: "cannot unpause, execution is not paused".into(),
2346                        context: SpanTrace::capture(),
2347                        source: None,
2348                        loc: Location::caller(),
2349                    }
2350                    .into());
2351                }
2352                let next_version =
2353                    Self::update_state_paused(tx, execution_id, &appending_version, false)?;
2354                return Ok((next_version, AppendNotifier::default()));
2355            }
2356
2357            ExecutionRequest::CancellationRequested => {
2358                match &combined_state.execution_with_state.pending_state {
2359                    PendingState::Finished { .. } => {
2360                        unreachable!("handled above");
2361                    }
2362                    PendingState::Cancelling(..) => {
2363                        return Err(DbErrorWriteNonRetriable::IllegalState {
2364                            reason: "cannot append CancellationRequested event, execution is already cancelling".into(),
2365                            context: SpanTrace::capture(),
2366                            source: None,
2367                            loc: Location::caller(),
2368                        }
2369                        .into());
2370                    }
2371                    // Paused executions are never running; a locked activity keeps its lock
2372                    // until teardown or lease expiry; a locked workflow's run is fenced by
2373                    // this event's version bump.
2374                    PendingState::PendingAt(..)
2375                    | PendingState::BlockedByJoinSet(..)
2376                    | PendingState::Paused(..)
2377                    | PendingState::Locked(..) => {}
2378                }
2379                let next_version =
2380                    Self::update_state_cancelling(tx, execution_id, &appending_version)?;
2381                return Ok((next_version, AppendNotifier::default()));
2382            }
2383
2384            ExecutionRequest::Finished { retval, .. } => {
2385                Self::update_state_finished(
2386                    tx,
2387                    execution_id,
2388                    &appending_version,
2389                    req.created_at,
2390                    PendingStateFinishedResultKind::from(retval),
2391                )?;
2392                return Ok((
2393                    appending_version,
2394                    AppendNotifier {
2395                        pending_at: None,
2396                        execution_finished: Some(NotifierExecutionFinished {
2397                            execution_id: execution_id.clone(),
2398                            retval: retval.clone(),
2399                        }),
2400                        response: None,
2401                    },
2402                ));
2403            }
2404
2405            ExecutionRequest::HistoryEvent {
2406                event:
2407                    HistoryEvent::JoinSetCreate { .. }
2408                    | HistoryEvent::JoinSetRequest {
2409                        request: JoinSetRequest::ChildExecutionRequest { .. },
2410                        ..
2411                    }
2412                    | HistoryEvent::Persist { .. }
2413                    | HistoryEvent::Schedule { .. }
2414                    | HistoryEvent::Stub { .. }
2415                    | HistoryEvent::JoinNextTooMany { .. }
2416                    | HistoryEvent::JoinNextTry { .. },
2417            } => {
2418                return Ok((
2419                    Self::bump_state_next_version(tx, execution_id, &appending_version, None)?,
2420                    AppendNotifier::default(),
2421                ));
2422            }
2423
2424            ExecutionRequest::HistoryEvent {
2425                event:
2426                    HistoryEvent::JoinSetRequest {
2427                        join_set_id,
2428                        request:
2429                            JoinSetRequest::DelayRequest {
2430                                delay_id,
2431                                expires_at,
2432                                paused,
2433                                ..
2434                            },
2435                    },
2436            } => {
2437                return Ok((
2438                    Self::bump_state_next_version(
2439                        tx,
2440                        execution_id,
2441                        &appending_version,
2442                        Some(DelayReq {
2443                            join_set_id: join_set_id.clone(),
2444                            delay_id: delay_id.clone(),
2445                            expires_at: *expires_at,
2446                            paused: *paused,
2447                        }),
2448                    )?,
2449                    AppendNotifier::default(),
2450                ));
2451            }
2452
2453            ExecutionRequest::HistoryEvent {
2454                event:
2455                    HistoryEvent::JoinNext {
2456                        join_set_id,
2457                        run_expires_at,
2458                        closing,
2459                        requested_ffqn: _,
2460                    },
2461            } => {
2462                // Did the response arrive already?
2463                let join_next_count = Self::count_join_next(tx, execution_id, join_set_id)?;
2464                let nth_response =
2465                    Self::nth_response(tx, execution_id, join_set_id, join_next_count - 1)?; // Skip n-1 rows
2466                trace!("join_next_count: {join_next_count}, nth_response: {nth_response:?}");
2467                assert!(join_next_count > 0);
2468                if let Some(ResponseWithCursor {
2469                    event:
2470                        JoinSetResponseEventOuter {
2471                            created_at: nth_created_at,
2472                            ..
2473                        },
2474                    cursor: _,
2475                }) = nth_response
2476                {
2477                    let scheduled_at = max(*run_expires_at, nth_created_at); // No need to block
2478                    let (next_version, notifier) = Self::update_state_pending_after_event_appended(
2479                        tx,
2480                        execution_id,
2481                        &appending_version,
2482                        PendingAfterEventUpdate {
2483                            scheduled_at,
2484                            intermittent_failure: false,
2485                            component_input_digest: combined_state
2486                                .execution_with_state
2487                                .component_digest,
2488                        },
2489                    )?;
2490                    return Ok((next_version, notifier));
2491                }
2492                return Ok((
2493                    Self::update_state_blocked(
2494                        tx,
2495                        execution_id,
2496                        &appending_version,
2497                        join_set_id,
2498                        *run_expires_at,
2499                        *closing,
2500                    )?,
2501                    AppendNotifier::default(),
2502                ));
2503            }
2504        }
2505    }
2506
2507    fn append_response(
2508        tx: &Transaction,
2509        execution_id: &ExecutionId,
2510        event: JoinSetResponseEventOuter,
2511    ) -> Result<AppendNotifier, DbErrorWrite> {
2512        // SQLite has a single global writer, so the read-modify-write of MAX(seq)+1
2513        // is serialized without extra locking; `seq` is contiguous and 1-based per execution.
2514        let mut stmt = tx.prepare(
2515            "INSERT INTO t_join_set_response (execution_id, created_at, join_set_id, delay_id, delay_success, child_execution_id, finished_version, seq) \
2516                    VALUES (:execution_id, :created_at, :join_set_id, :delay_id, :delay_success, :child_execution_id, :finished_version, \
2517                    (SELECT COALESCE(MAX(seq), 0) + 1 FROM t_join_set_response WHERE execution_id = :execution_id)) \
2518                    RETURNING seq",
2519        )?;
2520        let join_set_id = &event.event.join_set_id;
2521        let (delay_id, delay_success) = match &event.event.event {
2522            JoinSetResponse::DelayFinished { delay_id, result } => {
2523                (Some(delay_id.to_string()), Some(result.is_ok()))
2524            }
2525            JoinSetResponse::ChildExecutionFinished { .. } => (None, None),
2526        };
2527        let (child_execution_id, finished_version) = match &event.event.event {
2528            JoinSetResponse::ChildExecutionFinished {
2529                child_execution_id,
2530                finished_version,
2531                result: _,
2532            } => (
2533                Some(child_execution_id.to_string()),
2534                Some(finished_version.0),
2535            ),
2536            JoinSetResponse::DelayFinished { .. } => (None, None),
2537        };
2538
2539        let seq = stmt.query_row(
2540            named_params! {
2541                ":execution_id": execution_id.to_string(),
2542                ":created_at": event.created_at,
2543                ":join_set_id": join_set_id.to_string(),
2544                ":delay_id": delay_id,
2545                ":delay_success": delay_success,
2546                ":child_execution_id": child_execution_id,
2547                ":finished_version": finished_version,
2548            },
2549            |row| row.get::<_, u32>(0),
2550        )?;
2551        let cursor = ResponseCursor(seq);
2552
2553        // if the execution is going to be unblocked by this response...
2554        let combined_state = Self::get_combined_state(tx, execution_id)?;
2555        debug!("previous_pending_state: {combined_state:?}");
2556        // A cancelling execution is finished by the cancellation driver; it is never
2557        // picked up by executors again, so do not unblock or notify it.
2558        let mut notifier = if let PendingStateMerged::BlockedByJoinSet {
2559            state:
2560                PendingStateBlockedByJoinSet {
2561                    join_set_id: found_join_set_id,
2562                    lock_expires_at, // Set to a future time if the worker is keeping the execution warm waiting for the result.
2563                    closing: _,
2564                },
2565            lifecycle: Lifecycle::Active | Lifecycle::Paused,
2566        } =
2567            PendingStateMerged::from(combined_state.execution_with_state.pending_state)
2568            && *join_set_id == found_join_set_id
2569        {
2570            // PendingAt should be set to current time if called from expired_timers_watcher,
2571            // or to a future time if the execution is hot.
2572            let scheduled_at = max(lock_expires_at, event.created_at);
2573            // TODO: Add diff test
2574            // Unblock the state.
2575            Self::update_state_pending_after_response_appended(
2576                tx,
2577                execution_id,
2578                scheduled_at,
2579                combined_state.execution_with_state.component_digest,
2580            )?
2581        } else {
2582            AppendNotifier::default()
2583        };
2584        if let JoinSetResponseEvent {
2585            join_set_id,
2586            event:
2587                JoinSetResponse::DelayFinished {
2588                    delay_id,
2589                    result: _,
2590                },
2591        } = &event.event
2592        {
2593            debug!(%join_set_id, %delay_id, "Deleting from `t_delay`");
2594            let mut stmt =
2595                tx.prepare_cached("DELETE FROM t_delay WHERE execution_id = :execution_id AND join_set_id = :join_set_id AND delay_id = :delay_id")
2596                ?;
2597            stmt.execute(named_params! {
2598                ":execution_id": execution_id.to_string(),
2599                ":join_set_id": join_set_id.to_string(),
2600                ":delay_id": delay_id.to_string(),
2601            })?;
2602        }
2603        notifier.response = Some((execution_id.clone(), ResponseWithCursor { cursor, event }));
2604        Ok(notifier)
2605    }
2606
2607    fn append_backtrace(
2608        tx: &Transaction,
2609        backtrace_info: &BacktraceInfo,
2610    ) -> Result<usize, DbErrorWrite> {
2611        let backtrace_hash = backtrace_info.wasm_backtrace.hash();
2612
2613        tx.prepare("INSERT OR IGNORE INTO t_wasm_backtrace (backtrace_hash, wasm_backtrace) VALUES (:backtrace_hash, :wasm_backtrace)")?
2614        .execute(named_params! {
2615            ":backtrace_hash": backtrace_hash,
2616            ":wasm_backtrace": JsonWrapper(&backtrace_info.wasm_backtrace)
2617        })?;
2618
2619        tx.prepare(
2620                "INSERT OR IGNORE INTO t_execution_backtrace (execution_id, component_id, version_min_including, version_max_excluding, backtrace_hash) \
2621                    VALUES (:execution_id, :component_id, :version_min_including, :version_max_excluding, :backtrace_hash)",
2622        )?
2623        .execute(named_params! {
2624            ":execution_id": backtrace_info.execution_id.to_string(),
2625            ":component_id": JsonWrapper(&backtrace_info.component_id),
2626            ":version_min_including": backtrace_info.version_min_including.0,
2627            ":version_max_excluding": backtrace_info.version_max_excluding.0,
2628            ":backtrace_hash": backtrace_hash,
2629        })
2630        .map_err(DbErrorWrite::from)
2631    }
2632
2633    fn append_log(tx: &Transaction, row: &LogInfoAppendRow) -> Result<(), DbErrorWrite> {
2634        let mut stmt = tx.prepare(
2635            "INSERT INTO t_log (
2636            execution_id,
2637            run_id,
2638            created_at,
2639            level,
2640            message,
2641            stream_type,
2642            payload
2643        ) VALUES (
2644            :execution_id,
2645            :run_id,
2646            :created_at,
2647            :level,
2648            :message,
2649            :stream_type,
2650            :payload
2651        )",
2652        )?;
2653
2654        match &row.log_entry {
2655            LogEntry::Log {
2656                created_at,
2657                level,
2658                message,
2659            } => {
2660                stmt.execute(named_params! {
2661                    ":execution_id": row.execution_id,
2662                    ":run_id": row.run_id,
2663                    ":created_at": created_at,
2664                    ":level": *level as u8,
2665                    ":message": message,
2666                    ":stream_type": Option::<u8>::None,
2667                    ":payload": Option::<Vec<u8>>::None,
2668                })?;
2669            }
2670            LogEntry::Stream {
2671                created_at,
2672                payload,
2673                stream_type,
2674            } => {
2675                stmt.execute(named_params! {
2676                    ":execution_id": row.execution_id,
2677                    ":run_id": row.run_id,
2678                    ":created_at": created_at,
2679                    ":level": Option::<u8>::None,
2680                    ":message": Option::<String>::None,
2681                    ":stream_type": *stream_type as u8,
2682                    ":payload": payload,
2683                })?;
2684            }
2685        }
2686
2687        Ok(())
2688    }
2689
2690    fn get(
2691        tx: &Transaction,
2692        execution_id: &ExecutionId,
2693    ) -> Result<concepts::storage::ExecutionLog, DbErrorRead> {
2694        let mut stmt = tx.prepare(
2695            "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2696                        execution_id = :execution_id ORDER BY version",
2697        )?;
2698        let events = stmt
2699            .query_map(
2700                named_params! {
2701                    ":execution_id": execution_id.to_string(),
2702                },
2703                |row| {
2704                    let created_at = row.get("created_at")?;
2705                    let event = row
2706                        .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2707                        .map_err(|serde| {
2708                            error!("Cannot deserialize {row:?} - {serde:?}");
2709                            consistency_rusqlite("cannot deserialize event")
2710                        })?
2711                        .0;
2712                    let version = Version(row.get("version")?);
2713
2714                    Ok(ExecutionEvent {
2715                        created_at,
2716                        event,
2717                        backtrace_id: None,
2718                        version,
2719                    })
2720                },
2721            )?
2722            .collect::<Result<Vec<_>, _>>()?;
2723        if events.is_empty() {
2724            return Err(DbErrorRead::NotFound);
2725        }
2726        let combined_state = Self::get_combined_state(tx, execution_id)?;
2727        let responses = Self::list_responses(tx, execution_id, None)?;
2728        Ok(concepts::storage::ExecutionLog {
2729            execution_id: execution_id.clone(),
2730            events,
2731            responses,
2732            next_version: combined_state.get_next_version_or_finished(), // In case of finished, this will be the already last version
2733            pending_state: combined_state.execution_with_state.pending_state,
2734            component_digest: combined_state.execution_with_state.component_digest,
2735            component_type: combined_state.execution_with_state.component_type,
2736            deployment_id: combined_state.execution_with_state.deployment_id,
2737        })
2738    }
2739
2740    fn get_max_version(
2741        tx: &Transaction,
2742        execution_id: &ExecutionId,
2743    ) -> Result<Version, DbErrorRead> {
2744        tx.prepare("SELECT MAX(version) FROM t_execution_log WHERE execution_id = :execution_id")?
2745            .query_row(
2746                named_params! { ":execution_id": execution_id.to_string() },
2747                |row| row.get::<_, Option<VersionType>>(0),
2748            )
2749            .map(|v| v.map(Version::new).ok_or(DbErrorRead::NotFound))
2750            .map_err(DbErrorRead::from)
2751            .flatten()
2752    }
2753
2754    fn get_max_response_cursor(
2755        tx: &Transaction,
2756        execution_id: &ExecutionId,
2757    ) -> Result<ResponseCursor, DbErrorRead> {
2758        let max_cursor = tx
2759            .prepare("SELECT MAX(seq) FROM t_join_set_response WHERE execution_id = :execution_id")?
2760            .query_row(
2761                named_params! { ":execution_id": execution_id.to_string() },
2762                |row| row.get::<_, Option<u32>>(0),
2763            )?;
2764        // Assume the execution exists and has no responses
2765        let max_cursor = max_cursor.unwrap_or_default();
2766        Ok(ResponseCursor(max_cursor))
2767    }
2768
2769    fn list_execution_events(
2770        tx: &Transaction,
2771        execution_id: &ExecutionId,
2772        pagination: Pagination<VersionType>,
2773        include_backtrace_id: bool,
2774    ) -> Result<Vec<ExecutionEvent>, DbErrorRead> {
2775        let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![];
2776        params.push((":execution_id", Box::new(execution_id.to_string())));
2777
2778        let (cursor, length, rel, is_desc) = match &pagination {
2779            Pagination::NewerThan {
2780                cursor,
2781                length,
2782                including_cursor,
2783            } => (
2784                *cursor,
2785                *length,
2786                if *including_cursor { ">=" } else { ">" },
2787                false,
2788            ),
2789            Pagination::OlderThan {
2790                cursor,
2791                length,
2792                including_cursor,
2793            } => (
2794                *cursor,
2795                *length,
2796                if *including_cursor { "<=" } else { "<" },
2797                true,
2798            ),
2799        };
2800        params.push((":cursor", Box::new(cursor)));
2801
2802        let base_select = if include_backtrace_id {
2803            format!(
2804                "SELECT
2805                    log.created_at,
2806                    log.json_value,
2807                    log.version as version,
2808                    bt.version_min_including AS backtrace_id
2809                FROM
2810                    t_execution_log AS log
2811                LEFT OUTER JOIN
2812                    t_execution_backtrace AS bt ON log.execution_id = bt.execution_id
2813                                    AND log.version >= bt.version_min_including
2814                                    AND log.version < bt.version_max_excluding
2815                WHERE
2816                    log.execution_id = :execution_id
2817                    AND log.version {rel} :cursor"
2818            )
2819        } else {
2820            format!(
2821                "SELECT
2822                    created_at, json_value, NULL as backtrace_id, version
2823                FROM t_execution_log WHERE
2824                    execution_id = :execution_id AND version {rel} :cursor"
2825            )
2826        };
2827
2828        let order = if is_desc { "DESC" } else { "ASC" };
2829        let mut sql = format!("{base_select} ORDER BY version {order} LIMIT {length}");
2830
2831        // Re-order to ascending for consistent oldest-to-newest results
2832        if is_desc {
2833            sql = format!("SELECT * FROM ({sql}) ORDER BY version ASC");
2834        }
2835
2836        tx.prepare(&sql)?
2837            .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
2838                params
2839                    .iter()
2840                    .map(|(key, value)| (*key, value.as_ref()))
2841                    .collect::<Vec<_>>()
2842                    .as_ref(),
2843                |row| {
2844                    let created_at = row.get("created_at")?;
2845                    let backtrace_id = row
2846                        .get::<_, Option<VersionType>>("backtrace_id")?
2847                        .map(Version::new);
2848                    let version = Version(row.get("version")?);
2849
2850                    let event = row
2851                        .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2852                        .map(|event| ExecutionEvent {
2853                            created_at,
2854                            event: event.0,
2855                            backtrace_id,
2856                            version,
2857                        })
2858                        .map_err(|serde| {
2859                            error!("Cannot deserialize {row:?} - {serde:?}");
2860                            consistency_rusqlite("cannot deserialize")
2861                        })?;
2862                    Ok(event)
2863                },
2864            )?
2865            .collect::<Result<Vec<_>, _>>()
2866            .map_err(DbErrorRead::from)
2867    }
2868
2869    fn map_t_execution_log_row(row: &Row<'_>) -> Result<ExecutionEvent, rusqlite::Error> {
2870        let created_at = row.get("created_at")?;
2871        let event = row
2872            .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2873            .map_err(|serde| {
2874                error!("Cannot deserialize {row:?} - {serde:?}");
2875                consistency_rusqlite("cannot deserialize event")
2876            })?;
2877        let version = Version(row.get("version")?);
2878
2879        Ok(ExecutionEvent {
2880            created_at,
2881            event: event.0,
2882            backtrace_id: None,
2883            version,
2884        })
2885    }
2886
2887    fn get_execution_event(
2888        tx: &Transaction,
2889        execution_id: &ExecutionId,
2890        version: VersionType,
2891    ) -> Result<ExecutionEvent, DbErrorRead> {
2892        tx.prepare(
2893            "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2894                        execution_id = :execution_id AND version = :version",
2895        )?
2896        .query_row(
2897            named_params! {
2898                ":execution_id": execution_id.to_string(),
2899                ":version": version,
2900            },
2901            SqlitePool::map_t_execution_log_row,
2902        )
2903        .map_err(DbErrorRead::from)
2904    }
2905
2906    fn get_last_execution_event(
2907        tx: &Transaction,
2908        execution_id: &ExecutionId,
2909    ) -> Result<ExecutionEvent, DbErrorRead> {
2910        tx.prepare(
2911            "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2912                        execution_id = :execution_id ORDER BY version DESC LIMIT 1",
2913        )?
2914        .query_row(
2915            named_params! {
2916                ":execution_id": execution_id.to_string(),
2917            },
2918            SqlitePool::map_t_execution_log_row,
2919        )
2920        .map_err(DbErrorRead::from)
2921    }
2922
2923    fn get_delay_response(
2924        tx: &Transaction,
2925        execution_id: &ExecutionId,
2926        delay_id: &DelayId,
2927    ) -> Result<Option<bool>, DbErrorRead> {
2928        // TODO: Add test
2929        tx.prepare(
2930            "SELECT delay_success \
2931                    FROM t_join_set_response \
2932                    WHERE \
2933                    execution_id = :execution_id AND delay_id = :delay_id
2934                    ",
2935        )?
2936        .query_row(
2937            named_params! {
2938                ":execution_id": execution_id.to_string(),
2939                ":delay_id": delay_id.to_string(),
2940            },
2941            |row| {
2942                let delay_success = row.get::<_, bool>("delay_success")?;
2943                Ok(delay_success)
2944            },
2945        )
2946        .optional()
2947        .map_err(DbErrorRead::from)
2948    }
2949
2950    #[instrument(level = Level::TRACE, skip_all)]
2951    /// Find next responses for this execution
2952    fn get_responses_after(
2953        tx: &Transaction,
2954        execution_id: &ExecutionId,
2955        last_response: ResponseCursor,
2956    ) -> Result<Vec<ResponseWithCursor>, DbErrorRead> {
2957        // TODO: Add test
2958        tx.prepare(
2959            "SELECT r.id, r.seq, r.created_at, r.join_set_id, \
2960            r.delay_id, r.delay_success, \
2961            r.child_execution_id, r.finished_version, child.json_value \
2962            FROM t_join_set_response r LEFT OUTER JOIN t_execution_log child ON r.child_execution_id = child.execution_id \
2963            WHERE \
2964            r.seq > :last_response_seq AND \
2965            r.execution_id = :execution_id AND \
2966            ( \
2967            r.finished_version = child.version \
2968            OR r.child_execution_id IS NULL \
2969            ) \
2970            ORDER BY seq",
2971        )
2972        ?
2973        .query_map(
2974            named_params! {
2975                ":last_response_seq": last_response.0,
2976                ":execution_id": execution_id.to_string(),
2977            },
2978            Self::parse_response_with_cursor,
2979        )
2980        ?
2981        .collect::<Result<Vec<_>, _>>()
2982        .map_err(DbErrorRead::from)
2983    }
2984
2985    fn get_pending_of_single_ffqn(
2986        mut stmt: CachedStatement,
2987        batch_size: u32,
2988        pending_at_or_sooner: DateTime<Utc>,
2989        ffqn: &FunctionFqn,
2990    ) -> Result<Vec<(ExecutionId, Version)>, ()> {
2991        stmt.query_map(
2992            named_params! {
2993                ":pending_expires_finished": pending_at_or_sooner,
2994                ":ffqn": ffqn.to_string(),
2995                ":batch_size": batch_size,
2996            },
2997            |row| {
2998                let execution_id = row.get::<_, ExecutionId>("execution_id")?;
2999                let next_version =
3000                    Version::new(row.get::<_, VersionType>("corresponding_version")?).increment();
3001                Ok((execution_id, next_version))
3002            },
3003        )
3004        .map_err(|err| {
3005            warn!("Ignoring consistency error {err:?}");
3006        })?
3007        .collect::<Result<Vec<_>, _>>()
3008        .map_err(|err| {
3009            warn!("Ignoring consistency error {err:?}");
3010        })
3011    }
3012
3013    /// Get executions and their next versions
3014    fn get_pending_by_ffqns(
3015        conn: &Connection,
3016        batch_size: u32,
3017        pending_at_or_sooner: DateTime<Utc>,
3018        ffqns: &[FunctionFqn],
3019    ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3020        let batch_size = usize::try_from(batch_size).expect("16 bit systems are unsupported");
3021        let mut execution_ids_versions = Vec::with_capacity(batch_size);
3022        for ffqn in ffqns {
3023            let needed = batch_size - execution_ids_versions.len();
3024            if needed == 0 {
3025                break;
3026            }
3027            let needed =
3028                u32::try_from(needed).expect("`batch_size`:u32 - usize cannot overflow an u32");
3029            // Select executions in PendingAt.
3030            let stmt = conn.prepare_cached(&format!(
3031                r#"
3032                    SELECT execution_id, corresponding_version FROM t_state WHERE
3033                    state = "{STATE_PENDING_AT}" AND
3034                    pending_expires_finished <= :pending_expires_finished AND ffqn = :ffqn
3035                    AND lifecycle = 'active'
3036                    ORDER BY pending_expires_finished LIMIT :batch_size
3037                    "#
3038            ))?;
3039
3040            if let Ok(execs_and_versions) =
3041                Self::get_pending_of_single_ffqn(stmt, needed, pending_at_or_sooner, ffqn)
3042            {
3043                execution_ids_versions.extend(execs_and_versions);
3044                // consistency errors are ignored since we want to return at least some rows.
3045            }
3046        }
3047        Ok(execution_ids_versions)
3048    }
3049
3050    fn get_pending_by_ffqns_auto(
3051        conn: &Connection,
3052        batch_size: u32,
3053        pending_at_or_sooner: DateTime<Utc>,
3054        ffqns: &[FunctionFqn],
3055        current_digest: &ComponentDigest,
3056    ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3057        let batch_size = usize::try_from(batch_size).expect("16 bit systems are unsupported");
3058        let mut execution_ids_versions = Vec::with_capacity(batch_size);
3059        for ffqn in ffqns {
3060            let needed = batch_size - execution_ids_versions.len();
3061            if needed == 0 {
3062                break;
3063            }
3064            let mut stmt = conn.prepare_cached(&format!(
3065                r#"
3066                    SELECT execution_id, corresponding_version FROM t_state WHERE
3067                    state = "{STATE_PENDING_AT}" AND
3068                    pending_expires_finished <= :pending_expires_finished AND ffqn = :ffqn
3069                    AND lifecycle = 'active'
3070                    AND (incompatible_digest IS NULL OR incompatible_digest <> :current_digest)
3071                    ORDER BY pending_expires_finished LIMIT :batch_size
3072                    "#
3073            ))?;
3074
3075            if let Ok(execs_and_versions) = stmt
3076                .query_map(
3077                    named_params! {
3078                        ":pending_expires_finished": pending_at_or_sooner,
3079                        ":ffqn": ffqn.to_string(),
3080                        ":current_digest": current_digest,
3081                        ":batch_size": u32::try_from(needed)
3082                            .expect("`needed` is <= `batch_size` which is u32"),
3083                    },
3084                    |row| {
3085                        let execution_id = row.get::<_, ExecutionId>("execution_id")?;
3086                        let next_version =
3087                            Version::new(row.get::<_, VersionType>("corresponding_version")?)
3088                                .increment();
3089                        Ok((execution_id, next_version))
3090                    },
3091                )
3092                .and_then(|rows| rows.collect::<Result<Vec<_>, _>>())
3093            {
3094                execution_ids_versions.extend(execs_and_versions);
3095                if execution_ids_versions.len() == batch_size {
3096                    break;
3097                }
3098            }
3099        }
3100        Ok(execution_ids_versions)
3101    }
3102
3103    fn get_pending_by_component_input_digest(
3104        conn: &Connection,
3105        batch_size: u32,
3106        pending_at_or_sooner: DateTime<Utc>,
3107        input_digest: &ComponentDigest,
3108    ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3109        let mut stmt = conn.prepare_cached(&format!(
3110            r#"
3111                SELECT execution_id, corresponding_version FROM t_state WHERE
3112                state = "{STATE_PENDING_AT}" AND
3113                pending_expires_finished <= :pending_expires_finished AND
3114                component_id_input_digest = :component_id_input_digest
3115                AND lifecycle = 'active'
3116                ORDER BY pending_expires_finished LIMIT :batch_size
3117                "#
3118        ))?;
3119
3120        stmt.query_map(
3121            named_params! {
3122                ":pending_expires_finished": pending_at_or_sooner,
3123                ":component_id_input_digest": input_digest,
3124                ":batch_size": batch_size,
3125            },
3126            |row| {
3127                let execution_id = row.get::<_, ExecutionId>("execution_id")?;
3128                let next_version =
3129                    Version::new(row.get::<_, VersionType>("corresponding_version")?).increment();
3130                Ok((execution_id, next_version))
3131            },
3132        )?
3133        .collect::<Result<Vec<_>, _>>()
3134        .map_err(RusqliteError::from)
3135    }
3136
3137    // Must be called after write transaction for a correct happens-before relationship.
3138    #[instrument(level = Level::TRACE, skip_all)]
3139    fn notify_all(&self, notifiers: Vec<AppendNotifier>, current_time: DateTime<Utc>) {
3140        let (pending_ats, finished_execs, responses) = {
3141            let (mut pending_ats, mut finished_execs, mut responses) =
3142                (Vec::new(), Vec::new(), Vec::new());
3143            for notifier in notifiers {
3144                if let Some(pending_at) = notifier.pending_at {
3145                    pending_ats.push(pending_at);
3146                }
3147                if let Some(finished) = notifier.execution_finished {
3148                    finished_execs.push(finished);
3149                }
3150                if let Some(response) = notifier.response {
3151                    responses.push(response);
3152                }
3153            }
3154            (pending_ats, finished_execs, responses)
3155        };
3156
3157        // Notify pending_at subscribers.
3158        if !pending_ats.is_empty() {
3159            let guard = self.0.pending_subscribers.lock().unwrap();
3160            for pending_at in pending_ats {
3161                Self::notify_pending_locked(&pending_at, current_time, &guard);
3162            }
3163        }
3164        // Notify execution finished subscribers.
3165        // Every NotifierExecutionFinished value belongs to a different execution, since only `append(Finished)` can produce `NotifierExecutionFinished`.
3166        if !finished_execs.is_empty() {
3167            let mut guard = self.0.execution_finished_subscribers.lock().unwrap();
3168            for finished in finished_execs {
3169                if let Some(listeners_of_exe_id) = guard.remove(&finished.execution_id) {
3170                    for (_tag, sender) in listeners_of_exe_id {
3171                        // Sending while holding the lock but the oneshot sender does not block.
3172                        // If `wait_for_finished_result` happens after the append, it would receive the finished value instead.
3173                        let _ = sender.send(finished.retval.clone());
3174                    }
3175                }
3176            }
3177        }
3178        // Notify response subscribers.
3179        if !responses.is_empty() {
3180            let mut guard = self.0.response_subscribers.lock().unwrap();
3181            for (execution_id, _response) in responses {
3182                if let Some((sender, _)) = guard.remove(&execution_id) {
3183                    let _ = sender.send(());
3184                }
3185            }
3186        }
3187    }
3188
3189    fn notify_pending_locked(
3190        notifier: &NotifierPendingAt,
3191        current_time: DateTime<Utc>,
3192        ffqn_to_pending_subscription: &std::sync::MutexGuard<PendingFfqnSubscribersHolder>,
3193    ) {
3194        // No need to remove here, cleanup is handled by the caller.
3195        if notifier.scheduled_at <= current_time {
3196            ffqn_to_pending_subscription.notify(notifier);
3197        }
3198    }
3199
3200    fn upgrade_execution_component_single_write(
3201        tx: &Transaction,
3202        execution_id: &ExecutionId,
3203        old: &ComponentDigest,
3204        new: &ComponentDigest,
3205        reason: ComponentUpgradeReason,
3206    ) -> Result<(), DbErrorWrite> {
3207        let combined_state = Self::get_combined_state(tx, execution_id)?;
3208        if combined_state.execution_with_state.component_digest != *old {
3209            return Err(DbErrorWrite::NotFound);
3210        }
3211        let appending_version = combined_state.get_next_version_fail_if_finished()?;
3212        Self::append(
3213            tx,
3214            execution_id,
3215            AppendRequest {
3216                created_at: Utc::now(),
3217                event: ExecutionRequest::ComponentUpgradeFinished {
3218                    component_digest: new.clone(),
3219                    deployment_id: combined_state.execution_with_state.deployment_id,
3220                    outcome: ComponentUpgradeOutcome::Success { reason },
3221                },
3222            },
3223            appending_version,
3224        )?;
3225        Ok(())
3226    }
3227
3228    fn list_logs_tx(
3229        tx: &Transaction,
3230        execution_id: &ExecutionId,
3231        show_derived: bool,
3232        filter: &LogFilter,
3233        pagination: &Pagination<LogCursor>,
3234    ) -> Result<ListLogsResponse, DbErrorRead> {
3235        let length = pagination.length();
3236        let exec_id_str = execution_id.to_string();
3237        let exec_id_filter = if show_derived {
3238            "LIKE :execution_id || '%'"
3239        } else {
3240            "= :execution_id"
3241        };
3242        let mut query = format!(
3243            "SELECT id, run_id, created_at, level, message, stream_type, payload, execution_id
3244             FROM t_log
3245             WHERE execution_id {exec_id_filter}",
3246        );
3247
3248        let cursor = pagination.cursor();
3249        let created_after = filter.created_after();
3250        let created_before = filter.created_before();
3251        let mut params = vec![
3252            (":execution_id", &exec_id_str as &dyn rusqlite::ToSql),
3253            (":cursor", &cursor.0 as &dyn rusqlite::ToSql),
3254            (":length", &length as &dyn rusqlite::ToSql),
3255        ];
3256        if let Some(created_after) = &created_after {
3257            params.push((":created_after", created_after as &dyn rusqlite::ToSql));
3258        }
3259        if let Some(created_before) = &created_before {
3260            params.push((":created_before", created_before as &dyn rusqlite::ToSql));
3261        }
3262
3263        // Logs and streams filter
3264        let level_filter = if filter.should_show_logs() {
3265            let levels_str = if !filter.levels().is_empty() {
3266                filter
3267                    .levels()
3268                    .iter()
3269                    .map(|lvl| (*lvl as u8).to_string())
3270                    .collect::<Vec<_>>()
3271                    .join(",")
3272            } else {
3273                LogLevel::iter()
3274                    .map(|lvl| (lvl as u8).to_string())
3275                    .collect::<Vec<_>>()
3276                    .join(",")
3277            };
3278            Some(format!(" level IN ({levels_str})"))
3279        } else {
3280            None
3281        };
3282        let stream_filter = if filter.should_show_streams() {
3283            let streams_str = if !filter.stream_types().is_empty() {
3284                filter
3285                    .stream_types()
3286                    .iter()
3287                    .map(|st| (*st as u8).to_string())
3288                    .collect::<Vec<_>>()
3289                    .join(",")
3290            } else {
3291                LogStreamType::iter()
3292                    .map(|st| (st as u8).to_string())
3293                    .collect::<Vec<_>>()
3294                    .join(",")
3295            };
3296            Some(format!(" stream_type IN ({streams_str})"))
3297        } else {
3298            None
3299        };
3300        match (level_filter, stream_filter) {
3301            (Some(level_filter), Some(stream_filter)) => {
3302                write!(&mut query, " AND ({level_filter} OR {stream_filter})")
3303                    .expect("writing to string");
3304            }
3305            (Some(level_filter), None) => {
3306                write!(&mut query, " AND {level_filter}").expect("writing to string");
3307            }
3308            (None, Some(stream_filter)) => {
3309                write!(&mut query, " AND {stream_filter}").expect("writing to string");
3310            }
3311            (None, None) => unreachable!("guarded by constructor"),
3312        }
3313
3314        if created_after.is_some() {
3315            query.push_str(" AND created_at > :created_after");
3316        }
3317        if created_before.is_some() {
3318            query.push_str(" AND created_at < :created_before");
3319        }
3320
3321        // Pagination and ordering
3322        write!(&mut query, " AND id {} :cursor", pagination.rel()).expect("writing to string");
3323        query.push_str(" ORDER BY id ");
3324        query.push_str(pagination.asc_or_desc());
3325
3326        // Limit
3327        query.push_str(" LIMIT :length");
3328
3329        let mut stmt = tx.prepare(&query)?;
3330
3331        let items = stmt
3332            .query_map(params.as_slice(), |row| {
3333                let created_at: DateTime<Utc> = row.get("created_at")?;
3334                let run_id = row.get("run_id")?;
3335                let level: Option<u8> = row.get("level")?;
3336                let message: Option<String> = row.get("message")?;
3337                let stream_type: Option<u8> = row.get("stream_type")?;
3338                let payload: Option<Vec<u8>> = row.get("payload")?;
3339                let execution_id_str: String = row.get("execution_id")?;
3340                let execution_id = ExecutionId::from_str(&execution_id_str).map_err(|_| {
3341                    consistency_rusqlite(format!("cannot convert ExecutionId {execution_id_str}"))
3342                })?;
3343
3344                let log_entry = match (level, message, stream_type, payload) {
3345                    (Some(lvl), Some(msg), None, None) => LogEntry::Log {
3346                        created_at,
3347                        level: LogLevel::try_from(lvl).map_err(|_| {
3348                            consistency_rusqlite(format!("cannot convert {lvl} to LogLevel"))
3349                        })?,
3350                        message: msg,
3351                    },
3352                    (None, None, Some(stype), Some(pl)) => LogEntry::Stream {
3353                        created_at,
3354                        stream_type: LogStreamType::try_from(stype).map_err(|_| {
3355                            consistency_rusqlite(format!("cannot convert {stype} to LogStreamType"))
3356                        })?,
3357                        payload: pl,
3358                    },
3359                    _ => {
3360                        return Err(consistency_rusqlite("invalid t_log row".to_string()));
3361                    }
3362                };
3363                Ok(LogEntryRow {
3364                    cursor: LogCursor(row.get("id")?),
3365                    run_id,
3366                    log_entry,
3367                    execution_id,
3368                })
3369            })?
3370            .collect::<Result<Vec<_>, _>>()?;
3371
3372        Ok(ListLogsResponse {
3373            next_page: items
3374                .last()
3375                .map(|item| Pagination::NewerThan {
3376                    length: pagination.length(),
3377                    cursor: item.cursor,
3378                    including_cursor: false,
3379                })
3380                .unwrap_or({
3381                    if pagination.is_asc() {
3382                        *pagination // no new results, keep the same cursor
3383                    } else {
3384                        // no prev results, let's start from beginning
3385                        Pagination::NewerThan {
3386                            length: pagination.length(),
3387                            cursor: LogCursor(i64::MIN),
3388                            including_cursor: false,
3389                        }
3390                    }
3391                }),
3392            prev_page: match items.first() {
3393                Some(item) => Some(Pagination::OlderThan {
3394                    length: pagination.length(),
3395                    cursor: item.cursor,
3396                    including_cursor: false,
3397                }),
3398                None if pagination.is_asc() && pagination.cursor() != &LogCursor(i64::MIN) => {
3399                    // asked for a next page that does not exists (yet).
3400                    Some(pagination.invert())
3401                }
3402                None => None,
3403            },
3404            items,
3405        })
3406    }
3407
3408    fn list_deployment_states(
3409        tx: &Transaction,
3410        current_time: DateTime<Utc>,
3411        pagination: Pagination<Option<DeploymentId>>,
3412        include_deployment_toml: bool,
3413        execution_counts: DeploymentExecutionCounts,
3414    ) -> Result<Vec<DeploymentState>, DbErrorRead> {
3415        let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3416        let deployment_toml_col = if include_deployment_toml {
3417            "d.deployment_toml"
3418        } else {
3419            "NULL AS deployment_toml"
3420        };
3421        let include_execution_counts =
3422            matches!(execution_counts, DeploymentExecutionCounts::Count { .. });
3423        // Without counts, skip the SUMs (and the `t_state` join feeding them) and report zeros.
3424        let count_cols = if include_execution_counts {
3425            format!(
3426                r"
3427            COALESCE(SUM(s.state = '{STATE_LOCKED}' AND s.lifecycle = 'active'), 0) AS locked,
3428            COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.lifecycle = 'active' AND s.pending_expires_finished <= :now), 0) AS pending,
3429            COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.lifecycle = 'active' AND s.pending_expires_finished > :now), 0) AS scheduled,
3430            COALESCE(SUM(s.state = '{STATE_BLOCKED_BY_JOIN_SET}' AND s.lifecycle = 'active'), 0) AS blocked,
3431            COALESCE(SUM(s.lifecycle = 'paused'), 0) AS paused,
3432            COALESCE(SUM(s.lifecycle = 'cancelling'), 0) AS cancelling,
3433            COALESCE(SUM(s.state = '{STATE_FINISHED}' AND s.result_kind = '{RESULT_KIND_JSON_OK}'), 0) AS finished_ok,
3434            COALESCE(SUM(s.state = '{STATE_FINISHED}' AND s.result_kind = '{RESULT_KIND_JSON_ERROR}'), 0) AS finished_error,
3435            COALESCE(SUM(s.state = '{STATE_FINISHED}' AND s.result_kind IS NOT NULL
3436                AND s.result_kind NOT IN ('{RESULT_KIND_JSON_OK}', '{RESULT_KIND_JSON_ERROR}')), 0) AS finished_execution_failure,"
3437            )
3438        } else {
3439            "
3440            0 AS locked,
3441            0 AS pending,
3442            0 AS scheduled,
3443            0 AS blocked,
3444            0 AS paused,
3445            0 AS cancelling,
3446            0 AS finished_ok,
3447            0 AS finished_error,
3448            0 AS finished_execution_failure,"
3449                .to_string()
3450        };
3451        let mut sql = format!(
3452            r"
3453        SELECT
3454            d.deployment_id,
3455            d.description,
3456            d.digest,{count_cols}
3457            {deployment_toml_col},
3458            d.created_at,
3459            d.last_active_at,
3460            d.status
3461        FROM t_deployment d{join}",
3462            join = match execution_counts {
3463                DeploymentExecutionCounts::Count { include_derived } => {
3464                    let join_top_level = if include_derived {
3465                        ""
3466                    } else {
3467                        " AND s.is_top_level = true"
3468                    };
3469                    format!(
3470                        "\n        LEFT JOIN t_state s ON s.deployment_id = d.deployment_id{join_top_level}"
3471                    )
3472                }
3473                DeploymentExecutionCounts::Skip => String::new(),
3474            }
3475        );
3476
3477        if include_execution_counts {
3478            params.push((":now", Box::new(current_time)));
3479        }
3480
3481        if let Some(cursor) = pagination.cursor() {
3482            params.push((":cursor", Box::new(*cursor)));
3483            write!(
3484                sql,
3485                " WHERE d.deployment_id {rel} :cursor",
3486                rel = pagination.rel()
3487            )
3488            .expect("writing to string");
3489        }
3490
3491        // Inner query: fetch rows with cursor-based ordering
3492        // Outer query: always return results in descending order
3493        let (inner_order, outer_order) = if pagination.is_desc() {
3494            ("DESC", "")
3495        } else {
3496            ("ASC", "DESC")
3497        };
3498
3499        // GROUP BY only needed to collapse the rows multiplied by the `t_state` join.
3500        if include_execution_counts {
3501            write!(
3502                sql,
3503                " GROUP BY d.deployment_id, d.description, d.digest, d.deployment_toml, d.created_at, d.last_active_at, d.status"
3504            )
3505            .expect("writing to string");
3506        }
3507        write!(
3508            sql,
3509            " ORDER BY d.deployment_id {inner_order} LIMIT {limit}",
3510            limit = pagination.length()
3511        )
3512        .expect("writing to string");
3513
3514        let final_sql = if outer_order.is_empty() {
3515            sql
3516        } else {
3517            format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3518        };
3519
3520        let result: Vec<DeploymentState> = tx
3521            .prepare(&final_sql)?
3522            .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3523                params
3524                    .iter()
3525                    .map(|(k, v)| (*k, v.as_ref()))
3526                    .collect::<Vec<_>>()
3527                    .as_ref(),
3528                |row| {
3529                    let status_str: String = row.get("status")?;
3530                    let status = status_str.parse::<DeploymentStatus>().map_err(|_| {
3531                        rusqlite::Error::InvalidColumnType(
3532                            0,
3533                            "status".to_string(),
3534                            rusqlite::types::Type::Text,
3535                        )
3536                    })?;
3537                    Ok(DeploymentState {
3538                        deployment_id: row.get("deployment_id")?,
3539                        description: row.get("description")?,
3540                        digest: row.get("digest")?,
3541                        locked: row.get("locked")?,
3542                        pending: row.get("pending")?,
3543                        scheduled: row.get("scheduled")?,
3544                        blocked: row.get("blocked")?,
3545                        paused: row.get("paused")?,
3546                        cancelling: row.get("cancelling")?,
3547                        finished_ok: row.get("finished_ok")?,
3548                        finished_error: row.get("finished_error")?,
3549                        finished_execution_failure: row.get("finished_execution_failure")?,
3550                        deployment_toml: row.get("deployment_toml")?,
3551                        created_at: row.get("created_at")?,
3552                        last_active_at: row.get("last_active_at")?,
3553                        status,
3554                    })
3555                },
3556            )?
3557            .collect::<Result<Vec<_>, rusqlite::Error>>()
3558            .map_err(DbErrorRead::from)?;
3559
3560        Ok(result)
3561    }
3562
3563    fn insert_deployment_tx(
3564        tx: &Transaction,
3565        record: &DeploymentRecord,
3566    ) -> Result<(), DbErrorWrite> {
3567        assert_eq!(
3568            record.status,
3569            DeploymentStatus::Inactive,
3570            "insert_deployment requires Inactive status"
3571        );
3572        assert!(
3573            record.last_active_at.is_none(),
3574            "insert_deployment requires last_active_at == None"
3575        );
3576        tx.execute(
3577            "INSERT INTO t_deployment \
3578             (deployment_id, description, digest, created_at, status, deployment_toml, obelisk_version, created_by) \
3579             VALUES (:deployment_id, :description, :digest, :created_at, :status, :deployment_toml, :obelisk_version, :created_by)",
3580            rusqlite::named_params! {
3581                ":deployment_id": record.deployment_id.to_string(),
3582                ":description": record.description,
3583                ":digest": record.digest.to_string(),
3584                ":created_at": record.created_at,
3585                ":status": record.status.as_str(),
3586                ":deployment_toml": record.deployment_toml,
3587                ":obelisk_version": record.obelisk_version,
3588                ":created_by": record.created_by,
3589            },
3590        )
3591        .map_err(RusqliteError::from)?;
3592        Self::insert_deployment_files_tx(tx, record.deployment_id, &record.files)?;
3593        Ok(())
3594    }
3595
3596    fn insert_deployment_files_tx(
3597        tx: &Transaction,
3598        deployment_id: DeploymentId,
3599        files: &[DeploymentFileRecord],
3600    ) -> Result<(), DbErrorWrite> {
3601        let mut stmt = tx
3602            .prepare_cached(
3603                "INSERT INTO t_deployment_file (deployment_id, digest, path) \
3604                 VALUES (:deployment_id, :digest, :path)",
3605            )
3606            .map_err(RusqliteError::from)?;
3607        for file in files {
3608            stmt.execute(rusqlite::named_params! {
3609                ":deployment_id": deployment_id.to_string(),
3610                ":digest": file.digest.to_string(),
3611                ":path": file.path,
3612            })
3613            .map_err(RusqliteError::from)?;
3614        }
3615        Ok(())
3616    }
3617
3618    fn upsert_component_metadata_tx(
3619        tx: &Transaction,
3620        records: &[ComponentMetadataRecord],
3621    ) -> Result<(), DbErrorWrite> {
3622        let mut stmt = tx
3623            .prepare(
3624                "INSERT OR IGNORE INTO t_component_metadata \
3625                 (component_digest, imports_json, exports_json, wit, wit_origin) \
3626                 VALUES (:component_digest, :imports_json, :exports_json, :wit, :wit_origin)",
3627            )
3628            .map_err(RusqliteError::from)?;
3629        for record in records {
3630            let imports_json = serde_json::to_string(&record.imports).map_err(|err| {
3631                RusqliteError::from(rusqlite::Error::ToSqlConversionFailure(Box::new(err)))
3632            })?;
3633            let exports_json = serde_json::to_string(&record.exports).map_err(|err| {
3634                RusqliteError::from(rusqlite::Error::ToSqlConversionFailure(Box::new(err)))
3635            })?;
3636            stmt.execute(named_params! {
3637                ":component_digest": record.component_digest.clone(),
3638                ":imports_json": imports_json,
3639                ":exports_json": exports_json,
3640                ":wit": record.wit.clone(),
3641                ":wit_origin": record.wit_origin.clone(),
3642            })
3643            .map_err(RusqliteError::from)?;
3644        }
3645        Ok(())
3646    }
3647
3648    fn insert_deployment_components_tx(
3649        tx: &Transaction,
3650        deployment_id: DeploymentId,
3651        records: &[DeploymentComponentRecord],
3652    ) -> Result<(), DbErrorWrite> {
3653        let mut stmt = tx
3654            .prepare(
3655                "INSERT OR IGNORE INTO t_deployment_component \
3656                 (deployment_id, component_name, component_type, component_digest) \
3657                 VALUES (:deployment_id, :component_name, :component_type, :component_digest)",
3658            )
3659            .map_err(RusqliteError::from)?;
3660        for record in records {
3661            debug_assert_eq!(record.deployment_id, deployment_id);
3662            stmt.execute(named_params! {
3663                ":deployment_id": deployment_id.to_string(),
3664                ":component_name": record.component_name.to_string(),
3665                ":component_type": record.component_type.to_string(),
3666                ":component_digest": record.component_digest.clone(),
3667            })
3668            .map_err(RusqliteError::from)?;
3669        }
3670        Ok(())
3671    }
3672
3673    fn compute_file_digest(content: &[u8]) -> ContentDigest {
3674        let hash: [u8; 32] = Sha256::digest(content).into();
3675        ContentDigest(Digest(hash))
3676    }
3677
3678    fn upload_file_tx(
3679        tx: &Transaction,
3680        digest: &ContentDigest,
3681        content: &[u8],
3682    ) -> Result<(), DbErrorWrite> {
3683        let actual = Self::compute_file_digest(content);
3684        if &actual != digest {
3685            return Err(DbErrorWriteNonRetriable::ValidationFailed(
3686                format!("uploaded file digest mismatch: expected {digest}, got {actual}").into(),
3687            )
3688            .into());
3689        }
3690        let size = i64::try_from(content.len()).map_err(|err| DbErrorGeneric::Uncategorized {
3691            reason: format!("deployment file too large: {err}").into(),
3692            context: SpanTrace::capture(),
3693            source: Some(Arc::new(err)),
3694            loc: Location::caller(),
3695        })?;
3696        tx.execute(
3697            "INSERT INTO t_file (digest, content, size) VALUES (:digest, :content, :size) \
3698             ON CONFLICT (digest) DO NOTHING",
3699            rusqlite::named_params! {
3700                ":digest": digest.to_string(),
3701                ":content": content,
3702                ":size": size,
3703            },
3704        )
3705        .map_err(RusqliteError::from)?;
3706        Ok(())
3707    }
3708
3709    fn get_file_tx(
3710        tx: &Transaction,
3711        digest: &ContentDigest,
3712    ) -> Result<Option<Vec<u8>>, DbErrorRead> {
3713        tx.query_row(
3714            "SELECT content FROM t_file WHERE digest = :digest",
3715            rusqlite::named_params! { ":digest": digest.to_string() },
3716            |row| row.get("content"),
3717        )
3718        .optional()
3719        .map_err(|err| DbErrorRead::from(RusqliteError::from(err)))
3720    }
3721
3722    fn missing_digests_tx(
3723        tx: &Transaction,
3724        deployment_id: DeploymentId,
3725    ) -> Result<Vec<ContentDigest>, DbErrorRead> {
3726        tx.prepare(
3727            "SELECT df.digest \
3728             FROM t_deployment_file df \
3729             LEFT JOIN t_file f ON f.digest = df.digest \
3730             WHERE df.deployment_id = :deployment_id AND f.digest IS NULL \
3731             ORDER BY df.digest",
3732        )?
3733        .query_map(
3734            rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3735            |row| row.get("digest"),
3736        )?
3737        .collect::<Result<Vec<_>, rusqlite::Error>>()
3738        .map_err(DbErrorRead::from)
3739    }
3740
3741    fn list_deployment_files_tx(
3742        tx: &Transaction,
3743        deployment_id: DeploymentId,
3744    ) -> Result<Vec<DeploymentFileRecord>, DbErrorRead> {
3745        tx.prepare(
3746            // LEFT JOIN: no FK ties t_deployment_file to t_file, so report every referenced file
3747            // rather than silently dropping one whose metadata row is (unexpectedly) absent.
3748            "SELECT df.path, df.digest, f.size FROM t_deployment_file df \
3749             LEFT JOIN t_file f ON f.digest = df.digest \
3750             WHERE df.deployment_id = :deployment_id \
3751             ORDER BY df.path, df.digest",
3752        )?
3753        .query_map(
3754            rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3755            |row| {
3756                let path: String = row.get("path")?;
3757                let digest: ContentDigest = row.get("digest")?;
3758                let size = if let Some(size) = row.get::<_, Option<i64>>("size")? {
3759                    // size is stored as content.len(); a negative value means DB corruption.
3760                    u64::try_from(size)
3761                        .map_err(|_| rusqlite::Error::IntegralValueOutOfRange(2, size))?
3762                } else {
3763                    warn!(%path, %digest, "deployment file metadata missing from t_file, reporting size 0");
3764                    0
3765                };
3766                Ok(DeploymentFileRecord { path, digest, size })
3767            },
3768        )?
3769        .collect::<Result<Vec<_>, rusqlite::Error>>()
3770        .map_err(DbErrorRead::from)
3771    }
3772
3773    fn activate_deployment_tx(
3774        tx: &Transaction,
3775        deployment_id: DeploymentId,
3776        now: DateTime<Utc>,
3777    ) -> Result<(), DbErrorWrite> {
3778        // Demote the currently active or enqueued deployment to inactive.
3779        tx.execute(
3780            "UPDATE t_deployment SET status = 'inactive' WHERE status IN ('active', 'enqueued')",
3781            [],
3782        )
3783        .map_err(RusqliteError::from)?;
3784        // Set target deployment to active, recording activation time.
3785        let rows = tx
3786            .execute(
3787                "UPDATE t_deployment SET status = 'active', last_active_at = :now WHERE deployment_id = :deployment_id",
3788                rusqlite::named_params! {
3789                    ":now": now,
3790                    ":deployment_id": deployment_id.to_string(),
3791                },
3792            )
3793            .map_err(RusqliteError::from)?;
3794        if rows == 0 {
3795            return Err(DbErrorWrite::NotFound);
3796        }
3797        Ok(())
3798    }
3799
3800    fn enqueue_deployment_tx(
3801        tx: &Transaction,
3802        deployment_id: DeploymentId,
3803    ) -> Result<EnqueueOutcome, DbErrorWrite> {
3804        let status_opt: Option<String> = tx
3805            .query_row(
3806                "SELECT status FROM t_deployment WHERE deployment_id = :deployment_id",
3807                rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3808                |row| row.get(0),
3809            )
3810            .optional()
3811            .map_err(RusqliteError::from)?;
3812        let status = status_opt.as_deref();
3813        if status.is_none() {
3814            return Err(DbErrorWrite::NotFound);
3815        }
3816        // Demote any previously enqueued deployment to inactive.
3817        tx.execute(
3818            "UPDATE t_deployment SET status = 'inactive' WHERE status = 'enqueued'",
3819            [],
3820        )
3821        .map_err(RusqliteError::from)?;
3822        // The target is already active: it stays active, only the pending one was cleared.
3823        if status == Some("active") {
3824            return Ok(EnqueueOutcome::AlreadyActive);
3825        }
3826        // Set target deployment to enqueued.
3827        let rows = tx
3828            .execute(
3829                "UPDATE t_deployment SET status = 'enqueued' WHERE deployment_id = :deployment_id",
3830                rusqlite::named_params! {
3831                    ":deployment_id": deployment_id.to_string(),
3832                },
3833            )
3834            .map_err(RusqliteError::from)?;
3835        if rows == 0 {
3836            return Err(DbErrorWrite::NotFound);
3837        }
3838        Ok(EnqueueOutcome::Enqueued)
3839    }
3840
3841    fn get_deployment_tx(
3842        tx: &Transaction,
3843        deployment_id: DeploymentId,
3844    ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3845        let Some(record) = tx
3846            .query_row(
3847            "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
3848             FROM t_deployment WHERE deployment_id = :deployment_id",
3849                rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3850                deployment_record_from_row,
3851            )
3852            .optional()
3853            .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))?
3854        else {
3855            return Ok(None);
3856        };
3857        Self::with_deployment_files_tx(tx, record).map(Some)
3858    }
3859
3860    #[cfg(feature = "test")]
3861    fn get_active_deployment_tx(tx: &Transaction) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3862        let Some(record) = tx
3863            .query_row(
3864            "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
3865             FROM t_deployment WHERE status = 'active' LIMIT 1",
3866                [],
3867                deployment_record_from_row,
3868            )
3869            .optional()
3870            .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))?
3871        else {
3872            return Ok(None);
3873        };
3874        Self::with_deployment_files_tx(tx, record).map(Some)
3875    }
3876
3877    fn list_deployments_tx(
3878        tx: &Transaction,
3879        pagination: Pagination<Option<DeploymentId>>,
3880    ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
3881        let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3882        let mut sql = String::from(
3883            "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
3884             FROM t_deployment",
3885        );
3886
3887        if let Some(cursor) = pagination.cursor() {
3888            params.push((":cursor", Box::new(*cursor)));
3889            write!(
3890                sql,
3891                " WHERE deployment_id {rel} :cursor",
3892                rel = pagination.rel()
3893            )
3894            .expect("writing to string");
3895        }
3896
3897        let (inner_order, outer_order) = if pagination.is_desc() {
3898            ("DESC", "")
3899        } else {
3900            ("ASC", "DESC")
3901        };
3902
3903        write!(
3904            sql,
3905            " ORDER BY deployment_id {inner_order} LIMIT {limit}",
3906            limit = pagination.length()
3907        )
3908        .expect("writing to string");
3909
3910        let final_sql = if outer_order.is_empty() {
3911            sql
3912        } else {
3913            format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3914        };
3915
3916        let mut result: Vec<DeploymentRecord> = tx
3917            .prepare(&final_sql)?
3918            .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3919                params
3920                    .iter()
3921                    .map(|(k, v)| (*k, v.as_ref()))
3922                    .collect::<Vec<_>>()
3923                    .as_ref(),
3924                deployment_record_from_row,
3925            )?
3926            .collect::<Result<Vec<_>, rusqlite::Error>>()
3927            .map_err(DbErrorRead::from)?;
3928        for record in &mut result {
3929            record.files = Self::list_deployment_files_tx(tx, record.deployment_id)?;
3930        }
3931
3932        Ok(result)
3933    }
3934
3935    fn with_deployment_files_tx(
3936        tx: &Transaction,
3937        mut record: DeploymentRecord,
3938    ) -> Result<DeploymentRecord, DbErrorRead> {
3939        record.files = Self::list_deployment_files_tx(tx, record.deployment_id)?;
3940        Ok(record)
3941    }
3942
3943    fn pause_execution(
3944        tx: &Transaction,
3945        execution_id: &ExecutionId,
3946        paused_at: DateTime<Utc>,
3947    ) -> Result<Version, DbErrorWrite> {
3948        let combined_state = Self::get_combined_state(tx, execution_id)?;
3949        let mut appending_version = combined_state.get_next_version_fail_if_finished()?;
3950        debug!("Pausing with {appending_version}");
3951        if combined_state.reject_locked_activities()? {
3952            (appending_version, _) = Self::append(
3953                tx,
3954                execution_id,
3955                AppendRequest {
3956                    created_at: paused_at,
3957                    event: ExecutionRequest::Unlocked(Unlocked {
3958                        unlocked_at: paused_at, // does not matter, about to append `Paused` in same tx.
3959                        reason: "paused".into(),
3960                    }),
3961                },
3962                appending_version,
3963            )?;
3964        }
3965        let (next_version, _notifier) = Self::append(
3966            tx,
3967            execution_id,
3968            AppendRequest {
3969                created_at: paused_at,
3970                event: ExecutionRequest::Paused,
3971            },
3972            appending_version,
3973        )?;
3974        Ok(next_version)
3975    }
3976
3977    fn unpause_execution(
3978        tx: &Transaction,
3979        execution_id: &ExecutionId,
3980        paused_at: DateTime<Utc>,
3981    ) -> Result<Version, DbErrorWrite> {
3982        let combined_state = Self::get_combined_state(tx, execution_id)?;
3983        let appending_version = combined_state.get_next_version_fail_if_finished()?;
3984        debug!("Unpausing with {appending_version}");
3985        let (next_version, _) = Self::append(
3986            tx,
3987            execution_id,
3988            AppendRequest {
3989                created_at: paused_at,
3990                event: ExecutionRequest::Unpaused,
3991            },
3992            appending_version,
3993        )?;
3994        Ok(next_version)
3995    }
3996
3997    fn cancel_workflow(
3998        tx: &Transaction,
3999        execution_id: &ExecutionId,
4000        cancelled_at: DateTime<Utc>,
4001    ) -> Result<CancelOutcome, DbErrorWrite> {
4002        let combined_state = Self::get_combined_state(tx, execution_id)?;
4003        if let Some(outcome) = combined_state.cancel_short_circuit() {
4004            return Ok(outcome);
4005        }
4006        Self::append_cancellation_requested(
4007            tx,
4008            execution_id,
4009            cancelled_at,
4010            &combined_state,
4011            CancellationFfqnCheck::Required,
4012        )
4013    }
4014
4015    fn append_cancellation_requested(
4016        tx: &Transaction,
4017        execution_id: &ExecutionId,
4018        cancelled_at: DateTime<Utc>,
4019        combined_state: &CombinedState,
4020        ffqn_check: CancellationFfqnCheck,
4021    ) -> Result<CancelOutcome, DbErrorWrite> {
4022        if matches!(ffqn_check, CancellationFfqnCheck::Required) {
4023            combined_state.assert_cancellable_workflow_ffqn()?;
4024        }
4025        Self::append(
4026            tx,
4027            execution_id,
4028            AppendRequest {
4029                created_at: cancelled_at,
4030                event: ExecutionRequest::CancellationRequested,
4031            },
4032            combined_state.get_next_version_assert_not_finished(),
4033        )?;
4034        Ok(CancelOutcome::Cancelled)
4035    }
4036
4037    fn append_activity_cancellation_requested_tx(
4038        tx: &Transaction,
4039        execution_id: &ExecutionId,
4040        cancelled_at: DateTime<Utc>,
4041        combined_state: &CombinedState,
4042    ) -> Result<CancelOutcome, DbErrorWrite> {
4043        match &combined_state.execution_with_state.pending_state {
4044            PendingState::Finished(finished) => {
4045                if finished.result_kind
4046                    == PendingStateFinishedResultKind::Err(
4047                        PendingStateFinishedError::ExecutionFailure(
4048                            ExecutionFailureKind::Cancelled,
4049                        ),
4050                    )
4051                {
4052                    return Ok(CancelOutcome::Cancelled);
4053                }
4054                return Ok(CancelOutcome::AlreadyFinished);
4055            }
4056            PendingState::Cancelling(_) => return Ok(CancelOutcome::Cancelled),
4057            _ => {}
4058        }
4059        Self::append_cancellation_requested(
4060            tx,
4061            execution_id,
4062            cancelled_at,
4063            combined_state,
4064            CancellationFfqnCheck::Skipped,
4065        )
4066    }
4067}
4068
4069#[async_trait]
4070impl DbExecutor for SqlitePool {
4071    #[instrument(level = Level::TRACE, skip(self))]
4072    async fn lock_pending_by_ffqns(
4073        &self,
4074        batch_size: u32,
4075        pending_at_or_sooner: DateTime<Utc>,
4076        ffqns: Arc<[FunctionFqn]>,
4077        created_at: DateTime<Utc>,
4078        component_id: ComponentId,
4079        deployment_id: DeploymentId,
4080        executor_id: ExecutorId,
4081        lock_expires_at: DateTime<Utc>,
4082        run_id: RunId,
4083        retry_config: ComponentRetryConfig,
4084    ) -> Result<LockPendingResponse, DbErrorWrite> {
4085        let execution_ids_versions = self
4086            .transaction(
4087                move |conn| {
4088                    Self::get_pending_by_ffqns(conn, batch_size, pending_at_or_sooner, &ffqns)
4089                },
4090                TxType::Other, // read only
4091                "lock_pending_by_ffqns_get",
4092            )
4093            .await
4094            .map_err(to_generic_error)?;
4095        if execution_ids_versions.is_empty() {
4096            Ok(vec![])
4097        } else {
4098            debug!("Locking {execution_ids_versions:?}");
4099            self.transaction(
4100                move |tx| {
4101                    let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
4102                    // Append lock
4103                    for (execution_id, version) in &execution_ids_versions {
4104                        locked_execs.push(Self::lock_single_execution(
4105                            tx,
4106                            created_at,
4107                            &component_id,
4108                            true,
4109                            deployment_id,
4110                            execution_id,
4111                            run_id,
4112                            version,
4113                            executor_id,
4114                            lock_expires_at,
4115                            retry_config,
4116                        )?);
4117                    }
4118                    Ok::<_, DbErrorWrite>(locked_execs)
4119                },
4120                TxType::MultipleWrites,
4121                "lock_pending_by_ffqns_one",
4122            )
4123            .await
4124        }
4125    }
4126
4127    #[instrument(level = Level::TRACE, skip(self))]
4128    async fn lock_pending_by_ffqns_auto(
4129        &self,
4130        batch_size: u32,
4131        pending_at_or_sooner: DateTime<Utc>,
4132        ffqns: Arc<[FunctionFqn]>,
4133        created_at: DateTime<Utc>,
4134        component_id: ComponentId,
4135        deployment_id: DeploymentId,
4136        executor_id: ExecutorId,
4137        lock_expires_at: DateTime<Utc>,
4138        run_id: RunId,
4139        retry_config: ComponentRetryConfig,
4140    ) -> Result<LockPendingResponse, DbErrorWrite> {
4141        let current_digest = component_id.component_digest.clone();
4142        let execution_ids_versions = self
4143            .transaction(
4144                move |conn| {
4145                    Self::get_pending_by_ffqns_auto(
4146                        conn,
4147                        batch_size,
4148                        pending_at_or_sooner,
4149                        &ffqns,
4150                        &current_digest,
4151                    )
4152                },
4153                TxType::Other,
4154                "lock_pending_by_ffqns_auto_get",
4155            )
4156            .await
4157            .map_err(to_generic_error)?;
4158        if execution_ids_versions.is_empty() {
4159            Ok(vec![])
4160        } else {
4161            debug!("Auto-locking {execution_ids_versions:?}");
4162            self.transaction(
4163                move |tx| {
4164                    let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
4165                    for (execution_id, version) in &execution_ids_versions {
4166                        locked_execs.push(Self::lock_single_execution(
4167                            tx,
4168                            created_at,
4169                            &component_id,
4170                            false,
4171                            deployment_id,
4172                            execution_id,
4173                            run_id,
4174                            version,
4175                            executor_id,
4176                            lock_expires_at,
4177                            retry_config,
4178                        )?);
4179                    }
4180                    Ok::<_, DbErrorWrite>(locked_execs)
4181                },
4182                TxType::MultipleWrites,
4183                "lock_pending_by_ffqns_auto_one",
4184            )
4185            .await
4186        }
4187    }
4188
4189    #[instrument(level = Level::TRACE, skip(self))]
4190    async fn lock_pending_by_component_digest(
4191        &self,
4192        batch_size: u32,
4193        pending_at_or_sooner: DateTime<Utc>,
4194        component_id: &ComponentId,
4195        deployment_id: DeploymentId,
4196        created_at: DateTime<Utc>,
4197        executor_id: ExecutorId,
4198        lock_expires_at: DateTime<Utc>,
4199        run_id: RunId,
4200        retry_config: ComponentRetryConfig,
4201    ) -> Result<LockPendingResponse, DbErrorWrite> {
4202        let component_id = component_id.clone();
4203        let execution_ids_versions = self
4204            .transaction(
4205                {
4206                    let component_id = component_id.clone();
4207                    move |conn| {
4208                        Self::get_pending_by_component_input_digest(
4209                            conn,
4210                            batch_size,
4211                            pending_at_or_sooner,
4212                            &component_id.component_digest,
4213                        )
4214                    }
4215                },
4216                TxType::Other, // read only
4217                "lock_pending_by_component_id_get",
4218            )
4219            .await
4220            .map_err(to_generic_error)?;
4221        if execution_ids_versions.is_empty() {
4222            Ok(vec![])
4223        } else {
4224            debug!("Locking {execution_ids_versions:?}");
4225            self.transaction(
4226                move |tx| {
4227                    let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
4228                    // Append lock
4229                    for (execution_id, version) in &execution_ids_versions {
4230                        locked_execs.push(Self::lock_single_execution(
4231                            tx,
4232                            created_at,
4233                            &component_id,
4234                            true,
4235                            deployment_id,
4236                            execution_id,
4237                            run_id,
4238                            version,
4239                            executor_id,
4240                            lock_expires_at,
4241                            retry_config,
4242                        )?);
4243                    }
4244                    Ok::<_, DbErrorWrite>(locked_execs)
4245                },
4246                TxType::MultipleWrites,
4247                "lock_pending_by_component_id_one",
4248            )
4249            .await
4250        }
4251    }
4252
4253    #[cfg(feature = "test")]
4254    #[instrument(level = Level::DEBUG, skip(self))]
4255    async fn lock_one(
4256        &self,
4257        created_at: DateTime<Utc>,
4258        component_id: ComponentId,
4259        deployment_id: DeploymentId,
4260        execution_id: &ExecutionId,
4261        run_id: RunId,
4262        version: Version,
4263        executor_id: ExecutorId,
4264        lock_expires_at: DateTime<Utc>,
4265        retry_config: ComponentRetryConfig,
4266    ) -> Result<LockedExecution, DbErrorWrite> {
4267        debug!(%execution_id, "lock_one");
4268        let execution_id = execution_id.clone();
4269        self.transaction(
4270            move |tx| {
4271                Self::lock_single_execution(
4272                    tx,
4273                    created_at,
4274                    &component_id,
4275                    true,
4276                    deployment_id,
4277                    &execution_id,
4278                    run_id,
4279                    &version,
4280                    executor_id,
4281                    lock_expires_at,
4282                    retry_config,
4283                )
4284            },
4285            TxType::MultipleWrites, // insert + update t_state
4286            "lock_inner",
4287        )
4288        .await
4289    }
4290
4291    #[instrument(level = Level::DEBUG, skip(self, req))]
4292    async fn append(
4293        &self,
4294        execution_id: ExecutionId,
4295        version: Version,
4296        req: AppendRequest,
4297    ) -> Result<AppendResponse, DbErrorWrite> {
4298        debug!(%req, "append");
4299        trace!(?req, "append");
4300        let created_at = req.created_at;
4301        let (version, notifier) = self
4302            .transaction(
4303                move |tx| Self::append(tx, &execution_id, req.clone(), version.clone()),
4304                TxType::MultipleWrites, // insert + update t_state
4305                "append",
4306            )
4307            .await?;
4308        self.notify_all(vec![notifier], created_at);
4309        Ok(version)
4310    }
4311
4312    #[instrument(level = Level::DEBUG, skip_all)]
4313    async fn append_batch_respond_to_parent(
4314        &self,
4315        events: AppendEventsToExecution,
4316        response: AppendResponseToExecution,
4317        current_time: DateTime<Utc>,
4318    ) -> Result<AppendBatchResponse, DbErrorWrite> {
4319        debug!("append_batch_respond_to_parent");
4320        if events.execution_id == response.parent_execution_id {
4321            // Pending state would be wrong.
4322            // This is not a panic because it depends on DB state.
4323            return Err(DbErrorWrite::NonRetriable(
4324                DbErrorWriteNonRetriable::ValidationFailed(
4325                    "Parameters `execution_id` and `parent_execution_id` cannot be the same".into(),
4326                ),
4327            ));
4328        }
4329        if events.batch.is_empty() {
4330            error!("Batch cannot be empty");
4331            return Err(DbErrorWrite::NonRetriable(
4332                DbErrorWriteNonRetriable::ValidationFailed("batch cannot be empty".into()),
4333            ));
4334        }
4335        let (version, notifiers) = {
4336            self.transaction(
4337                move |tx| {
4338                    let mut version = events.version.clone();
4339                    let mut notifier_of_child = None;
4340                    for append_request in &events.batch {
4341                        let (v, n) = Self::append(
4342                            tx,
4343                            &events.execution_id,
4344                            append_request.clone(),
4345                            version,
4346                        )?;
4347                        version = v;
4348                        notifier_of_child = Some(n);
4349                    }
4350
4351                    let pending_at_parent = Self::append_response(
4352                        tx,
4353                        &response.parent_execution_id,
4354                        JoinSetResponseEventOuter {
4355                            created_at: response.created_at,
4356                            event: JoinSetResponseEvent {
4357                                join_set_id: response.join_set_id.clone(),
4358                                event: JoinSetResponse::ChildExecutionFinished {
4359                                    child_execution_id: response.child_execution_id.clone(),
4360                                    finished_version: response.finished_version.clone(),
4361                                    result: response.result.clone(),
4362                                },
4363                            },
4364                        },
4365                    )?;
4366                    Ok::<_, DbErrorWrite>((
4367                        version,
4368                        vec![
4369                            notifier_of_child.expect("checked that the batch is not empty"),
4370                            pending_at_parent,
4371                        ],
4372                    ))
4373                },
4374                TxType::MultipleWrites,
4375                "append_batch_respond_to_parent",
4376            )
4377            .await?
4378        };
4379        self.notify_all(notifiers, current_time);
4380        Ok(version)
4381    }
4382
4383    // Supports only one subscriber (executor) per ffqn.
4384    // A new subscriber replaces the old one, which will eventually time out, which is fine.
4385    #[instrument(level = Level::TRACE, skip(self, timeout_fut))]
4386    async fn wait_for_pending_by_ffqn(
4387        &self,
4388        pending_at_or_sooner: DateTime<Utc>,
4389        ffqns: Arc<[FunctionFqn]>,
4390        current_digest: Option<ComponentDigest>,
4391        timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
4392    ) {
4393        let unique_tag: u64 = rand::random();
4394        let (sender, mut receiver) = mpsc::channel(1); // senders must use `try_send`
4395        {
4396            let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4397            for ffqn in ffqns.as_ref() {
4398                pending_subscribers.insert_ffqn(ffqn.clone(), (sender.clone(), unique_tag));
4399            }
4400        }
4401        async {
4402            let Ok(execution_ids_versions) = self
4403                .transaction(
4404                    {
4405                        let ffqns = ffqns.clone();
4406                        move |conn| {
4407                            if let Some(current_digest) = &current_digest {
4408                                Self::get_pending_by_ffqns_auto(
4409                                    conn,
4410                                    1,
4411                                    pending_at_or_sooner,
4412                                    ffqns.as_ref(),
4413                                    current_digest,
4414                                )
4415                            } else {
4416                                Self::get_pending_by_ffqns(
4417                                    conn,
4418                                    1,
4419                                    pending_at_or_sooner,
4420                                    ffqns.as_ref(),
4421                                )
4422                            }
4423                        }
4424                    },
4425                    TxType::Other, // read only
4426                    "get_pending_by_ffqns",
4427                )
4428                .await
4429            else {
4430                trace!(
4431                    "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
4432                );
4433                timeout_fut.await;
4434                return;
4435            };
4436            if !execution_ids_versions.is_empty() {
4437                trace!("Not waiting, database already contains new pending executions");
4438                return;
4439            }
4440            tokio::select! { // future's liveness: Dropping the loser immediately.
4441                _ = receiver.recv() => {
4442                    trace!("Received a notification");
4443                }
4444                () = timeout_fut => {
4445                }
4446            }
4447        }.await;
4448        // Clean up ffqn_to_pending_subscription in any case
4449        {
4450            let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4451            for ffqn in ffqns.as_ref() {
4452                match pending_subscribers.remove_ffqn(ffqn) {
4453                    Some((_, tag)) if tag == unique_tag => {
4454                        // Cleanup OK.
4455                    }
4456                    Some(other) => {
4457                        // Reinsert foreign sender.
4458                        pending_subscribers.insert_ffqn(ffqn.clone(), other);
4459                    }
4460                    None => {
4461                        // Value was replaced and cleaned up already.
4462                    }
4463                }
4464            }
4465        }
4466    }
4467
4468    // Supports only one subscriber (executor) per component id.
4469    // A new subscriber replaces the old one, which will eventually time out, which is fine.
4470    #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
4471    async fn wait_for_pending_by_component_digest(
4472        &self,
4473        pending_at_or_sooner: DateTime<Utc>,
4474        component_digest: &ComponentDigest,
4475        timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
4476    ) {
4477        let unique_tag: u64 = rand::random();
4478        let (sender, mut receiver) = mpsc::channel(1); // senders must use `try_send`
4479        {
4480            let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4481            pending_subscribers
4482                .insert_by_component(component_digest.clone(), (sender.clone(), unique_tag));
4483        }
4484        async {
4485            let Ok(execution_ids_versions) = self
4486                .transaction(
4487                    {
4488                        let input_digest = component_digest.clone();
4489                        move |conn| Self::get_pending_by_component_input_digest(conn, 1, pending_at_or_sooner, &input_digest)
4490                    },
4491                    TxType::Other, // read only
4492                    "get_pending_by_component_input_digest",
4493                )
4494                .await
4495            else {
4496                trace!(
4497                    "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
4498                );
4499                timeout_fut.await;
4500                return;
4501            };
4502            if !execution_ids_versions.is_empty() {
4503                trace!("Not waiting, database already contains new pending executions");
4504                return;
4505            }
4506            tokio::select! { // future's liveness: Dropping the loser immediately.
4507                _ = receiver.recv() => {
4508                    trace!("Received a notification");
4509                }
4510                () = timeout_fut => {
4511                }
4512            }
4513        }.await;
4514        // Clean up ffqn_to_pending_subscription in any case
4515        {
4516            let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4517
4518            match pending_subscribers.remove_by_component(component_digest) {
4519                Some((_, tag)) if tag == unique_tag => {
4520                    // Cleanup OK.
4521                }
4522                Some(other) => {
4523                    // Reinsert foreign sender.
4524                    pending_subscribers.insert_by_component(component_digest.clone(), other);
4525                }
4526                None => {
4527                    // Value was replaced and cleaned up already.
4528                }
4529            }
4530        }
4531    }
4532
4533    async fn get_last_execution_event(
4534        &self,
4535        execution_id: &ExecutionId,
4536    ) -> Result<ExecutionEvent, DbErrorRead> {
4537        let execution_id = execution_id.clone();
4538        self.transaction(
4539            move |tx| Self::get_last_execution_event(tx, &execution_id),
4540            TxType::Other, // read only
4541            "get_last_execution_event",
4542        )
4543        .await
4544    }
4545
4546    #[instrument(skip(self))]
4547    async fn append_activity_cancellation_requested(
4548        &self,
4549        execution_id: &ExecutionId,
4550        cancelled_at: DateTime<Utc>,
4551    ) -> Result<CancelOutcome, DbErrorWrite> {
4552        let execution_id = execution_id.clone();
4553        self.transaction(
4554            move |tx| {
4555                let combined_state = Self::get_combined_state(tx, &execution_id)?;
4556                SqlitePool::append_activity_cancellation_requested_tx(
4557                    tx,
4558                    &execution_id,
4559                    cancelled_at,
4560                    &combined_state,
4561                )
4562            },
4563            TxType::MultipleWrites,
4564            "append_activity_cancellation_requested",
4565        )
4566        .await
4567    }
4568
4569    #[instrument(skip(self))]
4570    async fn cancel_workflow(
4571        &self,
4572        execution_id: &ExecutionId,
4573        cancelled_at: DateTime<Utc>,
4574    ) -> Result<CancelOutcome, DbErrorWrite> {
4575        let execution_id = execution_id.clone();
4576        self.transaction(
4577            move |tx| SqlitePool::cancel_workflow(tx, &execution_id, cancelled_at),
4578            TxType::MultipleWrites,
4579            "cancel_workflow",
4580        )
4581        .await
4582    }
4583}
4584
4585#[async_trait]
4586impl DbExternalApi for SqlitePool {
4587    #[instrument(skip(self))]
4588    async fn get_backtrace(
4589        &self,
4590        execution_id: &ExecutionId,
4591        filter: BacktraceFilter,
4592    ) -> Result<BacktraceInfo, DbErrorRead> {
4593        debug!("get_backtrace");
4594        let execution_id = execution_id.clone();
4595
4596        self.transaction(
4597            move |tx| {
4598                let select = "SELECT component_id, version_min_including, version_max_excluding, wasm_backtrace FROM t_execution_backtrace e \
4599                                INNER JOIN t_wasm_backtrace w ON e.backtrace_hash = w.backtrace_hash \
4600                                WHERE execution_id = :execution_id";
4601                let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![(":execution_id", Box::new(execution_id.to_string()))];
4602                let select = match &filter {
4603                    BacktraceFilter::Specific(version) =>{
4604                        params.push((":version", Box::new(version.0)));
4605                        format!("{select} AND version_min_including <= :version AND version_max_excluding > :version")
4606                    },
4607                    BacktraceFilter::First => format!("{select} ORDER BY version_min_including LIMIT 1"),
4608                    BacktraceFilter::Last => format!("{select} ORDER BY version_min_including DESC LIMIT 1")
4609                };
4610                tx
4611                    .prepare(&select)
4612                    ?
4613                    .query_row::<_, &[(&'static str, &dyn ToSql)], _>(
4614                        params
4615                            .iter()
4616                            .map(|(key, value)| (*key, value.as_ref()))
4617                            .collect::<Vec<_>>()
4618                            .as_ref(),
4619                    |row| {
4620                        Ok(BacktraceInfo {
4621                            execution_id: execution_id.clone(),
4622                            component_id: row.get::<_, JsonWrapper<_> >("component_id")?.0,
4623                            version_min_including: Version::new(row.get::<_, VersionType>("version_min_including")?),
4624                            version_max_excluding: Version::new(row.get::<_, VersionType>("version_max_excluding")?),
4625                            wasm_backtrace: row.get::<_, JsonWrapper<_>>("wasm_backtrace")?.0,
4626                        })
4627                    },
4628                ).map_err(DbErrorRead::from)
4629            },
4630            TxType::Other, // read only
4631            "get_last_backtrace",
4632        ).await
4633    }
4634
4635    #[instrument(skip_all)]
4636    async fn upsert_source_mapping(
4637        &self,
4638        component_digest: &ComponentDigest,
4639        frame_key: &str,
4640        is_suffix: bool,
4641        digest: &ContentDigest,
4642    ) -> Result<(), DbErrorWrite> {
4643        let component_digest = component_digest.clone();
4644        let frame_key = frame_key.to_owned();
4645        let digest = digest.to_string();
4646        self.transaction(
4647            move |tx| {
4648                tx.prepare(
4649                    "INSERT INTO t_component_source \
4650                     (component_digest, frame_key, is_suffix, digest) \
4651                     VALUES (:component_digest, :frame_key, :is_suffix, :digest) \
4652                     ON CONFLICT (component_digest, frame_key, is_suffix) \
4653                     DO UPDATE SET digest = excluded.digest",
4654                )?
4655                .execute(named_params! {
4656                    ":component_digest": component_digest,
4657                    ":frame_key": frame_key,
4658                    ":is_suffix": is_suffix,
4659                    ":digest": digest,
4660                })?;
4661                Ok(())
4662            },
4663            TxType::Other,
4664            "upsert_source_mapping",
4665        )
4666        .await
4667    }
4668
4669    #[instrument(skip_all)]
4670    async fn resolve_source_digest(
4671        &self,
4672        component_digest: &ComponentDigest,
4673        file: &str,
4674    ) -> Result<Option<ContentDigest>, DbErrorRead> {
4675        let component_digest = component_digest.clone();
4676        let file = file.to_owned();
4677        self.transaction(
4678            move |tx| {
4679                let mut stmt = tx.prepare(
4680                    "SELECT digest \
4681                     FROM t_component_source \
4682                     WHERE component_digest = :component_digest \
4683                       AND ( \
4684                           (is_suffix = 0 AND frame_key = :file) \
4685                        OR (is_suffix = 1 AND \
4686                            substr(:file, length(:file) - length(frame_key) + 1) = frame_key) \
4687                       )",
4688                )?;
4689                let rows: Vec<ContentDigest> = stmt
4690                    .query_map(
4691                        named_params! {
4692                            ":component_digest": component_digest,
4693                            ":file": file,
4694                        },
4695                        |row| row.get("digest"),
4696                    )?
4697                    .collect::<Result<_, _>>()?;
4698                match rows.len() {
4699                    0 => Ok(None),
4700                    1 => Ok(Some(rows.into_iter().next().unwrap())),
4701                    _ => {
4702                        warn!("Multiple suffix matches for '{file}', returning None");
4703                        Ok(None)
4704                    }
4705                }
4706            },
4707            TxType::Other,
4708            "resolve_source_digest",
4709        )
4710        .await
4711    }
4712
4713    #[instrument(skip_all)]
4714    async fn upsert_component_metadata(
4715        &self,
4716        records: Vec<ComponentMetadataRecord>,
4717    ) -> Result<(), DbErrorWrite> {
4718        self.transaction(
4719            move |tx| Self::upsert_component_metadata_tx(tx, &records),
4720            TxType::MultipleWrites,
4721            "upsert_component_metadata",
4722        )
4723        .await
4724    }
4725
4726    #[instrument(skip_all)]
4727    async fn insert_deployment_components(
4728        &self,
4729        deployment_id: DeploymentId,
4730        records: Vec<DeploymentComponentRecord>,
4731    ) -> Result<(), DbErrorWrite> {
4732        self.transaction(
4733            move |tx| Self::insert_deployment_components_tx(tx, deployment_id, &records),
4734            TxType::MultipleWrites,
4735            "insert_deployment_components",
4736        )
4737        .await
4738    }
4739
4740    #[instrument(skip_all)]
4741    async fn list_deployment_components(
4742        &self,
4743        deployment_id: DeploymentId,
4744    ) -> Result<Vec<DeploymentComponentDetail>, DbErrorRead> {
4745        self.transaction(
4746            move |tx| {
4747                let mut stmt = tx.prepare(
4748                    "SELECT dc.component_name, dc.component_type, dc.component_digest, \
4749                            cm.imports_json, cm.exports_json, cm.wit \
4750                     FROM t_deployment_component dc \
4751                     JOIN t_component_metadata cm ON dc.component_digest = cm.component_digest \
4752                     WHERE dc.deployment_id = :deployment_id \
4753                     ORDER BY dc.component_type, dc.component_name",
4754                )?;
4755                let rows = stmt
4756                    .query_map(
4757                        named_params! { ":deployment_id": deployment_id.to_string() },
4758                        |row| {
4759                            deployment_component_detail_from_row(row).map_err(|err| {
4760                                rusqlite::Error::ToSqlConversionFailure(Box::new(err))
4761                            })
4762                        },
4763                    )?
4764                    .collect::<Result<Vec<_>, _>>()?;
4765                Ok(rows)
4766            },
4767            TxType::Other,
4768            "list_deployment_components",
4769        )
4770        .await
4771    }
4772
4773    #[instrument(skip_all)]
4774    async fn get_deployment_component_wit(
4775        &self,
4776        deployment_id: DeploymentId,
4777        component_digest: &ComponentDigest,
4778    ) -> Result<Option<String>, DbErrorRead> {
4779        let component_digest = component_digest.clone();
4780        self.transaction(
4781            move |tx| {
4782                tx.prepare(
4783                    "SELECT cm.wit \
4784                     FROM t_deployment_component dc \
4785                     JOIN t_component_metadata cm ON dc.component_digest = cm.component_digest \
4786                     WHERE dc.deployment_id = :deployment_id \
4787                       AND dc.component_digest = :component_digest \
4788                     LIMIT 1",
4789                )?
4790                .query_row(
4791                    named_params! {
4792                        ":deployment_id": deployment_id.to_string(),
4793                        ":component_digest": component_digest,
4794                    },
4795                    |row| row.get(0),
4796                )
4797                .optional()
4798                .map_err(DbErrorRead::from)
4799            },
4800            TxType::Other,
4801            "get_deployment_component_wit",
4802        )
4803        .await
4804    }
4805
4806    #[instrument(skip(self))]
4807    async fn list_executions(
4808        &self,
4809        filter: ListExecutionsFilter,
4810        pagination: ExecutionListPagination,
4811    ) -> Result<Vec<ExecutionWithState>, DbErrorGeneric> {
4812        self.transaction(
4813            move |tx| Self::list_executions(tx, &filter, &pagination),
4814            TxType::Other, // read only
4815            "list_executions",
4816        )
4817        .await
4818        .map_err(to_generic_error)
4819    }
4820
4821    #[instrument(skip(self))]
4822    async fn list_execution_events(
4823        &self,
4824        execution_id: &ExecutionId,
4825        pagination: Pagination<VersionType>,
4826        include_backtrace_id: bool,
4827    ) -> Result<ListExecutionEventsResponse, DbErrorRead> {
4828        let execution_id = execution_id.clone();
4829        self.transaction(
4830            move |tx| {
4831                let events = Self::list_execution_events(
4832                    tx,
4833                    &execution_id,
4834                    pagination,
4835                    include_backtrace_id,
4836                )?;
4837                let max_version = Self::get_max_version(tx, &execution_id)?;
4838                Ok(ListExecutionEventsResponse {
4839                    events,
4840                    max_version,
4841                })
4842            },
4843            TxType::Other, // read only
4844            "get",
4845        )
4846        .await
4847    }
4848
4849    #[instrument(skip(self))]
4850    async fn list_responses(
4851        &self,
4852        execution_id: &ExecutionId,
4853        pagination: Pagination<u32>,
4854    ) -> Result<ListResponsesResponse, DbErrorRead> {
4855        let execution_id = execution_id.clone();
4856        self.transaction(
4857            move |tx| {
4858                let responses = Self::list_responses(tx, &execution_id, Some(pagination))?;
4859                let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4860                Ok(ListResponsesResponse {
4861                    responses,
4862                    max_cursor,
4863                })
4864            },
4865            TxType::Other, // read only
4866            "list_responses",
4867        )
4868        .await
4869    }
4870
4871    #[instrument(skip(self))]
4872    async fn list_execution_events_responses(
4873        &self,
4874        execution_id: &ExecutionId,
4875        req_since: &Version,
4876        req_max_length: VersionType,
4877        req_include_backtrace_id: bool,
4878        resp_pagination: Pagination<u32>,
4879    ) -> Result<ExecutionWithStateRequestsResponses, DbErrorRead> {
4880        let execution_id = execution_id.clone();
4881        let req_since = req_since.0;
4882        self.transaction(
4883            move |tx| {
4884                let combined_state = Self::get_combined_state(tx, &execution_id)?;
4885                let events = Self::list_execution_events(
4886                    tx,
4887                    &execution_id,
4888                    Pagination::NewerThan {
4889                        length: req_max_length
4890                            .try_into()
4891                            .expect("req_max_length fits in u16"),
4892                        cursor: req_since,
4893                        including_cursor: true,
4894                    },
4895                    req_include_backtrace_id,
4896                )?;
4897                let responses = Self::list_responses(tx, &execution_id, Some(resp_pagination))?;
4898                let max_version = Self::get_max_version(tx, &execution_id)?;
4899                let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4900                Ok(ExecutionWithStateRequestsResponses {
4901                    execution_with_state: combined_state.execution_with_state,
4902                    events,
4903                    responses,
4904                    max_version,
4905                    max_cursor,
4906                })
4907            },
4908            TxType::Other, // read only
4909            "list_execution_events_responses",
4910        )
4911        .await
4912    }
4913
4914    #[instrument(skip(self))]
4915    async fn upgrade_execution_component(
4916        &self,
4917        execution_id: &ExecutionId,
4918        old: &ComponentDigest,
4919        new: &ComponentDigest,
4920        reason: ComponentUpgradeReason,
4921    ) -> Result<(), DbErrorWrite> {
4922        let execution_id = execution_id.clone();
4923        let old = old.clone();
4924        let new = new.clone();
4925        self.transaction(
4926            move |tx| {
4927                Self::upgrade_execution_component_single_write(
4928                    tx,
4929                    &execution_id,
4930                    &old,
4931                    &new,
4932                    reason.clone(),
4933                )
4934            },
4935            TxType::Other, // single write
4936            "upgrade_execution_component",
4937        )
4938        .await
4939    }
4940
4941    #[instrument(skip(self))]
4942    async fn list_logs(
4943        &self,
4944        execution_id: &ExecutionId,
4945        show_derived: bool,
4946        filter: LogFilter,
4947        pagination: Pagination<LogCursor>,
4948    ) -> Result<ListLogsResponse, DbErrorRead> {
4949        let execution_id = execution_id.clone();
4950        self.transaction(
4951            move |tx| Self::list_logs_tx(tx, &execution_id, show_derived, &filter, &pagination),
4952            TxType::Other, // read only
4953            "list_logs",
4954        )
4955        .await
4956    }
4957
4958    #[instrument(skip(self))]
4959    async fn list_deployment_states(
4960        &self,
4961        current_time: DateTime<Utc>,
4962        pagination: Pagination<Option<DeploymentId>>,
4963        include_deployment_toml: bool,
4964        execution_counts: DeploymentExecutionCounts,
4965    ) -> Result<Vec<DeploymentState>, DbErrorRead> {
4966        self.transaction(
4967            move |tx| {
4968                Self::list_deployment_states(
4969                    tx,
4970                    current_time,
4971                    pagination,
4972                    include_deployment_toml,
4973                    execution_counts,
4974                )
4975            },
4976            TxType::Other, // read only
4977            "list_deployment_states",
4978        )
4979        .await
4980    }
4981
4982    #[instrument(skip(self))]
4983    async fn insert_deployment(&self, record: DeploymentRecord) -> Result<(), DbErrorWrite> {
4984        self.transaction(
4985            move |tx| Self::insert_deployment_tx(tx, &record),
4986            TxType::MultipleWrites,
4987            "insert_deployment",
4988        )
4989        .await
4990    }
4991
4992    async fn insert_deployment_with_components(
4993        &self,
4994        record: DeploymentRecord,
4995        component_metadata: Vec<ComponentMetadataRecord>,
4996        deployment_components: Vec<DeploymentComponentRecord>,
4997    ) -> Result<(), DbErrorWrite> {
4998        let deployment_id = record.deployment_id;
4999        self.transaction(
5000            move |tx| {
5001                // Parent row first so the t_deployment_component FK is satisfied within the tx.
5002                Self::insert_deployment_tx(tx, &record)?;
5003                Self::upsert_component_metadata_tx(tx, &component_metadata)?;
5004                Self::insert_deployment_components_tx(tx, deployment_id, &deployment_components)
5005            },
5006            TxType::MultipleWrites,
5007            "insert_deployment_with_components",
5008        )
5009        .await
5010    }
5011
5012    #[instrument(skip(self))]
5013    async fn missing_digests(
5014        &self,
5015        deployment_id: DeploymentId,
5016    ) -> Result<Vec<ContentDigest>, DbErrorRead> {
5017        self.transaction(
5018            move |tx| Self::missing_digests_tx(tx, deployment_id),
5019            TxType::Other,
5020            "missing_digests",
5021        )
5022        .await
5023    }
5024
5025    #[instrument(skip(self))]
5026    async fn list_deployment_files(
5027        &self,
5028        deployment_id: DeploymentId,
5029    ) -> Result<Vec<DeploymentFileRecord>, DbErrorRead> {
5030        self.transaction(
5031            move |tx| Self::list_deployment_files_tx(tx, deployment_id),
5032            TxType::Other,
5033            "list_deployment_files",
5034        )
5035        .await
5036    }
5037
5038    #[instrument(skip(self))]
5039    async fn gc_orphan_files(&self) -> Result<u64, DbErrorWrite> {
5040        self.transaction(
5041            move |tx| {
5042                let deleted = tx
5043                    .execute(
5044                        "DELETE FROM t_file WHERE digest NOT IN \
5045                         (SELECT digest FROM t_deployment_file \
5046                          UNION SELECT digest FROM t_component_source)",
5047                        [],
5048                    )
5049                    .map_err(RusqliteError::from)?;
5050                Ok(deleted as u64)
5051            },
5052            TxType::MultipleWrites,
5053            "gc_orphan_files",
5054        )
5055        .await
5056    }
5057
5058    #[instrument(skip(self))]
5059    async fn activate_deployment(
5060        &self,
5061        deployment_id: DeploymentId,
5062        now: DateTime<Utc>,
5063    ) -> Result<(), DbErrorWrite> {
5064        self.transaction(
5065            move |tx| Self::activate_deployment_tx(tx, deployment_id, now),
5066            TxType::MultipleWrites,
5067            "activate_deployment",
5068        )
5069        .await
5070    }
5071
5072    async fn enqueue_deployment(
5073        &self,
5074        deployment_id: DeploymentId,
5075    ) -> Result<EnqueueOutcome, DbErrorWrite> {
5076        self.transaction(
5077            move |tx| Self::enqueue_deployment_tx(tx, deployment_id),
5078            TxType::MultipleWrites,
5079            "enqueue_deployment",
5080        )
5081        .await
5082    }
5083
5084    #[instrument(skip(self))]
5085    async fn get_deployment(
5086        &self,
5087        deployment_id: DeploymentId,
5088    ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
5089        self.transaction(
5090            move |tx| Self::get_deployment_tx(tx, deployment_id),
5091            TxType::Other,
5092            "get_deployment",
5093        )
5094        .await
5095    }
5096
5097    #[cfg(feature = "test")]
5098    #[instrument(skip(self))]
5099    async fn get_active_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
5100        self.transaction(
5101            move |tx| Self::get_active_deployment_tx(tx),
5102            TxType::Other,
5103            "get_active_deployment",
5104        )
5105        .await
5106    }
5107
5108    #[instrument(skip(self))]
5109    async fn get_current_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
5110        self.transaction(
5111            move |tx| {
5112                let Some(record) = tx
5113                    .query_row(
5114                    "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
5115                     FROM t_deployment WHERE status IN ('enqueued', 'active') \
5116                     ORDER BY CASE status WHEN 'enqueued' THEN 0 ELSE 1 END LIMIT 1",
5117                        [],
5118                        deployment_record_from_row,
5119                    )
5120                    .optional()
5121                    .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))?
5122                else {
5123                    return Ok(None);
5124                };
5125                Self::with_deployment_files_tx(tx, record).map(Some)
5126            },
5127            TxType::Other,
5128            "get_current_deployment",
5129        )
5130        .await
5131    }
5132
5133    #[instrument(skip(self))]
5134    async fn list_deployments(
5135        &self,
5136        pagination: Pagination<Option<DeploymentId>>,
5137    ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
5138        self.transaction(
5139            move |tx| Self::list_deployments_tx(tx, pagination),
5140            TxType::Other,
5141            "list_deployments",
5142        )
5143        .await
5144    }
5145
5146    #[instrument(skip(self))]
5147    async fn pause_execution(
5148        &self,
5149        execution_id: &ExecutionId,
5150        paused_at: DateTime<Utc>,
5151    ) -> Result<AppendResponse, DbErrorWrite> {
5152        let execution_id = execution_id.clone();
5153        self.transaction(
5154            move |tx| SqlitePool::pause_execution(tx, &execution_id, paused_at),
5155            TxType::MultipleWrites,
5156            "pause_execution",
5157        )
5158        .await
5159    }
5160
5161    #[instrument(skip(self))]
5162    async fn unpause_execution(
5163        &self,
5164        execution_id: &ExecutionId,
5165        unpaused_at: DateTime<Utc>,
5166    ) -> Result<AppendResponse, DbErrorWrite> {
5167        let execution_id = execution_id.clone();
5168        self.transaction(
5169            move |tx| SqlitePool::unpause_execution(tx, &execution_id, unpaused_at),
5170            TxType::MultipleWrites,
5171            "unpause_execution",
5172        )
5173        .await
5174    }
5175
5176    #[instrument(skip(self))]
5177    async fn pause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite> {
5178        let delay_id = delay_id.clone();
5179        self.transaction(
5180            move |tx| {
5181                let (execution_id, join_set_id) = delay_id.split_to_parts();
5182                let rows_modified = tx.execute(
5183                    "UPDATE t_delay SET is_paused = 1 \
5184                     WHERE execution_id = :execution_id AND join_set_id = :join_set_id AND delay_id = :delay_id",
5185                    named_params! {
5186                        ":execution_id": execution_id.to_string(),
5187                        ":join_set_id": join_set_id.to_string(),
5188                        ":delay_id": delay_id.to_string(),
5189                    },
5190                )?;
5191                if rows_modified == 0 {
5192                    return Err(DbErrorWrite::NotFound);
5193                }
5194                Ok(())
5195            },
5196            TxType::Other,
5197            "pause_delay",
5198        )
5199        .await
5200    }
5201
5202    #[instrument(skip(self))]
5203    async fn unpause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite> {
5204        let delay_id = delay_id.clone();
5205        self.transaction(
5206            move |tx| {
5207                let (execution_id, join_set_id) = delay_id.split_to_parts();
5208                let rows_modified = tx.execute(
5209                    "UPDATE t_delay SET is_paused = 0 \
5210                     WHERE execution_id = :execution_id AND join_set_id = :join_set_id AND delay_id = :delay_id",
5211                    named_params! {
5212                        ":execution_id": execution_id.to_string(),
5213                        ":join_set_id": join_set_id.to_string(),
5214                        ":delay_id": delay_id.to_string(),
5215                    },
5216                )?;
5217                if rows_modified == 0 {
5218                    return Err(DbErrorWrite::NotFound);
5219                }
5220                Ok(())
5221            },
5222            TxType::Other,
5223            "unpause_delay",
5224        )
5225        .await
5226    }
5227}
5228
5229#[async_trait]
5230impl Cas for SqlitePool {
5231    async fn read_blob(&self, digest: &ContentDigest) -> Result<Option<Vec<u8>>, CasError> {
5232        let digest = digest.clone();
5233        self.transaction(
5234            move |tx| Self::get_file_tx(tx, &digest),
5235            TxType::Other,
5236            "cas_read_blob",
5237        )
5238        .await
5239        .map_err(|err| CasError::Uncategorized(err.to_string()))
5240    }
5241
5242    async fn write_blob(&self, content: &[u8]) -> Result<ContentDigest, CasError> {
5243        let digest = Self::compute_file_digest(content);
5244        let content = content.to_vec();
5245        {
5246            let digest = digest.clone();
5247            self.transaction(
5248                move |tx| Self::upload_file_tx(tx, &digest, &content),
5249                TxType::MultipleWrites,
5250                "cas_write_blob",
5251            )
5252            .await
5253            .map_err(|err| CasError::Uncategorized(err.to_string()))?;
5254        }
5255        Ok(digest)
5256    }
5257
5258    async fn contains_blob(&self, digest: &ContentDigest) -> Result<bool, CasError> {
5259        Ok(self.read_blob(digest).await?.is_some())
5260    }
5261}
5262
5263#[async_trait]
5264impl DbConnection for SqlitePool {
5265    #[instrument(level = Level::DEBUG, skip_all, fields(execution_id = %req.execution_id))]
5266    async fn create(&self, req: CreateRequest) -> Result<AppendResponse, DbErrorWrite> {
5267        debug!("create");
5268        trace!(?req, "create");
5269        let created_at = req.created_at;
5270        let (version, notifier) = self
5271            .transaction(
5272                move |tx| Self::create_inner(tx, req.clone()),
5273                TxType::MultipleWrites,
5274                "create",
5275            )
5276            .await?;
5277        self.notify_all(vec![notifier], created_at);
5278        Ok(version)
5279    }
5280
5281    #[instrument(level = Level::DEBUG, skip(self))]
5282    async fn get(
5283        &self,
5284        execution_id: &ExecutionId,
5285    ) -> Result<concepts::storage::ExecutionLog, DbErrorRead> {
5286        trace!("get");
5287        let execution_id = execution_id.clone();
5288        self.transaction(
5289            move |tx| Self::get(tx, &execution_id),
5290            TxType::Other, // read only
5291            "get",
5292        )
5293        .await
5294    }
5295
5296    #[instrument(level = Level::DEBUG, skip(self))]
5297    async fn get_cancelling(&self, batch_size: u32) -> Result<Vec<ExecutionId>, DbErrorRead> {
5298        self.transaction(
5299            move |tx| {
5300                let mut stmt = tx.prepare(
5301                    "SELECT execution_id FROM t_state WHERE lifecycle = :lifecycle \
5302                     ORDER BY created_at LIMIT :batch_size",
5303                )?;
5304                let rows = stmt
5305                    .query_map(
5306                        named_params! {
5307                            ":lifecycle": LIFECYCLE_CANCELLING,
5308                            ":batch_size": batch_size,
5309                        },
5310                        |row| row.get::<_, String>("execution_id"),
5311                    )?
5312                    .collect::<Result<Vec<_>, _>>()?;
5313                rows.into_iter()
5314                    .map(|id| {
5315                        ExecutionId::from_str(&id)
5316                            .map_err(|_| consistency_rusqlite("invalid t_state.execution_id"))
5317                            .map_err(DbErrorRead::from)
5318                    })
5319                    .collect()
5320            },
5321            TxType::Other, // read only
5322            "get_cancelling",
5323        )
5324        .await
5325    }
5326
5327    #[instrument(level = Level::DEBUG, skip(self, batch))]
5328    async fn append_batch(
5329        &self,
5330        current_time: DateTime<Utc>,
5331        batch: Vec<AppendRequest>,
5332        execution_id: ExecutionId,
5333        version: Version,
5334    ) -> Result<AppendBatchResponse, DbErrorWrite> {
5335        debug!("append_batch");
5336        trace!(?batch, "append_batch");
5337        assert!(!batch.is_empty(), "Empty batch request");
5338
5339        let (version, notifier) = self
5340            .transaction(
5341                move |tx| {
5342                    let mut version = version.clone();
5343                    let mut notifier = None;
5344                    for append_request in &batch {
5345                        let (v, n) =
5346                            Self::append(tx, &execution_id, append_request.clone(), version)?;
5347                        version = v;
5348                        notifier = Some(n);
5349                    }
5350                    Ok::<_, DbErrorWrite>((
5351                        version,
5352                        notifier.expect("checked that the batch is not empty"),
5353                    ))
5354                },
5355                TxType::MultipleWrites,
5356                "append_batch",
5357            )
5358            .await?;
5359
5360        self.notify_all(vec![notifier], current_time);
5361        Ok(version)
5362    }
5363
5364    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %version))]
5365    async fn append_batch_create_new_execution(
5366        &self,
5367        current_time: DateTime<Utc>,
5368        batch: Vec<AppendRequest>,
5369        execution_id: ExecutionId,
5370        version: Version,
5371        child_req: Vec<CreateRequest>,
5372        backtraces: Vec<BacktraceInfo>,
5373    ) -> Result<AppendBatchResponse, DbErrorWrite> {
5374        debug!("append_batch_create_new_execution");
5375        trace!(?batch, ?child_req, "append_batch_create_new_execution");
5376        assert!(!batch.is_empty(), "Empty batch request");
5377
5378        let (version, notifiers) = self
5379            .transaction(
5380                move |tx| {
5381                    let mut notifier = None;
5382                    let mut version = version.clone();
5383                    for append_request in &batch {
5384                        let (v, n) =
5385                            Self::append(tx, &execution_id, append_request.clone(), version)?;
5386                        version = v;
5387                        notifier = Some(n);
5388                    }
5389                    let mut notifiers = Vec::new();
5390                    notifiers.push(notifier.expect("checked that the batch is not empty"));
5391
5392                    for child_req in &child_req {
5393                        let (_, notifier) = Self::create_inner(tx, child_req.clone())?;
5394                        notifiers.push(notifier);
5395                    }
5396                    Ok::<_, DbErrorWrite>((version, notifiers))
5397                },
5398                TxType::MultipleWrites,
5399                "append_batch_create_new_execution_inner",
5400            )
5401            .await?;
5402        self.notify_all(notifiers, current_time);
5403        self.transaction_fire_forget(
5404            move |tx| {
5405                for backtrace in &backtraces {
5406                    Self::append_backtrace(tx, backtrace)?;
5407                }
5408                Ok::<_, DbErrorWrite>(())
5409            },
5410            "append_batch_create_new_execution_append_backtrace",
5411        )
5412        .await;
5413        Ok(version)
5414    }
5415
5416    // Supports only one subscriber per execution id.
5417    // A new call will overwrite the old subscriber, the old one will end
5418    // with a timeout, which is fine.
5419    #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
5420    async fn subscribe_to_next_responses(
5421        &self,
5422        execution_id: &ExecutionId,
5423        last_response: ResponseCursor,
5424        timeout_fut: Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>,
5425    ) -> Result<Vec<ResponseWithCursor>, DbErrorReadWithTimeout> {
5426        debug!("next_responses");
5427        let unique_tag: u64 = rand::random();
5428        let execution_id = execution_id.clone();
5429
5430        let cleanup = || {
5431            let mut guard = self.0.response_subscribers.lock().unwrap();
5432            match guard.remove(&execution_id) {
5433                Some((_, tag)) if tag == unique_tag => {} // Cleanup OK.
5434                Some(other) => {
5435                    // Reinsert foreign sender.
5436                    guard.insert(execution_id.clone(), other);
5437                }
5438                None => {} // Value was replaced and cleaned up already, or notification was sent.
5439            }
5440        };
5441
5442        let response_subscribers = self.0.response_subscribers.clone();
5443        let resp_or_receiver = {
5444            let execution_id = execution_id.clone();
5445            self.transaction(
5446                move |tx| {
5447                    let responses = Self::get_responses_after(tx, &execution_id, last_response)?;
5448                    if responses.is_empty() {
5449                        // cannot race as we have the transaction write lock
5450                        let (sender, receiver) = oneshot::channel();
5451                        response_subscribers
5452                            .lock()
5453                            .unwrap()
5454                            .insert(execution_id.clone(), (sender, unique_tag));
5455                        Ok::<_, DbErrorReadWithTimeout>(itertools::Either::Right(receiver))
5456                    } else {
5457                        Ok(itertools::Either::Left(responses))
5458                    }
5459                },
5460                TxType::Other, // read only
5461                "subscribe_to_next_responses",
5462            )
5463            .await
5464        }
5465        .inspect_err(|_| {
5466            cleanup();
5467        })?;
5468        match resp_or_receiver {
5469            itertools::Either::Left(resp) => Ok(resp), // no need for cleanup
5470            itertools::Either::Right(receiver) => {
5471                let woken = tokio::select! {
5472                    resp = receiver => match resp {
5473                        Ok(()) => Ok(()),
5474                        Err(_) => Err(DbErrorReadWithTimeout::from(DbErrorGeneric::Close)),
5475                    },
5476                    outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
5477                };
5478                cleanup();
5479                woken?;
5480                // Re-fetch from the `last_response` watermark.
5481                let execution_id = execution_id.clone();
5482                self.transaction(
5483                    move |tx| {
5484                        Self::get_responses_after(tx, &execution_id, last_response)
5485                            .map_err(DbErrorReadWithTimeout::from)
5486                    },
5487                    TxType::Other, // read only
5488                    "subscribe_to_next_responses_refetch",
5489                )
5490                .await
5491            }
5492        }
5493    }
5494
5495    // Supports multiple subscribers.
5496    #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
5497    async fn wait_for_finished_result(
5498        &self,
5499        execution_id: &ExecutionId,
5500        timeout_fut: Option<Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>>,
5501    ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout> {
5502        let unique_tag: u64 = rand::random();
5503        let execution_id = execution_id.clone();
5504        let execution_finished_subscription = self.0.execution_finished_subscribers.clone();
5505
5506        let cleanup = || {
5507            let mut guard = self.0.execution_finished_subscribers.lock().unwrap();
5508            if let Some(subscribers) = guard.get_mut(&execution_id) {
5509                subscribers.remove(&unique_tag);
5510            }
5511        };
5512
5513        let resp_or_receiver = {
5514            let execution_id = execution_id.clone();
5515            self.transaction(move |tx| {
5516                let pending_state =
5517                    Self::get_combined_state(tx, &execution_id)?.execution_with_state.pending_state;
5518                if let PendingState::Finished(finished) = pending_state {
5519                    let event =
5520                        Self::get_execution_event(tx, &execution_id, finished.version)?;
5521                    if let ExecutionRequest::Finished { retval, ..} = event.event {
5522                        Ok(itertools::Either::Left(retval))
5523                    } else {
5524                        error!("Mismatch, expected Finished row: {event:?} based on t_state {finished}");
5525                        Err(DbErrorReadWithTimeout::from(consistency_db_err(
5526                            "cannot get finished event based on t_state version"
5527                        )))
5528                    }
5529                } else {
5530                    // Cannot race with the notifier as we have the transaction write lock:
5531                    // Either the finished event was appended previously, thus `itertools::Either::Left` was selected,
5532                    // or we end up here. If this tx fails, the cleanup will remove this entry.
5533                    let (sender, receiver) = oneshot::channel();
5534                    let mut guard = execution_finished_subscription.lock().unwrap();
5535                    guard.entry(execution_id.clone()).or_default().insert(unique_tag, sender);
5536                    Ok(itertools::Either::Right(receiver))
5537                }
5538            },
5539            TxType::Other, // read only
5540            "wait_for_finished_result")
5541            .await
5542        }
5543        .inspect_err(|_| {
5544            // This cleanup can race with the notification sender, since both are running after a transaction was finished.
5545            // If the notification sender wins, it removes our oneshot sender and puts a value in it, cleanup will not find the unique tag.
5546            // If cleanup wins, it simply removes the oneshot sender.
5547            cleanup();
5548        })?;
5549
5550        let timeout_fut = timeout_fut.unwrap_or_else(|| Box::pin(std::future::pending()));
5551        match resp_or_receiver {
5552            itertools::Either::Left(resp) => Ok(resp), // no need for cleanup
5553            itertools::Either::Right(receiver) => {
5554                let res = tokio::select! {
5555                    resp = receiver => {
5556                        match resp {
5557                            Ok(retval) => Ok(retval),
5558                            Err(_recv_err) => Err(DbErrorGeneric::Close.into())
5559                        }
5560                    }
5561                    outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
5562                };
5563                cleanup();
5564                res
5565            }
5566        }
5567    }
5568
5569    #[instrument(level = Level::DEBUG, skip_all, fields(%join_set_id, %execution_id))]
5570    async fn append_delay_response(
5571        &self,
5572        created_at: DateTime<Utc>,
5573        execution_id: ExecutionId,
5574        join_set_id: JoinSetId,
5575        delay_id: DelayId,
5576        result: Result<(), ()>,
5577    ) -> Result<AppendDelayResponseOutcome, DbErrorWrite> {
5578        debug!("append_delay_response");
5579        let event = JoinSetResponseEventOuter {
5580            created_at,
5581            event: JoinSetResponseEvent {
5582                join_set_id,
5583                event: JoinSetResponse::DelayFinished {
5584                    delay_id: delay_id.clone(),
5585                    result,
5586                },
5587            },
5588        };
5589        let res = self
5590            .transaction(
5591                {
5592                    let execution_id = execution_id.clone();
5593                    move |tx| Self::append_response(tx, &execution_id, event.clone())
5594                },
5595                TxType::MultipleWrites,
5596                "append_delay_response",
5597            )
5598            .await;
5599        match res {
5600            Ok(notifier) => {
5601                self.notify_all(vec![notifier], created_at);
5602                Ok(AppendDelayResponseOutcome::Success)
5603            }
5604            Err(DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::Conflict)) => {
5605                let delay_success = self
5606                    .transaction(
5607                        move |tx| Self::get_delay_response(tx, &execution_id, &delay_id),
5608                        TxType::Other, // read only
5609                        "get_delay_response",
5610                    )
5611                    .await?;
5612                match delay_success {
5613                    Some(true) => Ok(AppendDelayResponseOutcome::AlreadyFinished),
5614                    Some(false) => Ok(AppendDelayResponseOutcome::AlreadyCancelled),
5615                    None => Err(DbErrorWrite::Generic(DbErrorGeneric::Uncategorized {
5616                        reason: "insert failed yet select did not find the response".into(),
5617                        context: SpanTrace::capture(),
5618                        source: None,
5619                        loc: Location::caller(),
5620                    })),
5621                }
5622            }
5623            Err(err) => Err(err),
5624        }
5625    }
5626
5627    #[instrument(level = Level::DEBUG, skip_all)]
5628    async fn append_backtrace(&self, append: BacktraceInfo) -> Result<(), DbErrorWrite> {
5629        trace!("append_backtrace");
5630        self.transaction_fire_forget(
5631            move |tx| Self::append_backtrace(tx, &append).map(drop),
5632            "append_backtrace",
5633        )
5634        .await;
5635        Ok(())
5636    }
5637
5638    #[instrument(level = Level::DEBUG, skip_all)]
5639    async fn append_backtrace_batch(
5640        &self,
5641        batch: Vec<BacktraceInfo>,
5642    ) -> Result<usize, DbErrorWrite> {
5643        trace!("append_backtrace_batch");
5644        self.transaction(
5645            move |tx| {
5646                let mut inserted = 0;
5647                for append in &batch {
5648                    inserted += Self::append_backtrace(tx, append)?;
5649                }
5650                Ok::<_, DbErrorWrite>(inserted)
5651            },
5652            TxType::MultipleWrites,
5653            "append_backtrace_batch",
5654        )
5655        .await
5656    }
5657
5658    #[instrument(level = Level::DEBUG, skip_all)]
5659    async fn append_log(&self, row: LogInfoAppendRow) -> Result<(), DbErrorWrite> {
5660        trace!("append_log");
5661        self.transaction_fire_forget(move |tx| Self::append_log(tx, &row), "append_log")
5662            .await;
5663        Ok(())
5664    }
5665
5666    #[instrument(level = Level::DEBUG, skip_all)]
5667    async fn append_log_batch(&self, batch: &[LogInfoAppendRow]) -> Result<(), DbErrorWrite> {
5668        trace!("append_log_batch");
5669        let batch = Vec::from(batch);
5670        self.transaction_fire_forget(
5671            move |tx| {
5672                for row in &batch {
5673                    Self::append_log(tx, row)?;
5674                }
5675                Ok::<_, DbErrorWrite>(())
5676            },
5677            "append_log_batch",
5678        )
5679        .await;
5680        Ok(())
5681    }
5682
5683    /// Get currently expired delays and locks.
5684    #[instrument(level = Level::TRACE, skip(self))]
5685    async fn get_expired_timers(
5686        &self,
5687        at: DateTime<Utc>,
5688    ) -> Result<Vec<ExpiredTimer>, DbErrorGeneric> {
5689        self.transaction(
5690            move |conn| {
5691                let mut expired_timers = conn.prepare(
5692                    "SELECT execution_id, join_set_id, delay_id FROM t_delay WHERE expires_at <= :at AND NOT is_paused",
5693                )?
5694                .query_map(
5695                        named_params! {
5696                            ":at": at,
5697                        },
5698                        |row| {
5699                            let execution_id = row.get("execution_id")?;
5700                            let join_set_id = row.get::<_, JoinSetId>("join_set_id")?;
5701                            let delay_id = row.get::<_, DelayId>("delay_id")?;
5702                            let delay = ExpiredDelay { execution_id, join_set_id, delay_id };
5703                            Ok(ExpiredTimer::Delay(delay))
5704                        },
5705                    )?
5706                    .collect::<Result<Vec<_>, _>>()?;
5707                // Extend with expired locks
5708                let expired = conn.prepare(&format!(r#"
5709                    SELECT execution_id, last_lock_version, corresponding_version, intermittent_event_count, max_retries, retry_exp_backoff_millis,
5710                    executor_id, run_id, lifecycle
5711                    FROM t_state
5712                    WHERE pending_expires_finished <= :at AND state = "{STATE_LOCKED}"
5713                    "#
5714                )
5715                )?
5716                .query_map(
5717                        named_params! {
5718                            ":at": at,
5719                        },
5720                        |row| {
5721                            let execution_id = row.get("execution_id")?;
5722                            let lifecycle: String = row.get("lifecycle")?;
5723                            if lifecycle != LIFECYCLE_ACTIVE {
5724                                error!(%execution_id, %lifecycle, "encountered invalid non-active locked execution while scanning expired locks");
5725                                return Ok(None);
5726                            }
5727                            let locked_at_version = Version::new(row.get("last_lock_version")?);
5728                            let next_version = Version::new(row.get("corresponding_version")?).increment();
5729                            let intermittent_event_count = row.get("intermittent_event_count")?;
5730                            let max_retries = row.get("max_retries")?;
5731                            let retry_exp_backoff_millis = u64::from(row.get::<_, u32>("retry_exp_backoff_millis")?);
5732                            let executor_id = row.get("executor_id")?;
5733                            let run_id = row.get("run_id")?;
5734                            let lock = ExpiredLock {
5735                                execution_id,
5736                                locked_at_version,
5737                                next_version,
5738                                intermittent_event_count,
5739                                max_retries,
5740                                retry_exp_backoff: Duration::from_millis(retry_exp_backoff_millis),
5741                                locked_by: LockedBy { executor_id, run_id },
5742                            };
5743                            Ok(Some(ExpiredTimer::Lock(lock)))
5744                        }
5745                    )?
5746                    .collect::<Result<Vec<_>, _>>()?;
5747                expired_timers.extend(expired.into_iter().flatten());
5748                if !expired_timers.is_empty() {
5749                    debug!("get_expired_timers found {expired_timers:?}");
5750                }
5751                Ok(expired_timers)
5752            },
5753            TxType::Other, // read only
5754            "get_expired_timers"
5755        )
5756        .await
5757        .map_err(to_generic_error)
5758    }
5759
5760    async fn get_execution_event(
5761        &self,
5762        execution_id: &ExecutionId,
5763        version: &Version,
5764    ) -> Result<ExecutionEvent, DbErrorRead> {
5765        let version = version.0;
5766        let execution_id = execution_id.clone();
5767        self.transaction(
5768            move |tx| Self::get_execution_event(tx, &execution_id, version),
5769            TxType::Other, // read only
5770            "get_execution_event",
5771        )
5772        .await
5773    }
5774
5775    #[instrument(level = Level::DEBUG, skip_all)]
5776    async fn upsert_stub_response(
5777        &self,
5778        execution_id: ExecutionIdDerived,
5779        version: Version,
5780        req: AppendRequest,
5781        response: AppendResponseToExecution,
5782        current_time: DateTime<Utc>,
5783    ) -> Result<(), DbErrorStubResponse> {
5784        debug!("upsert_stub_response");
5785        #[cfg(debug_assertions)]
5786        {
5787            let (expected_parent, expected_join_set) = execution_id.split_to_parts();
5788            debug_assert_eq!(expected_parent, response.parent_execution_id);
5789            debug_assert_eq!(expected_join_set, response.join_set_id);
5790            debug_assert_eq!(execution_id, response.child_execution_id);
5791        }
5792        let execution_id = ExecutionId::Derived(execution_id);
5793        let expected_retval = response.result.clone();
5794        let notifiers = self
5795            .transaction(
5796                move |tx| {
5797                    let version_raw = version.0;
5798                    match Self::append(tx, &execution_id, req.clone(), version.clone()) {
5799                        Ok((_next_version, notifier_of_child)) => {
5800                            let pending_at_parent = Self::append_response(
5801                                tx,
5802                                &response.parent_execution_id,
5803                                JoinSetResponseEventOuter {
5804                                    created_at: response.created_at,
5805                                    event: JoinSetResponseEvent {
5806                                        join_set_id: response.join_set_id.clone(),
5807                                        event: JoinSetResponse::ChildExecutionFinished {
5808                                            child_execution_id: response.child_execution_id.clone(),
5809                                            finished_version: response.finished_version.clone(),
5810                                            result: response.result.clone(),
5811                                        },
5812                                    },
5813                                },
5814                            )
5815                            .map_err(DbErrorStubResponse::Write)?;
5816                            Ok::<_, DbErrorStubResponse>(Some(vec![
5817                                notifier_of_child,
5818                                pending_at_parent,
5819                            ]))
5820                        }
5821                        Err(DbErrorWrite::NonRetriable(
5822                            DbErrorWriteNonRetriable::AlreadyFinished,
5823                        )) => {
5824                            // Idempotency check: read the existing event and compare retval.
5825                            let found = Self::get_execution_event(tx, &execution_id, version_raw)
5826                                .map_err(|_| DbErrorStubResponse::StubConflict)?;
5827                            match found.event {
5828                                ExecutionRequest::Finished { retval, .. }
5829                                    if retval == expected_retval =>
5830                                {
5831                                    Ok(None)
5832                                }
5833                                _ => Err(DbErrorStubResponse::StubConflict),
5834                            }
5835                        }
5836                        Err(other) => Err(DbErrorStubResponse::Write(other)),
5837                    }
5838                },
5839                TxType::MultipleWrites,
5840                "upsert_stub_response",
5841            )
5842            .await?;
5843        if let Some(notifiers) = notifiers {
5844            self.notify_all(notifiers, current_time);
5845        }
5846        Ok(())
5847    }
5848
5849    async fn get_pending_state(
5850        &self,
5851        execution_id: &ExecutionId,
5852    ) -> Result<ExecutionWithState, DbErrorRead> {
5853        let execution_id = execution_id.clone();
5854        Ok(self
5855            .transaction(
5856                move |tx| Self::get_combined_state(tx, &execution_id),
5857                TxType::Other, // read only
5858                "get_pending_state",
5859            )
5860            .await?
5861            .execution_with_state)
5862    }
5863}
5864
5865#[cfg(feature = "test")]
5866#[async_trait]
5867impl concepts::storage::DbConnectionTest for SqlitePool {
5868    #[instrument(level = Level::DEBUG, skip(self, response_event), fields(join_set_id = %response_event.join_set_id))]
5869    async fn append_response(
5870        &self,
5871        created_at: DateTime<Utc>,
5872        execution_id: ExecutionId,
5873        response_event: JoinSetResponseEvent,
5874    ) -> Result<(), DbErrorWrite> {
5875        debug!("append_response");
5876        let event = JoinSetResponseEventOuter {
5877            created_at,
5878            event: response_event,
5879        };
5880        let notifier = self
5881            .transaction(
5882                move |tx| Self::append_response(tx, &execution_id, event.clone()),
5883                TxType::Other, // read only
5884                "append_response",
5885            )
5886            .await?;
5887        self.notify_all(vec![notifier], created_at);
5888        Ok(())
5889    }
5890}
5891
5892#[cfg(any(test, feature = "tempfile"))]
5893pub mod tempfile {
5894    use super::{SqliteConfig, SqlitePool};
5895    use tempfile::NamedTempFile;
5896
5897    pub async fn sqlite_pool() -> (SqlitePool, Option<NamedTempFile>) {
5898        if let Ok(path) = std::env::var("SQLITE_FILE") {
5899            (
5900                SqlitePool::new(path, SqliteConfig::default())
5901                    .await
5902                    .unwrap(),
5903                None,
5904            )
5905        } else {
5906            let file = NamedTempFile::new().unwrap();
5907            let path = file.path();
5908            (
5909                SqlitePool::new(path, SqliteConfig::default())
5910                    .await
5911                    .unwrap(),
5912                Some(file),
5913            )
5914        }
5915    }
5916}
5917
5918#[cfg(test)]
5919mod tests {
5920    use crate::sqlite_dao::{SqlitePool, TxType, tempfile::sqlite_pool};
5921    use assert_matches::assert_matches;
5922    use chrono::DateTime;
5923    use concepts::{
5924        ComponentId, FunctionFqn, Params,
5925        prefixed_ulid::{DEPLOYMENT_ID_DUMMY, EXECUTION_ID_DUMMY},
5926        storage::{CreateRequest, DbErrorWrite, DbErrorWriteNonRetriable, DbPoolCloseable},
5927    };
5928    use rusqlite::named_params;
5929
5930    const SOME_FFQN: FunctionFqn = FunctionFqn::new_static("ns:pkg/ifc", "fn");
5931
5932    #[tokio::test]
5933    async fn failing_ltx_should_be_rolled_back() -> Result<(), DbErrorWrite> {
5934        let created_at = DateTime::from_timestamp_nanos(0);
5935        let (pool, _guard) = sqlite_pool().await;
5936        pool.transaction(
5937            move |tx| {
5938                let req = CreateRequest {
5939                    created_at,
5940                    execution_id: EXECUTION_ID_DUMMY,
5941                    ffqn: SOME_FFQN,
5942                    params: Params::empty(),
5943                    parent: None,
5944                    metadata: concepts::ExecutionMetadata::empty(),
5945                    scheduled_at: created_at,
5946                    component_id: ComponentId::dummy_activity(),
5947                    deployment_id: DEPLOYMENT_ID_DUMMY,
5948                    scheduled_by: None,
5949                    paused: false,
5950                };
5951                SqlitePool::create_inner(tx, req)?;
5952                SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at)?;
5953                Ok::<_, DbErrorWrite>(())
5954            },
5955            TxType::MultipleWrites,
5956            "create_inner + pause_execution",
5957        )
5958        .await?;
5959
5960        // Second tx should fail
5961        let err = pool
5962            .transaction(
5963                move |tx| SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at),
5964                TxType::MultipleWrites,
5965                "pause_execution",
5966            )
5967            .await
5968            .unwrap_err();
5969        let reason = assert_matches!(err, DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::IllegalState { reason, .. }) => reason);
5970        assert_eq!("cannot pause, execution is already paused", reason.as_ref());
5971
5972        let events = pool.transaction(
5973            move |tx| {
5974                let events =
5975                    tx.prepare(
5976                        "SELECT created_at, json_value, version FROM t_execution_log WHERE execution_id = :execution_id",
5977                    )?
5978                    .query_map(
5979                        named_params! {
5980                            ":execution_id": EXECUTION_ID_DUMMY.to_string(),
5981                        },
5982                        SqlitePool::map_t_execution_log_row,
5983                    )
5984                    .map_err(DbErrorWrite::from)?
5985                    .collect::<Result<Vec<_>, _>>()?;
5986
5987                Ok::<_, DbErrorWrite>(events)
5988            },
5989            TxType::Other, // read only
5990            "get_log",
5991        )
5992        .await?;
5993        assert_eq!(2, events.len());
5994        pool.close().await;
5995        Ok(())
5996    }
5997}