-- name: GetStepRun :one
SELECT
    "StepRun".*
FROM
    "StepRun"
WHERE
    "id" = @id::uuid AND
    "deletedAt" IS NULL;

-- name: GetStepRunDataForEngine :one
WITH expr_count AS (
    SELECT
        COUNT(*) AS "exprCount",
        sr."id" AS "id"
    FROM
        "StepRun" sr
    JOIN
        "Step" s ON sr."stepId" = s."id"
    JOIN
        "StepExpression" se ON s."id" = se."stepId"
    WHERE
        sr."id" = @id::uuid
    GROUP BY
        sr."id"
)
SELECT
    sr."input",
    sr."output",
    sr."error",
    jrld."data" AS "jobRunLookupData",
    wr."additionalMetadata",
    wr."childIndex",
    wr."childKey",
    wr."parentId",
    COALESCE(ec."exprCount", 0) AS "exprCount"
FROM
    "StepRun" sr
JOIN
    "JobRun" jr ON sr."jobRunId" = jr."id"
JOIN
    "JobRunLookupData" jrld ON jr."id" = jrld."jobRunId"
JOIN
    -- Take advantage of composite index on "JobRun"("workflowRunId", "tenantId")
    "WorkflowRun" wr ON jr."workflowRunId" = wr."id" AND wr."tenantId" = @tenantId::uuid
LEFT JOIN
    expr_count ec ON sr."id" = ec."id"
WHERE
    sr."id" = @id::uuid AND
    sr."tenantId" = @tenantId::uuid;

-- name: GetStepRunBulkDataForEngine :many
SELECT
    sr."id" AS "SR_id",
    sr."retryCount" AS "SR_retryCount",
    sr."input",
    sr."output",
    sr."error",
    sr."status",
    jr."id" AS "jobRunId",
    jr."status" AS "jobRunStatus",
    jr."status" AS "jobRunStatus",
    jr."workflowRunId" AS "workflowRunId",
    jrld."data" AS "jobRunLookupData",
    wr."additionalMetadata",
    wr."childIndex",
    wr."childKey",
    wr."parentId",
    jr."id" AS "jobRunId",
    s."id" AS "stepId",
    s."retries" AS "stepRetries",
    s."timeout" AS "stepTimeout",
    s."scheduleTimeout" AS "stepScheduleTimeout",
    s."readableId" AS "stepReadableId",
    s."customUserData" AS "stepCustomUserData",
    j."name" AS "jobName",
    j."id" AS "jobId",
    j."kind" AS "jobKind",
    j."workflowVersionId" AS "workflowVersionId",
    a."actionId" AS "actionId"
FROM
    "StepRun" sr
JOIN
    "Step" s ON sr."stepId" = s."id"
JOIN
    "Action" a ON s."actionId" = a."actionId" AND s."tenantId" = a."tenantId"
JOIN
    "JobRun" jr ON sr."jobRunId" = jr."id"
JOIN
    "Job" j ON jr."jobId" = j."id"
JOIN
    "JobRunLookupData" jrld ON jr."id" = jrld."jobRunId"
JOIN
    -- Take advantage of composite index on "JobRun"("workflowRunId", "tenantId")
    "WorkflowRun" wr ON jr."workflowRunId" = wr."id" AND wr."tenantId" = @tenantId::uuid
WHERE
    sr."id" = ANY(@ids::uuid[])
    AND sr."tenantId" = @tenantId::uuid;

-- name: ListStepRunExpressionEvals :many
SELECT
    *
FROM
    "StepRunExpressionEval" sre
WHERE
    "stepRunId" = ANY(@stepRunIds::uuid[]);

-- name: CreateStepRunExpressionEvalStrs :exec
INSERT INTO "StepRunExpressionEval" (
    "key",
    "stepRunId",
    "valueStr",
    "kind"
) VALUES (
    unnest(@keys::text[]),
    @stepRunId::uuid,
    unnest(@valuesStr::text[]),
    unnest(cast(@kinds::text[] as"StepExpressionKind"[]))
) ON CONFLICT ("key", "stepRunId", "kind") DO UPDATE
SET
    "valueStr" = EXCLUDED."valueStr",
    "valueInt" = EXCLUDED."valueInt";

-- name: CreateStepRunExpressionEvalInts :exec
INSERT INTO "StepRunExpressionEval" (
    "key",
    "stepRunId",
    "valueInt",
    "kind"
) VALUES (
    unnest(@keys::text[]),
    @stepRunId::uuid,
    unnest(@valuesInt::int[]),
    unnest(cast(@kinds::text[] as"StepExpressionKind"[]))
) ON CONFLICT ("key", "stepRunId", "kind") DO UPDATE
SET
    "valueStr" = EXCLUDED."valueStr",
    "valueInt" = EXCLUDED."valueInt";

-- name: GetStepExpressions :many
SELECT
    *
FROM
    "StepExpression"
WHERE
    "stepId" = @stepId::uuid;

-- name: GetStepRunMeta :one
SELECT
    jr."workflowRunId" AS "workflowRunId",
    sr."retryCount" AS "retryCount",
    s."retries" as "retries"
FROM "StepRun" sr
JOIN "Step" s ON sr."stepId" = s."id"
JOIN "JobRun" jr ON sr."jobRunId" = jr."id"
WHERE sr."id" = @stepRunId::uuid
AND sr."tenantId" = @tenantId::uuid;

-- name: GetStepRunForEngine :many
WITH child_count AS (
    SELECT
        COUNT(*) AS "childCount",
        sr."id" AS "id"
    FROM
        "StepRun" sr
    LEFT JOIN
        "_StepRunOrder" AS step_run_order ON sr."id" = step_run_order."A"
    WHERE
        sr."id" = ANY(@ids::uuid[])
        AND step_run_order IS NOT NULL
    GROUP BY
        sr."id"
)
SELECT
    DISTINCT ON (sr."id")
    sr."id" AS "SR_id",
    sr."createdAt" AS "SR_createdAt",
    sr."updatedAt" AS "SR_updatedAt",
    sr."deletedAt" AS "SR_deletedAt",
    sr."tenantId" AS "SR_tenantId",
    sr."queue" AS "SR_queue",
    sr."order" AS "SR_order",
    sqi."workerId" AS "SR_workerId",
    sr."tickerId" AS "SR_tickerId",
    sr."status" AS "SR_status",
    sr."requeueAfter" AS "SR_requeueAfter",
    sr."scheduleTimeoutAt" AS "SR_scheduleTimeoutAt",
    sr."startedAt" AS "SR_startedAt",
    sr."finishedAt" AS "SR_finishedAt",
    sr."timeoutAt" AS "SR_timeoutAt",
    sr."cancelledAt" AS "SR_cancelledAt",
    sr."cancelledReason" AS "SR_cancelledReason",
    sr."cancelledError" AS "SR_cancelledError",
    sr."callerFiles" AS "SR_callerFiles",
    sr."gitRepoBranch" AS "SR_gitRepoBranch",
    sr."retryCount" AS "SR_retryCount",
    sr."semaphoreReleased" AS "SR_semaphoreReleased",
    sr."priority" AS "SR_priority",
    COALESCE(cc."childCount", 0) AS "SR_childCount",
    -- TODO: everything below this line is cacheable and should be moved to a separate query
    jr."id" AS "jobRunId",
    s."id" AS "stepId",
    s."retries" AS "stepRetries",
    s."timeout" AS "stepTimeout",
    s."scheduleTimeout" AS "stepScheduleTimeout",
    s."readableId" AS "stepReadableId",
    s."customUserData" AS "stepCustomUserData",
    j."name" AS "jobName",
    j."id" AS "jobId",
    j."kind" AS "jobKind",
    j."workflowVersionId" AS "workflowVersionId",
    jr."status" AS "jobRunStatus",
    jr."workflowRunId" AS "workflowRunId",
    a."actionId" AS "actionId",
    sticky."strategy" AS "stickyStrategy",
    sticky."desiredWorkerId" AS "desiredWorkerId"
FROM
    "StepRun" sr
LEFT JOIN
    child_count cc ON sr."id" = cc."id"
JOIN
    "Step" s ON sr."stepId" = s."id"
JOIN
    "Action" a ON s."actionId" = a."actionId" AND s."tenantId" = a."tenantId"
JOIN
    "JobRun" jr ON sr."jobRunId" = jr."id"
JOIN
    "Job" j ON jr."jobId" = j."id"
LEFT JOIN
    "SemaphoreQueueItem" sqi ON sr."id" = sqi."stepRunId"
LEFT JOIN
    "WorkflowRunStickyState" sticky ON jr."workflowRunId" = sticky."workflowRunId"
WHERE
    sr."id" = ANY(@ids::uuid[]) AND
    sr."deletedAt" IS NULL AND
    jr."deletedAt" IS NULL AND
    (
        sqlc.narg('tenantId')::uuid IS NULL OR
        sr."tenantId" = sqlc.narg('tenantId')::uuid
    );

-- name: ListInitialStepRuns :many
SELECT
    DISTINCT ON (child_run."id")
    child_run."id" AS "id"
FROM
    "StepRun" AS child_run
LEFT JOIN
    "_StepRunOrder" AS step_run_order ON step_run_order."B" = child_run."id"
WHERE
    child_run."jobRunId" = @jobRunId::uuid
    AND child_run."status" = 'PENDING'
    AND step_run_order."A" IS NULL;

-- name: ListStartableStepRunsManyParents :many
SELECT
    DISTINCT ON (child_run."id")
    child_run."id" AS "id"
FROM
    "StepRun" AS parent_run
LEFT JOIN
    "_StepRunOrder" AS step_run_order ON step_run_order."A" = parent_run."id"
JOIN
    "StepRun" AS child_run ON step_run_order."B" = child_run."id"
WHERE
    parent_run."id" = @parentStepRunId::uuid
    AND child_run."status" = 'PENDING'
    -- we look for whether the step run is startable by ensuring that all parent step runs have succeeded
    AND NOT EXISTS (
        SELECT 1
        FROM "_StepRunOrder" AS parent_order
        JOIN "StepRun" AS parent_run ON parent_order."A" = parent_run."id"
        WHERE
            parent_order."B" = child_run."id"
            AND parent_run."status" != 'SUCCEEDED'
    )
    -- AND we ensure that there's at least 2 parent step runs
    AND EXISTS (
        SELECT 1
        FROM "_StepRunOrder" AS parent_order
        JOIN "StepRun" AS parent_run ON parent_order."A" = parent_run."id"
        WHERE
            parent_order."B" = child_run."id"
        OFFSET 1
    );

-- name: ListStartableStepRunsSingleParent :many
SELECT
    DISTINCT ON (child_run."id")
    child_run."id" AS "id"
FROM
    "StepRun" AS parent_run
LEFT JOIN
    "_StepRunOrder" AS step_run_order ON step_run_order."A" = parent_run."id"
JOIN
    "StepRun" AS child_run ON step_run_order."B" = child_run."id"
WHERE
    parent_run."id" = @parentStepRunId::uuid
    AND child_run."status" = 'PENDING'
    -- we look for whether the step run is startable ASSUMING that parentStepRunId has succeeded,
    -- but we only have one parent step run
    AND NOT EXISTS (
        SELECT 1
        FROM "_StepRunOrder" AS parent_order
        JOIN "StepRun" AS parent_run ON parent_order."A" = parent_run."id"
        WHERE
            parent_order."B" = child_run."id"
            AND parent_run."id" != @parentStepRunId::uuid
    );

-- name: ListStepRuns :many
SELECT
    DISTINCT ON ("StepRun"."id")
    "StepRun"."id"
FROM
    "StepRun"
JOIN
    "JobRun" ON "StepRun"."jobRunId" = "JobRun"."id"
WHERE
    "StepRun"."deletedAt" IS NULL AND
    "JobRun"."deletedAt" IS NULL AND
    (
        sqlc.narg('tenantId')::uuid IS NULL OR
        "StepRun"."tenantId" = sqlc.narg('tenantId')::uuid
    )
    AND (
        sqlc.narg('status')::"StepRunStatus" IS NULL OR
        "StepRun"."status" = sqlc.narg('status')::"StepRunStatus"
    )
    AND (
        sqlc.narg('workflowRunIds')::uuid[] IS NULL OR
        "JobRun"."workflowRunId" = ANY(sqlc.narg('workflowRunIds')::uuid[])
    )
    AND (
        sqlc.narg('jobRunId')::uuid IS NULL OR
        "StepRun"."jobRunId" = sqlc.narg('jobRunId')::uuid
    )
    AND (
        sqlc.narg('tickerId')::uuid IS NULL OR
        "StepRun"."tickerId" = sqlc.narg('tickerId')::uuid
    );

-- name: ListStepRunsToCancel :many
SELECT
    DISTINCT ON ("StepRun"."id")
    "StepRun"."id"
FROM
    "StepRun"
JOIN
    "JobRun" ON "StepRun"."jobRunId" = "JobRun"."id"
WHERE
    "StepRun"."deletedAt" IS NULL AND
    "JobRun"."deletedAt" IS NULL AND
    "StepRun"."tenantId" = @tenantId::uuid AND
    "StepRun"."jobRunId" = @jobRunId::uuid AND
    "StepRun"."status" = ANY(ARRAY['PENDING', 'PENDING_ASSIGNMENT', 'ASSIGNED', 'RUNNING']::"StepRunStatus"[]);

-- name: QueueStepRun :exec
UPDATE
    "StepRun"
SET
    "finishedAt" = NULL,
    "status" = 'PENDING_ASSIGNMENT',
    "input" = COALESCE(sqlc.narg('input')::jsonb, "input"),
    "output" = NULL,
    "error" = NULL,
    "cancelledAt" = NULL,
    "cancelledReason" = NULL,
    "retryCount" = CASE
        WHEN sqlc.narg('isRetry')::boolean IS NOT NULL THEN "retryCount" + 1
        ELSE "retryCount"
    END,
    "semaphoreReleased" = false
WHERE
  "id" = @id::uuid AND
  "tenantId" = @tenantId::uuid;

-- name: QueueStepRunBulkWithInput :exec
WITH input AS (
    SELECT
        unnest(@ids::uuid[]) AS "id",
        unnest(@inputs::jsonb[]) AS "input",
        unnest(@retryCounts::int[]) AS "retryCount"
)
UPDATE
    "StepRun" sr
SET
    "finishedAt" = NULL,
    "status" = 'PENDING_ASSIGNMENT',
    "input" = COALESCE(input."input", sr."input"),
    "output" = NULL,
    "error" = NULL,
    "cancelledAt" = NULL,
    "cancelledReason" = NULL,
    "retryCount" = input."retryCount",
    "semaphoreReleased" = false
FROM
    input
WHERE
    sr."id" = input."id";

-- name: QueueStepRunBulkNoInput :exec
WITH input AS (
    SELECT
        unnest(@ids::uuid[]) AS "id",
        unnest(@retryCounts::int[]) AS "retryCount"
)
UPDATE
    "StepRun" sr
SET
    "finishedAt" = NULL,
    "status" = 'PENDING_ASSIGNMENT',
    "output" = NULL,
    "error" = NULL,
    "cancelledAt" = NULL,
    "cancelledReason" = NULL,
    "retryCount" = input."retryCount",
    "semaphoreReleased" = false
FROM
    input
WHERE
    sr."id" = input."id";

-- name: ManualReleaseSemaphore :exec
UPDATE
    "StepRun"
SET
    -- note that workerId has already been removed via SemaphoreQueueItem
    "semaphoreReleased" = true
WHERE
    "id" = @stepRunId::uuid AND
    "tenantId" = @tenantId::uuid;

-- name: BulkStartStepRun :exec
UPDATE
    "StepRun"
SET
    "status" = CASE
        -- Final states are final, cannot be updated, and we cannot go from cancelling to a non-final state
        WHEN "status" IN ('SUCCEEDED', 'FAILED', 'CANCELLED', 'CANCELLING') THEN "status"
        ELSE 'RUNNING'
    END,
    "startedAt" = input."startedAt"
FROM (
    SELECT
        unnest(@stepRunIds::uuid[]) AS "id",
        unnest(@startedAts::timestamp[]) AS "startedAt"
    ) AS input
WHERE
    "StepRun"."id" = input."id";

-- name: ValidatesAsJson :exec
SELECT @input::jsonb AS "is_valid";

-- name: BulkFinishStepRun :exec
UPDATE
    "StepRun"
SET
    "status" = CASE
        WHEN "status" IN ('SUCCEEDED', 'FAILED', 'CANCELLED') THEN "status"
        ELSE 'SUCCEEDED'
    END,
    "finishedAt" = input."finishedAt",
    "output" = input."output"::jsonb
FROM (
    SELECT
        unnest(@stepRunIds::uuid[]) AS "id",
        unnest(@finishedAts::timestamp[]) AS "finishedAt",
        unnest(@outputs::jsonb[]) AS "output"
    ) AS input
WHERE
    "StepRun"."id" = input."id";

-- name: BulkCancelStepRun :exec
WITH input AS (
    SELECT
        unnest(@stepRunIds::uuid[]) AS "id",
        unnest(@finishedAts::timestamp[]) AS "finishedAt",
        unnest(@cancelledAts::timestamp[]) AS "cancelledAt",
        unnest(@cancelledReasons::text[]) AS "cancelledReason",
        unnest(@cancelledErrors::text[]) AS "cancelledError"
)
UPDATE "StepRun"
SET
    "status" = CASE
        -- Final states are final, cannot be updated
        WHEN "status" IN ('SUCCEEDED', 'FAILED', 'CANCELLED') THEN "status"
        ELSE 'CANCELLED'
    END,
    "finishedAt" = input."finishedAt",
    "cancelledAt" = input."cancelledAt",
    "cancelledReason" = input."cancelledReason",
    "cancelledError" = input."cancelledError"
FROM input
WHERE "StepRun"."id" = input."id";


-- name: BulkFailStepRun :exec
UPDATE
    "StepRun"
SET
    "status" = CASE
        -- Final states are final, cannot be updated
        WHEN "status" IN ('SUCCEEDED', 'FAILED', 'CANCELLED') THEN "status"
        ELSE 'FAILED'
    END,
    "finishedAt" = input."finishedAt",
    "error" = input."error"::text
FROM (
    SELECT
        unnest(@stepRunIds::uuid[]) AS "id",
        unnest(@finishedAts::timestamp[]) AS "finishedAt",
        unnest(@errors::text[]) AS "error"
    ) AS input
WHERE
    "StepRun"."id" = input."id";

-- name: ResolveLaterStepRuns :many
WITH RECURSIVE currStepRun AS (
  SELECT "id", "status", "cancelledReason"
  FROM "StepRun"
  WHERE
    "id" = @stepRunId::uuid
), childStepRuns AS (
  SELECT sr."id", sr."status"
  FROM "StepRun" sr
  JOIN "_StepRunOrder" sro ON sr."id" = sro."B"
  WHERE sro."A" = (SELECT "id" FROM currStepRun)

  UNION ALL

  SELECT sr."id", sr."status"
  FROM "StepRun" sr
  JOIN "_StepRunOrder" sro ON sr."id" = sro."B"
  JOIN childStepRuns csr ON sro."A" = csr."id"
)
UPDATE
    "StepRun" as sr
SET  "status" = CASE
    -- When the step is in a final state, it cannot be updated
    WHEN sr."status" IN ('SUCCEEDED', 'FAILED', 'CANCELLED') THEN sr."status"
    -- When the given step run has failed or been cancelled, then all child step runs are cancelled
    WHEN @status::"StepRunStatus" IN ('FAILED', 'CANCELLED') THEN 'CANCELLED'
    ELSE sr."status"
    END,
    -- When the previous step run timed out, the cancelled reason is set
    "cancelledReason" = CASE
    -- When the step is in a final state, it cannot be updated
    WHEN sr."status" IN ('SUCCEEDED', 'FAILED', 'CANCELLED') THEN sr."cancelledReason"
    WHEN @status::"StepRunStatus" = 'CANCELLED' AND (SELECT "cancelledReason" FROM currStepRun) = 'TIMED_OUT'::text THEN 'PREVIOUS_STEP_TIMED_OUT'
    WHEN @status::"StepRunStatus" = 'FAILED' THEN 'PREVIOUS_STEP_FAILED'
    WHEN @status::"StepRunStatus" = 'CANCELLED' THEN 'PREVIOUS_STEP_CANCELLED'
    ELSE NULL
    END
FROM
    childStepRuns csr
WHERE
    sr."id" = csr."id"
RETURNING sr.*;

-- name: UpdateStepRunOverridesData :one
UPDATE
    "StepRun" AS sr
SET
    "updatedAt" = CURRENT_TIMESTAMP,
    "input" = jsonb_set("input", @fieldPath::text[], @jsonData::jsonb, true),
    "callerFiles" = jsonb_set("callerFiles", @overridesKey::text[], to_jsonb(@callerFile::text), true)
WHERE
    sr."tenantId" = @tenantId::uuid AND
    sr."id" = @stepRunId::uuid
RETURNING "input";

-- name: UpdateStepRunInputSchema :one
UPDATE
    "StepRun" sr
SET
    "inputSchema" = coalesce(sqlc.narg('inputSchema')::jsonb, '{}'),
    "updatedAt" = CURRENT_TIMESTAMP
WHERE
    sr."tenantId" = @tenantId::uuid AND
    sr."id" = @stepRunId::uuid
RETURNING "inputSchema";

-- name: ArchiveStepRunResultFromStepRun :one
WITH step_run_data AS (
    SELECT
        "id" AS step_run_id,
        "createdAt",
        "updatedAt",
        "deletedAt",
        "retryCount",
        "order",
        "input",
        "output",
        "error",
        "startedAt",
        "finishedAt",
        "timeoutAt",
        "cancelledAt",
        "cancelledReason",
        "cancelledError"
    FROM "StepRun"
    WHERE
        "id" = @stepRunId::uuid
        AND "tenantId" = @tenantId::uuid
        AND "deletedAt" IS NULL
)
INSERT INTO "StepRunResultArchive" (
    "id",
    "createdAt",
    "updatedAt",
    "deletedAt",
    "stepRunId",
    "retryCount",
    "input",
    "output",
    "error",
    "startedAt",
    "finishedAt",
    "timeoutAt",
    "cancelledAt",
    "cancelledReason",
    "cancelledError"
)
SELECT
    COALESCE(sqlc.arg('id')::uuid, gen_random_uuid()),
    CURRENT_TIMESTAMP,
    CURRENT_TIMESTAMP,
    step_run_data."deletedAt",
    step_run_data.step_run_id,
    step_run_data."retryCount",
    step_run_data."input",
    step_run_data."output",
    COALESCE(sqlc.narg('error')::text, step_run_data."error"),
    step_run_data."startedAt",
    step_run_data."finishedAt",
    step_run_data."timeoutAt",
    step_run_data."cancelledAt",
    step_run_data."cancelledReason",
    step_run_data."cancelledError"
FROM step_run_data
RETURNING *;

-- name: ListStepRunsToReassign :many
WITH step_runs_on_inactive_workers AS (
    SELECT
        sr."id",
        sr."tenantId",
        sr."scheduleTimeoutAt",
        sr."retryCount",
        sr."internalRetryCount",
        sqi."workerId",
        s."actionId",
        s."id" AS "stepId",
        s."timeout" AS "stepTimeout",
        s."scheduleTimeout" AS "scheduleTimeout"
    FROM
        "Worker" w
    LEFT JOIN
        "SemaphoreQueueItem" sqi ON w."id" = sqi."workerId"
    JOIN
        "StepRun" sr ON sr."id" = sqi."stepRunId"
    JOIN
        "Step" s ON sr."stepId" = s."id"
    WHERE
        w."tenantId" = @tenantId::uuid
        AND w."lastHeartbeatAt" < NOW() - INTERVAL '30 seconds'
),
step_runs_to_reassign AS (
    SELECT
        *
    FROM
        step_runs_on_inactive_workers
    WHERE
        "internalRetryCount" < @maxInternalRetryCount::int
),
step_runs_to_fail AS (
    SELECT
        *
    FROM
        step_runs_on_inactive_workers
    WHERE
        "internalRetryCount" >= @maxInternalRetryCount::int
),
deleted_sqis AS (
    DELETE FROM
        "SemaphoreQueueItem" sqi
    -- delete when step run id AND worker id tuples match
    USING
        step_runs_on_inactive_workers srs
    WHERE
        sqi."stepRunId" = srs."id"
        AND sqi."workerId" = srs."workerId"
),
deleted_tqis AS (
    DELETE FROM
        "TimeoutQueueItem" tqi
    -- delete when step run id AND retry count tuples match
    USING
        step_runs_on_inactive_workers srs
    WHERE
        tqi."stepRunId" = srs."id"
        AND tqi."retryCount" = srs."retryCount"
),
inserted_queue_items AS (
    INSERT INTO "QueueItem" (
        "stepRunId",
        "stepId",
        "actionId",
        "scheduleTimeoutAt",
        "stepTimeout",
        "priority",
        "isQueued",
        "tenantId",
        "queue"
    )
    SELECT
        srs."id",
        srs."stepId",
        srs."actionId",
        CURRENT_TIMESTAMP + COALESCE(convert_duration_to_interval(srs."scheduleTimeout"), INTERVAL '5 minutes'),
        srs."stepTimeout",
        -- Queue with priority 4 so that reassignment gets highest priority
        4,
        true,
        srs."tenantId",
        srs."actionId"
    FROM
        step_runs_to_reassign srs
),
updated_step_runs AS (
    UPDATE "StepRun" sr
    SET
        "status" = 'PENDING_ASSIGNMENT',
        "scheduleTimeoutAt" = CURRENT_TIMESTAMP + COALESCE(convert_duration_to_interval(srs."scheduleTimeout"), INTERVAL '5 minutes'),
        "updatedAt" = CURRENT_TIMESTAMP,
        "internalRetryCount" = sr."internalRetryCount" + 1
    FROM step_runs_to_reassign srs
    WHERE sr."id" = srs."id"
    RETURNING sr."id"
)
SELECT
    srs1."id",
    srs1."workerId",
    srs1."retryCount",
    'REASSIGNED' AS "operation"
FROM
    step_runs_to_reassign srs1
UNION ALL
SELECT
    srs2."id",
    srs2."workerId",
    srs2."retryCount",
    'FAILED' AS "operation"
FROM
    step_runs_to_fail srs2;

-- name: ListStepRunsToTimeout :many
SELECT "id"
FROM "StepRun"
WHERE
    "status" = ANY(ARRAY['RUNNING', 'ASSIGNED']::"StepRunStatus"[])
    AND "timeoutAt" < NOW()
    AND "tenantId" = @tenantId::uuid
LIMIT 100;

-- name: RefreshTimeoutBy :one
WITH step_run AS (
    SELECT
        "id",
        "retryCount",
        "tenantId"
    FROM
        "StepRun"
    WHERE
        "id" = @stepRunId::uuid AND
        "tenantId" = @tenantId::uuid
)
INSERT INTO
    "TimeoutQueueItem" (
        "stepRunId",
        "retryCount",
        "timeoutAt",
        "tenantId",
        "isQueued"
    )
SELECT
    sr."id",
    sr."retryCount",
    NOW() + convert_duration_to_interval(sqlc.narg('incrementTimeoutBy')::text),
    sr."tenantId",
    true
FROM
    step_run sr
ON CONFLICT ("stepRunId", "retryCount") DO UPDATE
SET
    "timeoutAt" = "TimeoutQueueItem"."timeoutAt" + convert_duration_to_interval(sqlc.narg('incrementTimeoutBy')::text)
RETURNING "TimeoutQueueItem"."timeoutAt";

-- name: UpdateStepRunUnsetWorkerId :one
WITH oldsr AS (
    SELECT
        "id",
        "retryCount"
    FROM
        "StepRun"
    WHERE
        "id" = @stepRunId::uuid AND
        "tenantId" = @tenantId::uuid
), deleted_sqi AS (
    DELETE FROM
        "SemaphoreQueueItem" sqi
    WHERE
        sqi."stepRunId" = @stepRunId::uuid
    RETURNING sqi."workerId"
)
SELECT
    deleted_sqi."workerId" AS "workerId",
    oldsr."retryCount" AS "retryCount"
FROM
    deleted_sqi, oldsr;

-- name: VerifiedStepRunTenantIds :many
WITH input AS (
    SELECT
        unnest(@stepRunIds::uuid[]) AS "id",
        unnest(@tenantIds::uuid[]) AS "tenantId"
)
SELECT
    sr."id"
FROM "StepRun" sr
JOIN input ON sr."id" = input."id" AND sr."tenantId" = input."tenantId"
-- stable ordering as it minimizes the chance of deadlocks
ORDER BY sr."id";

-- name: UpdateStepRunUnsetWorkerIdBulk :exec
DELETE FROM
    "SemaphoreQueueItem"
WHERE
    "stepRunId" = ANY(@stepRunIds::uuid[]);

-- name: RemoveTimeoutQueueItems :exec
DELETE FROM
    "TimeoutQueueItem"
WHERE
    "stepRunId" = ANY(@stepRunIds::uuid[]);

-- name: CheckWorker :one
SELECT
    "id"
FROM
    "Worker"
WHERE
    "tenantId" = @tenantId::uuid
    AND "dispatcherId" IS NOT NULL
    AND "isActive" = true
    AND "isPaused" = false
    AND "lastHeartbeatAt" > NOW() - INTERVAL '5 seconds'
    AND "id" = @workerId::uuid;

-- name: GetWorkerDispatcherActions :many
WITH actions AS (
    SELECT
        "id",
        "actionId"
    FROM
        "Action"
    WHERE
        "tenantId" = @tenantId::uuid AND
        "actionId" = ANY(@actionIds::text[])
)
SELECT
    w."id",
    a."actionId",
    w."dispatcherId"
FROM
    "Worker" w
JOIN
    "_ActionToWorker" atw ON w."id" = atw."B"
JOIN
    actions a ON atw."A" = a."id"
WHERE
    w."tenantId" = @tenantId::uuid
    AND w."dispatcherId" IS NOT NULL
    AND w."lastHeartbeatAt" > NOW() - INTERVAL '5 seconds'
    AND w."isActive" = true
    AND w."isPaused" = false;

-- name: CreateWorkerAssignEvents :exec
INSERT INTO "WorkerAssignEvent" (
    "workerId",
    "assignedStepRuns"
)
SELECT
    input."workerId",
    input."assignedStepRuns"
FROM (
    SELECT
        unnest(@workerIds::uuid[]) AS "workerId",
        unnest(@assignedStepRuns::jsonb[]) AS "assignedStepRuns"
    ) AS input
RETURNING *;

-- name: UpdateStepRunsToAssigned :many
WITH input AS (
    SELECT
        "id",
        "stepTimeout",
        "workerId"
    FROM
        (
            SELECT
                unnest(@stepRunIds::uuid[]) AS "id",
                unnest(@stepRunTimeouts::text[]) AS "stepTimeout",
                unnest(@workerIds::uuid[]) AS "workerId"
        ) AS subquery
    ORDER BY "id"
), updated_step_runs AS (
    SELECT
        sr."id",
        sr."retryCount",
        sr."tenantId",
        CURRENT_TIMESTAMP + convert_duration_to_interval(input."stepTimeout") AS "timeoutAt"
    FROM
        input
    JOIN
        "StepRun" sr ON sr."id" = input."id"
    ORDER BY sr."id"
), assigned_step_runs AS (
    INSERT INTO "SemaphoreQueueItem" (
        "stepRunId",
        "workerId",
        "tenantId"
    )
    SELECT
        input."id",
        input."workerId",
        @tenantId::uuid
    FROM
        input
    ON CONFLICT ("stepRunId") DO NOTHING
    -- only return the step run ids that were successfully assigned
    RETURNING "stepRunId", "workerId"
), timeout_insert AS (
    -- bulk insert into timeout queue items
    INSERT INTO
        "TimeoutQueueItem" (
            "stepRunId",
            "retryCount",
            "timeoutAt",
            "tenantId",
            "isQueued"
        )
    SELECT
        sr."id",
        sr."retryCount",
        sr."timeoutAt",
        sr."tenantId",
        true
    FROM
        updated_step_runs sr
    JOIN
        assigned_step_runs asr ON sr."id" = asr."stepRunId"
    ON CONFLICT ("stepRunId", "retryCount") DO UPDATE
    SET
        "timeoutAt" = EXCLUDED."timeoutAt"
    RETURNING
        "stepRunId"
)
SELECT
    asr."stepRunId",
    asr."workerId"
FROM
    assigned_step_runs asr;

-- name: GetFinalizedStepRuns :many
SELECT
    "id", "status"
FROM
    "StepRun"
WHERE
    "id" = ANY(@stepRunIds::uuid[])
    AND "status" = ANY(ARRAY['SUCCEEDED', 'FAILED', 'CANCELLED', 'CANCELLING']::"StepRunStatus"[]);

-- name: BulkMarkStepRunsAsCancelling :many
UPDATE
    "StepRun" sr
SET
    "status" = CASE
        -- Final states are final, we cannot go from a final state to cancelling
        WHEN "status" IN ('SUCCEEDED', 'FAILED', 'CANCELLED') THEN "status"
        ELSE 'CANCELLING'
    END,
    "updatedAt" = CURRENT_TIMESTAMP
FROM (
    SELECT
        unnest(@stepRunIds::uuid[]) AS "id"
    ) AS input
WHERE
    sr."id" = input."id"
RETURNING sr."id";

-- name: GetDesiredLabels :many
SELECT
    "key",
    "strValue",
    "intValue",
    "required",
    "weight",
    "comparator",
    "stepId"
FROM
    "StepDesiredWorkerLabel"
WHERE
    "stepId" = ANY(@stepIds::uuid[]);

-- name: GetWorkerLabels :many
SELECT
    "key",
    "strValue",
    "intValue"
FROM
    "WorkerLabel"
WHERE
    "workerId" = @workerId::uuid;

-- name: UpsertDesiredWorkerLabel :one
INSERT INTO "StepDesiredWorkerLabel" (
    "createdAt",
    "updatedAt",
    "stepId",
    "key",
    "intValue",
    "strValue",
    "required",
    "weight",
    "comparator"
) VALUES (
    CURRENT_TIMESTAMP,
    CURRENT_TIMESTAMP,
    @stepId::uuid,
    @key::text,
    COALESCE(sqlc.narg('intValue')::int, NULL),
    COALESCE(sqlc.narg('strValue')::text, NULL),
    COALESCE(sqlc.narg('required')::boolean, false),
    COALESCE(sqlc.narg('weight')::int, 100),
    COALESCE(sqlc.narg('comparator')::"WorkerLabelComparator", 'EQUAL')
) ON CONFLICT ("stepId", "key") DO UPDATE
SET
    "updatedAt" = CURRENT_TIMESTAMP,
    "intValue" = COALESCE(sqlc.narg('intValue')::int, null),
    "strValue" = COALESCE(sqlc.narg('strValue')::text, null),
    "required" = COALESCE(sqlc.narg('required')::boolean, false),
    "weight" = COALESCE(sqlc.narg('weight')::int, 100),
    "comparator" = COALESCE(sqlc.narg('comparator')::"WorkerLabelComparator", 'EQUAL')
RETURNING *;

-- name: GetStepDesiredWorkerLabels :one
SELECT
    jsonb_agg(
        jsonb_build_object(
            'key', dwl."key",
            'strValue', dwl."strValue",
            'intValue', dwl."intValue",
            'required', dwl."required",
            'weight', dwl."weight",
            'comparator', dwl."comparator",
            'is_true', false
        )
    ) AS desired_labels
FROM
    "StepDesiredWorkerLabel" dwl
WHERE
    dwl."stepId" = @stepId::uuid;

-- name: CreateStepRunEvent :exec
WITH input_values AS (
    SELECT
        CURRENT_TIMESTAMP AS "timeFirstSeen",
        CURRENT_TIMESTAMP AS "timeLastSeen",
        @stepRunId::uuid AS "stepRunId",
        @jobRunid::uuid AS "jobRunId",
        @reason::"StepRunEventReason" AS "reason",
        @severity::"StepRunEventSeverity" AS "severity",
        @message::text AS "message",
        1 AS "count",
        sqlc.narg('data')::jsonb AS "data"
),
updated AS (
    UPDATE "StepRunEvent"
    SET
        "timeLastSeen" = CURRENT_TIMESTAMP,
        "message" = input_values."message",
        "count" = "StepRunEvent"."count" + 1,
        "data" = input_values."data"
    FROM input_values
    WHERE
        "StepRunEvent"."stepRunId" = input_values."stepRunId"
        AND "StepRunEvent"."reason" = input_values."reason"
        AND "StepRunEvent"."severity" = input_values."severity"
        AND "StepRunEvent"."id" = (
            SELECT "id"
            FROM "StepRunEvent"
            WHERE "stepRunId" = input_values."stepRunId"
            ORDER BY "id" DESC
            LIMIT 1
        )
    RETURNING "StepRunEvent".*
)
INSERT INTO "StepRunEvent" (
    "timeFirstSeen",
    "timeLastSeen",
    "stepRunId",
    "reason",
    "severity",
    "message",
    "count",
    "data"
)
SELECT
    "timeFirstSeen",
    "timeLastSeen",
    "stepRunId",
    "reason",
    "severity",
    "message",
    "count",
    "data"
FROM input_values
WHERE NOT EXISTS (
    SELECT 1 FROM updated WHERE "stepRunId" = input_values."stepRunId"
);

-- name: BulkCreateStepRunEvent :exec
WITH input_values AS (
    SELECT
        unnest(@timeSeen::timestamp[]) AS "timeFirstSeen",
        unnest(@timeSeen::timestamp[]) AS "timeLastSeen",
        unnest(@stepRunIds::uuid[]) AS "stepRunId",
        unnest(cast(@reasons::text[] as"StepRunEventReason"[])) AS "reason",
        unnest(cast(@severities::text[] as "StepRunEventSeverity"[])) AS "severity",
        unnest(@messages::text[]) AS "message",
        1 AS "count",
        unnest(@data::jsonb[]) AS "data"
),
matched_rows AS (
    SELECT DISTINCT ON (sre."stepRunId")
        sre."stepRunId", sre."reason", sre."severity", sre."id"
    FROM "StepRunEvent" sre
    WHERE
        sre."stepRunId" = ANY(@stepRunIds::uuid[])
    ORDER BY sre."stepRunId", sre."id" DESC
),
locked_rows AS (
    SELECT sre."id", iv.*
    FROM "StepRunEvent" sre
    JOIN
        matched_rows mr ON sre."id" = mr."id"
    JOIN
        input_values iv ON sre."stepRunId" = iv."stepRunId" AND sre."reason" = iv."reason" AND sre."severity" = iv."severity"
    ORDER BY "id"
    FOR UPDATE
),
updated AS (
    UPDATE "StepRunEvent"
    SET
        "timeLastSeen" = locked_rows."timeLastSeen",
        "message" = locked_rows."message",
        "count" = "StepRunEvent"."count" + 1,
        "data" = locked_rows."data"
    FROM locked_rows
    WHERE
        "StepRunEvent"."id" = locked_rows."id"
    RETURNING "StepRunEvent".*
)
INSERT INTO "StepRunEvent" (
    "timeFirstSeen",
    "timeLastSeen",
    "stepRunId",
    "reason",
    "severity",
    "message",
    "count",
    "data"
)
SELECT
    "timeFirstSeen",
    "timeLastSeen",
    "stepRunId",
    "reason",
    "severity",
    "message",
    "count",
    "data"
FROM input_values
WHERE NOT EXISTS (
    SELECT 1 FROM updated WHERE "stepRunId" = input_values."stepRunId" AND "reason" = input_values."reason" AND "severity" = input_values."severity"
);

-- name: CountStepRunEvents :one
SELECT
    count(*) OVER() AS total
FROM
    "StepRunEvent"
WHERE
    "stepRunId" = @stepRunId::uuid;

-- name: ListStepRunEvents :many
SELECT
    *
FROM
    "StepRunEvent"
WHERE
    "stepRunId" = @stepRunId::uuid
ORDER BY
    "id" DESC
OFFSET
    COALESCE(sqlc.narg('offset'), 0)
LIMIT
    COALESCE(sqlc.narg('limit'), 50);


-- name: ListStepRunEventsByWorkflowRunId :many
SELECT
    sre.*
FROM
    "StepRunEvent" sre
JOIN
    "StepRun" sr ON sr."id" = sre."stepRunId"
JOIN
    "JobRun" jr ON jr."id" = sr."jobRunId"
WHERE
    jr."workflowRunId" = @workflowRunId::uuid
    AND jr."tenantId" = @tenantId::uuid
    AND sre."id" > COALESCE(sqlc.narg('lastId'), 0)
    -- / TODO ID > Last ID
ORDER BY
    sre."id" DESC;

-- name: ReplayStepRunResetWorkflowRun :one
UPDATE
    "WorkflowRun"
SET
    "status" = 'PENDING',
    "updatedAt" = CURRENT_TIMESTAMP,
    "startedAt" = NULL,
    "finishedAt" = NULL,
    "duration" = NULL,
    "concurrencyGroupId" = NULL,
    "error" = NULL
WHERE
    "id" =  @workflowRunId::uuid
RETURNING *;


-- name: ReplayWorkflowRunResetGetGroupKeyRun :one
UPDATE
    "GetGroupKeyRun"
SET
    "status" = 'PENDING',
    "scheduleTimeoutAt" = NULL,
    "finishedAt" = NULL,
    "startedAt" = NULL,
    "timeoutAt" = NULL,
    "output" = NULL,
    "error" = NULL,
    "cancelledAt" = NULL,
    "cancelledReason" = NULL,
    "cancelledError" = NULL
WHERE
    "workflowRunId" = @workflowRunId::uuid
RETURNING *;


-- name: ReplayStepRunResetJobRun :one
UPDATE
    "JobRun"
SET
    "status" = 'RUNNING',
    "updatedAt" = CURRENT_TIMESTAMP,
    "startedAt" = NULL,
    "finishedAt" = NULL,
    "timeoutAt" = NULL,
    "cancelledAt" = NULL,
    "cancelledReason" = NULL,
    "cancelledError" = NULL
WHERE
    "id" = @jobRunId::uuid
RETURNING *;

-- name: GetLaterStepRuns :many
WITH RECURSIVE currStepRun AS (
    SELECT *
    FROM "StepRun"
    WHERE
        "id" = @stepRunId::uuid
), childStepRuns AS (
    SELECT sr."id", sr."status"
    FROM "StepRun" sr
    JOIN "_StepRunOrder" sro ON sr."id" = sro."B"
    WHERE sro."A" = (SELECT "id" FROM currStepRun)

    UNION ALL

    SELECT sr."id", sr."status"
    FROM "StepRun" sr
    JOIN "_StepRunOrder" sro ON sr."id" = sro."B"
    JOIN childStepRuns csr ON sro."A" = csr."id"
)
SELECT
    sr.*
FROM
    "StepRun" sr
JOIN
    childStepRuns csr ON sr."id" = csr."id";

-- name: ReplayStepRunResetStepRuns :many
WITH RECURSIVE currStepRun AS (
    SELECT *
    FROM "StepRun"
    WHERE
        "id" = @stepRunId::uuid
), childStepRuns AS (
    SELECT sr."id", sr."status"
    FROM "StepRun" sr
    JOIN "_StepRunOrder" sro ON sr."id" = sro."B"
    WHERE sro."A" = (SELECT "id" FROM currStepRun)

    UNION ALL

    SELECT sr."id", sr."status"
    FROM "StepRun" sr
    JOIN "_StepRunOrder" sro ON sr."id" = sro."B"
    JOIN childStepRuns csr ON sro."A" = csr."id"
)
UPDATE
    "StepRun" as sr
SET
    "status" = 'PENDING',
    "scheduleTimeoutAt" = NULL,
    "finishedAt" = NULL,
    "startedAt" = NULL,
    "output" = NULL,
    "error" = NULL,
    "cancelledAt" = NULL,
    "cancelledReason" = NULL,
    "input" = CASE
        WHEN sr."id" = @stepRunId::uuid THEN COALESCE(sqlc.narg('input')::jsonb, "input")
        ELSE NULL
    END,
    "retryCount" = 0
FROM
    childStepRuns csr
WHERE
    sr."id" = csr."id" OR
    sr."id" = @stepRunId::uuid
RETURNING sr.*;

-- name: ResetStepRunsByIds :many
UPDATE
    "StepRun" as sr
SET
    "status" = 'PENDING',
    "scheduleTimeoutAt" = NULL,
    "finishedAt" = NULL,
    "startedAt" = NULL,
    "output" = NULL,
    "error" = NULL,
    "cancelledAt" = NULL,
    "cancelledReason" = NULL,
    "input" = NULL,
    "retryCount" = 0
WHERE
    sr."id" = ANY(@ids::uuid[]) AND
    sr."tenantId" = @tenantId::uuid
RETURNING sr.*;

-- name: ListNonFinalChildStepRuns :many
WITH RECURSIVE currStepRun AS (
    SELECT *
    FROM "StepRun"
    WHERE
        "id" = @stepRunId::uuid
), childStepRuns AS (
    SELECT sr."id", sr."status"
    FROM "StepRun" sr
    JOIN "_StepRunOrder" sro ON sr."id" = sro."B"
    WHERE sro."A" = (SELECT "id" FROM currStepRun)
        AND sr."deletedAt" IS NULL

    UNION ALL

    SELECT sr."id", sr."status"
    FROM "StepRun" sr
    JOIN "_StepRunOrder" sro ON sr."id" = sro."B"
    JOIN childStepRuns csr ON sro."A" = csr."id"
)
-- Select all child step runs that are not in a final state
SELECT
    sr.*
FROM
    "StepRun" sr
JOIN
    childStepRuns csr ON sr."id" = csr."id"
WHERE
    sr."deletedAt" IS NULL AND
    sr."status" NOT IN ('SUCCEEDED', 'FAILED', 'CANCELLED');

-- name: ListStepRunArchives :many
SELECT
    "StepRunResultArchive".*
FROM
    "StepRunResultArchive"
JOIN
    "StepRun" ON "StepRunResultArchive"."stepRunId" = "StepRun"."id"
WHERE
    "StepRunResultArchive"."stepRunId" = @stepRunId::uuid AND
    "StepRun"."tenantId" = @tenantId::uuid AND
    "StepRun"."deletedAt" IS NULL
ORDER BY
    "StepRunResultArchive"."createdAt" DESC
OFFSET
    COALESCE(sqlc.narg('offset'), 0)
LIMIT
    COALESCE(sqlc.narg('limit'), 50);

-- name: CountStepRunArchives :one
SELECT
    count(*) OVER() AS total
FROM
    "StepRunResultArchive"
WHERE
    "stepRunId" = @stepRunId::uuid;


-- name: ClearStepRunPayloadData :one
WITH for_delete AS (
    SELECT
        sr2."id"
    FROM "StepRun" sr2
    WHERE
        sr2."tenantId" = @tenantId::uuid AND
        sr2."deletedAt" IS NOT NULL AND
        (sr2."input" IS NOT NULL OR sr2."output" IS NOT NULL OR sr2."error" IS NOT NULL)
    ORDER BY "deletedAt" ASC
    LIMIT sqlc.arg('limit') + 1
),
deleted_with_limit AS (
    SELECT
        for_delete."id" as "id"
    FROM for_delete
    LIMIT sqlc.arg('limit')
),
deleted_archives AS (
    SELECT sra1."id" as "id"
    FROM "StepRunResultArchive" sra1
    WHERE
        sra1."stepRunId" IN (SELECT "id" FROM deleted_with_limit)
        AND (sra1."input" IS NOT NULL OR sra1."output" IS NOT NULL OR sra1."error" IS NOT NULL)
),
has_more AS (
    SELECT
        CASE
            WHEN COUNT(*) > sqlc.arg('limit') THEN TRUE
            ELSE FALSE
        END as has_more
    FROM for_delete
),
cleared_archives AS (
    UPDATE "StepRunResultArchive"
    SET
        "input" = NULL,
        "output" = NULL,
        "error" = NULL
    WHERE
        "id" IN (SELECT "id" FROM deleted_archives)
)
UPDATE
    "StepRun"
SET
    "input" = NULL,
    "output" = NULL,
    "error" = NULL
WHERE
    "id" IN (SELECT "id" FROM deleted_with_limit)
RETURNING
    (SELECT has_more FROM has_more) as has_more;

-- name: HasActiveWorkersForActionId :one
SELECT
    COUNT(DISTINCT w."id") AS "total"
FROM
    "Worker" w
JOIN
    "_ActionToWorker" atw ON w."id" = atw."B"
JOIN
    "Action" a ON atw."A" = a."id"
WHERE
    w."tenantId" = @tenantId::uuid
    AND a."actionId" = @actionId::text
    AND w."isActive" = true
    AND w."lastHeartbeatAt" > NOW() - INTERVAL '6 seconds';

-- name: ListChildWorkflowRunIds :many
SELECT
    "id"
FROM
    "WorkflowRun"
WHERE
    "parentStepRunId" = @stepRun::uuid
    AND "tenantId" = @tenantId::uuid
    AND "deletedAt" IS NULL;
