Skip to main content

runledger_postgres/jobs/workflows/
read.rs

1use runledger_core::jobs::WorkflowType;
2use sqlx::types::Uuid;
3
4use crate::{DbPool, DbTx, Result};
5
6use super::super::errors::validate_pagination;
7use super::super::row_decode::parse_workflow_release_mode;
8use super::super::rows::{WorkflowRunRow, WorkflowStepRow};
9use super::super::workflow_types::{
10    WorkflowRunCountFilter, WorkflowRunDbRecord, WorkflowRunListFilter, WorkflowRunReadCountFilter,
11    WorkflowRunReadListFilter, WorkflowRunReadScope, WorkflowStepDbRecord,
12    WorkflowStepDependencyDbRecord,
13};
14
15#[derive(sqlx::FromRow)]
16struct WorkflowStepDependencyLookupRow {
17    workflow_run_id: Uuid,
18    prerequisite_step_id: Uuid,
19    dependent_step_id: Uuid,
20    release_mode: String,
21    created_at: chrono::DateTime<chrono::Utc>,
22}
23
24fn workflow_step_dependency_db_record_from_lookup_row(
25    row: WorkflowStepDependencyLookupRow,
26) -> Result<WorkflowStepDependencyDbRecord> {
27    Ok(WorkflowStepDependencyDbRecord {
28        workflow_run_id: row.workflow_run_id,
29        prerequisite_step_id: row.prerequisite_step_id,
30        dependent_step_id: row.dependent_step_id,
31        release_mode: parse_workflow_release_mode(row.release_mode)?,
32        created_at: row.created_at,
33    })
34}
35
36const fn legacy_workflow_read_scope(organization_id: Option<Uuid>) -> WorkflowRunReadScope {
37    match organization_id {
38        Some(organization_id) => WorkflowRunReadScope::Organization(organization_id),
39        None => WorkflowRunReadScope::Admin,
40    }
41}
42
43/// Loads a workflow run using the legacy nullable visibility scope.
44///
45/// `None` retains its historical admin visibility across global and
46/// organization-owned workflow runs. Prefer
47/// [`get_workflow_run_by_id_with_scope`] for new code.
48pub async fn get_workflow_run_by_id(
49    pool: &DbPool,
50    organization_id: Option<Uuid>,
51    workflow_run_id: Uuid,
52) -> Result<Option<WorkflowRunDbRecord>> {
53    get_workflow_run_by_id_with_scope(
54        pool,
55        legacy_workflow_read_scope(organization_id),
56        workflow_run_id,
57    )
58    .await
59}
60
61/// Loads a workflow run within an explicit read-visibility scope.
62pub async fn get_workflow_run_by_id_with_scope(
63    pool: &DbPool,
64    scope: WorkflowRunReadScope,
65    workflow_run_id: Uuid,
66) -> Result<Option<WorkflowRunDbRecord>> {
67    let (is_admin, organization_id) = scope.visibility_predicate();
68    let row = sqlx::query_as!(
69        WorkflowRunRow,
70        "SELECT
71            id,
72            workflow_type,
73            organization_id,
74            status::text AS \"status!\",
75            idempotency_key,
76            result_step_key,
77            metadata,
78            started_at,
79            finished_at,
80            created_at,
81            updated_at
82         FROM workflow_runs
83         WHERE id = $1
84           AND ($2::bool OR organization_id IS NOT DISTINCT FROM $3::uuid)
85         LIMIT 1",
86        workflow_run_id,
87        is_admin,
88        organization_id,
89    )
90    .fetch_optional(pool)
91    .await
92    .map_err(|error| crate::Error::from_query_sqlx_with_context("get workflow run by id", error))?;
93
94    row.map(WorkflowRunRow::into_record).transpose()
95}
96
97pub(in crate::jobs::workflows) async fn load_workflow_run_by_id_tx(
98    tx: &mut DbTx<'_>,
99    workflow_run_id: Uuid,
100    context: &'static str,
101) -> Result<WorkflowRunDbRecord> {
102    let run_row = sqlx::query_as!(
103        WorkflowRunRow,
104        "SELECT
105            id,
106            workflow_type,
107            organization_id,
108            status::text AS \"status!\",
109            idempotency_key,
110            result_step_key,
111            metadata,
112            started_at,
113            finished_at,
114            created_at,
115            updated_at
116         FROM workflow_runs
117         WHERE id = $1",
118        workflow_run_id,
119    )
120    .fetch_one(&mut **tx)
121    .await
122    .map_err(|error| crate::Error::from_query_sqlx_with_context(context, error))?;
123
124    run_row.into_record()
125}
126
127/// Lists workflow steps using the legacy nullable visibility scope.
128///
129/// `None` retains its historical admin visibility across global and
130/// organization-owned workflow runs. Prefer [`list_workflow_steps_with_scope`]
131/// for new code.
132pub async fn list_workflow_steps(
133    pool: &DbPool,
134    organization_id: Option<Uuid>,
135    workflow_run_id: Uuid,
136) -> Result<Vec<WorkflowStepDbRecord>> {
137    list_workflow_steps_with_scope(
138        pool,
139        legacy_workflow_read_scope(organization_id),
140        workflow_run_id,
141    )
142    .await
143}
144
145/// Lists workflow steps within an explicit read-visibility scope.
146pub async fn list_workflow_steps_with_scope(
147    pool: &DbPool,
148    scope: WorkflowRunReadScope,
149    workflow_run_id: Uuid,
150) -> Result<Vec<WorkflowStepDbRecord>> {
151    let (is_admin, organization_id) = scope.visibility_predicate();
152    let rows = sqlx::query_as::<_, WorkflowStepRow>(
153        "SELECT
154            ws.id,
155            ws.workflow_run_id,
156            ws.step_key,
157            ws.execution_kind::text AS execution_kind,
158            ws.job_type,
159            ws.organization_id,
160            ws.payload,
161            ws.priority,
162            ws.max_attempts,
163            ws.timeout_seconds,
164            ws.stage,
165            ws.allow_handler_continuation,
166            ws.execution_resource_key,
167            ws.status::text AS status,
168            ws.job_id,
169            ws.released_at,
170            ws.started_at,
171            ws.finished_at,
172            ws.dependency_count_total,
173            ws.dependency_count_pending,
174            ws.dependency_count_unsatisfied,
175            ws.status_reason,
176            ws.last_error_code,
177            ws.last_error_message,
178            ws.output,
179            ws.created_at,
180            ws.updated_at
181         FROM workflow_steps ws
182         JOIN workflow_runs wr ON wr.id = ws.workflow_run_id
183         WHERE ws.workflow_run_id = $1
184           AND ($2::bool OR wr.organization_id IS NOT DISTINCT FROM $3::uuid)
185         ORDER BY ws.created_at ASC, ws.id ASC",
186    )
187    .bind(workflow_run_id)
188    .bind(is_admin)
189    .bind(organization_id)
190    .fetch_all(pool)
191    .await
192    .map_err(|error| crate::Error::from_query_sqlx_with_context("list workflow steps", error))?;
193
194    rows.into_iter().map(WorkflowStepRow::into_record).collect()
195}
196
197/// Lists a page of workflow steps using the legacy nullable visibility scope.
198///
199/// `None` retains its historical admin visibility across global and
200/// organization-owned workflow runs. Prefer
201/// [`list_workflow_steps_page_with_scope`] for new code.
202pub async fn list_workflow_steps_page(
203    pool: &DbPool,
204    organization_id: Option<Uuid>,
205    workflow_run_id: Uuid,
206    limit: i64,
207    offset: i64,
208) -> Result<Vec<WorkflowStepDbRecord>> {
209    list_workflow_steps_page_with_scope(
210        pool,
211        legacy_workflow_read_scope(organization_id),
212        workflow_run_id,
213        limit,
214        offset,
215    )
216    .await
217}
218
219/// Lists a page of workflow steps within an explicit read-visibility scope.
220pub async fn list_workflow_steps_page_with_scope(
221    pool: &DbPool,
222    scope: WorkflowRunReadScope,
223    workflow_run_id: Uuid,
224    limit: i64,
225    offset: i64,
226) -> Result<Vec<WorkflowStepDbRecord>> {
227    validate_pagination(limit, offset)?;
228
229    let (is_admin, organization_id) = scope.visibility_predicate();
230    let rows = sqlx::query_as::<_, WorkflowStepRow>(
231        "SELECT
232            ws.id,
233            ws.workflow_run_id,
234            ws.step_key,
235            ws.execution_kind::text AS execution_kind,
236            ws.job_type,
237            ws.organization_id,
238            ws.payload,
239            ws.priority,
240            ws.max_attempts,
241            ws.timeout_seconds,
242            ws.stage,
243            ws.allow_handler_continuation,
244            ws.execution_resource_key,
245            ws.status::text AS status,
246            ws.job_id,
247            ws.released_at,
248            ws.started_at,
249            ws.finished_at,
250            ws.dependency_count_total,
251            ws.dependency_count_pending,
252            ws.dependency_count_unsatisfied,
253            ws.status_reason,
254            ws.last_error_code,
255            ws.last_error_message,
256            ws.output,
257            ws.created_at,
258            ws.updated_at
259         FROM workflow_steps ws
260         JOIN workflow_runs wr ON wr.id = ws.workflow_run_id
261         WHERE ws.workflow_run_id = $1
262           AND ($2::bool OR wr.organization_id IS NOT DISTINCT FROM $3::uuid)
263         ORDER BY ws.created_at ASC, ws.id ASC
264         LIMIT $4 OFFSET $5",
265    )
266    .bind(workflow_run_id)
267    .bind(is_admin)
268    .bind(organization_id)
269    .bind(limit)
270    .bind(offset)
271    .fetch_all(pool)
272    .await
273    .map_err(|error| {
274        crate::Error::from_query_sqlx_with_context("list workflow steps page", error)
275    })?;
276
277    rows.into_iter().map(WorkflowStepRow::into_record).collect()
278}
279
280/// Counts workflow steps using the legacy nullable visibility scope.
281///
282/// `None` retains its historical admin visibility across global and
283/// organization-owned workflow runs. Prefer [`count_workflow_steps_with_scope`]
284/// for new code.
285pub async fn count_workflow_steps(
286    pool: &DbPool,
287    organization_id: Option<Uuid>,
288    workflow_run_id: Uuid,
289) -> Result<i64> {
290    count_workflow_steps_with_scope(
291        pool,
292        legacy_workflow_read_scope(organization_id),
293        workflow_run_id,
294    )
295    .await
296}
297
298/// Counts workflow steps within an explicit read-visibility scope.
299pub async fn count_workflow_steps_with_scope(
300    pool: &DbPool,
301    scope: WorkflowRunReadScope,
302    workflow_run_id: Uuid,
303) -> Result<i64> {
304    let (is_admin, organization_id) = scope.visibility_predicate();
305    sqlx::query_scalar::<_, i64>(
306        "SELECT COUNT(*)::bigint
307         FROM workflow_steps ws
308         JOIN workflow_runs wr ON wr.id = ws.workflow_run_id
309         WHERE ws.workflow_run_id = $1
310           AND ($2::bool OR wr.organization_id IS NOT DISTINCT FROM $3::uuid)",
311    )
312    .bind(workflow_run_id)
313    .bind(is_admin)
314    .bind(organization_id)
315    .fetch_one(pool)
316    .await
317    .map_err(|error| crate::Error::from_query_sqlx_with_context("count workflow steps", error))
318}
319
320/// Lists workflow runs using the legacy nullable visibility filter.
321///
322/// `filter.organization_id = None` retains its historical admin visibility
323/// across global and organization-owned workflow runs. Prefer
324/// [`list_workflow_runs_with_scope`] with [`WorkflowRunReadListFilter`] for
325/// new code.
326pub async fn list_workflow_runs(
327    pool: &DbPool,
328    filter: &WorkflowRunListFilter<'_>,
329) -> Result<Vec<WorkflowRunDbRecord>> {
330    let scoped_filter = WorkflowRunReadListFilter {
331        scope: legacy_workflow_read_scope(filter.organization_id),
332        status: filter.status,
333        workflow_type: filter.workflow_type,
334        limit: filter.limit,
335        offset: filter.offset,
336    };
337    list_workflow_runs_with_scope(pool, &scoped_filter).await
338}
339
340/// Lists workflow runs within an explicit read-visibility scope.
341pub async fn list_workflow_runs_with_scope(
342    pool: &DbPool,
343    filter: &WorkflowRunReadListFilter<'_>,
344) -> Result<Vec<WorkflowRunDbRecord>> {
345    validate_pagination(filter.limit, filter.offset)?;
346
347    let (is_admin, organization_id) = filter.scope.visibility_predicate();
348    let status_text = filter.status.map(|status| status.as_db_value());
349
350    let rows = sqlx::query_as::<_, WorkflowRunRow>(
351        "SELECT
352            id,
353            workflow_type,
354            organization_id,
355            status::text AS status,
356            idempotency_key,
357            result_step_key,
358            metadata,
359            started_at,
360            finished_at,
361            created_at,
362            updated_at
363         FROM workflow_runs
364         WHERE ($1::bool OR organization_id IS NOT DISTINCT FROM $2::uuid)
365           AND ($3::text IS NULL OR status = $3::text::workflow_run_status)
366           AND ($4::text IS NULL OR workflow_type ILIKE '%' || $4 || '%')
367         ORDER BY created_at DESC, id DESC
368         LIMIT $5 OFFSET $6",
369    )
370    .bind(is_admin)
371    .bind(organization_id)
372    .bind(status_text)
373    .bind(filter.workflow_type)
374    .bind(filter.limit)
375    .bind(filter.offset)
376    .fetch_all(pool)
377    .await
378    .map_err(|error| crate::Error::from_query_sqlx_with_context("list workflow runs", error))?;
379
380    rows.into_iter().map(WorkflowRunRow::into_record).collect()
381}
382
383/// Counts workflow runs using the legacy nullable visibility filter.
384///
385/// `filter.organization_id = None` retains its historical admin visibility
386/// across global and organization-owned workflow runs. Prefer
387/// [`count_workflow_runs_with_scope`] with [`WorkflowRunReadCountFilter`] for
388/// new code.
389pub async fn count_workflow_runs(
390    pool: &DbPool,
391    filter: &WorkflowRunCountFilter<'_>,
392) -> Result<i64> {
393    let scoped_filter = WorkflowRunReadCountFilter {
394        scope: legacy_workflow_read_scope(filter.organization_id),
395        status: filter.status,
396        workflow_type: filter.workflow_type,
397    };
398    count_workflow_runs_with_scope(pool, &scoped_filter).await
399}
400
401/// Counts workflow runs within an explicit read-visibility scope.
402pub async fn count_workflow_runs_with_scope(
403    pool: &DbPool,
404    filter: &WorkflowRunReadCountFilter<'_>,
405) -> Result<i64> {
406    let (is_admin, organization_id) = filter.scope.visibility_predicate();
407    let status_text = filter.status.map(|status| status.as_db_value());
408    sqlx::query_scalar::<_, i64>(
409        "SELECT COUNT(*)::bigint
410         FROM workflow_runs
411         WHERE ($1::bool OR organization_id IS NOT DISTINCT FROM $2::uuid)
412           AND ($3::text IS NULL OR status = $3::text::workflow_run_status)
413           AND ($4::text IS NULL OR workflow_type ILIKE '%' || $4 || '%')",
414    )
415    .bind(is_admin)
416    .bind(organization_id)
417    .bind(status_text)
418    .bind(filter.workflow_type)
419    .fetch_one(pool)
420    .await
421    .map_err(|error| crate::Error::from_query_sqlx_with_context("count workflow runs", error))
422}
423
424/// Loads the latest workflow run using the legacy nullable visibility scope.
425///
426/// `None` retains its historical admin visibility across global and
427/// organization-owned workflow runs. Prefer
428/// [`get_latest_workflow_run_by_type_with_scope`] for new code.
429pub async fn get_latest_workflow_run_by_type(
430    pool: &DbPool,
431    organization_id: Option<Uuid>,
432    workflow_type: WorkflowType<'_>,
433) -> Result<Option<WorkflowRunDbRecord>> {
434    get_latest_workflow_run_by_type_with_scope(
435        pool,
436        legacy_workflow_read_scope(organization_id),
437        workflow_type,
438    )
439    .await
440}
441
442/// Loads the latest workflow run within an explicit read-visibility scope.
443pub async fn get_latest_workflow_run_by_type_with_scope(
444    pool: &DbPool,
445    scope: WorkflowRunReadScope,
446    workflow_type: WorkflowType<'_>,
447) -> Result<Option<WorkflowRunDbRecord>> {
448    let (is_admin, organization_id) = scope.visibility_predicate();
449    let row = sqlx::query_as::<_, WorkflowRunRow>(
450        "SELECT
451            id,
452            workflow_type,
453            organization_id,
454            status::text AS status,
455            idempotency_key,
456            result_step_key,
457            metadata,
458            started_at,
459            finished_at,
460            created_at,
461            updated_at
462         FROM workflow_runs
463         WHERE ($1::bool OR organization_id IS NOT DISTINCT FROM $2::uuid)
464           AND workflow_type = $3
465         ORDER BY created_at DESC, id DESC
466         LIMIT 1",
467    )
468    .bind(is_admin)
469    .bind(organization_id)
470    .bind(workflow_type.as_str())
471    .fetch_optional(pool)
472    .await
473    .map_err(|error| {
474        crate::Error::from_query_sqlx_with_context("get latest workflow run by type", error)
475    })?;
476
477    let Some(row) = row else {
478        return Ok(None);
479    };
480
481    Ok(Some(row.into_record()?))
482}
483
484/// Lists workflow step dependencies using the legacy nullable visibility scope.
485///
486/// `None` retains its historical admin visibility across global and
487/// organization-owned workflow runs. Prefer
488/// [`list_workflow_step_dependencies_with_scope`] for new code.
489pub async fn list_workflow_step_dependencies(
490    pool: &DbPool,
491    organization_id: Option<Uuid>,
492    workflow_run_id: Uuid,
493) -> Result<Vec<WorkflowStepDependencyDbRecord>> {
494    list_workflow_step_dependencies_with_scope(
495        pool,
496        legacy_workflow_read_scope(organization_id),
497        workflow_run_id,
498    )
499    .await
500}
501
502/// Lists workflow step dependencies within an explicit read-visibility scope.
503pub async fn list_workflow_step_dependencies_with_scope(
504    pool: &DbPool,
505    scope: WorkflowRunReadScope,
506    workflow_run_id: Uuid,
507) -> Result<Vec<WorkflowStepDependencyDbRecord>> {
508    let (is_admin, organization_id) = scope.visibility_predicate();
509    let rows = sqlx::query_as::<_, WorkflowStepDependencyLookupRow>(
510        "SELECT
511            wsd.workflow_run_id,
512            wsd.prerequisite_step_id,
513            wsd.dependent_step_id,
514            wsd.release_mode::text AS release_mode,
515            wsd.created_at
516         FROM workflow_step_dependencies wsd
517         JOIN workflow_runs wr ON wr.id = wsd.workflow_run_id
518         WHERE wsd.workflow_run_id = $1
519           AND ($2::bool OR wr.organization_id IS NOT DISTINCT FROM $3::uuid)
520         ORDER BY
521           wsd.prerequisite_step_id ASC,
522           wsd.dependent_step_id ASC",
523    )
524    .bind(workflow_run_id)
525    .bind(is_admin)
526    .bind(organization_id)
527    .fetch_all(pool)
528    .await
529    .map_err(|error| {
530        crate::Error::from_query_sqlx_with_context("list workflow step dependencies", error)
531    })?;
532
533    rows.into_iter()
534        .map(workflow_step_dependency_db_record_from_lookup_row)
535        .collect()
536}
537
538/// Lists a page of workflow step dependencies using the legacy nullable
539/// visibility scope.
540///
541/// `None` retains its historical admin visibility across global and
542/// organization-owned workflow runs. Prefer
543/// [`list_workflow_step_dependencies_page_with_scope`] for new code.
544pub async fn list_workflow_step_dependencies_page(
545    pool: &DbPool,
546    organization_id: Option<Uuid>,
547    workflow_run_id: Uuid,
548    limit: i64,
549    offset: i64,
550) -> Result<Vec<WorkflowStepDependencyDbRecord>> {
551    list_workflow_step_dependencies_page_with_scope(
552        pool,
553        legacy_workflow_read_scope(organization_id),
554        workflow_run_id,
555        limit,
556        offset,
557    )
558    .await
559}
560
561/// Lists a page of workflow step dependencies within an explicit
562/// read-visibility scope.
563pub async fn list_workflow_step_dependencies_page_with_scope(
564    pool: &DbPool,
565    scope: WorkflowRunReadScope,
566    workflow_run_id: Uuid,
567    limit: i64,
568    offset: i64,
569) -> Result<Vec<WorkflowStepDependencyDbRecord>> {
570    validate_pagination(limit, offset)?;
571
572    let (is_admin, organization_id) = scope.visibility_predicate();
573    let rows = sqlx::query_as::<_, WorkflowStepDependencyLookupRow>(
574        "SELECT
575            wsd.workflow_run_id,
576            wsd.prerequisite_step_id,
577            wsd.dependent_step_id,
578            wsd.release_mode::text AS release_mode,
579            wsd.created_at
580         FROM workflow_step_dependencies wsd
581         JOIN workflow_runs wr ON wr.id = wsd.workflow_run_id
582         WHERE wsd.workflow_run_id = $1
583           AND ($2::bool OR wr.organization_id IS NOT DISTINCT FROM $3::uuid)
584         ORDER BY
585           wsd.prerequisite_step_id ASC,
586           wsd.dependent_step_id ASC
587         LIMIT $4 OFFSET $5",
588    )
589    .bind(workflow_run_id)
590    .bind(is_admin)
591    .bind(organization_id)
592    .bind(limit)
593    .bind(offset)
594    .fetch_all(pool)
595    .await
596    .map_err(|error| {
597        crate::Error::from_query_sqlx_with_context("list workflow step dependencies page", error)
598    })?;
599
600    rows.into_iter()
601        .map(workflow_step_dependency_db_record_from_lookup_row)
602        .collect()
603}
604
605/// Counts workflow step dependencies using the legacy nullable visibility
606/// scope.
607///
608/// `None` retains its historical admin visibility across global and
609/// organization-owned workflow runs. Prefer
610/// [`count_workflow_step_dependencies_with_scope`] for new code.
611pub async fn count_workflow_step_dependencies(
612    pool: &DbPool,
613    organization_id: Option<Uuid>,
614    workflow_run_id: Uuid,
615) -> Result<i64> {
616    count_workflow_step_dependencies_with_scope(
617        pool,
618        legacy_workflow_read_scope(organization_id),
619        workflow_run_id,
620    )
621    .await
622}
623
624/// Counts workflow step dependencies within an explicit read-visibility scope.
625pub async fn count_workflow_step_dependencies_with_scope(
626    pool: &DbPool,
627    scope: WorkflowRunReadScope,
628    workflow_run_id: Uuid,
629) -> Result<i64> {
630    let (is_admin, organization_id) = scope.visibility_predicate();
631    sqlx::query_scalar::<_, i64>(
632        "SELECT COUNT(*)::bigint
633         FROM workflow_step_dependencies wsd
634         JOIN workflow_runs wr ON wr.id = wsd.workflow_run_id
635         WHERE wsd.workflow_run_id = $1
636           AND ($2::bool OR wr.organization_id IS NOT DISTINCT FROM $3::uuid)",
637    )
638    .bind(workflow_run_id)
639    .bind(is_admin)
640    .bind(organization_id)
641    .fetch_one(pool)
642    .await
643    .map_err(|error| {
644        crate::Error::from_query_sqlx_with_context("count workflow step dependencies", error)
645    })
646}
647
648pub async fn get_workflow_run_id_for_job(pool: &DbPool, job_id: Uuid) -> Result<Option<Uuid>> {
649    sqlx::query_scalar!(
650        "SELECT ws.workflow_run_id FROM workflow_steps ws WHERE ws.job_id = $1",
651        job_id,
652    )
653    .fetch_optional(pool)
654    .await
655    .map_err(|error| {
656        crate::Error::from_query_sqlx_with_context("get workflow run id for job", error)
657    })
658}
659
660pub async fn get_workflow_run_by_type_and_idempotency_key(
661    pool: &DbPool,
662    organization_id: Option<Uuid>,
663    workflow_type: WorkflowType<'_>,
664    idempotency_key: &str,
665) -> Result<Option<WorkflowRunDbRecord>> {
666    let mut tx = pool
667        .begin()
668        .await
669        .map_err(|error| crate::Error::ConnectionError(error.to_string()))?;
670    let run = get_workflow_run_by_type_and_idempotency_key_tx(
671        &mut tx,
672        organization_id,
673        workflow_type,
674        idempotency_key,
675    )
676    .await?;
677    tx.commit()
678        .await
679        .map_err(|error| crate::Error::ConnectionError(error.to_string()))?;
680    Ok(run)
681}
682
683pub async fn get_workflow_run_by_type_and_idempotency_key_tx(
684    tx: &mut DbTx<'_>,
685    organization_id: Option<Uuid>,
686    workflow_type: WorkflowType<'_>,
687    idempotency_key: &str,
688) -> Result<Option<WorkflowRunDbRecord>> {
689    let row = if let Some(organization_id) = organization_id {
690        sqlx::query_as!(
691            WorkflowRunRow,
692            "SELECT
693                id,
694                workflow_type,
695                organization_id,
696                status::text AS \"status!\",
697                idempotency_key,
698                result_step_key,
699                metadata,
700                started_at,
701                finished_at,
702                created_at,
703                updated_at
704             FROM workflow_runs
705             WHERE workflow_type = $1
706               AND idempotency_key = $2
707               AND organization_id = $3
708             LIMIT 1",
709            workflow_type as _,
710            idempotency_key,
711            organization_id,
712        )
713        .fetch_optional(&mut **tx)
714        .await
715    } else {
716        sqlx::query_as!(
717            WorkflowRunRow,
718            "SELECT
719                id,
720                workflow_type,
721                organization_id,
722                status::text AS \"status!\",
723                idempotency_key,
724                result_step_key,
725                metadata,
726                started_at,
727                finished_at,
728                created_at,
729                updated_at
730             FROM workflow_runs
731             WHERE workflow_type = $1
732               AND idempotency_key = $2
733               AND organization_id IS NULL
734             LIMIT 1",
735            workflow_type as _,
736            idempotency_key,
737        )
738        .fetch_optional(&mut **tx)
739        .await
740    }
741    .map_err(|error| {
742        crate::Error::from_query_sqlx_with_context(
743            "get workflow run by type and idempotency key",
744            error,
745        )
746    })?;
747
748    row.map(WorkflowRunRow::into_record).transpose()
749}