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,
809                        TxType::Other => Ok(()),
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<(), 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 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
2631        Ok(())
2632    }
2633
2634    fn append_log(tx: &Transaction, row: &LogInfoAppendRow) -> Result<(), DbErrorWrite> {
2635        let mut stmt = tx.prepare(
2636            "INSERT INTO t_log (
2637            execution_id,
2638            run_id,
2639            created_at,
2640            level,
2641            message,
2642            stream_type,
2643            payload
2644        ) VALUES (
2645            :execution_id,
2646            :run_id,
2647            :created_at,
2648            :level,
2649            :message,
2650            :stream_type,
2651            :payload
2652        )",
2653        )?;
2654
2655        match &row.log_entry {
2656            LogEntry::Log {
2657                created_at,
2658                level,
2659                message,
2660            } => {
2661                stmt.execute(named_params! {
2662                    ":execution_id": row.execution_id,
2663                    ":run_id": row.run_id,
2664                    ":created_at": created_at,
2665                    ":level": *level as u8,
2666                    ":message": message,
2667                    ":stream_type": Option::<u8>::None,
2668                    ":payload": Option::<Vec<u8>>::None,
2669                })?;
2670            }
2671            LogEntry::Stream {
2672                created_at,
2673                payload,
2674                stream_type,
2675            } => {
2676                stmt.execute(named_params! {
2677                    ":execution_id": row.execution_id,
2678                    ":run_id": row.run_id,
2679                    ":created_at": created_at,
2680                    ":level": Option::<u8>::None,
2681                    ":message": Option::<String>::None,
2682                    ":stream_type": *stream_type as u8,
2683                    ":payload": payload,
2684                })?;
2685            }
2686        }
2687
2688        Ok(())
2689    }
2690
2691    fn get(
2692        tx: &Transaction,
2693        execution_id: &ExecutionId,
2694    ) -> Result<concepts::storage::ExecutionLog, DbErrorRead> {
2695        let mut stmt = tx.prepare(
2696            "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2697                        execution_id = :execution_id ORDER BY version",
2698        )?;
2699        let events = stmt
2700            .query_map(
2701                named_params! {
2702                    ":execution_id": execution_id.to_string(),
2703                },
2704                |row| {
2705                    let created_at = row.get("created_at")?;
2706                    let event = row
2707                        .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2708                        .map_err(|serde| {
2709                            error!("Cannot deserialize {row:?} - {serde:?}");
2710                            consistency_rusqlite("cannot deserialize event")
2711                        })?
2712                        .0;
2713                    let version = Version(row.get("version")?);
2714
2715                    Ok(ExecutionEvent {
2716                        created_at,
2717                        event,
2718                        backtrace_id: None,
2719                        version,
2720                    })
2721                },
2722            )?
2723            .collect::<Result<Vec<_>, _>>()?;
2724        if events.is_empty() {
2725            return Err(DbErrorRead::NotFound);
2726        }
2727        let combined_state = Self::get_combined_state(tx, execution_id)?;
2728        let responses = Self::list_responses(tx, execution_id, None)?;
2729        Ok(concepts::storage::ExecutionLog {
2730            execution_id: execution_id.clone(),
2731            events,
2732            responses,
2733            next_version: combined_state.get_next_version_or_finished(), // In case of finished, this will be the already last version
2734            pending_state: combined_state.execution_with_state.pending_state,
2735            component_digest: combined_state.execution_with_state.component_digest,
2736            component_type: combined_state.execution_with_state.component_type,
2737            deployment_id: combined_state.execution_with_state.deployment_id,
2738        })
2739    }
2740
2741    fn get_max_version(
2742        tx: &Transaction,
2743        execution_id: &ExecutionId,
2744    ) -> Result<Version, DbErrorRead> {
2745        tx.prepare("SELECT MAX(version) FROM t_execution_log WHERE execution_id = :execution_id")?
2746            .query_row(
2747                named_params! { ":execution_id": execution_id.to_string() },
2748                |row| row.get::<_, Option<VersionType>>(0),
2749            )
2750            .map(|v| v.map(Version::new).ok_or(DbErrorRead::NotFound))
2751            .map_err(DbErrorRead::from)
2752            .flatten()
2753    }
2754
2755    fn get_max_response_cursor(
2756        tx: &Transaction,
2757        execution_id: &ExecutionId,
2758    ) -> Result<ResponseCursor, DbErrorRead> {
2759        let max_cursor = tx
2760            .prepare("SELECT MAX(seq) FROM t_join_set_response WHERE execution_id = :execution_id")?
2761            .query_row(
2762                named_params! { ":execution_id": execution_id.to_string() },
2763                |row| row.get::<_, Option<u32>>(0),
2764            )?;
2765        // Assume the execution exists and has no responses
2766        let max_cursor = max_cursor.unwrap_or_default();
2767        Ok(ResponseCursor(max_cursor))
2768    }
2769
2770    fn list_execution_events(
2771        tx: &Transaction,
2772        execution_id: &ExecutionId,
2773        pagination: Pagination<VersionType>,
2774        include_backtrace_id: bool,
2775    ) -> Result<Vec<ExecutionEvent>, DbErrorRead> {
2776        let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![];
2777        params.push((":execution_id", Box::new(execution_id.to_string())));
2778
2779        let (cursor, length, rel, is_desc) = match &pagination {
2780            Pagination::NewerThan {
2781                cursor,
2782                length,
2783                including_cursor,
2784            } => (
2785                *cursor,
2786                *length,
2787                if *including_cursor { ">=" } else { ">" },
2788                false,
2789            ),
2790            Pagination::OlderThan {
2791                cursor,
2792                length,
2793                including_cursor,
2794            } => (
2795                *cursor,
2796                *length,
2797                if *including_cursor { "<=" } else { "<" },
2798                true,
2799            ),
2800        };
2801        params.push((":cursor", Box::new(cursor)));
2802
2803        let base_select = if include_backtrace_id {
2804            format!(
2805                "SELECT
2806                    log.created_at,
2807                    log.json_value,
2808                    log.version as version,
2809                    bt.version_min_including AS backtrace_id
2810                FROM
2811                    t_execution_log AS log
2812                LEFT OUTER JOIN
2813                    t_execution_backtrace AS bt ON log.execution_id = bt.execution_id
2814                                    AND log.version >= bt.version_min_including
2815                                    AND log.version < bt.version_max_excluding
2816                WHERE
2817                    log.execution_id = :execution_id
2818                    AND log.version {rel} :cursor"
2819            )
2820        } else {
2821            format!(
2822                "SELECT
2823                    created_at, json_value, NULL as backtrace_id, version
2824                FROM t_execution_log WHERE
2825                    execution_id = :execution_id AND version {rel} :cursor"
2826            )
2827        };
2828
2829        let order = if is_desc { "DESC" } else { "ASC" };
2830        let mut sql = format!("{base_select} ORDER BY version {order} LIMIT {length}");
2831
2832        // Re-order to ascending for consistent oldest-to-newest results
2833        if is_desc {
2834            sql = format!("SELECT * FROM ({sql}) ORDER BY version ASC");
2835        }
2836
2837        tx.prepare(&sql)?
2838            .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
2839                params
2840                    .iter()
2841                    .map(|(key, value)| (*key, value.as_ref()))
2842                    .collect::<Vec<_>>()
2843                    .as_ref(),
2844                |row| {
2845                    let created_at = row.get("created_at")?;
2846                    let backtrace_id = row
2847                        .get::<_, Option<VersionType>>("backtrace_id")?
2848                        .map(Version::new);
2849                    let version = Version(row.get("version")?);
2850
2851                    let event = row
2852                        .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2853                        .map(|event| ExecutionEvent {
2854                            created_at,
2855                            event: event.0,
2856                            backtrace_id,
2857                            version,
2858                        })
2859                        .map_err(|serde| {
2860                            error!("Cannot deserialize {row:?} - {serde:?}");
2861                            consistency_rusqlite("cannot deserialize")
2862                        })?;
2863                    Ok(event)
2864                },
2865            )?
2866            .collect::<Result<Vec<_>, _>>()
2867            .map_err(DbErrorRead::from)
2868    }
2869
2870    fn map_t_execution_log_row(row: &Row<'_>) -> Result<ExecutionEvent, rusqlite::Error> {
2871        let created_at = row.get("created_at")?;
2872        let event = row
2873            .get::<_, JsonWrapper<ExecutionRequest>>("json_value")
2874            .map_err(|serde| {
2875                error!("Cannot deserialize {row:?} - {serde:?}");
2876                consistency_rusqlite("cannot deserialize event")
2877            })?;
2878        let version = Version(row.get("version")?);
2879
2880        Ok(ExecutionEvent {
2881            created_at,
2882            event: event.0,
2883            backtrace_id: None,
2884            version,
2885        })
2886    }
2887
2888    fn get_execution_event(
2889        tx: &Transaction,
2890        execution_id: &ExecutionId,
2891        version: VersionType,
2892    ) -> Result<ExecutionEvent, DbErrorRead> {
2893        tx.prepare(
2894            "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2895                        execution_id = :execution_id AND version = :version",
2896        )?
2897        .query_row(
2898            named_params! {
2899                ":execution_id": execution_id.to_string(),
2900                ":version": version,
2901            },
2902            SqlitePool::map_t_execution_log_row,
2903        )
2904        .map_err(DbErrorRead::from)
2905    }
2906
2907    fn get_last_execution_event(
2908        tx: &Transaction,
2909        execution_id: &ExecutionId,
2910    ) -> Result<ExecutionEvent, DbErrorRead> {
2911        tx.prepare(
2912            "SELECT created_at, json_value, version FROM t_execution_log WHERE \
2913                        execution_id = :execution_id ORDER BY version DESC LIMIT 1",
2914        )?
2915        .query_row(
2916            named_params! {
2917                ":execution_id": execution_id.to_string(),
2918            },
2919            SqlitePool::map_t_execution_log_row,
2920        )
2921        .map_err(DbErrorRead::from)
2922    }
2923
2924    fn get_delay_response(
2925        tx: &Transaction,
2926        execution_id: &ExecutionId,
2927        delay_id: &DelayId,
2928    ) -> Result<Option<bool>, DbErrorRead> {
2929        // TODO: Add test
2930        tx.prepare(
2931            "SELECT delay_success \
2932                    FROM t_join_set_response \
2933                    WHERE \
2934                    execution_id = :execution_id AND delay_id = :delay_id
2935                    ",
2936        )?
2937        .query_row(
2938            named_params! {
2939                ":execution_id": execution_id.to_string(),
2940                ":delay_id": delay_id.to_string(),
2941            },
2942            |row| {
2943                let delay_success = row.get::<_, bool>("delay_success")?;
2944                Ok(delay_success)
2945            },
2946        )
2947        .optional()
2948        .map_err(DbErrorRead::from)
2949    }
2950
2951    #[instrument(level = Level::TRACE, skip_all)]
2952    /// Find next responses for this execution
2953    fn get_responses_after(
2954        tx: &Transaction,
2955        execution_id: &ExecutionId,
2956        last_response: ResponseCursor,
2957    ) -> Result<Vec<ResponseWithCursor>, DbErrorRead> {
2958        // TODO: Add test
2959        tx.prepare(
2960            "SELECT r.id, r.seq, r.created_at, r.join_set_id, \
2961            r.delay_id, r.delay_success, \
2962            r.child_execution_id, r.finished_version, child.json_value \
2963            FROM t_join_set_response r LEFT OUTER JOIN t_execution_log child ON r.child_execution_id = child.execution_id \
2964            WHERE \
2965            r.seq > :last_response_seq AND \
2966            r.execution_id = :execution_id AND \
2967            ( \
2968            r.finished_version = child.version \
2969            OR r.child_execution_id IS NULL \
2970            ) \
2971            ORDER BY seq",
2972        )
2973        ?
2974        .query_map(
2975            named_params! {
2976                ":last_response_seq": last_response.0,
2977                ":execution_id": execution_id.to_string(),
2978            },
2979            Self::parse_response_with_cursor,
2980        )
2981        ?
2982        .collect::<Result<Vec<_>, _>>()
2983        .map_err(DbErrorRead::from)
2984    }
2985
2986    fn get_pending_of_single_ffqn(
2987        mut stmt: CachedStatement,
2988        batch_size: u32,
2989        pending_at_or_sooner: DateTime<Utc>,
2990        ffqn: &FunctionFqn,
2991    ) -> Result<Vec<(ExecutionId, Version)>, ()> {
2992        stmt.query_map(
2993            named_params! {
2994                ":pending_expires_finished": pending_at_or_sooner,
2995                ":ffqn": ffqn.to_string(),
2996                ":batch_size": batch_size,
2997            },
2998            |row| {
2999                let execution_id = row.get::<_, ExecutionId>("execution_id")?;
3000                let next_version =
3001                    Version::new(row.get::<_, VersionType>("corresponding_version")?).increment();
3002                Ok((execution_id, next_version))
3003            },
3004        )
3005        .map_err(|err| {
3006            warn!("Ignoring consistency error {err:?}");
3007        })?
3008        .collect::<Result<Vec<_>, _>>()
3009        .map_err(|err| {
3010            warn!("Ignoring consistency error {err:?}");
3011        })
3012    }
3013
3014    /// Get executions and their next versions
3015    fn get_pending_by_ffqns(
3016        conn: &Connection,
3017        batch_size: u32,
3018        pending_at_or_sooner: DateTime<Utc>,
3019        ffqns: &[FunctionFqn],
3020    ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3021        let batch_size = usize::try_from(batch_size).expect("16 bit systems are unsupported");
3022        let mut execution_ids_versions = Vec::with_capacity(batch_size);
3023        for ffqn in ffqns {
3024            let needed = batch_size - execution_ids_versions.len();
3025            if needed == 0 {
3026                break;
3027            }
3028            let needed =
3029                u32::try_from(needed).expect("`batch_size`:u32 - usize cannot overflow an u32");
3030            // Select executions in PendingAt.
3031            let stmt = conn.prepare_cached(&format!(
3032                r#"
3033                    SELECT execution_id, corresponding_version FROM t_state WHERE
3034                    state = "{STATE_PENDING_AT}" AND
3035                    pending_expires_finished <= :pending_expires_finished AND ffqn = :ffqn
3036                    AND lifecycle = 'active'
3037                    ORDER BY pending_expires_finished LIMIT :batch_size
3038                    "#
3039            ))?;
3040
3041            if let Ok(execs_and_versions) =
3042                Self::get_pending_of_single_ffqn(stmt, needed, pending_at_or_sooner, ffqn)
3043            {
3044                execution_ids_versions.extend(execs_and_versions);
3045                // consistency errors are ignored since we want to return at least some rows.
3046            }
3047        }
3048        Ok(execution_ids_versions)
3049    }
3050
3051    fn get_pending_by_ffqns_auto(
3052        conn: &Connection,
3053        batch_size: u32,
3054        pending_at_or_sooner: DateTime<Utc>,
3055        ffqns: &[FunctionFqn],
3056        current_digest: &ComponentDigest,
3057    ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3058        let batch_size = usize::try_from(batch_size).expect("16 bit systems are unsupported");
3059        let mut execution_ids_versions = Vec::with_capacity(batch_size);
3060        for ffqn in ffqns {
3061            let needed = batch_size - execution_ids_versions.len();
3062            if needed == 0 {
3063                break;
3064            }
3065            let mut stmt = conn.prepare_cached(&format!(
3066                r#"
3067                    SELECT execution_id, corresponding_version FROM t_state WHERE
3068                    state = "{STATE_PENDING_AT}" AND
3069                    pending_expires_finished <= :pending_expires_finished AND ffqn = :ffqn
3070                    AND lifecycle = 'active'
3071                    AND (incompatible_digest IS NULL OR incompatible_digest <> :current_digest)
3072                    ORDER BY pending_expires_finished LIMIT :batch_size
3073                    "#
3074            ))?;
3075
3076            if let Ok(execs_and_versions) = stmt
3077                .query_map(
3078                    named_params! {
3079                        ":pending_expires_finished": pending_at_or_sooner,
3080                        ":ffqn": ffqn.to_string(),
3081                        ":current_digest": current_digest,
3082                        ":batch_size": u32::try_from(needed)
3083                            .expect("`needed` is <= `batch_size` which is u32"),
3084                    },
3085                    |row| {
3086                        let execution_id = row.get::<_, ExecutionId>("execution_id")?;
3087                        let next_version =
3088                            Version::new(row.get::<_, VersionType>("corresponding_version")?)
3089                                .increment();
3090                        Ok((execution_id, next_version))
3091                    },
3092                )
3093                .and_then(|rows| rows.collect::<Result<Vec<_>, _>>())
3094            {
3095                execution_ids_versions.extend(execs_and_versions);
3096                if execution_ids_versions.len() == batch_size {
3097                    break;
3098                }
3099            }
3100        }
3101        Ok(execution_ids_versions)
3102    }
3103
3104    fn get_pending_by_component_input_digest(
3105        conn: &Connection,
3106        batch_size: u32,
3107        pending_at_or_sooner: DateTime<Utc>,
3108        input_digest: &ComponentDigest,
3109    ) -> Result<Vec<(ExecutionId, Version)>, RusqliteError> {
3110        let mut stmt = conn.prepare_cached(&format!(
3111            r#"
3112                SELECT execution_id, corresponding_version FROM t_state WHERE
3113                state = "{STATE_PENDING_AT}" AND
3114                pending_expires_finished <= :pending_expires_finished AND
3115                component_id_input_digest = :component_id_input_digest
3116                AND lifecycle = 'active'
3117                ORDER BY pending_expires_finished LIMIT :batch_size
3118                "#
3119        ))?;
3120
3121        stmt.query_map(
3122            named_params! {
3123                ":pending_expires_finished": pending_at_or_sooner,
3124                ":component_id_input_digest": input_digest,
3125                ":batch_size": batch_size,
3126            },
3127            |row| {
3128                let execution_id = row.get::<_, ExecutionId>("execution_id")?;
3129                let next_version =
3130                    Version::new(row.get::<_, VersionType>("corresponding_version")?).increment();
3131                Ok((execution_id, next_version))
3132            },
3133        )?
3134        .collect::<Result<Vec<_>, _>>()
3135        .map_err(RusqliteError::from)
3136    }
3137
3138    // Must be called after write transaction for a correct happens-before relationship.
3139    #[instrument(level = Level::TRACE, skip_all)]
3140    fn notify_all(&self, notifiers: Vec<AppendNotifier>, current_time: DateTime<Utc>) {
3141        let (pending_ats, finished_execs, responses) = {
3142            let (mut pending_ats, mut finished_execs, mut responses) =
3143                (Vec::new(), Vec::new(), Vec::new());
3144            for notifier in notifiers {
3145                if let Some(pending_at) = notifier.pending_at {
3146                    pending_ats.push(pending_at);
3147                }
3148                if let Some(finished) = notifier.execution_finished {
3149                    finished_execs.push(finished);
3150                }
3151                if let Some(response) = notifier.response {
3152                    responses.push(response);
3153                }
3154            }
3155            (pending_ats, finished_execs, responses)
3156        };
3157
3158        // Notify pending_at subscribers.
3159        if !pending_ats.is_empty() {
3160            let guard = self.0.pending_subscribers.lock().unwrap();
3161            for pending_at in pending_ats {
3162                Self::notify_pending_locked(&pending_at, current_time, &guard);
3163            }
3164        }
3165        // Notify execution finished subscribers.
3166        // Every NotifierExecutionFinished value belongs to a different execution, since only `append(Finished)` can produce `NotifierExecutionFinished`.
3167        if !finished_execs.is_empty() {
3168            let mut guard = self.0.execution_finished_subscribers.lock().unwrap();
3169            for finished in finished_execs {
3170                if let Some(listeners_of_exe_id) = guard.remove(&finished.execution_id) {
3171                    for (_tag, sender) in listeners_of_exe_id {
3172                        // Sending while holding the lock but the oneshot sender does not block.
3173                        // If `wait_for_finished_result` happens after the append, it would receive the finished value instead.
3174                        let _ = sender.send(finished.retval.clone());
3175                    }
3176                }
3177            }
3178        }
3179        // Notify response subscribers.
3180        if !responses.is_empty() {
3181            let mut guard = self.0.response_subscribers.lock().unwrap();
3182            for (execution_id, _response) in responses {
3183                if let Some((sender, _)) = guard.remove(&execution_id) {
3184                    let _ = sender.send(());
3185                }
3186            }
3187        }
3188    }
3189
3190    fn notify_pending_locked(
3191        notifier: &NotifierPendingAt,
3192        current_time: DateTime<Utc>,
3193        ffqn_to_pending_subscription: &std::sync::MutexGuard<PendingFfqnSubscribersHolder>,
3194    ) {
3195        // No need to remove here, cleanup is handled by the caller.
3196        if notifier.scheduled_at <= current_time {
3197            ffqn_to_pending_subscription.notify(notifier);
3198        }
3199    }
3200
3201    fn upgrade_execution_component_single_write(
3202        tx: &Transaction,
3203        execution_id: &ExecutionId,
3204        old: &ComponentDigest,
3205        new: &ComponentDigest,
3206        reason: ComponentUpgradeReason,
3207    ) -> Result<(), DbErrorWrite> {
3208        let combined_state = Self::get_combined_state(tx, execution_id)?;
3209        if combined_state.execution_with_state.component_digest != *old {
3210            return Err(DbErrorWrite::NotFound);
3211        }
3212        let appending_version = combined_state.get_next_version_fail_if_finished()?;
3213        Self::append(
3214            tx,
3215            execution_id,
3216            AppendRequest {
3217                created_at: Utc::now(),
3218                event: ExecutionRequest::ComponentUpgradeFinished {
3219                    component_digest: new.clone(),
3220                    deployment_id: combined_state.execution_with_state.deployment_id,
3221                    outcome: ComponentUpgradeOutcome::Success { reason },
3222                },
3223            },
3224            appending_version,
3225        )?;
3226        Ok(())
3227    }
3228
3229    fn list_logs_tx(
3230        tx: &Transaction,
3231        execution_id: &ExecutionId,
3232        show_derived: bool,
3233        filter: &LogFilter,
3234        pagination: &Pagination<LogCursor>,
3235    ) -> Result<ListLogsResponse, DbErrorRead> {
3236        let length = pagination.length();
3237        let exec_id_str = execution_id.to_string();
3238        let exec_id_filter = if show_derived {
3239            "LIKE :execution_id || '%'"
3240        } else {
3241            "= :execution_id"
3242        };
3243        let mut query = format!(
3244            "SELECT id, run_id, created_at, level, message, stream_type, payload, execution_id
3245             FROM t_log
3246             WHERE execution_id {exec_id_filter}",
3247        );
3248
3249        let cursor = pagination.cursor();
3250        let created_after = filter.created_after();
3251        let created_before = filter.created_before();
3252        let mut params = vec![
3253            (":execution_id", &exec_id_str as &dyn rusqlite::ToSql),
3254            (":cursor", &cursor.0 as &dyn rusqlite::ToSql),
3255            (":length", &length as &dyn rusqlite::ToSql),
3256        ];
3257        if let Some(created_after) = &created_after {
3258            params.push((":created_after", created_after as &dyn rusqlite::ToSql));
3259        }
3260        if let Some(created_before) = &created_before {
3261            params.push((":created_before", created_before as &dyn rusqlite::ToSql));
3262        }
3263
3264        // Logs and streams filter
3265        let level_filter = if filter.should_show_logs() {
3266            let levels_str = if !filter.levels().is_empty() {
3267                filter
3268                    .levels()
3269                    .iter()
3270                    .map(|lvl| (*lvl as u8).to_string())
3271                    .collect::<Vec<_>>()
3272                    .join(",")
3273            } else {
3274                LogLevel::iter()
3275                    .map(|lvl| (lvl as u8).to_string())
3276                    .collect::<Vec<_>>()
3277                    .join(",")
3278            };
3279            Some(format!(" level IN ({levels_str})"))
3280        } else {
3281            None
3282        };
3283        let stream_filter = if filter.should_show_streams() {
3284            let streams_str = if !filter.stream_types().is_empty() {
3285                filter
3286                    .stream_types()
3287                    .iter()
3288                    .map(|st| (*st as u8).to_string())
3289                    .collect::<Vec<_>>()
3290                    .join(",")
3291            } else {
3292                LogStreamType::iter()
3293                    .map(|st| (st as u8).to_string())
3294                    .collect::<Vec<_>>()
3295                    .join(",")
3296            };
3297            Some(format!(" stream_type IN ({streams_str})"))
3298        } else {
3299            None
3300        };
3301        match (level_filter, stream_filter) {
3302            (Some(level_filter), Some(stream_filter)) => {
3303                write!(&mut query, " AND ({level_filter} OR {stream_filter})")
3304                    .expect("writing to string");
3305            }
3306            (Some(level_filter), None) => {
3307                write!(&mut query, " AND {level_filter}").expect("writing to string");
3308            }
3309            (None, Some(stream_filter)) => {
3310                write!(&mut query, " AND {stream_filter}").expect("writing to string");
3311            }
3312            (None, None) => unreachable!("guarded by constructor"),
3313        }
3314
3315        if created_after.is_some() {
3316            query.push_str(" AND created_at > :created_after");
3317        }
3318        if created_before.is_some() {
3319            query.push_str(" AND created_at < :created_before");
3320        }
3321
3322        // Pagination and ordering
3323        write!(&mut query, " AND id {} :cursor", pagination.rel()).expect("writing to string");
3324        query.push_str(" ORDER BY id ");
3325        query.push_str(pagination.asc_or_desc());
3326
3327        // Limit
3328        query.push_str(" LIMIT :length");
3329
3330        let mut stmt = tx.prepare(&query)?;
3331
3332        let items = stmt
3333            .query_map(params.as_slice(), |row| {
3334                let created_at: DateTime<Utc> = row.get("created_at")?;
3335                let run_id = row.get("run_id")?;
3336                let level: Option<u8> = row.get("level")?;
3337                let message: Option<String> = row.get("message")?;
3338                let stream_type: Option<u8> = row.get("stream_type")?;
3339                let payload: Option<Vec<u8>> = row.get("payload")?;
3340                let execution_id_str: String = row.get("execution_id")?;
3341                let execution_id = ExecutionId::from_str(&execution_id_str).map_err(|_| {
3342                    consistency_rusqlite(format!("cannot convert ExecutionId {execution_id_str}"))
3343                })?;
3344
3345                let log_entry = match (level, message, stream_type, payload) {
3346                    (Some(lvl), Some(msg), None, None) => LogEntry::Log {
3347                        created_at,
3348                        level: LogLevel::try_from(lvl).map_err(|_| {
3349                            consistency_rusqlite(format!("cannot convert {lvl} to LogLevel"))
3350                        })?,
3351                        message: msg,
3352                    },
3353                    (None, None, Some(stype), Some(pl)) => LogEntry::Stream {
3354                        created_at,
3355                        stream_type: LogStreamType::try_from(stype).map_err(|_| {
3356                            consistency_rusqlite(format!("cannot convert {stype} to LogStreamType"))
3357                        })?,
3358                        payload: pl,
3359                    },
3360                    _ => {
3361                        return Err(consistency_rusqlite("invalid t_log row".to_string()));
3362                    }
3363                };
3364                Ok(LogEntryRow {
3365                    cursor: LogCursor(row.get("id")?),
3366                    run_id,
3367                    log_entry,
3368                    execution_id,
3369                })
3370            })?
3371            .collect::<Result<Vec<_>, _>>()?;
3372
3373        Ok(ListLogsResponse {
3374            next_page: items
3375                .last()
3376                .map(|item| Pagination::NewerThan {
3377                    length: pagination.length(),
3378                    cursor: item.cursor,
3379                    including_cursor: false,
3380                })
3381                .unwrap_or({
3382                    if pagination.is_asc() {
3383                        *pagination // no new results, keep the same cursor
3384                    } else {
3385                        // no prev results, let's start from beginning
3386                        Pagination::NewerThan {
3387                            length: pagination.length(),
3388                            cursor: LogCursor(i64::MIN),
3389                            including_cursor: false,
3390                        }
3391                    }
3392                }),
3393            prev_page: match items.first() {
3394                Some(item) => Some(Pagination::OlderThan {
3395                    length: pagination.length(),
3396                    cursor: item.cursor,
3397                    including_cursor: false,
3398                }),
3399                None if pagination.is_asc() && pagination.cursor() != &LogCursor(i64::MIN) => {
3400                    // asked for a next page that does not exists (yet).
3401                    Some(pagination.invert())
3402                }
3403                None => None,
3404            },
3405            items,
3406        })
3407    }
3408
3409    fn list_deployment_states(
3410        tx: &Transaction,
3411        current_time: DateTime<Utc>,
3412        pagination: Pagination<Option<DeploymentId>>,
3413        include_deployment_toml: bool,
3414        execution_counts: DeploymentExecutionCounts,
3415    ) -> Result<Vec<DeploymentState>, DbErrorRead> {
3416        let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3417        let deployment_toml_col = if include_deployment_toml {
3418            "d.deployment_toml"
3419        } else {
3420            "NULL AS deployment_toml"
3421        };
3422        let include_execution_counts =
3423            matches!(execution_counts, DeploymentExecutionCounts::Count { .. });
3424        // Without counts, skip the SUMs (and the `t_state` join feeding them) and report zeros.
3425        let count_cols = if include_execution_counts {
3426            format!(
3427                r"
3428            COALESCE(SUM(s.state = '{STATE_LOCKED}' AND s.lifecycle = 'active'), 0) AS locked,
3429            COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.lifecycle = 'active' AND s.pending_expires_finished <= :now), 0) AS pending,
3430            COALESCE(SUM(s.state = '{STATE_PENDING_AT}' AND s.lifecycle = 'active' AND s.pending_expires_finished > :now), 0) AS scheduled,
3431            COALESCE(SUM(s.state = '{STATE_BLOCKED_BY_JOIN_SET}' AND s.lifecycle = 'active'), 0) AS blocked,
3432            COALESCE(SUM(s.lifecycle = 'paused'), 0) AS paused,
3433            COALESCE(SUM(s.lifecycle = 'cancelling'), 0) AS cancelling,
3434            COALESCE(SUM(s.state = '{STATE_FINISHED}' AND s.result_kind = '{RESULT_KIND_JSON_OK}'), 0) AS finished_ok,
3435            COALESCE(SUM(s.state = '{STATE_FINISHED}' AND s.result_kind = '{RESULT_KIND_JSON_ERROR}'), 0) AS finished_error,
3436            COALESCE(SUM(s.state = '{STATE_FINISHED}' AND s.result_kind IS NOT NULL
3437                AND s.result_kind NOT IN ('{RESULT_KIND_JSON_OK}', '{RESULT_KIND_JSON_ERROR}')), 0) AS finished_execution_failure,"
3438            )
3439        } else {
3440            "
3441            0 AS locked,
3442            0 AS pending,
3443            0 AS scheduled,
3444            0 AS blocked,
3445            0 AS paused,
3446            0 AS cancelling,
3447            0 AS finished_ok,
3448            0 AS finished_error,
3449            0 AS finished_execution_failure,"
3450                .to_string()
3451        };
3452        let mut sql = format!(
3453            r"
3454        SELECT
3455            d.deployment_id,
3456            d.description,
3457            d.digest,{count_cols}
3458            {deployment_toml_col},
3459            d.created_at,
3460            d.last_active_at,
3461            d.status
3462        FROM t_deployment d{join}",
3463            join = match execution_counts {
3464                DeploymentExecutionCounts::Count { include_derived } => {
3465                    let join_top_level = if include_derived {
3466                        ""
3467                    } else {
3468                        " AND s.is_top_level = true"
3469                    };
3470                    format!(
3471                        "\n        LEFT JOIN t_state s ON s.deployment_id = d.deployment_id{join_top_level}"
3472                    )
3473                }
3474                DeploymentExecutionCounts::Skip => String::new(),
3475            }
3476        );
3477
3478        if include_execution_counts {
3479            params.push((":now", Box::new(current_time)));
3480        }
3481
3482        if let Some(cursor) = pagination.cursor() {
3483            params.push((":cursor", Box::new(*cursor)));
3484            write!(
3485                sql,
3486                " WHERE d.deployment_id {rel} :cursor",
3487                rel = pagination.rel()
3488            )
3489            .expect("writing to string");
3490        }
3491
3492        // Inner query: fetch rows with cursor-based ordering
3493        // Outer query: always return results in descending order
3494        let (inner_order, outer_order) = if pagination.is_desc() {
3495            ("DESC", "")
3496        } else {
3497            ("ASC", "DESC")
3498        };
3499
3500        // GROUP BY only needed to collapse the rows multiplied by the `t_state` join.
3501        if include_execution_counts {
3502            write!(
3503                sql,
3504                " GROUP BY d.deployment_id, d.description, d.digest, d.deployment_toml, d.created_at, d.last_active_at, d.status"
3505            )
3506            .expect("writing to string");
3507        }
3508        write!(
3509            sql,
3510            " ORDER BY d.deployment_id {inner_order} LIMIT {limit}",
3511            limit = pagination.length()
3512        )
3513        .expect("writing to string");
3514
3515        let final_sql = if outer_order.is_empty() {
3516            sql
3517        } else {
3518            format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3519        };
3520
3521        let result: Vec<DeploymentState> = tx
3522            .prepare(&final_sql)?
3523            .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3524                params
3525                    .iter()
3526                    .map(|(k, v)| (*k, v.as_ref()))
3527                    .collect::<Vec<_>>()
3528                    .as_ref(),
3529                |row| {
3530                    let status_str: String = row.get("status")?;
3531                    let status = status_str.parse::<DeploymentStatus>().map_err(|_| {
3532                        rusqlite::Error::InvalidColumnType(
3533                            0,
3534                            "status".to_string(),
3535                            rusqlite::types::Type::Text,
3536                        )
3537                    })?;
3538                    Ok(DeploymentState {
3539                        deployment_id: row.get("deployment_id")?,
3540                        description: row.get("description")?,
3541                        digest: row.get("digest")?,
3542                        locked: row.get("locked")?,
3543                        pending: row.get("pending")?,
3544                        scheduled: row.get("scheduled")?,
3545                        blocked: row.get("blocked")?,
3546                        paused: row.get("paused")?,
3547                        cancelling: row.get("cancelling")?,
3548                        finished_ok: row.get("finished_ok")?,
3549                        finished_error: row.get("finished_error")?,
3550                        finished_execution_failure: row.get("finished_execution_failure")?,
3551                        deployment_toml: row.get("deployment_toml")?,
3552                        created_at: row.get("created_at")?,
3553                        last_active_at: row.get("last_active_at")?,
3554                        status,
3555                    })
3556                },
3557            )?
3558            .collect::<Result<Vec<_>, rusqlite::Error>>()
3559            .map_err(DbErrorRead::from)?;
3560
3561        Ok(result)
3562    }
3563
3564    fn insert_deployment_tx(
3565        tx: &Transaction,
3566        record: &DeploymentRecord,
3567    ) -> Result<(), DbErrorWrite> {
3568        assert_eq!(
3569            record.status,
3570            DeploymentStatus::Inactive,
3571            "insert_deployment requires Inactive status"
3572        );
3573        assert!(
3574            record.last_active_at.is_none(),
3575            "insert_deployment requires last_active_at == None"
3576        );
3577        tx.execute(
3578            "INSERT INTO t_deployment \
3579             (deployment_id, description, digest, created_at, status, deployment_toml, obelisk_version, created_by) \
3580             VALUES (:deployment_id, :description, :digest, :created_at, :status, :deployment_toml, :obelisk_version, :created_by)",
3581            rusqlite::named_params! {
3582                ":deployment_id": record.deployment_id.to_string(),
3583                ":description": record.description,
3584                ":digest": record.digest.to_string(),
3585                ":created_at": record.created_at,
3586                ":status": record.status.as_str(),
3587                ":deployment_toml": record.deployment_toml,
3588                ":obelisk_version": record.obelisk_version,
3589                ":created_by": record.created_by,
3590            },
3591        )
3592        .map_err(RusqliteError::from)?;
3593        Self::insert_deployment_files_tx(tx, record.deployment_id, &record.files)?;
3594        Ok(())
3595    }
3596
3597    fn insert_deployment_files_tx(
3598        tx: &Transaction,
3599        deployment_id: DeploymentId,
3600        files: &[DeploymentFileRecord],
3601    ) -> Result<(), DbErrorWrite> {
3602        let mut stmt = tx
3603            .prepare_cached(
3604                "INSERT INTO t_deployment_file (deployment_id, digest, path) \
3605                 VALUES (:deployment_id, :digest, :path)",
3606            )
3607            .map_err(RusqliteError::from)?;
3608        for file in files {
3609            stmt.execute(rusqlite::named_params! {
3610                ":deployment_id": deployment_id.to_string(),
3611                ":digest": file.digest.to_string(),
3612                ":path": file.path,
3613            })
3614            .map_err(RusqliteError::from)?;
3615        }
3616        Ok(())
3617    }
3618
3619    fn upsert_component_metadata_tx(
3620        tx: &Transaction,
3621        records: &[ComponentMetadataRecord],
3622    ) -> Result<(), DbErrorWrite> {
3623        let mut stmt = tx
3624            .prepare(
3625                "INSERT OR IGNORE INTO t_component_metadata \
3626                 (component_digest, imports_json, exports_json, wit, wit_origin) \
3627                 VALUES (:component_digest, :imports_json, :exports_json, :wit, :wit_origin)",
3628            )
3629            .map_err(RusqliteError::from)?;
3630        for record in records {
3631            let imports_json = serde_json::to_string(&record.imports).map_err(|err| {
3632                RusqliteError::from(rusqlite::Error::ToSqlConversionFailure(Box::new(err)))
3633            })?;
3634            let exports_json = serde_json::to_string(&record.exports).map_err(|err| {
3635                RusqliteError::from(rusqlite::Error::ToSqlConversionFailure(Box::new(err)))
3636            })?;
3637            stmt.execute(named_params! {
3638                ":component_digest": record.component_digest.clone(),
3639                ":imports_json": imports_json,
3640                ":exports_json": exports_json,
3641                ":wit": record.wit.clone(),
3642                ":wit_origin": record.wit_origin.clone(),
3643            })
3644            .map_err(RusqliteError::from)?;
3645        }
3646        Ok(())
3647    }
3648
3649    fn insert_deployment_components_tx(
3650        tx: &Transaction,
3651        deployment_id: DeploymentId,
3652        records: &[DeploymentComponentRecord],
3653    ) -> Result<(), DbErrorWrite> {
3654        let mut stmt = tx
3655            .prepare(
3656                "INSERT OR IGNORE INTO t_deployment_component \
3657                 (deployment_id, component_name, component_type, component_digest) \
3658                 VALUES (:deployment_id, :component_name, :component_type, :component_digest)",
3659            )
3660            .map_err(RusqliteError::from)?;
3661        for record in records {
3662            debug_assert_eq!(record.deployment_id, deployment_id);
3663            stmt.execute(named_params! {
3664                ":deployment_id": deployment_id.to_string(),
3665                ":component_name": record.component_name.to_string(),
3666                ":component_type": record.component_type.to_string(),
3667                ":component_digest": record.component_digest.clone(),
3668            })
3669            .map_err(RusqliteError::from)?;
3670        }
3671        Ok(())
3672    }
3673
3674    fn compute_file_digest(content: &[u8]) -> ContentDigest {
3675        let hash: [u8; 32] = Sha256::digest(content).into();
3676        ContentDigest(Digest(hash))
3677    }
3678
3679    fn upload_file_tx(
3680        tx: &Transaction,
3681        digest: &ContentDigest,
3682        content: &[u8],
3683    ) -> Result<(), DbErrorWrite> {
3684        let actual = Self::compute_file_digest(content);
3685        if &actual != digest {
3686            return Err(DbErrorWriteNonRetriable::ValidationFailed(
3687                format!("uploaded file digest mismatch: expected {digest}, got {actual}").into(),
3688            )
3689            .into());
3690        }
3691        let size = i64::try_from(content.len()).map_err(|err| DbErrorGeneric::Uncategorized {
3692            reason: format!("deployment file too large: {err}").into(),
3693            context: SpanTrace::capture(),
3694            source: Some(Arc::new(err)),
3695            loc: Location::caller(),
3696        })?;
3697        tx.execute(
3698            "INSERT INTO t_file (digest, content, size) VALUES (:digest, :content, :size) \
3699             ON CONFLICT (digest) DO NOTHING",
3700            rusqlite::named_params! {
3701                ":digest": digest.to_string(),
3702                ":content": content,
3703                ":size": size,
3704            },
3705        )
3706        .map_err(RusqliteError::from)?;
3707        Ok(())
3708    }
3709
3710    fn get_file_tx(
3711        tx: &Transaction,
3712        digest: &ContentDigest,
3713    ) -> Result<Option<Vec<u8>>, DbErrorRead> {
3714        tx.query_row(
3715            "SELECT content FROM t_file WHERE digest = :digest",
3716            rusqlite::named_params! { ":digest": digest.to_string() },
3717            |row| row.get("content"),
3718        )
3719        .optional()
3720        .map_err(|err| DbErrorRead::from(RusqliteError::from(err)))
3721    }
3722
3723    fn missing_digests_tx(
3724        tx: &Transaction,
3725        deployment_id: DeploymentId,
3726    ) -> Result<Vec<ContentDigest>, DbErrorRead> {
3727        tx.prepare(
3728            "SELECT df.digest \
3729             FROM t_deployment_file df \
3730             LEFT JOIN t_file f ON f.digest = df.digest \
3731             WHERE df.deployment_id = :deployment_id AND f.digest IS NULL \
3732             ORDER BY df.digest",
3733        )?
3734        .query_map(
3735            rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3736            |row| row.get("digest"),
3737        )?
3738        .collect::<Result<Vec<_>, rusqlite::Error>>()
3739        .map_err(DbErrorRead::from)
3740    }
3741
3742    fn list_deployment_files_tx(
3743        tx: &Transaction,
3744        deployment_id: DeploymentId,
3745    ) -> Result<Vec<DeploymentFileRecord>, DbErrorRead> {
3746        tx.prepare(
3747            "SELECT path, digest FROM t_deployment_file \
3748             WHERE deployment_id = :deployment_id \
3749             ORDER BY path, digest",
3750        )?
3751        .query_map(
3752            rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3753            |row| {
3754                Ok(DeploymentFileRecord {
3755                    path: row.get("path")?,
3756                    digest: row.get("digest")?,
3757                })
3758            },
3759        )?
3760        .collect::<Result<Vec<_>, rusqlite::Error>>()
3761        .map_err(DbErrorRead::from)
3762    }
3763
3764    fn activate_deployment_tx(
3765        tx: &Transaction,
3766        deployment_id: DeploymentId,
3767        now: DateTime<Utc>,
3768    ) -> Result<(), DbErrorWrite> {
3769        // Demote the currently active or enqueued deployment to inactive.
3770        tx.execute(
3771            "UPDATE t_deployment SET status = 'inactive' WHERE status IN ('active', 'enqueued')",
3772            [],
3773        )
3774        .map_err(RusqliteError::from)?;
3775        // Set target deployment to active, recording activation time.
3776        let rows = tx
3777            .execute(
3778                "UPDATE t_deployment SET status = 'active', last_active_at = :now WHERE deployment_id = :deployment_id",
3779                rusqlite::named_params! {
3780                    ":now": now,
3781                    ":deployment_id": deployment_id.to_string(),
3782                },
3783            )
3784            .map_err(RusqliteError::from)?;
3785        if rows == 0 {
3786            return Err(DbErrorWrite::NotFound);
3787        }
3788        Ok(())
3789    }
3790
3791    fn enqueue_deployment_tx(
3792        tx: &Transaction,
3793        deployment_id: DeploymentId,
3794    ) -> Result<EnqueueOutcome, DbErrorWrite> {
3795        let status_opt: Option<String> = tx
3796            .query_row(
3797                "SELECT status FROM t_deployment WHERE deployment_id = :deployment_id",
3798                rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3799                |row| row.get(0),
3800            )
3801            .optional()
3802            .map_err(RusqliteError::from)?;
3803        let status = status_opt.as_deref();
3804        if status.is_none() {
3805            return Err(DbErrorWrite::NotFound);
3806        }
3807        // Demote any previously enqueued deployment to inactive.
3808        tx.execute(
3809            "UPDATE t_deployment SET status = 'inactive' WHERE status = 'enqueued'",
3810            [],
3811        )
3812        .map_err(RusqliteError::from)?;
3813        // The target is already active: it stays active, only the pending one was cleared.
3814        if status == Some("active") {
3815            return Ok(EnqueueOutcome::AlreadyActive);
3816        }
3817        // Set target deployment to enqueued.
3818        let rows = tx
3819            .execute(
3820                "UPDATE t_deployment SET status = 'enqueued' WHERE deployment_id = :deployment_id",
3821                rusqlite::named_params! {
3822                    ":deployment_id": deployment_id.to_string(),
3823                },
3824            )
3825            .map_err(RusqliteError::from)?;
3826        if rows == 0 {
3827            return Err(DbErrorWrite::NotFound);
3828        }
3829        Ok(EnqueueOutcome::Enqueued)
3830    }
3831
3832    fn get_deployment_tx(
3833        tx: &Transaction,
3834        deployment_id: DeploymentId,
3835    ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3836        let Some(record) = tx
3837            .query_row(
3838            "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
3839             FROM t_deployment WHERE deployment_id = :deployment_id",
3840                rusqlite::named_params! { ":deployment_id": deployment_id.to_string() },
3841                deployment_record_from_row,
3842            )
3843            .optional()
3844            .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))?
3845        else {
3846            return Ok(None);
3847        };
3848        Self::with_deployment_files_tx(tx, record).map(Some)
3849    }
3850
3851    #[cfg(feature = "test")]
3852    fn get_active_deployment_tx(tx: &Transaction) -> Result<Option<DeploymentRecord>, DbErrorRead> {
3853        let Some(record) = tx
3854            .query_row(
3855            "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
3856             FROM t_deployment WHERE status = 'active' LIMIT 1",
3857                [],
3858                deployment_record_from_row,
3859            )
3860            .optional()
3861            .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))?
3862        else {
3863            return Ok(None);
3864        };
3865        Self::with_deployment_files_tx(tx, record).map(Some)
3866    }
3867
3868    fn list_deployments_tx(
3869        tx: &Transaction,
3870        pagination: Pagination<Option<DeploymentId>>,
3871    ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
3872        let mut params: Vec<(&'static str, Box<dyn ToSql>)> = vec![];
3873        let mut sql = String::from(
3874            "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
3875             FROM t_deployment",
3876        );
3877
3878        if let Some(cursor) = pagination.cursor() {
3879            params.push((":cursor", Box::new(*cursor)));
3880            write!(
3881                sql,
3882                " WHERE deployment_id {rel} :cursor",
3883                rel = pagination.rel()
3884            )
3885            .expect("writing to string");
3886        }
3887
3888        let (inner_order, outer_order) = if pagination.is_desc() {
3889            ("DESC", "")
3890        } else {
3891            ("ASC", "DESC")
3892        };
3893
3894        write!(
3895            sql,
3896            " ORDER BY deployment_id {inner_order} LIMIT {limit}",
3897            limit = pagination.length()
3898        )
3899        .expect("writing to string");
3900
3901        let final_sql = if outer_order.is_empty() {
3902            sql
3903        } else {
3904            format!("SELECT * FROM ({sql}) AS sub ORDER BY deployment_id {outer_order}")
3905        };
3906
3907        let mut result: Vec<DeploymentRecord> = tx
3908            .prepare(&final_sql)?
3909            .query_map::<_, &[(&'static str, &dyn ToSql)], _>(
3910                params
3911                    .iter()
3912                    .map(|(k, v)| (*k, v.as_ref()))
3913                    .collect::<Vec<_>>()
3914                    .as_ref(),
3915                deployment_record_from_row,
3916            )?
3917            .collect::<Result<Vec<_>, rusqlite::Error>>()
3918            .map_err(DbErrorRead::from)?;
3919        for record in &mut result {
3920            record.files = Self::list_deployment_files_tx(tx, record.deployment_id)?;
3921        }
3922
3923        Ok(result)
3924    }
3925
3926    fn with_deployment_files_tx(
3927        tx: &Transaction,
3928        mut record: DeploymentRecord,
3929    ) -> Result<DeploymentRecord, DbErrorRead> {
3930        record.files = Self::list_deployment_files_tx(tx, record.deployment_id)?;
3931        Ok(record)
3932    }
3933
3934    fn pause_execution(
3935        tx: &Transaction,
3936        execution_id: &ExecutionId,
3937        paused_at: DateTime<Utc>,
3938    ) -> Result<Version, DbErrorWrite> {
3939        let combined_state = Self::get_combined_state(tx, execution_id)?;
3940        let mut appending_version = combined_state.get_next_version_fail_if_finished()?;
3941        debug!("Pausing with {appending_version}");
3942        if combined_state.reject_locked_activities()? {
3943            (appending_version, _) = Self::append(
3944                tx,
3945                execution_id,
3946                AppendRequest {
3947                    created_at: paused_at,
3948                    event: ExecutionRequest::Unlocked(Unlocked {
3949                        unlocked_at: paused_at, // does not matter, about to append `Paused` in same tx.
3950                        reason: "paused".into(),
3951                    }),
3952                },
3953                appending_version,
3954            )?;
3955        }
3956        let (next_version, _notifier) = Self::append(
3957            tx,
3958            execution_id,
3959            AppendRequest {
3960                created_at: paused_at,
3961                event: ExecutionRequest::Paused,
3962            },
3963            appending_version,
3964        )?;
3965        Ok(next_version)
3966    }
3967
3968    fn unpause_execution(
3969        tx: &Transaction,
3970        execution_id: &ExecutionId,
3971        paused_at: DateTime<Utc>,
3972    ) -> Result<Version, DbErrorWrite> {
3973        let combined_state = Self::get_combined_state(tx, execution_id)?;
3974        let appending_version = combined_state.get_next_version_fail_if_finished()?;
3975        debug!("Unpausing with {appending_version}");
3976        let (next_version, _) = Self::append(
3977            tx,
3978            execution_id,
3979            AppendRequest {
3980                created_at: paused_at,
3981                event: ExecutionRequest::Unpaused,
3982            },
3983            appending_version,
3984        )?;
3985        Ok(next_version)
3986    }
3987
3988    fn cancel_workflow(
3989        tx: &Transaction,
3990        execution_id: &ExecutionId,
3991        cancelled_at: DateTime<Utc>,
3992    ) -> Result<CancelOutcome, DbErrorWrite> {
3993        let combined_state = Self::get_combined_state(tx, execution_id)?;
3994        if let Some(outcome) = combined_state.cancel_short_circuit() {
3995            return Ok(outcome);
3996        }
3997        Self::append_cancellation_requested(
3998            tx,
3999            execution_id,
4000            cancelled_at,
4001            &combined_state,
4002            CancellationFfqnCheck::Required,
4003        )
4004    }
4005
4006    fn append_cancellation_requested(
4007        tx: &Transaction,
4008        execution_id: &ExecutionId,
4009        cancelled_at: DateTime<Utc>,
4010        combined_state: &CombinedState,
4011        ffqn_check: CancellationFfqnCheck,
4012    ) -> Result<CancelOutcome, DbErrorWrite> {
4013        if matches!(ffqn_check, CancellationFfqnCheck::Required) {
4014            combined_state.assert_cancellable_workflow_ffqn()?;
4015        }
4016        Self::append(
4017            tx,
4018            execution_id,
4019            AppendRequest {
4020                created_at: cancelled_at,
4021                event: ExecutionRequest::CancellationRequested,
4022            },
4023            combined_state.get_next_version_assert_not_finished(),
4024        )?;
4025        Ok(CancelOutcome::Cancelled)
4026    }
4027
4028    fn append_activity_cancellation_requested_tx(
4029        tx: &Transaction,
4030        execution_id: &ExecutionId,
4031        cancelled_at: DateTime<Utc>,
4032        combined_state: &CombinedState,
4033    ) -> Result<CancelOutcome, DbErrorWrite> {
4034        match &combined_state.execution_with_state.pending_state {
4035            PendingState::Finished(finished) => {
4036                if finished.result_kind
4037                    == PendingStateFinishedResultKind::Err(
4038                        PendingStateFinishedError::ExecutionFailure(
4039                            ExecutionFailureKind::Cancelled,
4040                        ),
4041                    )
4042                {
4043                    return Ok(CancelOutcome::Cancelled);
4044                }
4045                return Ok(CancelOutcome::AlreadyFinished);
4046            }
4047            PendingState::Cancelling(_) => return Ok(CancelOutcome::Cancelled),
4048            _ => {}
4049        }
4050        Self::append_cancellation_requested(
4051            tx,
4052            execution_id,
4053            cancelled_at,
4054            combined_state,
4055            CancellationFfqnCheck::Skipped,
4056        )
4057    }
4058}
4059
4060#[async_trait]
4061impl DbExecutor for SqlitePool {
4062    #[instrument(level = Level::TRACE, skip(self))]
4063    async fn lock_pending_by_ffqns(
4064        &self,
4065        batch_size: u32,
4066        pending_at_or_sooner: DateTime<Utc>,
4067        ffqns: Arc<[FunctionFqn]>,
4068        created_at: DateTime<Utc>,
4069        component_id: ComponentId,
4070        deployment_id: DeploymentId,
4071        executor_id: ExecutorId,
4072        lock_expires_at: DateTime<Utc>,
4073        run_id: RunId,
4074        retry_config: ComponentRetryConfig,
4075    ) -> Result<LockPendingResponse, DbErrorWrite> {
4076        let execution_ids_versions = self
4077            .transaction(
4078                move |conn| {
4079                    Self::get_pending_by_ffqns(conn, batch_size, pending_at_or_sooner, &ffqns)
4080                },
4081                TxType::Other, // read only
4082                "lock_pending_by_ffqns_get",
4083            )
4084            .await
4085            .map_err(to_generic_error)?;
4086        if execution_ids_versions.is_empty() {
4087            Ok(vec![])
4088        } else {
4089            debug!("Locking {execution_ids_versions:?}");
4090            self.transaction(
4091                move |tx| {
4092                    let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
4093                    // Append lock
4094                    for (execution_id, version) in &execution_ids_versions {
4095                        locked_execs.push(Self::lock_single_execution(
4096                            tx,
4097                            created_at,
4098                            &component_id,
4099                            true,
4100                            deployment_id,
4101                            execution_id,
4102                            run_id,
4103                            version,
4104                            executor_id,
4105                            lock_expires_at,
4106                            retry_config,
4107                        )?);
4108                    }
4109                    Ok::<_, DbErrorWrite>(locked_execs)
4110                },
4111                TxType::MultipleWrites,
4112                "lock_pending_by_ffqns_one",
4113            )
4114            .await
4115        }
4116    }
4117
4118    #[instrument(level = Level::TRACE, skip(self))]
4119    async fn lock_pending_by_ffqns_auto(
4120        &self,
4121        batch_size: u32,
4122        pending_at_or_sooner: DateTime<Utc>,
4123        ffqns: Arc<[FunctionFqn]>,
4124        created_at: DateTime<Utc>,
4125        component_id: ComponentId,
4126        deployment_id: DeploymentId,
4127        executor_id: ExecutorId,
4128        lock_expires_at: DateTime<Utc>,
4129        run_id: RunId,
4130        retry_config: ComponentRetryConfig,
4131    ) -> Result<LockPendingResponse, DbErrorWrite> {
4132        let current_digest = component_id.component_digest.clone();
4133        let execution_ids_versions = self
4134            .transaction(
4135                move |conn| {
4136                    Self::get_pending_by_ffqns_auto(
4137                        conn,
4138                        batch_size,
4139                        pending_at_or_sooner,
4140                        &ffqns,
4141                        &current_digest,
4142                    )
4143                },
4144                TxType::Other,
4145                "lock_pending_by_ffqns_auto_get",
4146            )
4147            .await
4148            .map_err(to_generic_error)?;
4149        if execution_ids_versions.is_empty() {
4150            Ok(vec![])
4151        } else {
4152            debug!("Auto-locking {execution_ids_versions:?}");
4153            self.transaction(
4154                move |tx| {
4155                    let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
4156                    for (execution_id, version) in &execution_ids_versions {
4157                        locked_execs.push(Self::lock_single_execution(
4158                            tx,
4159                            created_at,
4160                            &component_id,
4161                            false,
4162                            deployment_id,
4163                            execution_id,
4164                            run_id,
4165                            version,
4166                            executor_id,
4167                            lock_expires_at,
4168                            retry_config,
4169                        )?);
4170                    }
4171                    Ok::<_, DbErrorWrite>(locked_execs)
4172                },
4173                TxType::MultipleWrites,
4174                "lock_pending_by_ffqns_auto_one",
4175            )
4176            .await
4177        }
4178    }
4179
4180    #[instrument(level = Level::TRACE, skip(self))]
4181    async fn lock_pending_by_component_digest(
4182        &self,
4183        batch_size: u32,
4184        pending_at_or_sooner: DateTime<Utc>,
4185        component_id: &ComponentId,
4186        deployment_id: DeploymentId,
4187        created_at: DateTime<Utc>,
4188        executor_id: ExecutorId,
4189        lock_expires_at: DateTime<Utc>,
4190        run_id: RunId,
4191        retry_config: ComponentRetryConfig,
4192    ) -> Result<LockPendingResponse, DbErrorWrite> {
4193        let component_id = component_id.clone();
4194        let execution_ids_versions = self
4195            .transaction(
4196                {
4197                    let component_id = component_id.clone();
4198                    move |conn| {
4199                        Self::get_pending_by_component_input_digest(
4200                            conn,
4201                            batch_size,
4202                            pending_at_or_sooner,
4203                            &component_id.component_digest,
4204                        )
4205                    }
4206                },
4207                TxType::Other, // read only
4208                "lock_pending_by_component_id_get",
4209            )
4210            .await
4211            .map_err(to_generic_error)?;
4212        if execution_ids_versions.is_empty() {
4213            Ok(vec![])
4214        } else {
4215            debug!("Locking {execution_ids_versions:?}");
4216            self.transaction(
4217                move |tx| {
4218                    let mut locked_execs = Vec::with_capacity(execution_ids_versions.len());
4219                    // Append lock
4220                    for (execution_id, version) in &execution_ids_versions {
4221                        locked_execs.push(Self::lock_single_execution(
4222                            tx,
4223                            created_at,
4224                            &component_id,
4225                            true,
4226                            deployment_id,
4227                            execution_id,
4228                            run_id,
4229                            version,
4230                            executor_id,
4231                            lock_expires_at,
4232                            retry_config,
4233                        )?);
4234                    }
4235                    Ok::<_, DbErrorWrite>(locked_execs)
4236                },
4237                TxType::MultipleWrites,
4238                "lock_pending_by_component_id_one",
4239            )
4240            .await
4241        }
4242    }
4243
4244    #[cfg(feature = "test")]
4245    #[instrument(level = Level::DEBUG, skip(self))]
4246    async fn lock_one(
4247        &self,
4248        created_at: DateTime<Utc>,
4249        component_id: ComponentId,
4250        deployment_id: DeploymentId,
4251        execution_id: &ExecutionId,
4252        run_id: RunId,
4253        version: Version,
4254        executor_id: ExecutorId,
4255        lock_expires_at: DateTime<Utc>,
4256        retry_config: ComponentRetryConfig,
4257    ) -> Result<LockedExecution, DbErrorWrite> {
4258        debug!(%execution_id, "lock_one");
4259        let execution_id = execution_id.clone();
4260        self.transaction(
4261            move |tx| {
4262                Self::lock_single_execution(
4263                    tx,
4264                    created_at,
4265                    &component_id,
4266                    true,
4267                    deployment_id,
4268                    &execution_id,
4269                    run_id,
4270                    &version,
4271                    executor_id,
4272                    lock_expires_at,
4273                    retry_config,
4274                )
4275            },
4276            TxType::MultipleWrites, // insert + update t_state
4277            "lock_inner",
4278        )
4279        .await
4280    }
4281
4282    #[instrument(level = Level::DEBUG, skip(self, req))]
4283    async fn append(
4284        &self,
4285        execution_id: ExecutionId,
4286        version: Version,
4287        req: AppendRequest,
4288    ) -> Result<AppendResponse, DbErrorWrite> {
4289        debug!(%req, "append");
4290        trace!(?req, "append");
4291        let created_at = req.created_at;
4292        let (version, notifier) = self
4293            .transaction(
4294                move |tx| Self::append(tx, &execution_id, req.clone(), version.clone()),
4295                TxType::MultipleWrites, // insert + update t_state
4296                "append",
4297            )
4298            .await?;
4299        self.notify_all(vec![notifier], created_at);
4300        Ok(version)
4301    }
4302
4303    #[instrument(level = Level::DEBUG, skip_all)]
4304    async fn append_batch_respond_to_parent(
4305        &self,
4306        events: AppendEventsToExecution,
4307        response: AppendResponseToExecution,
4308        current_time: DateTime<Utc>,
4309    ) -> Result<AppendBatchResponse, DbErrorWrite> {
4310        debug!("append_batch_respond_to_parent");
4311        if events.execution_id == response.parent_execution_id {
4312            // Pending state would be wrong.
4313            // This is not a panic because it depends on DB state.
4314            return Err(DbErrorWrite::NonRetriable(
4315                DbErrorWriteNonRetriable::ValidationFailed(
4316                    "Parameters `execution_id` and `parent_execution_id` cannot be the same".into(),
4317                ),
4318            ));
4319        }
4320        if events.batch.is_empty() {
4321            error!("Batch cannot be empty");
4322            return Err(DbErrorWrite::NonRetriable(
4323                DbErrorWriteNonRetriable::ValidationFailed("batch cannot be empty".into()),
4324            ));
4325        }
4326        let (version, notifiers) = {
4327            self.transaction(
4328                move |tx| {
4329                    let mut version = events.version.clone();
4330                    let mut notifier_of_child = None;
4331                    for append_request in &events.batch {
4332                        let (v, n) = Self::append(
4333                            tx,
4334                            &events.execution_id,
4335                            append_request.clone(),
4336                            version,
4337                        )?;
4338                        version = v;
4339                        notifier_of_child = Some(n);
4340                    }
4341
4342                    let pending_at_parent = Self::append_response(
4343                        tx,
4344                        &response.parent_execution_id,
4345                        JoinSetResponseEventOuter {
4346                            created_at: response.created_at,
4347                            event: JoinSetResponseEvent {
4348                                join_set_id: response.join_set_id.clone(),
4349                                event: JoinSetResponse::ChildExecutionFinished {
4350                                    child_execution_id: response.child_execution_id.clone(),
4351                                    finished_version: response.finished_version.clone(),
4352                                    result: response.result.clone(),
4353                                },
4354                            },
4355                        },
4356                    )?;
4357                    Ok::<_, DbErrorWrite>((
4358                        version,
4359                        vec![
4360                            notifier_of_child.expect("checked that the batch is not empty"),
4361                            pending_at_parent,
4362                        ],
4363                    ))
4364                },
4365                TxType::MultipleWrites,
4366                "append_batch_respond_to_parent",
4367            )
4368            .await?
4369        };
4370        self.notify_all(notifiers, current_time);
4371        Ok(version)
4372    }
4373
4374    // Supports only one subscriber (executor) per ffqn.
4375    // A new subscriber replaces the old one, which will eventually time out, which is fine.
4376    #[instrument(level = Level::TRACE, skip(self, timeout_fut))]
4377    async fn wait_for_pending_by_ffqn(
4378        &self,
4379        pending_at_or_sooner: DateTime<Utc>,
4380        ffqns: Arc<[FunctionFqn]>,
4381        current_digest: Option<ComponentDigest>,
4382        timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
4383    ) {
4384        let unique_tag: u64 = rand::random();
4385        let (sender, mut receiver) = mpsc::channel(1); // senders must use `try_send`
4386        {
4387            let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4388            for ffqn in ffqns.as_ref() {
4389                pending_subscribers.insert_ffqn(ffqn.clone(), (sender.clone(), unique_tag));
4390            }
4391        }
4392        async {
4393            let Ok(execution_ids_versions) = self
4394                .transaction(
4395                    {
4396                        let ffqns = ffqns.clone();
4397                        move |conn| {
4398                            if let Some(current_digest) = &current_digest {
4399                                Self::get_pending_by_ffqns_auto(
4400                                    conn,
4401                                    1,
4402                                    pending_at_or_sooner,
4403                                    ffqns.as_ref(),
4404                                    current_digest,
4405                                )
4406                            } else {
4407                                Self::get_pending_by_ffqns(
4408                                    conn,
4409                                    1,
4410                                    pending_at_or_sooner,
4411                                    ffqns.as_ref(),
4412                                )
4413                            }
4414                        }
4415                    },
4416                    TxType::Other, // read only
4417                    "get_pending_by_ffqns",
4418                )
4419                .await
4420            else {
4421                trace!(
4422                    "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
4423                );
4424                timeout_fut.await;
4425                return;
4426            };
4427            if !execution_ids_versions.is_empty() {
4428                trace!("Not waiting, database already contains new pending executions");
4429                return;
4430            }
4431            tokio::select! { // future's liveness: Dropping the loser immediately.
4432                _ = receiver.recv() => {
4433                    trace!("Received a notification");
4434                }
4435                () = timeout_fut => {
4436                }
4437            }
4438        }.await;
4439        // Clean up ffqn_to_pending_subscription in any case
4440        {
4441            let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4442            for ffqn in ffqns.as_ref() {
4443                match pending_subscribers.remove_ffqn(ffqn) {
4444                    Some((_, tag)) if tag == unique_tag => {
4445                        // Cleanup OK.
4446                    }
4447                    Some(other) => {
4448                        // Reinsert foreign sender.
4449                        pending_subscribers.insert_ffqn(ffqn.clone(), other);
4450                    }
4451                    None => {
4452                        // Value was replaced and cleaned up already.
4453                    }
4454                }
4455            }
4456        }
4457    }
4458
4459    // Supports only one subscriber (executor) per component id.
4460    // A new subscriber replaces the old one, which will eventually time out, which is fine.
4461    #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
4462    async fn wait_for_pending_by_component_digest(
4463        &self,
4464        pending_at_or_sooner: DateTime<Utc>,
4465        component_digest: &ComponentDigest,
4466        timeout_fut: Pin<Box<dyn Future<Output = ()> + Send>>,
4467    ) {
4468        let unique_tag: u64 = rand::random();
4469        let (sender, mut receiver) = mpsc::channel(1); // senders must use `try_send`
4470        {
4471            let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4472            pending_subscribers
4473                .insert_by_component(component_digest.clone(), (sender.clone(), unique_tag));
4474        }
4475        async {
4476            let Ok(execution_ids_versions) = self
4477                .transaction(
4478                    {
4479                        let input_digest = component_digest.clone();
4480                        move |conn| Self::get_pending_by_component_input_digest(conn, 1, pending_at_or_sooner, &input_digest)
4481                    },
4482                    TxType::Other, // read only
4483                    "get_pending_by_component_input_digest",
4484                )
4485                .await
4486            else {
4487                trace!(
4488                    "Ignoring get_pending error and waiting in for timeout to avoid executor repolling too soon"
4489                );
4490                timeout_fut.await;
4491                return;
4492            };
4493            if !execution_ids_versions.is_empty() {
4494                trace!("Not waiting, database already contains new pending executions");
4495                return;
4496            }
4497            tokio::select! { // future's liveness: Dropping the loser immediately.
4498                _ = receiver.recv() => {
4499                    trace!("Received a notification");
4500                }
4501                () = timeout_fut => {
4502                }
4503            }
4504        }.await;
4505        // Clean up ffqn_to_pending_subscription in any case
4506        {
4507            let mut pending_subscribers = self.0.pending_subscribers.lock().unwrap();
4508
4509            match pending_subscribers.remove_by_component(component_digest) {
4510                Some((_, tag)) if tag == unique_tag => {
4511                    // Cleanup OK.
4512                }
4513                Some(other) => {
4514                    // Reinsert foreign sender.
4515                    pending_subscribers.insert_by_component(component_digest.clone(), other);
4516                }
4517                None => {
4518                    // Value was replaced and cleaned up already.
4519                }
4520            }
4521        }
4522    }
4523
4524    async fn get_last_execution_event(
4525        &self,
4526        execution_id: &ExecutionId,
4527    ) -> Result<ExecutionEvent, DbErrorRead> {
4528        let execution_id = execution_id.clone();
4529        self.transaction(
4530            move |tx| Self::get_last_execution_event(tx, &execution_id),
4531            TxType::Other, // read only
4532            "get_last_execution_event",
4533        )
4534        .await
4535    }
4536
4537    #[instrument(skip(self))]
4538    async fn append_activity_cancellation_requested(
4539        &self,
4540        execution_id: &ExecutionId,
4541        cancelled_at: DateTime<Utc>,
4542    ) -> Result<CancelOutcome, DbErrorWrite> {
4543        let execution_id = execution_id.clone();
4544        self.transaction(
4545            move |tx| {
4546                let combined_state = Self::get_combined_state(tx, &execution_id)?;
4547                SqlitePool::append_activity_cancellation_requested_tx(
4548                    tx,
4549                    &execution_id,
4550                    cancelled_at,
4551                    &combined_state,
4552                )
4553            },
4554            TxType::MultipleWrites,
4555            "append_activity_cancellation_requested",
4556        )
4557        .await
4558    }
4559
4560    #[instrument(skip(self))]
4561    async fn cancel_workflow(
4562        &self,
4563        execution_id: &ExecutionId,
4564        cancelled_at: DateTime<Utc>,
4565    ) -> Result<CancelOutcome, DbErrorWrite> {
4566        let execution_id = execution_id.clone();
4567        self.transaction(
4568            move |tx| SqlitePool::cancel_workflow(tx, &execution_id, cancelled_at),
4569            TxType::MultipleWrites,
4570            "cancel_workflow",
4571        )
4572        .await
4573    }
4574}
4575
4576#[async_trait]
4577impl DbExternalApi for SqlitePool {
4578    #[instrument(skip(self))]
4579    async fn get_backtrace(
4580        &self,
4581        execution_id: &ExecutionId,
4582        filter: BacktraceFilter,
4583    ) -> Result<BacktraceInfo, DbErrorRead> {
4584        debug!("get_backtrace");
4585        let execution_id = execution_id.clone();
4586
4587        self.transaction(
4588            move |tx| {
4589                let select = "SELECT component_id, version_min_including, version_max_excluding, wasm_backtrace FROM t_execution_backtrace e \
4590                                INNER JOIN t_wasm_backtrace w ON e.backtrace_hash = w.backtrace_hash \
4591                                WHERE execution_id = :execution_id";
4592                let mut params: Vec<(&'static str, Box<dyn rusqlite::ToSql>)> = vec![(":execution_id", Box::new(execution_id.to_string()))];
4593                let select = match &filter {
4594                    BacktraceFilter::Specific(version) =>{
4595                        params.push((":version", Box::new(version.0)));
4596                        format!("{select} AND version_min_including <= :version AND version_max_excluding > :version")
4597                    },
4598                    BacktraceFilter::First => format!("{select} ORDER BY version_min_including LIMIT 1"),
4599                    BacktraceFilter::Last => format!("{select} ORDER BY version_min_including DESC LIMIT 1")
4600                };
4601                tx
4602                    .prepare(&select)
4603                    ?
4604                    .query_row::<_, &[(&'static str, &dyn ToSql)], _>(
4605                        params
4606                            .iter()
4607                            .map(|(key, value)| (*key, value.as_ref()))
4608                            .collect::<Vec<_>>()
4609                            .as_ref(),
4610                    |row| {
4611                        Ok(BacktraceInfo {
4612                            execution_id: execution_id.clone(),
4613                            component_id: row.get::<_, JsonWrapper<_> >("component_id")?.0,
4614                            version_min_including: Version::new(row.get::<_, VersionType>("version_min_including")?),
4615                            version_max_excluding: Version::new(row.get::<_, VersionType>("version_max_excluding")?),
4616                            wasm_backtrace: row.get::<_, JsonWrapper<_>>("wasm_backtrace")?.0,
4617                        })
4618                    },
4619                ).map_err(DbErrorRead::from)
4620            },
4621            TxType::Other, // read only
4622            "get_last_backtrace",
4623        ).await
4624    }
4625
4626    #[instrument(skip_all)]
4627    async fn upsert_source_mapping(
4628        &self,
4629        component_digest: &ComponentDigest,
4630        frame_key: &str,
4631        is_suffix: bool,
4632        digest: &ContentDigest,
4633    ) -> Result<(), DbErrorWrite> {
4634        let component_digest = component_digest.clone();
4635        let frame_key = frame_key.to_owned();
4636        let digest = digest.to_string();
4637        self.transaction(
4638            move |tx| {
4639                tx.prepare(
4640                    "INSERT INTO t_component_source \
4641                     (component_digest, frame_key, is_suffix, digest) \
4642                     VALUES (:component_digest, :frame_key, :is_suffix, :digest) \
4643                     ON CONFLICT (component_digest, frame_key, is_suffix) \
4644                     DO UPDATE SET digest = excluded.digest",
4645                )?
4646                .execute(named_params! {
4647                    ":component_digest": component_digest,
4648                    ":frame_key": frame_key,
4649                    ":is_suffix": is_suffix,
4650                    ":digest": digest,
4651                })?;
4652                Ok(())
4653            },
4654            TxType::Other,
4655            "upsert_source_mapping",
4656        )
4657        .await
4658    }
4659
4660    #[instrument(skip_all)]
4661    async fn resolve_source_digest(
4662        &self,
4663        component_digest: &ComponentDigest,
4664        file: &str,
4665    ) -> Result<Option<ContentDigest>, DbErrorRead> {
4666        let component_digest = component_digest.clone();
4667        let file = file.to_owned();
4668        self.transaction(
4669            move |tx| {
4670                let mut stmt = tx.prepare(
4671                    "SELECT digest \
4672                     FROM t_component_source \
4673                     WHERE component_digest = :component_digest \
4674                       AND ( \
4675                           (is_suffix = 0 AND frame_key = :file) \
4676                        OR (is_suffix = 1 AND \
4677                            substr(:file, length(:file) - length(frame_key) + 1) = frame_key) \
4678                       )",
4679                )?;
4680                let rows: Vec<ContentDigest> = stmt
4681                    .query_map(
4682                        named_params! {
4683                            ":component_digest": component_digest,
4684                            ":file": file,
4685                        },
4686                        |row| row.get("digest"),
4687                    )?
4688                    .collect::<Result<_, _>>()?;
4689                match rows.len() {
4690                    0 => Ok(None),
4691                    1 => Ok(Some(rows.into_iter().next().unwrap())),
4692                    _ => {
4693                        warn!("Multiple suffix matches for '{file}', returning None");
4694                        Ok(None)
4695                    }
4696                }
4697            },
4698            TxType::Other,
4699            "resolve_source_digest",
4700        )
4701        .await
4702    }
4703
4704    #[instrument(skip_all)]
4705    async fn upsert_component_metadata(
4706        &self,
4707        records: Vec<ComponentMetadataRecord>,
4708    ) -> Result<(), DbErrorWrite> {
4709        self.transaction(
4710            move |tx| Self::upsert_component_metadata_tx(tx, &records),
4711            TxType::MultipleWrites,
4712            "upsert_component_metadata",
4713        )
4714        .await
4715    }
4716
4717    #[instrument(skip_all)]
4718    async fn insert_deployment_components(
4719        &self,
4720        deployment_id: DeploymentId,
4721        records: Vec<DeploymentComponentRecord>,
4722    ) -> Result<(), DbErrorWrite> {
4723        self.transaction(
4724            move |tx| Self::insert_deployment_components_tx(tx, deployment_id, &records),
4725            TxType::MultipleWrites,
4726            "insert_deployment_components",
4727        )
4728        .await
4729    }
4730
4731    #[instrument(skip_all)]
4732    async fn list_deployment_components(
4733        &self,
4734        deployment_id: DeploymentId,
4735    ) -> Result<Vec<DeploymentComponentDetail>, DbErrorRead> {
4736        self.transaction(
4737            move |tx| {
4738                let mut stmt = tx.prepare(
4739                    "SELECT dc.component_name, dc.component_type, dc.component_digest, \
4740                            cm.imports_json, cm.exports_json, cm.wit \
4741                     FROM t_deployment_component dc \
4742                     JOIN t_component_metadata cm ON dc.component_digest = cm.component_digest \
4743                     WHERE dc.deployment_id = :deployment_id \
4744                     ORDER BY dc.component_type, dc.component_name",
4745                )?;
4746                let rows = stmt
4747                    .query_map(
4748                        named_params! { ":deployment_id": deployment_id.to_string() },
4749                        |row| {
4750                            deployment_component_detail_from_row(row).map_err(|err| {
4751                                rusqlite::Error::ToSqlConversionFailure(Box::new(err))
4752                            })
4753                        },
4754                    )?
4755                    .collect::<Result<Vec<_>, _>>()?;
4756                Ok(rows)
4757            },
4758            TxType::Other,
4759            "list_deployment_components",
4760        )
4761        .await
4762    }
4763
4764    #[instrument(skip_all)]
4765    async fn get_deployment_component_wit(
4766        &self,
4767        deployment_id: DeploymentId,
4768        component_digest: &ComponentDigest,
4769    ) -> Result<Option<String>, DbErrorRead> {
4770        let component_digest = component_digest.clone();
4771        self.transaction(
4772            move |tx| {
4773                tx.prepare(
4774                    "SELECT cm.wit \
4775                     FROM t_deployment_component dc \
4776                     JOIN t_component_metadata cm ON dc.component_digest = cm.component_digest \
4777                     WHERE dc.deployment_id = :deployment_id \
4778                       AND dc.component_digest = :component_digest \
4779                     LIMIT 1",
4780                )?
4781                .query_row(
4782                    named_params! {
4783                        ":deployment_id": deployment_id.to_string(),
4784                        ":component_digest": component_digest,
4785                    },
4786                    |row| row.get(0),
4787                )
4788                .optional()
4789                .map_err(DbErrorRead::from)
4790            },
4791            TxType::Other,
4792            "get_deployment_component_wit",
4793        )
4794        .await
4795    }
4796
4797    #[instrument(skip(self))]
4798    async fn list_executions(
4799        &self,
4800        filter: ListExecutionsFilter,
4801        pagination: ExecutionListPagination,
4802    ) -> Result<Vec<ExecutionWithState>, DbErrorGeneric> {
4803        self.transaction(
4804            move |tx| Self::list_executions(tx, &filter, &pagination),
4805            TxType::Other, // read only
4806            "list_executions",
4807        )
4808        .await
4809        .map_err(to_generic_error)
4810    }
4811
4812    #[instrument(skip(self))]
4813    async fn list_execution_events(
4814        &self,
4815        execution_id: &ExecutionId,
4816        pagination: Pagination<VersionType>,
4817        include_backtrace_id: bool,
4818    ) -> Result<ListExecutionEventsResponse, DbErrorRead> {
4819        let execution_id = execution_id.clone();
4820        self.transaction(
4821            move |tx| {
4822                let events = Self::list_execution_events(
4823                    tx,
4824                    &execution_id,
4825                    pagination,
4826                    include_backtrace_id,
4827                )?;
4828                let max_version = Self::get_max_version(tx, &execution_id)?;
4829                Ok(ListExecutionEventsResponse {
4830                    events,
4831                    max_version,
4832                })
4833            },
4834            TxType::Other, // read only
4835            "get",
4836        )
4837        .await
4838    }
4839
4840    #[instrument(skip(self))]
4841    async fn list_responses(
4842        &self,
4843        execution_id: &ExecutionId,
4844        pagination: Pagination<u32>,
4845    ) -> Result<ListResponsesResponse, DbErrorRead> {
4846        let execution_id = execution_id.clone();
4847        self.transaction(
4848            move |tx| {
4849                let responses = Self::list_responses(tx, &execution_id, Some(pagination))?;
4850                let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4851                Ok(ListResponsesResponse {
4852                    responses,
4853                    max_cursor,
4854                })
4855            },
4856            TxType::Other, // read only
4857            "list_responses",
4858        )
4859        .await
4860    }
4861
4862    #[instrument(skip(self))]
4863    async fn list_execution_events_responses(
4864        &self,
4865        execution_id: &ExecutionId,
4866        req_since: &Version,
4867        req_max_length: VersionType,
4868        req_include_backtrace_id: bool,
4869        resp_pagination: Pagination<u32>,
4870    ) -> Result<ExecutionWithStateRequestsResponses, DbErrorRead> {
4871        let execution_id = execution_id.clone();
4872        let req_since = req_since.0;
4873        self.transaction(
4874            move |tx| {
4875                let combined_state = Self::get_combined_state(tx, &execution_id)?;
4876                let events = Self::list_execution_events(
4877                    tx,
4878                    &execution_id,
4879                    Pagination::NewerThan {
4880                        length: req_max_length
4881                            .try_into()
4882                            .expect("req_max_length fits in u16"),
4883                        cursor: req_since,
4884                        including_cursor: true,
4885                    },
4886                    req_include_backtrace_id,
4887                )?;
4888                let responses = Self::list_responses(tx, &execution_id, Some(resp_pagination))?;
4889                let max_version = Self::get_max_version(tx, &execution_id)?;
4890                let max_cursor = Self::get_max_response_cursor(tx, &execution_id)?;
4891                Ok(ExecutionWithStateRequestsResponses {
4892                    execution_with_state: combined_state.execution_with_state,
4893                    events,
4894                    responses,
4895                    max_version,
4896                    max_cursor,
4897                })
4898            },
4899            TxType::Other, // read only
4900            "list_execution_events_responses",
4901        )
4902        .await
4903    }
4904
4905    #[instrument(skip(self))]
4906    async fn upgrade_execution_component(
4907        &self,
4908        execution_id: &ExecutionId,
4909        old: &ComponentDigest,
4910        new: &ComponentDigest,
4911        reason: ComponentUpgradeReason,
4912    ) -> Result<(), DbErrorWrite> {
4913        let execution_id = execution_id.clone();
4914        let old = old.clone();
4915        let new = new.clone();
4916        self.transaction(
4917            move |tx| {
4918                Self::upgrade_execution_component_single_write(
4919                    tx,
4920                    &execution_id,
4921                    &old,
4922                    &new,
4923                    reason.clone(),
4924                )
4925            },
4926            TxType::Other, // single write
4927            "upgrade_execution_component",
4928        )
4929        .await
4930    }
4931
4932    #[instrument(skip(self))]
4933    async fn list_logs(
4934        &self,
4935        execution_id: &ExecutionId,
4936        show_derived: bool,
4937        filter: LogFilter,
4938        pagination: Pagination<LogCursor>,
4939    ) -> Result<ListLogsResponse, DbErrorRead> {
4940        let execution_id = execution_id.clone();
4941        self.transaction(
4942            move |tx| Self::list_logs_tx(tx, &execution_id, show_derived, &filter, &pagination),
4943            TxType::Other, // read only
4944            "list_logs",
4945        )
4946        .await
4947    }
4948
4949    #[instrument(skip(self))]
4950    async fn list_deployment_states(
4951        &self,
4952        current_time: DateTime<Utc>,
4953        pagination: Pagination<Option<DeploymentId>>,
4954        include_deployment_toml: bool,
4955        execution_counts: DeploymentExecutionCounts,
4956    ) -> Result<Vec<DeploymentState>, DbErrorRead> {
4957        self.transaction(
4958            move |tx| {
4959                Self::list_deployment_states(
4960                    tx,
4961                    current_time,
4962                    pagination,
4963                    include_deployment_toml,
4964                    execution_counts,
4965                )
4966            },
4967            TxType::Other, // read only
4968            "list_deployment_states",
4969        )
4970        .await
4971    }
4972
4973    #[instrument(skip(self))]
4974    async fn insert_deployment(&self, record: DeploymentRecord) -> Result<(), DbErrorWrite> {
4975        self.transaction(
4976            move |tx| Self::insert_deployment_tx(tx, &record),
4977            TxType::MultipleWrites,
4978            "insert_deployment",
4979        )
4980        .await
4981    }
4982
4983    async fn insert_deployment_with_components(
4984        &self,
4985        record: DeploymentRecord,
4986        component_metadata: Vec<ComponentMetadataRecord>,
4987        deployment_components: Vec<DeploymentComponentRecord>,
4988    ) -> Result<(), DbErrorWrite> {
4989        let deployment_id = record.deployment_id;
4990        self.transaction(
4991            move |tx| {
4992                // Parent row first so the t_deployment_component FK is satisfied within the tx.
4993                Self::insert_deployment_tx(tx, &record)?;
4994                Self::upsert_component_metadata_tx(tx, &component_metadata)?;
4995                Self::insert_deployment_components_tx(tx, deployment_id, &deployment_components)
4996            },
4997            TxType::MultipleWrites,
4998            "insert_deployment_with_components",
4999        )
5000        .await
5001    }
5002
5003    #[instrument(skip(self))]
5004    async fn missing_digests(
5005        &self,
5006        deployment_id: DeploymentId,
5007    ) -> Result<Vec<ContentDigest>, DbErrorRead> {
5008        self.transaction(
5009            move |tx| Self::missing_digests_tx(tx, deployment_id),
5010            TxType::Other,
5011            "missing_digests",
5012        )
5013        .await
5014    }
5015
5016    #[instrument(skip(self))]
5017    async fn list_deployment_files(
5018        &self,
5019        deployment_id: DeploymentId,
5020    ) -> Result<Vec<DeploymentFileRecord>, DbErrorRead> {
5021        self.transaction(
5022            move |tx| Self::list_deployment_files_tx(tx, deployment_id),
5023            TxType::Other,
5024            "list_deployment_files",
5025        )
5026        .await
5027    }
5028
5029    #[instrument(skip(self))]
5030    async fn gc_orphan_files(&self) -> Result<u64, DbErrorWrite> {
5031        self.transaction(
5032            move |tx| {
5033                let deleted = tx
5034                    .execute(
5035                        "DELETE FROM t_file WHERE digest NOT IN \
5036                         (SELECT digest FROM t_deployment_file \
5037                          UNION SELECT digest FROM t_component_source)",
5038                        [],
5039                    )
5040                    .map_err(RusqliteError::from)?;
5041                Ok(deleted as u64)
5042            },
5043            TxType::MultipleWrites,
5044            "gc_orphan_files",
5045        )
5046        .await
5047    }
5048
5049    #[instrument(skip(self))]
5050    async fn activate_deployment(
5051        &self,
5052        deployment_id: DeploymentId,
5053        now: DateTime<Utc>,
5054    ) -> Result<(), DbErrorWrite> {
5055        self.transaction(
5056            move |tx| Self::activate_deployment_tx(tx, deployment_id, now),
5057            TxType::MultipleWrites,
5058            "activate_deployment",
5059        )
5060        .await
5061    }
5062
5063    async fn enqueue_deployment(
5064        &self,
5065        deployment_id: DeploymentId,
5066    ) -> Result<EnqueueOutcome, DbErrorWrite> {
5067        self.transaction(
5068            move |tx| Self::enqueue_deployment_tx(tx, deployment_id),
5069            TxType::MultipleWrites,
5070            "enqueue_deployment",
5071        )
5072        .await
5073    }
5074
5075    #[instrument(skip(self))]
5076    async fn get_deployment(
5077        &self,
5078        deployment_id: DeploymentId,
5079    ) -> Result<Option<DeploymentRecord>, DbErrorRead> {
5080        self.transaction(
5081            move |tx| Self::get_deployment_tx(tx, deployment_id),
5082            TxType::Other,
5083            "get_deployment",
5084        )
5085        .await
5086    }
5087
5088    #[cfg(feature = "test")]
5089    #[instrument(skip(self))]
5090    async fn get_active_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
5091        self.transaction(
5092            move |tx| Self::get_active_deployment_tx(tx),
5093            TxType::Other,
5094            "get_active_deployment",
5095        )
5096        .await
5097    }
5098
5099    #[instrument(skip(self))]
5100    async fn get_current_deployment(&self) -> Result<Option<DeploymentRecord>, DbErrorRead> {
5101        self.transaction(
5102            move |tx| {
5103                let Some(record) = tx
5104                    .query_row(
5105                    "SELECT deployment_id, description, digest, created_at, last_active_at, status, deployment_toml, obelisk_version, created_by \
5106                     FROM t_deployment WHERE status IN ('enqueued', 'active') \
5107                     ORDER BY CASE status WHEN 'enqueued' THEN 0 ELSE 1 END LIMIT 1",
5108                        [],
5109                        deployment_record_from_row,
5110                    )
5111                    .optional()
5112                    .map_err(|e| DbErrorRead::from(RusqliteError::from(e)))?
5113                else {
5114                    return Ok(None);
5115                };
5116                Self::with_deployment_files_tx(tx, record).map(Some)
5117            },
5118            TxType::Other,
5119            "get_current_deployment",
5120        )
5121        .await
5122    }
5123
5124    #[instrument(skip(self))]
5125    async fn list_deployments(
5126        &self,
5127        pagination: Pagination<Option<DeploymentId>>,
5128    ) -> Result<Vec<DeploymentRecord>, DbErrorRead> {
5129        self.transaction(
5130            move |tx| Self::list_deployments_tx(tx, pagination),
5131            TxType::Other,
5132            "list_deployments",
5133        )
5134        .await
5135    }
5136
5137    #[instrument(skip(self))]
5138    async fn pause_execution(
5139        &self,
5140        execution_id: &ExecutionId,
5141        paused_at: DateTime<Utc>,
5142    ) -> Result<AppendResponse, DbErrorWrite> {
5143        let execution_id = execution_id.clone();
5144        self.transaction(
5145            move |tx| SqlitePool::pause_execution(tx, &execution_id, paused_at),
5146            TxType::MultipleWrites,
5147            "pause_execution",
5148        )
5149        .await
5150    }
5151
5152    #[instrument(skip(self))]
5153    async fn unpause_execution(
5154        &self,
5155        execution_id: &ExecutionId,
5156        unpaused_at: DateTime<Utc>,
5157    ) -> Result<AppendResponse, DbErrorWrite> {
5158        let execution_id = execution_id.clone();
5159        self.transaction(
5160            move |tx| SqlitePool::unpause_execution(tx, &execution_id, unpaused_at),
5161            TxType::MultipleWrites,
5162            "unpause_execution",
5163        )
5164        .await
5165    }
5166
5167    #[instrument(skip(self))]
5168    async fn pause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite> {
5169        let delay_id = delay_id.clone();
5170        self.transaction(
5171            move |tx| {
5172                let (execution_id, join_set_id) = delay_id.split_to_parts();
5173                let rows_modified = tx.execute(
5174                    "UPDATE t_delay SET is_paused = 1 \
5175                     WHERE execution_id = :execution_id AND join_set_id = :join_set_id AND delay_id = :delay_id",
5176                    named_params! {
5177                        ":execution_id": execution_id.to_string(),
5178                        ":join_set_id": join_set_id.to_string(),
5179                        ":delay_id": delay_id.to_string(),
5180                    },
5181                )?;
5182                if rows_modified == 0 {
5183                    return Err(DbErrorWrite::NotFound);
5184                }
5185                Ok(())
5186            },
5187            TxType::Other,
5188            "pause_delay",
5189        )
5190        .await
5191    }
5192
5193    #[instrument(skip(self))]
5194    async fn unpause_delay(&self, delay_id: &DelayId) -> Result<(), DbErrorWrite> {
5195        let delay_id = delay_id.clone();
5196        self.transaction(
5197            move |tx| {
5198                let (execution_id, join_set_id) = delay_id.split_to_parts();
5199                let rows_modified = tx.execute(
5200                    "UPDATE t_delay SET is_paused = 0 \
5201                     WHERE execution_id = :execution_id AND join_set_id = :join_set_id AND delay_id = :delay_id",
5202                    named_params! {
5203                        ":execution_id": execution_id.to_string(),
5204                        ":join_set_id": join_set_id.to_string(),
5205                        ":delay_id": delay_id.to_string(),
5206                    },
5207                )?;
5208                if rows_modified == 0 {
5209                    return Err(DbErrorWrite::NotFound);
5210                }
5211                Ok(())
5212            },
5213            TxType::Other,
5214            "unpause_delay",
5215        )
5216        .await
5217    }
5218}
5219
5220#[async_trait]
5221impl Cas for SqlitePool {
5222    async fn read_blob(&self, digest: &ContentDigest) -> Result<Option<Vec<u8>>, CasError> {
5223        let digest = digest.clone();
5224        self.transaction(
5225            move |tx| Self::get_file_tx(tx, &digest),
5226            TxType::Other,
5227            "cas_read_blob",
5228        )
5229        .await
5230        .map_err(|err| CasError::Uncategorized(err.to_string()))
5231    }
5232
5233    async fn write_blob(&self, content: &[u8]) -> Result<ContentDigest, CasError> {
5234        let digest = Self::compute_file_digest(content);
5235        let content = content.to_vec();
5236        {
5237            let digest = digest.clone();
5238            self.transaction(
5239                move |tx| Self::upload_file_tx(tx, &digest, &content),
5240                TxType::MultipleWrites,
5241                "cas_write_blob",
5242            )
5243            .await
5244            .map_err(|err| CasError::Uncategorized(err.to_string()))?;
5245        }
5246        Ok(digest)
5247    }
5248
5249    async fn contains_blob(&self, digest: &ContentDigest) -> Result<bool, CasError> {
5250        Ok(self.read_blob(digest).await?.is_some())
5251    }
5252}
5253
5254#[async_trait]
5255impl DbConnection for SqlitePool {
5256    #[instrument(level = Level::DEBUG, skip_all, fields(execution_id = %req.execution_id))]
5257    async fn create(&self, req: CreateRequest) -> Result<AppendResponse, DbErrorWrite> {
5258        debug!("create");
5259        trace!(?req, "create");
5260        let created_at = req.created_at;
5261        let (version, notifier) = self
5262            .transaction(
5263                move |tx| Self::create_inner(tx, req.clone()),
5264                TxType::MultipleWrites,
5265                "create",
5266            )
5267            .await?;
5268        self.notify_all(vec![notifier], created_at);
5269        Ok(version)
5270    }
5271
5272    #[instrument(level = Level::DEBUG, skip(self))]
5273    async fn get(
5274        &self,
5275        execution_id: &ExecutionId,
5276    ) -> Result<concepts::storage::ExecutionLog, DbErrorRead> {
5277        trace!("get");
5278        let execution_id = execution_id.clone();
5279        self.transaction(
5280            move |tx| Self::get(tx, &execution_id),
5281            TxType::Other, // read only
5282            "get",
5283        )
5284        .await
5285    }
5286
5287    #[instrument(level = Level::DEBUG, skip(self))]
5288    async fn get_cancelling(&self, batch_size: u32) -> Result<Vec<ExecutionId>, DbErrorRead> {
5289        self.transaction(
5290            move |tx| {
5291                let mut stmt = tx.prepare(
5292                    "SELECT execution_id FROM t_state WHERE lifecycle = :lifecycle \
5293                     ORDER BY created_at LIMIT :batch_size",
5294                )?;
5295                let rows = stmt
5296                    .query_map(
5297                        named_params! {
5298                            ":lifecycle": LIFECYCLE_CANCELLING,
5299                            ":batch_size": batch_size,
5300                        },
5301                        |row| row.get::<_, String>("execution_id"),
5302                    )?
5303                    .collect::<Result<Vec<_>, _>>()?;
5304                rows.into_iter()
5305                    .map(|id| {
5306                        ExecutionId::from_str(&id)
5307                            .map_err(|_| consistency_rusqlite("invalid t_state.execution_id"))
5308                            .map_err(DbErrorRead::from)
5309                    })
5310                    .collect()
5311            },
5312            TxType::Other, // read only
5313            "get_cancelling",
5314        )
5315        .await
5316    }
5317
5318    #[instrument(level = Level::DEBUG, skip(self, batch))]
5319    async fn append_batch(
5320        &self,
5321        current_time: DateTime<Utc>,
5322        batch: Vec<AppendRequest>,
5323        execution_id: ExecutionId,
5324        version: Version,
5325    ) -> Result<AppendBatchResponse, DbErrorWrite> {
5326        debug!("append_batch");
5327        trace!(?batch, "append_batch");
5328        assert!(!batch.is_empty(), "Empty batch request");
5329
5330        let (version, notifier) = self
5331            .transaction(
5332                move |tx| {
5333                    let mut version = version.clone();
5334                    let mut notifier = None;
5335                    for append_request in &batch {
5336                        let (v, n) =
5337                            Self::append(tx, &execution_id, append_request.clone(), version)?;
5338                        version = v;
5339                        notifier = Some(n);
5340                    }
5341                    Ok::<_, DbErrorWrite>((
5342                        version,
5343                        notifier.expect("checked that the batch is not empty"),
5344                    ))
5345                },
5346                TxType::MultipleWrites,
5347                "append_batch",
5348            )
5349            .await?;
5350
5351        self.notify_all(vec![notifier], current_time);
5352        Ok(version)
5353    }
5354
5355    #[instrument(level = Level::DEBUG, skip_all, fields(%execution_id, %version))]
5356    async fn append_batch_create_new_execution(
5357        &self,
5358        current_time: DateTime<Utc>,
5359        batch: Vec<AppendRequest>,
5360        execution_id: ExecutionId,
5361        version: Version,
5362        child_req: Vec<CreateRequest>,
5363        backtraces: Vec<BacktraceInfo>,
5364    ) -> Result<AppendBatchResponse, DbErrorWrite> {
5365        debug!("append_batch_create_new_execution");
5366        trace!(?batch, ?child_req, "append_batch_create_new_execution");
5367        assert!(!batch.is_empty(), "Empty batch request");
5368
5369        let (version, notifiers) = self
5370            .transaction(
5371                move |tx| {
5372                    let mut notifier = None;
5373                    let mut version = version.clone();
5374                    for append_request in &batch {
5375                        let (v, n) =
5376                            Self::append(tx, &execution_id, append_request.clone(), version)?;
5377                        version = v;
5378                        notifier = Some(n);
5379                    }
5380                    let mut notifiers = Vec::new();
5381                    notifiers.push(notifier.expect("checked that the batch is not empty"));
5382
5383                    for child_req in &child_req {
5384                        let (_, notifier) = Self::create_inner(tx, child_req.clone())?;
5385                        notifiers.push(notifier);
5386                    }
5387                    Ok::<_, DbErrorWrite>((version, notifiers))
5388                },
5389                TxType::MultipleWrites,
5390                "append_batch_create_new_execution_inner",
5391            )
5392            .await?;
5393        self.notify_all(notifiers, current_time);
5394        self.transaction_fire_forget(
5395            move |tx| {
5396                for backtrace in &backtraces {
5397                    Self::append_backtrace(tx, backtrace)?;
5398                }
5399                Ok::<_, DbErrorWrite>(())
5400            },
5401            "append_batch_create_new_execution_append_backtrace",
5402        )
5403        .await;
5404        Ok(version)
5405    }
5406
5407    // Supports only one subscriber per execution id.
5408    // A new call will overwrite the old subscriber, the old one will end
5409    // with a timeout, which is fine.
5410    #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
5411    async fn subscribe_to_next_responses(
5412        &self,
5413        execution_id: &ExecutionId,
5414        last_response: ResponseCursor,
5415        timeout_fut: Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>,
5416    ) -> Result<Vec<ResponseWithCursor>, DbErrorReadWithTimeout> {
5417        debug!("next_responses");
5418        let unique_tag: u64 = rand::random();
5419        let execution_id = execution_id.clone();
5420
5421        let cleanup = || {
5422            let mut guard = self.0.response_subscribers.lock().unwrap();
5423            match guard.remove(&execution_id) {
5424                Some((_, tag)) if tag == unique_tag => {} // Cleanup OK.
5425                Some(other) => {
5426                    // Reinsert foreign sender.
5427                    guard.insert(execution_id.clone(), other);
5428                }
5429                None => {} // Value was replaced and cleaned up already, or notification was sent.
5430            }
5431        };
5432
5433        let response_subscribers = self.0.response_subscribers.clone();
5434        let resp_or_receiver = {
5435            let execution_id = execution_id.clone();
5436            self.transaction(
5437                move |tx| {
5438                    let responses = Self::get_responses_after(tx, &execution_id, last_response)?;
5439                    if responses.is_empty() {
5440                        // cannot race as we have the transaction write lock
5441                        let (sender, receiver) = oneshot::channel();
5442                        response_subscribers
5443                            .lock()
5444                            .unwrap()
5445                            .insert(execution_id.clone(), (sender, unique_tag));
5446                        Ok::<_, DbErrorReadWithTimeout>(itertools::Either::Right(receiver))
5447                    } else {
5448                        Ok(itertools::Either::Left(responses))
5449                    }
5450                },
5451                TxType::Other, // read only
5452                "subscribe_to_next_responses",
5453            )
5454            .await
5455        }
5456        .inspect_err(|_| {
5457            cleanup();
5458        })?;
5459        match resp_or_receiver {
5460            itertools::Either::Left(resp) => Ok(resp), // no need for cleanup
5461            itertools::Either::Right(receiver) => {
5462                let woken = tokio::select! {
5463                    resp = receiver => match resp {
5464                        Ok(()) => Ok(()),
5465                        Err(_) => Err(DbErrorReadWithTimeout::from(DbErrorGeneric::Close)),
5466                    },
5467                    outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
5468                };
5469                cleanup();
5470                woken?;
5471                // Re-fetch from the `last_response` watermark.
5472                let execution_id = execution_id.clone();
5473                self.transaction(
5474                    move |tx| {
5475                        Self::get_responses_after(tx, &execution_id, last_response)
5476                            .map_err(DbErrorReadWithTimeout::from)
5477                    },
5478                    TxType::Other, // read only
5479                    "subscribe_to_next_responses_refetch",
5480                )
5481                .await
5482            }
5483        }
5484    }
5485
5486    // Supports multiple subscribers.
5487    #[instrument(level = Level::DEBUG, skip(self, timeout_fut))]
5488    async fn wait_for_finished_result(
5489        &self,
5490        execution_id: &ExecutionId,
5491        timeout_fut: Option<Pin<Box<dyn Future<Output = TimeoutOutcome> + Send>>>,
5492    ) -> Result<SupportedFunctionReturnValue, DbErrorReadWithTimeout> {
5493        let unique_tag: u64 = rand::random();
5494        let execution_id = execution_id.clone();
5495        let execution_finished_subscription = self.0.execution_finished_subscribers.clone();
5496
5497        let cleanup = || {
5498            let mut guard = self.0.execution_finished_subscribers.lock().unwrap();
5499            if let Some(subscribers) = guard.get_mut(&execution_id) {
5500                subscribers.remove(&unique_tag);
5501            }
5502        };
5503
5504        let resp_or_receiver = {
5505            let execution_id = execution_id.clone();
5506            self.transaction(move |tx| {
5507                let pending_state =
5508                    Self::get_combined_state(tx, &execution_id)?.execution_with_state.pending_state;
5509                if let PendingState::Finished(finished) = pending_state {
5510                    let event =
5511                        Self::get_execution_event(tx, &execution_id, finished.version)?;
5512                    if let ExecutionRequest::Finished { retval, ..} = event.event {
5513                        Ok(itertools::Either::Left(retval))
5514                    } else {
5515                        error!("Mismatch, expected Finished row: {event:?} based on t_state {finished}");
5516                        Err(DbErrorReadWithTimeout::from(consistency_db_err(
5517                            "cannot get finished event based on t_state version"
5518                        )))
5519                    }
5520                } else {
5521                    // Cannot race with the notifier as we have the transaction write lock:
5522                    // Either the finished event was appended previously, thus `itertools::Either::Left` was selected,
5523                    // or we end up here. If this tx fails, the cleanup will remove this entry.
5524                    let (sender, receiver) = oneshot::channel();
5525                    let mut guard = execution_finished_subscription.lock().unwrap();
5526                    guard.entry(execution_id.clone()).or_default().insert(unique_tag, sender);
5527                    Ok(itertools::Either::Right(receiver))
5528                }
5529            },
5530            TxType::Other, // read only
5531            "wait_for_finished_result")
5532            .await
5533        }
5534        .inspect_err(|_| {
5535            // This cleanup can race with the notification sender, since both are running after a transaction was finished.
5536            // If the notification sender wins, it removes our oneshot sender and puts a value in it, cleanup will not find the unique tag.
5537            // If cleanup wins, it simply removes the oneshot sender.
5538            cleanup();
5539        })?;
5540
5541        let timeout_fut = timeout_fut.unwrap_or_else(|| Box::pin(std::future::pending()));
5542        match resp_or_receiver {
5543            itertools::Either::Left(resp) => Ok(resp), // no need for cleanup
5544            itertools::Either::Right(receiver) => {
5545                let res = tokio::select! {
5546                    resp = receiver => {
5547                        match resp {
5548                            Ok(retval) => Ok(retval),
5549                            Err(_recv_err) => Err(DbErrorGeneric::Close.into())
5550                        }
5551                    }
5552                    outcome = timeout_fut => Err(DbErrorReadWithTimeout::Timeout(outcome)),
5553                };
5554                cleanup();
5555                res
5556            }
5557        }
5558    }
5559
5560    #[instrument(level = Level::DEBUG, skip_all, fields(%join_set_id, %execution_id))]
5561    async fn append_delay_response(
5562        &self,
5563        created_at: DateTime<Utc>,
5564        execution_id: ExecutionId,
5565        join_set_id: JoinSetId,
5566        delay_id: DelayId,
5567        result: Result<(), ()>,
5568    ) -> Result<AppendDelayResponseOutcome, DbErrorWrite> {
5569        debug!("append_delay_response");
5570        let event = JoinSetResponseEventOuter {
5571            created_at,
5572            event: JoinSetResponseEvent {
5573                join_set_id,
5574                event: JoinSetResponse::DelayFinished {
5575                    delay_id: delay_id.clone(),
5576                    result,
5577                },
5578            },
5579        };
5580        let res = self
5581            .transaction(
5582                {
5583                    let execution_id = execution_id.clone();
5584                    move |tx| Self::append_response(tx, &execution_id, event.clone())
5585                },
5586                TxType::MultipleWrites,
5587                "append_delay_response",
5588            )
5589            .await;
5590        match res {
5591            Ok(notifier) => {
5592                self.notify_all(vec![notifier], created_at);
5593                Ok(AppendDelayResponseOutcome::Success)
5594            }
5595            Err(DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::Conflict)) => {
5596                let delay_success = self
5597                    .transaction(
5598                        move |tx| Self::get_delay_response(tx, &execution_id, &delay_id),
5599                        TxType::Other, // read only
5600                        "get_delay_response",
5601                    )
5602                    .await?;
5603                match delay_success {
5604                    Some(true) => Ok(AppendDelayResponseOutcome::AlreadyFinished),
5605                    Some(false) => Ok(AppendDelayResponseOutcome::AlreadyCancelled),
5606                    None => Err(DbErrorWrite::Generic(DbErrorGeneric::Uncategorized {
5607                        reason: "insert failed yet select did not find the response".into(),
5608                        context: SpanTrace::capture(),
5609                        source: None,
5610                        loc: Location::caller(),
5611                    })),
5612                }
5613            }
5614            Err(err) => Err(err),
5615        }
5616    }
5617
5618    #[instrument(level = Level::DEBUG, skip_all)]
5619    async fn append_backtrace(&self, append: BacktraceInfo) -> Result<(), DbErrorWrite> {
5620        trace!("append_backtrace");
5621        self.transaction_fire_forget(
5622            move |tx| Self::append_backtrace(tx, &append),
5623            "append_backtrace",
5624        )
5625        .await;
5626        Ok(())
5627    }
5628
5629    #[instrument(level = Level::DEBUG, skip_all)]
5630    async fn append_backtrace_batch(&self, batch: Vec<BacktraceInfo>) -> Result<(), DbErrorWrite> {
5631        trace!("append_backtrace_batch");
5632        self.transaction_fire_forget(
5633            move |tx| {
5634                for append in &batch {
5635                    Self::append_backtrace(tx, append)?;
5636                }
5637                Ok::<_, DbErrorWrite>(())
5638            },
5639            "append_backtrace_batch",
5640        )
5641        .await;
5642        Ok(())
5643    }
5644
5645    #[instrument(level = Level::DEBUG, skip_all)]
5646    async fn append_log(&self, row: LogInfoAppendRow) -> Result<(), DbErrorWrite> {
5647        trace!("append_log");
5648        self.transaction_fire_forget(move |tx| Self::append_log(tx, &row), "append_log")
5649            .await;
5650        Ok(())
5651    }
5652
5653    #[instrument(level = Level::DEBUG, skip_all)]
5654    async fn append_log_batch(&self, batch: &[LogInfoAppendRow]) -> Result<(), DbErrorWrite> {
5655        trace!("append_log_batch");
5656        let batch = Vec::from(batch);
5657        self.transaction_fire_forget(
5658            move |tx| {
5659                for row in &batch {
5660                    Self::append_log(tx, row)?;
5661                }
5662                Ok::<_, DbErrorWrite>(())
5663            },
5664            "append_log_batch",
5665        )
5666        .await;
5667        Ok(())
5668    }
5669
5670    /// Get currently expired delays and locks.
5671    #[instrument(level = Level::TRACE, skip(self))]
5672    async fn get_expired_timers(
5673        &self,
5674        at: DateTime<Utc>,
5675    ) -> Result<Vec<ExpiredTimer>, DbErrorGeneric> {
5676        self.transaction(
5677            move |conn| {
5678                let mut expired_timers = conn.prepare(
5679                    "SELECT execution_id, join_set_id, delay_id FROM t_delay WHERE expires_at <= :at AND NOT is_paused",
5680                )?
5681                .query_map(
5682                        named_params! {
5683                            ":at": at,
5684                        },
5685                        |row| {
5686                            let execution_id = row.get("execution_id")?;
5687                            let join_set_id = row.get::<_, JoinSetId>("join_set_id")?;
5688                            let delay_id = row.get::<_, DelayId>("delay_id")?;
5689                            let delay = ExpiredDelay { execution_id, join_set_id, delay_id };
5690                            Ok(ExpiredTimer::Delay(delay))
5691                        },
5692                    )?
5693                    .collect::<Result<Vec<_>, _>>()?;
5694                // Extend with expired locks
5695                let expired = conn.prepare(&format!(r#"
5696                    SELECT execution_id, last_lock_version, corresponding_version, intermittent_event_count, max_retries, retry_exp_backoff_millis,
5697                    executor_id, run_id, lifecycle
5698                    FROM t_state
5699                    WHERE pending_expires_finished <= :at AND state = "{STATE_LOCKED}"
5700                    "#
5701                )
5702                )?
5703                .query_map(
5704                        named_params! {
5705                            ":at": at,
5706                        },
5707                        |row| {
5708                            let execution_id = row.get("execution_id")?;
5709                            let lifecycle: String = row.get("lifecycle")?;
5710                            if lifecycle != LIFECYCLE_ACTIVE {
5711                                error!(%execution_id, %lifecycle, "encountered invalid non-active locked execution while scanning expired locks");
5712                                return Ok(None);
5713                            }
5714                            let locked_at_version = Version::new(row.get("last_lock_version")?);
5715                            let next_version = Version::new(row.get("corresponding_version")?).increment();
5716                            let intermittent_event_count = row.get("intermittent_event_count")?;
5717                            let max_retries = row.get("max_retries")?;
5718                            let retry_exp_backoff_millis = u64::from(row.get::<_, u32>("retry_exp_backoff_millis")?);
5719                            let executor_id = row.get("executor_id")?;
5720                            let run_id = row.get("run_id")?;
5721                            let lock = ExpiredLock {
5722                                execution_id,
5723                                locked_at_version,
5724                                next_version,
5725                                intermittent_event_count,
5726                                max_retries,
5727                                retry_exp_backoff: Duration::from_millis(retry_exp_backoff_millis),
5728                                locked_by: LockedBy { executor_id, run_id },
5729                            };
5730                            Ok(Some(ExpiredTimer::Lock(lock)))
5731                        }
5732                    )?
5733                    .collect::<Result<Vec<_>, _>>()?;
5734                expired_timers.extend(expired.into_iter().flatten());
5735                if !expired_timers.is_empty() {
5736                    debug!("get_expired_timers found {expired_timers:?}");
5737                }
5738                Ok(expired_timers)
5739            },
5740            TxType::Other, // read only
5741            "get_expired_timers"
5742        )
5743        .await
5744        .map_err(to_generic_error)
5745    }
5746
5747    async fn get_execution_event(
5748        &self,
5749        execution_id: &ExecutionId,
5750        version: &Version,
5751    ) -> Result<ExecutionEvent, DbErrorRead> {
5752        let version = version.0;
5753        let execution_id = execution_id.clone();
5754        self.transaction(
5755            move |tx| Self::get_execution_event(tx, &execution_id, version),
5756            TxType::Other, // read only
5757            "get_execution_event",
5758        )
5759        .await
5760    }
5761
5762    #[instrument(level = Level::DEBUG, skip_all)]
5763    async fn upsert_stub_response(
5764        &self,
5765        execution_id: ExecutionIdDerived,
5766        version: Version,
5767        req: AppendRequest,
5768        response: AppendResponseToExecution,
5769        current_time: DateTime<Utc>,
5770    ) -> Result<(), DbErrorStubResponse> {
5771        debug!("upsert_stub_response");
5772        #[cfg(debug_assertions)]
5773        {
5774            let (expected_parent, expected_join_set) = execution_id.split_to_parts();
5775            debug_assert_eq!(expected_parent, response.parent_execution_id);
5776            debug_assert_eq!(expected_join_set, response.join_set_id);
5777            debug_assert_eq!(execution_id, response.child_execution_id);
5778        }
5779        let execution_id = ExecutionId::Derived(execution_id);
5780        let expected_retval = response.result.clone();
5781        let notifiers = self
5782            .transaction(
5783                move |tx| {
5784                    let version_raw = version.0;
5785                    match Self::append(tx, &execution_id, req.clone(), version.clone()) {
5786                        Ok((_next_version, notifier_of_child)) => {
5787                            let pending_at_parent = Self::append_response(
5788                                tx,
5789                                &response.parent_execution_id,
5790                                JoinSetResponseEventOuter {
5791                                    created_at: response.created_at,
5792                                    event: JoinSetResponseEvent {
5793                                        join_set_id: response.join_set_id.clone(),
5794                                        event: JoinSetResponse::ChildExecutionFinished {
5795                                            child_execution_id: response.child_execution_id.clone(),
5796                                            finished_version: response.finished_version.clone(),
5797                                            result: response.result.clone(),
5798                                        },
5799                                    },
5800                                },
5801                            )
5802                            .map_err(DbErrorStubResponse::Write)?;
5803                            Ok::<_, DbErrorStubResponse>(Some(vec![
5804                                notifier_of_child,
5805                                pending_at_parent,
5806                            ]))
5807                        }
5808                        Err(DbErrorWrite::NonRetriable(
5809                            DbErrorWriteNonRetriable::AlreadyFinished,
5810                        )) => {
5811                            // Idempotency check: read the existing event and compare retval.
5812                            let found = Self::get_execution_event(tx, &execution_id, version_raw)
5813                                .map_err(|_| DbErrorStubResponse::StubConflict)?;
5814                            match found.event {
5815                                ExecutionRequest::Finished { retval, .. }
5816                                    if retval == expected_retval =>
5817                                {
5818                                    Ok(None)
5819                                }
5820                                _ => Err(DbErrorStubResponse::StubConflict),
5821                            }
5822                        }
5823                        Err(other) => Err(DbErrorStubResponse::Write(other)),
5824                    }
5825                },
5826                TxType::MultipleWrites,
5827                "upsert_stub_response",
5828            )
5829            .await?;
5830        if let Some(notifiers) = notifiers {
5831            self.notify_all(notifiers, current_time);
5832        }
5833        Ok(())
5834    }
5835
5836    async fn get_pending_state(
5837        &self,
5838        execution_id: &ExecutionId,
5839    ) -> Result<ExecutionWithState, DbErrorRead> {
5840        let execution_id = execution_id.clone();
5841        Ok(self
5842            .transaction(
5843                move |tx| Self::get_combined_state(tx, &execution_id),
5844                TxType::Other, // read only
5845                "get_pending_state",
5846            )
5847            .await?
5848            .execution_with_state)
5849    }
5850}
5851
5852#[cfg(feature = "test")]
5853#[async_trait]
5854impl concepts::storage::DbConnectionTest for SqlitePool {
5855    #[instrument(level = Level::DEBUG, skip(self, response_event), fields(join_set_id = %response_event.join_set_id))]
5856    async fn append_response(
5857        &self,
5858        created_at: DateTime<Utc>,
5859        execution_id: ExecutionId,
5860        response_event: JoinSetResponseEvent,
5861    ) -> Result<(), DbErrorWrite> {
5862        debug!("append_response");
5863        let event = JoinSetResponseEventOuter {
5864            created_at,
5865            event: response_event,
5866        };
5867        let notifier = self
5868            .transaction(
5869                move |tx| Self::append_response(tx, &execution_id, event.clone()),
5870                TxType::Other, // read only
5871                "append_response",
5872            )
5873            .await?;
5874        self.notify_all(vec![notifier], created_at);
5875        Ok(())
5876    }
5877}
5878
5879#[cfg(any(test, feature = "tempfile"))]
5880pub mod tempfile {
5881    use super::{SqliteConfig, SqlitePool};
5882    use tempfile::NamedTempFile;
5883
5884    pub async fn sqlite_pool() -> (SqlitePool, Option<NamedTempFile>) {
5885        if let Ok(path) = std::env::var("SQLITE_FILE") {
5886            (
5887                SqlitePool::new(path, SqliteConfig::default())
5888                    .await
5889                    .unwrap(),
5890                None,
5891            )
5892        } else {
5893            let file = NamedTempFile::new().unwrap();
5894            let path = file.path();
5895            (
5896                SqlitePool::new(path, SqliteConfig::default())
5897                    .await
5898                    .unwrap(),
5899                Some(file),
5900            )
5901        }
5902    }
5903}
5904
5905#[cfg(test)]
5906mod tests {
5907    use crate::sqlite_dao::{SqlitePool, TxType, tempfile::sqlite_pool};
5908    use assert_matches::assert_matches;
5909    use chrono::DateTime;
5910    use concepts::{
5911        ComponentId, FunctionFqn, Params,
5912        prefixed_ulid::{DEPLOYMENT_ID_DUMMY, EXECUTION_ID_DUMMY},
5913        storage::{CreateRequest, DbErrorWrite, DbErrorWriteNonRetriable, DbPoolCloseable},
5914    };
5915    use rusqlite::named_params;
5916
5917    const SOME_FFQN: FunctionFqn = FunctionFqn::new_static("ns:pkg/ifc", "fn");
5918
5919    #[tokio::test]
5920    async fn failing_ltx_should_be_rolled_back() -> Result<(), DbErrorWrite> {
5921        let created_at = DateTime::from_timestamp_nanos(0);
5922        let (pool, _guard) = sqlite_pool().await;
5923        pool.transaction(
5924            move |tx| {
5925                let req = CreateRequest {
5926                    created_at,
5927                    execution_id: EXECUTION_ID_DUMMY,
5928                    ffqn: SOME_FFQN,
5929                    params: Params::empty(),
5930                    parent: None,
5931                    metadata: concepts::ExecutionMetadata::empty(),
5932                    scheduled_at: created_at,
5933                    component_id: ComponentId::dummy_activity(),
5934                    deployment_id: DEPLOYMENT_ID_DUMMY,
5935                    scheduled_by: None,
5936                    paused: false,
5937                };
5938                SqlitePool::create_inner(tx, req)?;
5939                SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at)?;
5940                Ok::<_, DbErrorWrite>(())
5941            },
5942            TxType::MultipleWrites,
5943            "create_inner + pause_execution",
5944        )
5945        .await?;
5946
5947        // Second tx should fail
5948        let err = pool
5949            .transaction(
5950                move |tx| SqlitePool::pause_execution(tx, &EXECUTION_ID_DUMMY, created_at),
5951                TxType::MultipleWrites,
5952                "pause_execution",
5953            )
5954            .await
5955            .unwrap_err();
5956        let reason = assert_matches!(err, DbErrorWrite::NonRetriable(DbErrorWriteNonRetriable::IllegalState { reason, .. }) => reason);
5957        assert_eq!("cannot pause, execution is already paused", reason.as_ref());
5958
5959        let events = pool.transaction(
5960            move |tx| {
5961                let events =
5962                    tx.prepare(
5963                        "SELECT created_at, json_value, version FROM t_execution_log WHERE execution_id = :execution_id",
5964                    )?
5965                    .query_map(
5966                        named_params! {
5967                            ":execution_id": EXECUTION_ID_DUMMY.to_string(),
5968                        },
5969                        SqlitePool::map_t_execution_log_row,
5970                    )
5971                    .map_err(DbErrorWrite::from)?
5972                    .collect::<Result<Vec<_>, _>>()?;
5973
5974                Ok::<_, DbErrorWrite>(events)
5975            },
5976            TxType::Other, // read only
5977            "get_log",
5978        )
5979        .await?;
5980        assert_eq!(2, events.len());
5981        pool.close().await;
5982        Ok(())
5983    }
5984}