Skip to main content

icydb_core/db/
mutation_job.rs

1//! Module: db::mutation_job
2//! Responsibility: bounded durable mutation-job lifecycle, replay, and current payload codec.
3//! Does not own: SQL lowering, target traversal, row mutation, or commit-marker recovery.
4//! Boundary: trusted mutation coordinator -> excluded progress-record envelope.
5
6#[cfg(feature = "sql")]
7mod intent;
8
9use crate::db::codec::{ByteDecodeError, ByteReader};
10
11use serde::Deserialize;
12use std::{error::Error as StdError, fmt};
13
14#[cfg(feature = "sql")]
15pub(in crate::db) use intent::CanonicalMutationIntent;
16
17/// Maximum UTF-8 bytes in one mutation-job idempotency key.
18pub const MAX_MUTATION_JOB_IDEMPOTENCY_KEY_BYTES: usize = 256;
19/// Maximum current engine-continuation bytes retained by one mutation job.
20pub const MAX_MUTATION_JOB_CONTINUATION_BYTES: usize = 2 * 1024;
21/// Maximum canonical accepted-intent bytes retained by one mutation job.
22pub const MAX_MUTATION_JOB_INTENT_BYTES: usize = 16 * 1024;
23/// Maximum encoded replay-receipt bytes retained by one mutation job.
24pub const MAX_MUTATION_JOB_RECEIPT_BYTES: usize = 8 * 1024;
25/// Maximum complete encoded mutation-job record, including its storage envelope.
26pub const MAX_MUTATION_JOB_RECORD_BYTES: usize = 64 * 1024;
27
28/// Maximum authoritative keys examined by one mutation-job advance.
29pub const MAX_MUTATION_JOB_STEP_KEYS_SCANNED: u64 = 4_096;
30/// Maximum target rows changed by one mutation-job advance.
31pub const MAX_MUTATION_JOB_STEP_ROWS_UPDATED: u64 =
32    crate::db::executor::MAX_MUTATION_PROGRESS_BATCH_ROWS_AT_MAX_INDEX_FANOUT as u64;
33
34/// Nonzero application-owned identity for one durable mutation job incarnation.
35///
36/// An application must allocate a fresh identity for every logical job. An
37/// identity is never reusable after cancellation, acknowledgement, failure,
38/// or an absent-record response.
39#[derive(
40    serde::Serialize, Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd,
41)]
42pub struct MutationJobId([u8; 32]);
43
44impl MutationJobId {
45    /// Admit one nonzero application-owned identity.
46    pub fn try_from_bytes(bytes: [u8; 32]) -> Result<Self, MutationJobError> {
47        if bytes == [0; 32] {
48            return Err(MutationJobError::InvalidJobId);
49        }
50        Ok(Self(bytes))
51    }
52
53    /// Return the application-owned identity bytes.
54    #[must_use]
55    pub const fn to_bytes(self) -> [u8; 32] {
56        self.0
57    }
58
59    pub(in crate::db) fn validate(self) -> Result<(), MutationJobError> {
60        if self.0 == [0; 32] {
61            return Err(MutationJobError::InvalidJobId);
62        }
63        Ok(())
64    }
65}
66
67/// Bounded request identity reused exactly after a lost response.
68#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
69pub struct MutationJobIdempotencyKey(String);
70
71impl MutationJobIdempotencyKey {
72    /// Admit one nonempty bounded UTF-8 request identity.
73    pub fn new(value: impl Into<String>) -> Result<Self, MutationJobError> {
74        let value = value.into();
75        if value.is_empty() || value.len() > MAX_MUTATION_JOB_IDEMPOTENCY_KEY_BYTES {
76            return Err(MutationJobError::InvalidIdempotencyKey);
77        }
78        Ok(Self(value))
79    }
80
81    /// Borrow the request identity.
82    #[must_use]
83    pub const fn as_str(&self) -> &str {
84        self.0.as_str()
85    }
86
87    const fn validate(&self) -> Result<(), MutationJobError> {
88        if self.0.is_empty() || self.0.len() > MAX_MUTATION_JOB_IDEMPOTENCY_KEY_BYTES {
89            return Err(MutationJobError::InvalidIdempotencyKey);
90        }
91        Ok(())
92    }
93}
94
95/// Terminal reason why a valid retained mutation job cannot continue safely.
96#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
97pub enum MutationJobRestartReason {
98    /// The accepted schema identity changed after the job started.
99    AcceptedSchemaChanged,
100    /// The target allocation or database incarnation changed.
101    TargetAllocationChanged,
102    /// The frozen canonical intent is no longer eligible.
103    IntentIneligible,
104    /// The engine-owned batch-policy identity changed.
105    BatchPolicyChanged,
106    /// The retained current record names an unsupported internal continuation.
107    UnsupportedContinuation,
108    /// The current managed-write time would move a target row backward.
109    ManagedTimestampRegression,
110    /// One valid mutation candidate cannot fit the current fixed page policy.
111    CandidateExceedsBatchPolicy,
112    /// Admitted page work exceeded the execution policy calibrated for it.
113    ExecutionBudgetPolicyExceeded,
114}
115
116/// Bounded reason why target mutation execution failed before commit.
117#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
118pub enum MutationJobTargetFailureReason {
119    /// Writer and page-packer staged-byte authorities diverged.
120    StagingByteBudgetExceeded,
121    /// Another target mutation failure occurred without safe semantic detail.
122    Other,
123}
124
125/// Durable lifecycle of one mutation job.
126#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
127pub enum MutationJobStatus {
128    /// One bounded Forward or Verify step may still run.
129    Active,
130    /// Stable clean Verify exhaustion committed.
131    Completed,
132    /// Authority or policy drift requires a new job.
133    RestartRequired(MutationJobRestartReason),
134}
135
136/// Current convergence phase of one mutation job.
137#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
138pub enum MutationJobPhase {
139    /// Scan authoritative keys and converge stale rows.
140    Forward,
141    /// Prove a clean scan at one unchanged durable target revision.
142    Verify,
143}
144
145/// Public bounded state for one retained mutation job.
146#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
147pub struct MutationJobState {
148    /// Application-owned job identity.
149    pub job_id: MutationJobId,
150    /// Sequence expected by the next non-replay advance.
151    pub sequence: u64,
152    /// Current durable lifecycle.
153    pub status: MutationJobStatus,
154    /// Current convergence phase.
155    pub phase: MutationJobPhase,
156    /// Authoritative keys examined across committed advances.
157    pub keys_scanned_total: u64,
158    /// Rows changed across committed advances.
159    pub rows_updated_total: u64,
160    /// Verify passes restarted because stable convergence was not proven.
161    pub verify_restarts_total: u64,
162}
163
164/// Identity and expected sequence for one idempotent bounded advance.
165#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
166pub struct MutationJobAdvanceRequest {
167    /// Target mutation job.
168    pub job_id: MutationJobId,
169    /// Exact sequence observed before issuing this request.
170    pub expected_sequence: u64,
171    /// Stable request identity reused after a lost reply.
172    pub idempotency_key: MutationJobIdempotencyKey,
173}
174
175impl MutationJobAdvanceRequest {
176    /// Construct one request from already admitted identities.
177    #[must_use]
178    pub const fn new(
179        job_id: MutationJobId,
180        expected_sequence: u64,
181        idempotency_key: MutationJobIdempotencyKey,
182    ) -> Self {
183        Self {
184            job_id,
185            expected_sequence,
186            idempotency_key,
187        }
188    }
189
190    fn validate(&self) -> Result<(), MutationJobError> {
191        self.job_id.validate()?;
192        self.idempotency_key.validate()
193    }
194}
195
196/// Replayable result of one committed bounded advance.
197#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
198pub struct MutationJobAdvanceReceipt {
199    /// Sequence named by the request.
200    pub request_sequence: u64,
201    /// Durable sequence after the advance committed.
202    pub committed_sequence: u64,
203    /// Durable lifecycle after the advance committed.
204    pub status: MutationJobStatus,
205    /// Durable convergence phase after the advance committed.
206    pub phase: MutationJobPhase,
207    /// Authoritative keys examined by this advance.
208    pub keys_scanned: u64,
209    /// Rows changed by this advance.
210    pub rows_updated: u64,
211    /// Authoritative keys examined across committed advances.
212    pub keys_scanned_total: u64,
213    /// Rows changed across committed advances.
214    pub rows_updated_total: u64,
215    /// Verify restarts across committed advances.
216    pub verify_restarts_total: u64,
217}
218
219/// Variable-sized component constrained by the durable record protocol.
220#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
221pub enum MutationJobPayloadKind {
222    /// Canonical accepted mutation intent.
223    Intent,
224    /// Private engine continuation.
225    Continuation,
226    /// Retained replay receipt and request identity.
227    Receipt,
228    /// Complete progress-store record envelope.
229    Record,
230}
231
232/// Typed mutation-job protocol, lifecycle, or persistence failure.
233#[derive(Clone, Debug, Deserialize, Eq, PartialEq, serde::Serialize)]
234pub enum MutationJobError {
235    /// Job identity was all zeroes.
236    InvalidJobId,
237    /// Idempotency key was empty or exceeded its byte bound.
238    InvalidIdempotencyKey,
239    /// A retained job id was reused for different canonical meaning.
240    IdentityConflict,
241    /// The requested job does not exist.
242    NotFound,
243    /// The request did not name the job's current sequence.
244    StaleSequence { expected: u64, actual: u64 },
245    /// Acknowledgement targeted an active job.
246    Active,
247    /// A non-replay advance targeted a completed job.
248    Completed,
249    /// A non-replay advance targeted a job that must restart.
250    RestartRequired(MutationJobRestartReason),
251    /// A bounded persisted component exceeded its engine-owned limit.
252    PayloadTooLarge {
253        kind: MutationJobPayloadKind,
254        limit: u64,
255        observed: u64,
256    },
257    /// Current accepted authority no longer matches the frozen intent.
258    AuthorityMismatch,
259    /// The requested mutation cannot be represented by the fixed-intent engine.
260    IneligibleIntent,
261    /// The shared excluded progress store reached its hard capacity.
262    CapacityExceeded,
263    /// A checked sequence or cumulative counter would overflow.
264    CounterOverflow,
265    /// Retained progress bytes or their state closure were corrupt.
266    CorruptProgressStore,
267    /// Retained progress bytes use an unsupported format version.
268    IncompatibleProgressFormat,
269    /// Commit or recovery evidence cannot prove one exact transition.
270    CommitCorruption,
271    /// Target mutation execution failed before progress committed.
272    TargetMutationFailed(MutationJobTargetFailureReason),
273    /// Target or recovery admission, or traversal, failed before progress committed.
274    TargetQueryFailed,
275    /// An internal database invariant prevented the operation.
276    Internal,
277    /// The enclosing request exhausted aggregate IcyDB work allowance.
278    ExecutionBudgetExceeded {
279        resource: u64,
280        limit: u64,
281        observed: u64,
282        scope: u64,
283        lane: u64,
284        normalized_shape_fingerprint_prefix: u64,
285    },
286}
287
288#[cfg(feature = "sql")]
289impl MutationJobError {
290    // Identity preparation failures are not evidence of corrupt persisted data
291    // or ineligible syntax. Keep the existing bounded resource error intact.
292    pub(in crate::db) fn from_internal_error(error: &crate::error::InternalError) -> Self {
293        use icydb_diagnostic_code::{
294            DiagnosticDetail, DiagnosticFactTag as Fact, RuntimeBoundaryCode,
295        };
296
297        if !matches!(
298            error.diagnostic().detail(),
299            Some(DiagnosticDetail::RuntimeBoundary {
300                boundary: RuntimeBoundaryCode::ExecutionBudgetExceeded,
301            })
302        ) {
303            return Self::Internal;
304        }
305        let facts = error.diagnostic_facts();
306        let [
307            Some(resource),
308            Some(limit),
309            Some(observed),
310            Some(scope),
311            Some(lane),
312            Some(prefix),
313        ] = [
314            Fact::BudgetResource,
315            Fact::Limit,
316            Fact::Actual,
317            Fact::ExecutionBudgetScope,
318            Fact::ExecutionLane,
319            Fact::QueryShapeFingerprintPrefix,
320        ]
321        .map(|tag| {
322            facts
323                .iter()
324                .find(|(fact, _)| *fact == tag)
325                .map(|(_, value)| *value)
326        })
327        else {
328            return Self::Internal;
329        };
330        Self::ExecutionBudgetExceeded {
331            resource,
332            limit,
333            observed,
334            scope,
335            lane,
336            normalized_shape_fingerprint_prefix: prefix,
337        }
338    }
339
340    pub(in crate::db) fn from_query_error(error: crate::db::query::intent::QueryError) -> Self {
341        match error {
342            crate::db::query::intent::QueryError::Execute(error) => {
343                if matches!(
344                    error.diagnostic().detail(),
345                    Some(icydb_diagnostic_code::DiagnosticDetail::SqlWriteBoundary { .. })
346                ) {
347                    Self::IneligibleIntent
348                } else {
349                    Self::from_internal_error(error.as_internal())
350                }
351            }
352            _ => Self::IneligibleIntent,
353        }
354    }
355}
356
357impl fmt::Display for MutationJobError {
358    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
359        formatter.write_str("mutation job operation failed")
360    }
361}
362
363impl From<ByteDecodeError> for MutationJobError {
364    fn from(_: ByteDecodeError) -> Self {
365        Self::CorruptProgressStore
366    }
367}
368
369impl StdError for MutationJobError {}
370
371#[derive(Clone, Debug, Eq, PartialEq)]
372struct RetainedMutationJobReceipt {
373    receipt: MutationJobAdvanceReceipt,
374    idempotency_key: MutationJobIdempotencyKey,
375}
376
377#[derive(Clone, Debug, Eq, PartialEq)]
378pub(in crate::db) struct MutationJobRecord {
379    state: MutationJobState,
380    canonical_intent: Vec<u8>,
381    engine_continuation: Vec<u8>,
382    last_receipt: Option<RetainedMutationJobReceipt>,
383}
384
385impl MutationJobRecord {
386    pub(in crate::db) fn new(
387        job_id: MutationJobId,
388        canonical_intent: Vec<u8>,
389        engine_continuation: Vec<u8>,
390    ) -> Result<Self, MutationJobError> {
391        let record = Self {
392            state: MutationJobState {
393                job_id,
394                sequence: 0,
395                status: MutationJobStatus::Active,
396                phase: MutationJobPhase::Forward,
397                keys_scanned_total: 0,
398                rows_updated_total: 0,
399                verify_restarts_total: 0,
400            },
401            canonical_intent,
402            engine_continuation,
403            last_receipt: None,
404        };
405        record.validate()?;
406        Ok(record)
407    }
408
409    pub(in crate::db) const fn state(&self) -> &MutationJobState {
410        &self.state
411    }
412
413    pub(in crate::db) const fn canonical_intent(&self) -> &[u8] {
414        self.canonical_intent.as_slice()
415    }
416
417    pub(in crate::db) const fn engine_continuation(&self) -> &[u8] {
418        self.engine_continuation.as_slice()
419    }
420
421    /// Prove that cancellation can remove only the exact initial record.
422    pub(in crate::db) fn ensure_cancelable_at_sequence(
423        &self,
424        expected_sequence: u64,
425    ) -> Result<&[u8], MutationJobError> {
426        self.validate()?;
427        if self.state.sequence != expected_sequence {
428            return Err(MutationJobError::StaleSequence {
429                expected: expected_sequence,
430                actual: self.state.sequence,
431            });
432        }
433        if self.state.sequence != 0 {
434            return Err(MutationJobError::StaleSequence {
435                expected: 0,
436                actual: self.state.sequence,
437            });
438        }
439        if self.state.status != MutationJobStatus::Active
440            || self.state.phase != MutationJobPhase::Forward
441            || self.state.keys_scanned_total != 0
442            || self.state.rows_updated_total != 0
443            || self.state.verify_restarts_total != 0
444            || self.last_receipt.is_some()
445        {
446            return Err(MutationJobError::CorruptProgressStore);
447        }
448        Ok(self.engine_continuation())
449    }
450
451    pub(in crate::db) fn exact_replay(
452        &self,
453        request: &MutationJobAdvanceRequest,
454    ) -> Result<Option<&MutationJobAdvanceReceipt>, MutationJobError> {
455        self.validate()?;
456        request.validate()?;
457        if request.job_id != self.state.job_id {
458            return Err(MutationJobError::NotFound);
459        }
460        Ok(self.last_receipt.as_ref().and_then(|retained| {
461            (retained.receipt.request_sequence == request.expected_sequence
462                && retained.idempotency_key == request.idempotency_key)
463                .then_some(&retained.receipt)
464        }))
465    }
466
467    pub(in crate::db) fn ensure_can_advance(
468        &self,
469        request: &MutationJobAdvanceRequest,
470    ) -> Result<(), MutationJobError> {
471        self.validate()?;
472        request.validate()?;
473        if request.job_id != self.state.job_id {
474            return Err(MutationJobError::NotFound);
475        }
476        if self.state.sequence != request.expected_sequence {
477            return Err(MutationJobError::StaleSequence {
478                expected: request.expected_sequence,
479                actual: self.state.sequence,
480            });
481        }
482        match self.state.status {
483            MutationJobStatus::Active => Ok(()),
484            MutationJobStatus::Completed => Err(MutationJobError::Completed),
485            MutationJobStatus::RestartRequired(reason) => {
486                Err(MutationJobError::RestartRequired(reason))
487            }
488        }
489    }
490
491    pub(in crate::db) fn apply_transition(
492        &self,
493        request: &MutationJobAdvanceRequest,
494        transition: MutationJobTransition,
495    ) -> Result<(Self, MutationJobAdvanceReceipt), MutationJobError> {
496        self.ensure_can_advance(request)?;
497        transition.validate(self.state.phase)?;
498        let committed_sequence = self
499            .state
500            .sequence
501            .checked_add(1)
502            .ok_or(MutationJobError::CounterOverflow)?;
503        let keys_scanned_total = self
504            .state
505            .keys_scanned_total
506            .checked_add(transition.keys_scanned)
507            .ok_or(MutationJobError::CounterOverflow)?;
508        let rows_updated_total = self
509            .state
510            .rows_updated_total
511            .checked_add(transition.rows_updated)
512            .ok_or(MutationJobError::CounterOverflow)?;
513        let verify_restarts_total = self
514            .state
515            .verify_restarts_total
516            .checked_add(transition.verify_restarts)
517            .ok_or(MutationJobError::CounterOverflow)?;
518        let receipt = MutationJobAdvanceReceipt {
519            request_sequence: request.expected_sequence,
520            committed_sequence,
521            status: transition.status,
522            phase: transition.phase,
523            keys_scanned: transition.keys_scanned,
524            rows_updated: transition.rows_updated,
525            keys_scanned_total,
526            rows_updated_total,
527            verify_restarts_total,
528        };
529        let record = Self {
530            state: MutationJobState {
531                job_id: self.state.job_id,
532                sequence: committed_sequence,
533                status: transition.status,
534                phase: transition.phase,
535                keys_scanned_total,
536                rows_updated_total,
537                verify_restarts_total,
538            },
539            canonical_intent: self.canonical_intent.clone(),
540            engine_continuation: transition.engine_continuation,
541            last_receipt: Some(RetainedMutationJobReceipt {
542                receipt: receipt.clone(),
543                idempotency_key: request.idempotency_key.clone(),
544            }),
545        };
546        record.validate()?;
547        Ok((record, receipt))
548    }
549
550    pub(in crate::db) fn validate(&self) -> Result<(), MutationJobError> {
551        self.state.job_id.validate()?;
552        validate_nonempty_bytes(
553            &self.canonical_intent,
554            MAX_MUTATION_JOB_INTENT_BYTES,
555            MutationJobPayloadKind::Intent,
556        )?;
557        validate_bytes(
558            &self.engine_continuation,
559            MAX_MUTATION_JOB_CONTINUATION_BYTES,
560            MutationJobPayloadKind::Continuation,
561        )?;
562        if self.state.rows_updated_total > self.state.keys_scanned_total
563            || matches!(self.state.status, MutationJobStatus::Active)
564                && self.engine_continuation.is_empty()
565            || matches!(self.state.status, MutationJobStatus::Completed)
566                && (self.state.phase != MutationJobPhase::Verify
567                    || !self.engine_continuation.is_empty())
568            || matches!(self.state.status, MutationJobStatus::RestartRequired(_))
569                && !self.engine_continuation.is_empty()
570        {
571            return Err(MutationJobError::CorruptProgressStore);
572        }
573        match &self.last_receipt {
574            None => {
575                if self.state.sequence != 0
576                    || self.state.status != MutationJobStatus::Active
577                    || self.state.phase != MutationJobPhase::Forward
578                    || self.state.keys_scanned_total != 0
579                    || self.state.rows_updated_total != 0
580                    || self.state.verify_restarts_total != 0
581                {
582                    return Err(MutationJobError::CorruptProgressStore);
583                }
584            }
585            Some(retained) => {
586                retained.idempotency_key.validate()?;
587                validate_receipt(&retained.receipt)?;
588                if retained.receipt.committed_sequence != self.state.sequence
589                    || retained.receipt.request_sequence.checked_add(1)
590                        != Some(retained.receipt.committed_sequence)
591                    || retained.receipt.status != self.state.status
592                    || retained.receipt.phase != self.state.phase
593                    || retained.receipt.keys_scanned_total != self.state.keys_scanned_total
594                    || retained.receipt.rows_updated_total != self.state.rows_updated_total
595                    || retained.receipt.verify_restarts_total != self.state.verify_restarts_total
596                {
597                    return Err(MutationJobError::CorruptProgressStore);
598                }
599                let receipt_bytes = retained_receipt_encoded_len(retained)?;
600                if receipt_bytes > MAX_MUTATION_JOB_RECEIPT_BYTES {
601                    return Err(payload_too_large(
602                        MutationJobPayloadKind::Receipt,
603                        MAX_MUTATION_JOB_RECEIPT_BYTES,
604                        receipt_bytes,
605                    ));
606                }
607            }
608        }
609        Ok(())
610    }
611}
612
613#[derive(Clone, Debug, Eq, PartialEq)]
614pub(in crate::db) struct MutationJobTransition {
615    status: MutationJobStatus,
616    phase: MutationJobPhase,
617    engine_continuation: Vec<u8>,
618    keys_scanned: u64,
619    rows_updated: u64,
620    verify_restarts: u64,
621}
622
623impl MutationJobTransition {
624    pub(in crate::db) const fn new(
625        status: MutationJobStatus,
626        phase: MutationJobPhase,
627        engine_continuation: Vec<u8>,
628        keys_scanned: u64,
629        rows_updated: u64,
630        verify_restarts: u64,
631    ) -> Self {
632        Self {
633            status,
634            phase,
635            engine_continuation,
636            keys_scanned,
637            rows_updated,
638            verify_restarts,
639        }
640    }
641
642    fn validate(&self, previous_phase: MutationJobPhase) -> Result<(), MutationJobError> {
643        validate_bytes(
644            &self.engine_continuation,
645            MAX_MUTATION_JOB_CONTINUATION_BYTES,
646            MutationJobPayloadKind::Continuation,
647        )?;
648        let expected_verify_restarts = u64::from(
649            previous_phase == MutationJobPhase::Verify
650                && self.phase == MutationJobPhase::Forward
651                && self.status == MutationJobStatus::Active,
652        );
653        if self.keys_scanned > MAX_MUTATION_JOB_STEP_KEYS_SCANNED
654            || self.rows_updated > MAX_MUTATION_JOB_STEP_ROWS_UPDATED
655            || self.rows_updated > self.keys_scanned
656            || matches!(self.status, MutationJobStatus::Active)
657                && self.engine_continuation.is_empty()
658            || previous_phase == MutationJobPhase::Verify && self.rows_updated != 0
659            || self.verify_restarts != expected_verify_restarts
660            || matches!(self.status, MutationJobStatus::Completed)
661                && (previous_phase != MutationJobPhase::Verify
662                    || self.phase != MutationJobPhase::Verify
663                    || self.rows_updated != 0
664                    || !self.engine_continuation.is_empty())
665            || matches!(self.status, MutationJobStatus::RestartRequired(_))
666                && (self.phase != previous_phase
667                    || !self.engine_continuation.is_empty()
668                    || self.keys_scanned != 0
669                    || self.rows_updated != 0)
670        {
671            return Err(MutationJobError::CorruptProgressStore);
672        }
673        Ok(())
674    }
675}
676
677pub(in crate::db) fn encode_mutation_job_payload(
678    record: &MutationJobRecord,
679) -> Result<Vec<u8>, MutationJobError> {
680    record.validate()?;
681    let mut bytes = Vec::new();
682    bytes.extend_from_slice(&record.state.job_id.to_bytes());
683    bytes.extend_from_slice(&record.state.sequence.to_be_bytes());
684    write_status(&mut bytes, record.state.status);
685    write_phase(&mut bytes, record.state.phase);
686    bytes.extend_from_slice(&record.state.keys_scanned_total.to_be_bytes());
687    bytes.extend_from_slice(&record.state.rows_updated_total.to_be_bytes());
688    bytes.extend_from_slice(&record.state.verify_restarts_total.to_be_bytes());
689    write_bytes(&mut bytes, &record.canonical_intent)?;
690    write_bytes(&mut bytes, &record.engine_continuation)?;
691    match &record.last_receipt {
692        None => bytes.push(0),
693        Some(retained) => {
694            bytes.push(1);
695            let receipt = &retained.receipt;
696            bytes.extend_from_slice(&receipt.request_sequence.to_be_bytes());
697            bytes.extend_from_slice(&receipt.committed_sequence.to_be_bytes());
698            write_status(&mut bytes, receipt.status);
699            write_phase(&mut bytes, receipt.phase);
700            bytes.extend_from_slice(&receipt.keys_scanned.to_be_bytes());
701            bytes.extend_from_slice(&receipt.rows_updated.to_be_bytes());
702            bytes.extend_from_slice(&receipt.keys_scanned_total.to_be_bytes());
703            bytes.extend_from_slice(&receipt.rows_updated_total.to_be_bytes());
704            bytes.extend_from_slice(&receipt.verify_restarts_total.to_be_bytes());
705            write_bytes(&mut bytes, retained.idempotency_key.as_str().as_bytes())?;
706        }
707    }
708    Ok(bytes)
709}
710
711pub(in crate::db) fn decode_mutation_job_payload(
712    bytes: &[u8],
713) -> Result<MutationJobRecord, MutationJobError> {
714    if bytes.len() > MAX_MUTATION_JOB_RECORD_BYTES {
715        return Err(MutationJobError::CorruptProgressStore);
716    }
717    let mut reader = ByteReader::new(bytes);
718    let job_id = MutationJobId::try_from_bytes(reader.read_array()?)
719        .map_err(|_| MutationJobError::CorruptProgressStore)?;
720    let state = MutationJobState {
721        job_id,
722        sequence: reader.read_u64()?,
723        status: read_status(&mut reader)?,
724        phase: read_phase(&mut reader)?,
725        keys_scanned_total: reader.read_u64()?,
726        rows_updated_total: reader.read_u64()?,
727        verify_restarts_total: reader.read_u64()?,
728    };
729    let canonical_intent = reader
730        .read_bounded_len_prefixed_bytes(MAX_MUTATION_JOB_INTENT_BYTES)?
731        .to_vec();
732    let engine_continuation = reader
733        .read_bounded_len_prefixed_bytes(MAX_MUTATION_JOB_CONTINUATION_BYTES)?
734        .to_vec();
735    let last_receipt = match reader.read_u8()? {
736        0 => None,
737        1 => {
738            let receipt = MutationJobAdvanceReceipt {
739                request_sequence: reader.read_u64()?,
740                committed_sequence: reader.read_u64()?,
741                status: read_status(&mut reader)?,
742                phase: read_phase(&mut reader)?,
743                keys_scanned: reader.read_u64()?,
744                rows_updated: reader.read_u64()?,
745                keys_scanned_total: reader.read_u64()?,
746                rows_updated_total: reader.read_u64()?,
747                verify_restarts_total: reader.read_u64()?,
748            };
749            let idempotency_key = MutationJobIdempotencyKey::new(
750                reader.read_bounded_string(MAX_MUTATION_JOB_IDEMPOTENCY_KEY_BYTES)?,
751            )
752            .map_err(|_| MutationJobError::CorruptProgressStore)?;
753            Some(RetainedMutationJobReceipt {
754                receipt,
755                idempotency_key,
756            })
757        }
758        _ => return Err(MutationJobError::CorruptProgressStore),
759    };
760    reader.finish()?;
761    let record = MutationJobRecord {
762        state,
763        canonical_intent,
764        engine_continuation,
765        last_receipt,
766    };
767    record
768        .validate()
769        .map_err(|_| MutationJobError::CorruptProgressStore)?;
770    Ok(record)
771}
772
773fn validate_receipt(receipt: &MutationJobAdvanceReceipt) -> Result<(), MutationJobError> {
774    if receipt.keys_scanned > MAX_MUTATION_JOB_STEP_KEYS_SCANNED
775        || receipt.rows_updated > MAX_MUTATION_JOB_STEP_ROWS_UPDATED
776        || receipt.rows_updated > receipt.keys_scanned
777        || receipt.rows_updated_total > receipt.keys_scanned_total
778        || receipt.keys_scanned > receipt.keys_scanned_total
779        || receipt.rows_updated > receipt.rows_updated_total
780        || matches!(receipt.status, MutationJobStatus::Completed)
781            && (receipt.phase != MutationJobPhase::Verify || receipt.rows_updated != 0)
782        || matches!(receipt.status, MutationJobStatus::RestartRequired(_))
783            && (receipt.keys_scanned != 0 || receipt.rows_updated != 0)
784    {
785        return Err(MutationJobError::CorruptProgressStore);
786    }
787    Ok(())
788}
789
790fn retained_receipt_encoded_len(
791    retained: &RetainedMutationJobReceipt,
792) -> Result<usize, MutationJobError> {
793    let status_bytes = match retained.receipt.status {
794        MutationJobStatus::RestartRequired(_) => 2,
795        MutationJobStatus::Active | MutationJobStatus::Completed => 1,
796    };
797    8_usize
798        .checked_add(8)
799        .and_then(|value| value.checked_add(status_bytes))
800        .and_then(|value| value.checked_add(1))
801        .and_then(|value| value.checked_add(5 * 8))
802        .and_then(|value| value.checked_add(4))
803        .and_then(|value| value.checked_add(retained.idempotency_key.as_str().len()))
804        .ok_or(MutationJobError::CounterOverflow)
805}
806
807fn validate_nonempty_bytes(
808    value: &[u8],
809    limit: usize,
810    kind: MutationJobPayloadKind,
811) -> Result<(), MutationJobError> {
812    if value.is_empty() {
813        return Err(MutationJobError::CorruptProgressStore);
814    }
815    validate_bytes(value, limit, kind)
816}
817
818fn validate_bytes(
819    value: &[u8],
820    limit: usize,
821    kind: MutationJobPayloadKind,
822) -> Result<(), MutationJobError> {
823    if value.len() > limit {
824        return Err(payload_too_large(kind, limit, value.len()));
825    }
826    Ok(())
827}
828
829fn payload_too_large(
830    kind: MutationJobPayloadKind,
831    limit: usize,
832    observed: usize,
833) -> MutationJobError {
834    MutationJobError::PayloadTooLarge {
835        kind,
836        limit: u64::try_from(limit).unwrap_or(u64::MAX),
837        observed: u64::try_from(observed).unwrap_or(u64::MAX),
838    }
839}
840
841fn write_status(bytes: &mut Vec<u8>, status: MutationJobStatus) {
842    match status {
843        MutationJobStatus::Active => bytes.push(0),
844        MutationJobStatus::Completed => bytes.push(1),
845        MutationJobStatus::RestartRequired(reason) => {
846            bytes.push(2);
847            bytes.push(match reason {
848                MutationJobRestartReason::AcceptedSchemaChanged => 0,
849                MutationJobRestartReason::TargetAllocationChanged => 1,
850                MutationJobRestartReason::IntentIneligible => 2,
851                MutationJobRestartReason::BatchPolicyChanged => 3,
852                MutationJobRestartReason::UnsupportedContinuation => 4,
853                MutationJobRestartReason::ManagedTimestampRegression => 5,
854                MutationJobRestartReason::CandidateExceedsBatchPolicy => 6,
855                MutationJobRestartReason::ExecutionBudgetPolicyExceeded => 7,
856            });
857        }
858    }
859}
860
861fn read_status(reader: &mut ByteReader<'_>) -> Result<MutationJobStatus, MutationJobError> {
862    match reader.read_u8()? {
863        0 => Ok(MutationJobStatus::Active),
864        1 => Ok(MutationJobStatus::Completed),
865        2 => Ok(MutationJobStatus::RestartRequired(
866            match reader.read_u8()? {
867                0 => MutationJobRestartReason::AcceptedSchemaChanged,
868                1 => MutationJobRestartReason::TargetAllocationChanged,
869                2 => MutationJobRestartReason::IntentIneligible,
870                3 => MutationJobRestartReason::BatchPolicyChanged,
871                4 => MutationJobRestartReason::UnsupportedContinuation,
872                5 => MutationJobRestartReason::ManagedTimestampRegression,
873                6 => MutationJobRestartReason::CandidateExceedsBatchPolicy,
874                7 => MutationJobRestartReason::ExecutionBudgetPolicyExceeded,
875                _ => return Err(MutationJobError::CorruptProgressStore),
876            },
877        )),
878        _ => Err(MutationJobError::CorruptProgressStore),
879    }
880}
881
882fn write_phase(bytes: &mut Vec<u8>, phase: MutationJobPhase) {
883    bytes.push(match phase {
884        MutationJobPhase::Forward => 0,
885        MutationJobPhase::Verify => 1,
886    });
887}
888
889fn read_phase(reader: &mut ByteReader<'_>) -> Result<MutationJobPhase, MutationJobError> {
890    match reader.read_u8()? {
891        0 => Ok(MutationJobPhase::Forward),
892        1 => Ok(MutationJobPhase::Verify),
893        _ => Err(MutationJobError::CorruptProgressStore),
894    }
895}
896
897fn write_bytes(bytes: &mut Vec<u8>, value: &[u8]) -> Result<(), MutationJobError> {
898    let len = u32::try_from(value.len()).map_err(|_| MutationJobError::Internal)?;
899    bytes.extend_from_slice(&len.to_be_bytes());
900    bytes.extend_from_slice(value);
901    Ok(())
902}
903
904#[cfg(test)]
905mod tests {
906    use super::*;
907
908    fn job_id() -> MutationJobId {
909        MutationJobId::try_from_bytes([7; 32]).expect("nonzero mutation job id should admit")
910    }
911
912    fn request(sequence: u64, key: &str) -> MutationJobAdvanceRequest {
913        MutationJobAdvanceRequest::new(
914            job_id(),
915            sequence,
916            MutationJobIdempotencyKey::new(key).expect("bounded replay key should admit"),
917        )
918    }
919
920    fn initial_record() -> MutationJobRecord {
921        MutationJobRecord::new(job_id(), vec![1, 2, 3], vec![4, 5])
922            .expect("bounded mutation record should admit")
923    }
924
925    #[test]
926    fn identities_and_variable_components_enforce_current_bounds() {
927        assert_eq!(
928            MutationJobId::try_from_bytes([0; 32]),
929            Err(MutationJobError::InvalidJobId),
930        );
931        assert_eq!(
932            MutationJobIdempotencyKey::new(""),
933            Err(MutationJobError::InvalidIdempotencyKey),
934        );
935        assert!(MutationJobIdempotencyKey::new("k".repeat(256)).is_ok());
936        assert_eq!(
937            MutationJobIdempotencyKey::new("k".repeat(257)),
938            Err(MutationJobError::InvalidIdempotencyKey),
939        );
940        assert_eq!(
941            MutationJobRecord::new(job_id(), vec![1], Vec::new()),
942            Err(MutationJobError::CorruptProgressStore),
943        );
944
945        assert!(MutationJobRecord::new(job_id(), vec![1; 16 * 1024], vec![2; 2 * 1024]).is_ok());
946        assert!(matches!(
947            MutationJobRecord::new(job_id(), vec![1; 16 * 1024 + 1], Vec::new()),
948            Err(MutationJobError::PayloadTooLarge {
949                kind: MutationJobPayloadKind::Intent,
950                ..
951            }),
952        ));
953        assert!(matches!(
954            MutationJobRecord::new(job_id(), vec![1], vec![2; 2 * 1024 + 1]),
955            Err(MutationJobError::PayloadTooLarge {
956                kind: MutationJobPayloadKind::Continuation,
957                ..
958            }),
959        ));
960
961        let maximum_key =
962            MutationJobIdempotencyKey::new("k".repeat(MAX_MUTATION_JOB_IDEMPOTENCY_KEY_BYTES))
963                .expect("maximum replay key should admit");
964        let request = MutationJobAdvanceRequest::new(job_id(), 0, maximum_key);
965        let (record, _) = initial_record()
966            .apply_transition(
967                &request,
968                MutationJobTransition::new(
969                    MutationJobStatus::Active,
970                    MutationJobPhase::Forward,
971                    vec![7],
972                    1,
973                    0,
974                    0,
975                ),
976            )
977            .expect("maximum replay identity should retain");
978        assert_eq!(
979            record
980                .last_receipt
981                .as_ref()
982                .map(retained_receipt_encoded_len),
983            Some(Ok(318)),
984        );
985
986        let maximum_key =
987            MutationJobIdempotencyKey::new("k".repeat(MAX_MUTATION_JOB_IDEMPOTENCY_KEY_BYTES))
988                .expect("maximum replay key should admit");
989        let (restart, _) = initial_record()
990            .apply_transition(
991                &MutationJobAdvanceRequest::new(job_id(), 0, maximum_key),
992                MutationJobTransition::new(
993                    MutationJobStatus::RestartRequired(
994                        MutationJobRestartReason::BatchPolicyChanged,
995                    ),
996                    MutationJobPhase::Forward,
997                    Vec::new(),
998                    0,
999                    0,
1000                    0,
1001                ),
1002            )
1003            .expect("maximum restart receipt should retain");
1004        assert_eq!(
1005            restart
1006                .last_receipt
1007                .as_ref()
1008                .map(retained_receipt_encoded_len),
1009            Some(Ok(319)),
1010        );
1011    }
1012
1013    #[test]
1014    fn current_payload_round_trips_every_lifecycle() {
1015        let initial = initial_record();
1016        let (active, _) = initial
1017            .apply_transition(
1018                &request(0, "forward-0"),
1019                MutationJobTransition::new(
1020                    MutationJobStatus::Active,
1021                    MutationJobPhase::Verify,
1022                    vec![6],
1023                    13,
1024                    4,
1025                    0,
1026                ),
1027            )
1028            .expect("bounded active transition should admit");
1029        let (completed, _) = active
1030            .apply_transition(
1031                &request(1, "verify-0"),
1032                MutationJobTransition::new(
1033                    MutationJobStatus::Completed,
1034                    MutationJobPhase::Verify,
1035                    Vec::new(),
1036                    9,
1037                    0,
1038                    0,
1039                ),
1040            )
1041            .expect("clean terminal transition should admit");
1042        let (restart, _) = initial
1043            .apply_transition(
1044                &request(0, "restart"),
1045                MutationJobTransition::new(
1046                    MutationJobStatus::RestartRequired(
1047                        MutationJobRestartReason::ManagedTimestampRegression,
1048                    ),
1049                    MutationJobPhase::Forward,
1050                    Vec::new(),
1051                    0,
1052                    0,
1053                    0,
1054                ),
1055            )
1056            .expect("typed restart transition should admit");
1057        let (oversized_candidate, _) = initial
1058            .apply_transition(
1059                &request(0, "candidate-exceeds-policy"),
1060                MutationJobTransition::new(
1061                    MutationJobStatus::RestartRequired(
1062                        MutationJobRestartReason::CandidateExceedsBatchPolicy,
1063                    ),
1064                    MutationJobPhase::Forward,
1065                    Vec::new(),
1066                    0,
1067                    0,
1068                    0,
1069                ),
1070            )
1071            .expect("candidate policy restart should admit");
1072        let (execution_budget, _) = initial
1073            .apply_transition(
1074                &request(0, "execution-budget-policy"),
1075                MutationJobTransition::new(
1076                    MutationJobStatus::RestartRequired(
1077                        MutationJobRestartReason::ExecutionBudgetPolicyExceeded,
1078                    ),
1079                    MutationJobPhase::Forward,
1080                    Vec::new(),
1081                    0,
1082                    0,
1083                    0,
1084                ),
1085            )
1086            .expect("execution budget policy restart should admit");
1087
1088        for record in [
1089            initial,
1090            active,
1091            completed,
1092            restart,
1093            oversized_candidate,
1094            execution_budget,
1095        ] {
1096            let bytes = encode_mutation_job_payload(&record)
1097                .expect("current mutation payload should encode");
1098            assert!(!bytes.starts_with(b"DIDL"));
1099            assert_eq!(
1100                decode_mutation_job_payload(&bytes)
1101                    .expect("current mutation payload should decode"),
1102                record,
1103            );
1104        }
1105    }
1106
1107    #[test]
1108    fn public_state_request_receipt_and_error_are_wire_compatible() {
1109        let state = initial_record().state().clone();
1110        let request = request(0, "wire-request");
1111        let receipt = MutationJobAdvanceReceipt {
1112            request_sequence: 0,
1113            committed_sequence: 1,
1114            status: MutationJobStatus::Active,
1115            phase: MutationJobPhase::Forward,
1116            keys_scanned: 8,
1117            rows_updated: 3,
1118            keys_scanned_total: 8,
1119            rows_updated_total: 3,
1120            verify_restarts_total: 0,
1121        };
1122        let error = MutationJobError::TargetMutationFailed(
1123            MutationJobTargetFailureReason::StagingByteBudgetExceeded,
1124        );
1125
1126        let state_bytes =
1127            icydb_schema::wire::encode_one(&state).expect("mutation state should encode");
1128        let request_bytes =
1129            icydb_schema::wire::encode_one(&request).expect("mutation request should encode");
1130        let receipt_bytes =
1131            icydb_schema::wire::encode_one(&receipt).expect("mutation receipt should encode");
1132        let error_bytes =
1133            icydb_schema::wire::encode_one(&error).expect("mutation error should encode");
1134        assert_eq!(
1135            icydb_schema::wire::decode_one::<MutationJobState>(&state_bytes)
1136                .expect("mutation state should decode"),
1137            state,
1138        );
1139        assert_eq!(
1140            icydb_schema::wire::decode_one::<MutationJobAdvanceRequest>(&request_bytes)
1141                .expect("mutation request should decode"),
1142            request,
1143        );
1144        assert_eq!(
1145            icydb_schema::wire::decode_one::<MutationJobAdvanceReceipt>(&receipt_bytes)
1146                .expect("mutation receipt should decode"),
1147            receipt,
1148        );
1149        assert_eq!(
1150            icydb_schema::wire::decode_one::<MutationJobError>(&error_bytes)
1151                .expect("mutation error should decode"),
1152            error,
1153        );
1154    }
1155
1156    #[test]
1157    fn exact_replay_precedes_stale_and_terminal_rejection() {
1158        let initial = initial_record();
1159        let (verifying, _) = initial
1160            .apply_transition(
1161                &request(0, "forward-0"),
1162                MutationJobTransition::new(
1163                    MutationJobStatus::Active,
1164                    MutationJobPhase::Verify,
1165                    vec![7],
1166                    8,
1167                    3,
1168                    0,
1169                ),
1170            )
1171            .expect("Forward exhaustion should enter Verify");
1172        let terminal_request = request(1, "verify-0");
1173        let (completed, receipt) = verifying
1174            .apply_transition(
1175                &terminal_request,
1176                MutationJobTransition::new(
1177                    MutationJobStatus::Completed,
1178                    MutationJobPhase::Verify,
1179                    Vec::new(),
1180                    8,
1181                    0,
1182                    0,
1183                ),
1184            )
1185            .expect("terminal transition should admit");
1186
1187        assert_eq!(
1188            completed
1189                .exact_replay(&terminal_request)
1190                .expect("exact replay lookup should succeed"),
1191            Some(&receipt),
1192        );
1193        assert_eq!(
1194            completed.ensure_can_advance(&request(1, "different")),
1195            Err(MutationJobError::StaleSequence {
1196                expected: 1,
1197                actual: 2,
1198            }),
1199        );
1200        assert_eq!(
1201            completed.ensure_can_advance(&request(2, "next")),
1202            Err(MutationJobError::Completed),
1203        );
1204    }
1205
1206    #[test]
1207    fn payload_decode_is_bounded_fallible_and_rejects_trailing_bytes() {
1208        let bytes = encode_mutation_job_payload(&initial_record())
1209            .expect("current mutation payload should encode");
1210        for end in 0..bytes.len() {
1211            assert_eq!(
1212                decode_mutation_job_payload(&bytes[..end]),
1213                Err(MutationJobError::CorruptProgressStore),
1214                "truncation at byte {end} must retain corruption classification",
1215            );
1216        }
1217        let mut trailing = bytes;
1218        trailing.push(0);
1219        assert_eq!(
1220            decode_mutation_job_payload(&trailing),
1221            Err(MutationJobError::CorruptProgressStore),
1222        );
1223        let mut unknown_status = encode_mutation_job_payload(&initial_record())
1224            .expect("current mutation payload should encode");
1225        unknown_status[32 + 8] = u8::MAX;
1226        assert_eq!(
1227            decode_mutation_job_payload(&unknown_status),
1228            Err(MutationJobError::CorruptProgressStore),
1229        );
1230        let mut zero_job_id = encode_mutation_job_payload(&initial_record())
1231            .expect("current mutation payload should encode");
1232        zero_job_id[..32].fill(0);
1233        assert_eq!(
1234            decode_mutation_job_payload(&zero_job_id),
1235            Err(MutationJobError::CorruptProgressStore),
1236        );
1237
1238        let initial = initial_record();
1239        let mut bytes =
1240            encode_mutation_job_payload(&initial).expect("current mutation payload should encode");
1241        let intent_len_offset = 32 + 8 + 1 + 1 + 3 * 8;
1242        let continuation_len_offset = intent_len_offset + 4 + initial.canonical_intent.len();
1243        let continuation_offset = continuation_len_offset + 4;
1244        let continuation_end = continuation_offset + initial.engine_continuation.len();
1245        bytes[continuation_len_offset..continuation_offset].fill(0);
1246        bytes.drain(continuation_offset..continuation_end);
1247        assert_eq!(
1248            decode_mutation_job_payload(&bytes),
1249            Err(MutationJobError::CorruptProgressStore),
1250        );
1251    }
1252
1253    #[test]
1254    fn transition_totals_fail_closed_on_overflow() {
1255        assert_eq!(
1256            initial_record().apply_transition(
1257                &request(0, "empty-active-continuation"),
1258                MutationJobTransition::new(
1259                    MutationJobStatus::Active,
1260                    MutationJobPhase::Forward,
1261                    Vec::new(),
1262                    1,
1263                    0,
1264                    0,
1265                ),
1266            ),
1267            Err(MutationJobError::CorruptProgressStore),
1268        );
1269
1270        let mut record = initial_record();
1271        record.state.keys_scanned_total = u64::MAX;
1272        record.state.rows_updated_total = u64::MAX;
1273        record.last_receipt = Some(RetainedMutationJobReceipt {
1274            receipt: MutationJobAdvanceReceipt {
1275                request_sequence: 0,
1276                committed_sequence: 1,
1277                status: MutationJobStatus::Active,
1278                phase: MutationJobPhase::Forward,
1279                keys_scanned: 1,
1280                rows_updated: 1,
1281                keys_scanned_total: u64::MAX,
1282                rows_updated_total: u64::MAX,
1283                verify_restarts_total: 0,
1284            },
1285            idempotency_key: MutationJobIdempotencyKey::new("prior")
1286                .expect("bounded replay key should admit"),
1287        });
1288        record.state.sequence = 1;
1289        assert_eq!(
1290            record.apply_transition(
1291                &request(1, "overflow"),
1292                MutationJobTransition::new(
1293                    MutationJobStatus::Active,
1294                    MutationJobPhase::Forward,
1295                    vec![7],
1296                    1,
1297                    1,
1298                    0,
1299                ),
1300            ),
1301            Err(MutationJobError::CounterOverflow),
1302        );
1303
1304        let mut sequence_record = initial_record();
1305        sequence_record.state.sequence = u64::MAX;
1306        sequence_record.last_receipt = Some(RetainedMutationJobReceipt {
1307            receipt: MutationJobAdvanceReceipt {
1308                request_sequence: u64::MAX - 1,
1309                committed_sequence: u64::MAX,
1310                status: MutationJobStatus::Active,
1311                phase: MutationJobPhase::Forward,
1312                keys_scanned: 0,
1313                rows_updated: 0,
1314                keys_scanned_total: 0,
1315                rows_updated_total: 0,
1316                verify_restarts_total: 0,
1317            },
1318            idempotency_key: MutationJobIdempotencyKey::new("prior-sequence")
1319                .expect("bounded replay key should admit"),
1320        });
1321        assert_eq!(
1322            sequence_record.apply_transition(
1323                &request(u64::MAX, "sequence-overflow"),
1324                MutationJobTransition::new(
1325                    MutationJobStatus::Active,
1326                    MutationJobPhase::Forward,
1327                    vec![7],
1328                    0,
1329                    0,
1330                    0,
1331                ),
1332            ),
1333            Err(MutationJobError::CounterOverflow),
1334        );
1335    }
1336}