Skip to main content

mz_adapter/
frontend_read_then_write.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Frontend sequencing for read-then-write operations.
11//!
12//! This module implements INSERT [...] SELECT FROM [...], DELETE and UPDATE
13//! operations using a subscribe with optimistic concurrency control (OCC),
14//! sequenced from the session task rather than the Coordinator.
15//!
16//! The motivation is correctness with concurrent writers, including writers in
17//! different `environmentd` processes, which the coordinator's in-process write
18//! locks cannot provide. The OCC path also fixes a serializability defect of the
19//! lock path, which reads at one timestamp and commits at a later one while
20//! locking only the selection's direct dependency items. See the design doc,
21//! `doc/developer/design/20260210_incremental_occ_read_then_write.md`, and the
22//! comment on the retry arm in `run_occ_loop`. Relieving the coordinator loop is
23//! a side benefit, and only sequencing moves off it. The subscribe's data path
24//! still runs through the coordinator.
25//!
26//! ## Whether the write reads persisted state
27//!
28//! Two predicates answer that one question, and they have to agree. Before
29//! anything runs, `SessionClient::try_frontend_read_then_write` decides it
30//! syntactically, from `depends_on()` on the planned selection, because inside
31//! a transaction a read-dependent write has to be refused while refusing is
32//! still possible. Once the dataflow runs, the subscribe answers it
33//! dynamically: the channel closes on its own only once the sink's output
34//! frontier reaches the empty antichain, which means the selection can never
35//! change again.
36//!
37//! Reading nothing persisted is the common case for a clean close, not the
38//! guarantee. An input whose frontier seals closes cleanly too, despite reading
39//! persisted state, for example a `REFRESH AT` materialized view past its last
40//! refresh, whose write frontier advances to the empty antichain. What holds in
41//! either case is the property the write side actually needs: past the close,
42//! the consolidated diffs are frontier-independent. The syntactic predicate is
43//! correspondingly stricter than the dynamic one, since it refuses a sealed-MV
44//! `INSERT ... SELECT` in a transaction that would technically be bufferable.
45//!
46//! The two answers are used for different things, and that separation matters.
47//!
48//! The syntactic answer decides whether the statement can belong to a
49//! transaction. A selection that reads nothing produces diffs that are valid at
50//! any timestamp, so they are staged as session write ops and land when the
51//! transaction commits, which is what makes the statement atomic with whatever
52//! surrounds it. A selection that reads persisted state cannot belong to a
53//! transaction, and we refuse it in an explicit one. An extended-protocol
54//! pipeline is an implicit transaction, so it must not quietly join one either:
55//! it ends its own transaction instead of spanning the rest of the pipeline,
56//! which is how PostgreSQL treats statements that cannot run in a transaction
57//! block. It is durable once it reports success, and a later failure in the
58//! pipeline does not undo it.
59//!
60//! The dynamic answer only decides how the write is submitted, a timestamped
61//! write from inside the loop or a blind submission after it. It cannot decide
62//! transaction membership, because it is a property of the inputs rather than
63//! of the statement. A sealed input closes the subscribe cleanly, so an
64//! `INSERT ... SELECT` over a `REFRESH AT` materialized view past its last
65//! refresh takes the blind exit while still reading persisted state. Its diffs
66//! really are frontier-independent and staging them would be safe, and we still
67//! do not stage them. Otherwise whether a statement's rows survive a failure
68//! later in the pipeline would depend on whether one of its inputs happened to
69//! pass its last refresh, which no one reading the statement could predict.
70//!
71//! Staging is also what earns the right to span a pipeline in the first place.
72//! `TransactionStatus::may_span_pipeline` lets an implicit transaction stay open
73//! only for writes, precisely because they are merely staged. A statement that
74//! committed on its own has no business claiming it.
75//!
76//! Disagreement is caught on both sides, and only one side can still refuse.
77//! `frontend_read_then_write` re-checks the syntactic predicate before running a
78//! dataflow, which catches a caller that skipped the gate. If the syntactic
79//! predicate were laxer than the dynamic one, that check would pass and a write
80//! meant for staging would commit on its own, so the loop asserts wherever the
81//! two answers can be compared. The `Committed` arm catches a write timestamp
82//! for a statement we meant to stage, and the zero-row arm catches a read
83//! timestamp for one. Neither can undo anything by then, the write is already
84//! durable in the first case and there was never anything to write in the
85//! second, so all they do is make the disagreement loud.
86//!
87//! ## The frontier certifies, the oracle chooses
88//!
89//! The target `T` comes from the oracle, and a progress message at `F` only
90//! certifies completeness below `F`, so it gates the write rather than choosing
91//! its timestamp. The design doc's "The OCC loop" says why. Three invariants
92//! hold for every write this path makes:
93//!
94//! * `F >= T` before it submits, so the payload is a complete view of `T - 1`.
95//! * The payload is every diff below `T`, strictly. A diff at `T` is concurrent
96//!   with the write and waits for a later target.
97//! * `T > as_of`, so the snapshot, which arrives at `as_of`, is in the payload.
98//!
99//! NOTE: `F >= T` does not make the two equal, because `F` is a minimum over the
100//! selection's inputs. Where `F` runs above `T`, a selection that reads the
101//! target table makes the compare-and-append refuse, and retrying higher is the
102//! design rather than a failure.
103//!
104//! ## A zero-row answer
105//!
106//! A write linearizes itself, because group commit advances the oracle before it
107//! acknowledges. An answer of "no rows" performs no write, so the loop reports
108//! the timestamp its view is complete through and the caller waits for the
109//! oracle to reach it. A selection empty at `as_of` is complete through `as_of`,
110//! which the caller put behind the oracle before the subscribe started, so that
111//! answer waits for nothing, while reporting the frontier the subscribe observed
112//! would cost a group commit every time. The design doc's "Linearization" argues
113//! why the lower timestamp is not the weaker guarantee.
114
115use std::collections::BTreeMap;
116use std::collections::BTreeSet;
117use std::num::{NonZeroI64, NonZeroUsize};
118use std::sync::atomic::{AtomicBool, Ordering};
119use std::sync::{Arc, Mutex};
120use std::time::Duration;
121
122use bytesize::ByteSize;
123use differential_dataflow::consolidation;
124use mz_catalog::memory::error::ErrorKind;
125use mz_catalog::memory::objects::CatalogItem;
126use mz_cluster_client::ReplicaId;
127use mz_compute_types::ComputeInstanceId;
128use mz_expr::Eval;
129use mz_expr::row::RowCollection;
130use mz_expr::{CollectionPlan, Id, LocalId, MirRelationExpr, MirScalarExpr, RowSetFinishing};
131use mz_ore::cast::CastFrom;
132use mz_ore::{soft_assert_or_log, soft_panic_or_log};
133use mz_repr::optimize::OverrideFrom;
134use mz_repr::{CatalogItemId, Diff, GlobalId, RelationDesc, Row, RowArena, Timestamp};
135use mz_sql::catalog::CatalogError;
136use mz_sql::plan::{self, MutationKind, QueryWhen};
137use mz_sql::session::metadata::SessionMetadata;
138use mz_sql::session::vars::IsolationLevel;
139use mz_storage_client::client::TableData;
140use mz_storage_types::sources::Timeline;
141use mz_timestamp_oracle::TimestampOracle;
142use prometheus::Histogram;
143use timely::progress::Antichain;
144use tokio::sync::mpsc;
145use uuid::Uuid;
146
147use crate::active_compute_sink::ActiveSubscribeOwner;
148use crate::catalog::Catalog;
149use crate::command::{Command, ExecuteResponse, WriteAttemptKind};
150use crate::coord::appends::WriteResult;
151use crate::coord::read_then_write::{DependencyPolicy, validate_read_then_write_dependencies};
152use crate::coord::timestamp_selection::TimestampProvider;
153use crate::coord::{Coordinator, TargetCluster};
154use crate::error::AdapterError;
155use crate::metrics::{OCC_CALLER_BACKGROUND, OCC_CALLER_SESSION};
156use crate::optimize::Optimize;
157use crate::optimize::dataflows::{ComputeInstanceSnapshot, EvalTime, ExprPrep, ExprPrepOneShot};
158use crate::peek_client::CoordinatorClient;
159use crate::session::{Session, TransactionOps, WriteOp};
160use crate::statement_logging::{StatementLifecycleEvent, StatementLoggingId};
161use crate::{PeekClient, PeekResponseUnary, TimelineContext, optimize};
162
163/// Reason a frontend write attempt is being torn down early.
164#[derive(Clone, Copy)]
165pub(crate) enum FrontendWriteCancellation {
166    Canceled,
167    StatementTimeout,
168}
169
170impl From<FrontendWriteCancellation> for AdapterError {
171    fn from(cancellation: FrontendWriteCancellation) -> Self {
172        match cancellation {
173            FrontendWriteCancellation::Canceled => AdapterError::Canceled,
174            FrontendWriteCancellation::StatementTimeout => AdapterError::StatementTimeout,
175        }
176    }
177}
178
179/// State shared between an in-flight frontend write attempt and its
180/// cancellation wrapper,
181/// `SessionClient::try_frontend_read_then_write_with_cancel`.
182///
183/// The contract: `write_submitted` is true from just before the
184/// `AttemptWrite` command is sent until the attempt resolves as definitively
185/// not committed. While it is true, cancellation and statement timeout must
186/// not synthesize an error but await the definitive write result instead,
187/// because the write may already be durable.
188///
189/// The wrapper and the attempt it wraps are polled by the same task, so the
190/// mutex and the atomic are here to satisfy `Send`, not to arbitrate between
191/// concurrent writers. There is one writer for each field.
192pub(crate) struct FrontendWriteAttemptState {
193    write_submitted: AtomicBool,
194    /// Set at most once, by the cancellation wrapper.
195    cancellation: Mutex<Option<FrontendWriteCancellation>>,
196}
197
198impl FrontendWriteAttemptState {
199    pub(crate) fn new() -> Self {
200        Self {
201            write_submitted: AtomicBool::new(false),
202            cancellation: Mutex::new(None),
203        }
204    }
205
206    pub(crate) fn mark_write_submitted(&self) {
207        self.write_submitted.store(true, Ordering::Release);
208    }
209
210    /// Marks the submitted write as definitively not committed.
211    ///
212    /// NOTE: This must only be called for outcomes where the write is known
213    /// to not have landed (`TimestampPassed`). Terminal outcomes leave
214    /// `write_submitted` set so a concurrent cancellation path can never
215    /// fabricate an error for a write that may have committed.
216    fn mark_write_resolved(&self) {
217        self.write_submitted.store(false, Ordering::Release);
218    }
219
220    pub(crate) fn write_submitted(&self) -> bool {
221        self.write_submitted.load(Ordering::Acquire)
222    }
223
224    /// Records why the attempt is being torn down. The first reason recorded
225    /// is the one the attempt reports.
226    pub(crate) fn request(&self, cancellation: FrontendWriteCancellation) {
227        self.cancellation
228            .lock()
229            .expect("cancellation lock poisoned")
230            .get_or_insert(cancellation);
231    }
232
233    fn requested_error(&self) -> Option<AdapterError> {
234        self.cancellation
235            .lock()
236            .expect("cancellation lock poisoned")
237            .map(AdapterError::from)
238    }
239}
240
241/// Which kind of caller is driving a read-then-write.
242///
243/// Dependency rules, replica selection, and write cancellation follow from the
244/// caller kind.
245#[derive(Clone, Copy)]
246enum RtwCaller {
247    /// A user statement. Cancelled with its connection, and restricted to
248    /// reading user tables.
249    Session,
250    /// Coordinator-owned maintenance, pinned to one replica.
251    ///
252    /// The caller must build the statement itself rather than accept one from a
253    /// user, and must tolerate reading a log relation that is sealed empty,
254    /// which is how a replica with introspection disabled presents one.
255    Background { replica_id: ReplicaId },
256}
257
258impl RtwCaller {
259    fn is_background(&self) -> bool {
260        matches!(self, RtwCaller::Background { .. })
261    }
262
263    /// Which relations the selection may read.
264    fn dependency_policy(&self) -> DependencyPolicy {
265        match self {
266            RtwCaller::Session => DependencyPolicy::UserDml,
267            RtwCaller::Background { .. } => DependencyPolicy::SystemReads,
268        }
269    }
270
271    /// The replica a background caller pins its subscribe to, overriding the
272    /// session's replica selection.
273    fn replica_override(&self) -> Option<ReplicaId> {
274        match self {
275            RtwCaller::Background { replica_id } => Some(*replica_id),
276            RtwCaller::Session => None,
277        }
278    }
279
280    /// Who owns the subscribe, which decides whether it is cancelled with a
281    /// connection and whether it counts against one.
282    fn subscribe_owner(
283        &self,
284        conn_id: &mz_adapter_types::connection::ConnectionId,
285        session_uuid: Uuid,
286    ) -> ActiveSubscribeOwner {
287        match self {
288            RtwCaller::Session => ActiveSubscribeOwner::Session {
289                conn_id: conn_id.clone(),
290                session_uuid,
291            },
292            RtwCaller::Background { .. } => ActiveSubscribeOwner::Background,
293        }
294    }
295
296    /// The connection a pending write is cancelled with, if any.
297    fn write_conn_id(
298        &self,
299        conn_id: &mz_adapter_types::connection::ConnectionId,
300    ) -> Option<mz_adapter_types::connection::ConnectionId> {
301        match self {
302            RtwCaller::Session => Some(conn_id.clone()),
303            RtwCaller::Background { .. } => None,
304        }
305    }
306}
307
308/// What the OCC loop produced.
309enum OccOutcome {
310    /// The write is durable at `write_ts`.
311    Committed {
312        response: ExecuteResponse,
313        write_ts: Timestamp,
314    },
315    /// The selection was empty, so there was nothing to write.
316    ///
317    /// `empty_as_of` is the timestamp the emptiness holds at, and the caller must
318    /// bring the oracle's read timestamp up to it before responding. `None` when
319    /// the subscribe ran to completion, where the emptiness holds at every
320    /// timestamp. See the module docs for why the choice of timestamp matters.
321    NoRowsMatched {
322        response: ExecuteResponse,
323        empty_as_of: Option<Timestamp>,
324    },
325    /// Diffs no frontier can change, from a subscribe that ran to completion.
326    /// The close says the selection can never change again, not that it reads
327    /// nothing persisted, and either way the caller chooses whether to submit
328    /// them now or buffer them into the transaction.
329    Blind {
330        response: ExecuteResponse,
331        diffs: Vec<(Row, Diff)>,
332    },
333}
334
335/// What the coordinator's answer to a submitted write means for the statement.
336enum WriteOutcome {
337    /// The write is durable at this timestamp.
338    Committed(Timestamp),
339    /// The write did not land, and resubmitting these diffs cannot change
340    /// that. This is the error to report.
341    Failed(AdapterError),
342    /// Another writer advanced the target's upper past the timestamp we asked
343    /// for. The diffs still describe the mutation, so the OCC loop can
344    /// resubmit them once the subscribe has caught up.
345    Conflict { next_eligible_timestamp: Timestamp },
346}
347
348/// Maps a [`WriteResult`] to the outcome the statement reports, or to the one
349/// conflict the OCC loop can retry.
350fn classify_write_result(
351    result: WriteResult,
352    target_id: CatalogItemId,
353    attempt_state: &FrontendWriteAttemptState,
354) -> WriteOutcome {
355    match result {
356        WriteResult::Success { timestamp } => WriteOutcome::Committed(timestamp),
357        WriteResult::TimestampPassed {
358            next_eligible_timestamp,
359            ..
360        } => WriteOutcome::Conflict {
361            next_eligible_timestamp,
362        },
363        WriteResult::Canceled => WriteOutcome::Failed(
364            attempt_state
365                .requested_error()
366                .unwrap_or(AdapterError::Canceled),
367        ),
368        WriteResult::TimestampTooFarAhead {
369            target_timestamp,
370            limit,
371        } => WriteOutcome::Failed(AdapterError::ReadThenWriteTimestampTooFarAhead {
372            target_timestamp,
373            limit,
374        }),
375        WriteResult::ReadOnly => WriteOutcome::Failed(AdapterError::ReadOnly),
376        WriteResult::TargetChanged => {
377            // A concurrent DDL gave the table a new generation after we
378            // computed these diffs against the old one. The same error the
379            // coordinator raises when a dependency changes underneath a
380            // statement, so clients see one retryable outcome for both.
381            WriteOutcome::Failed(AdapterError::ConcurrentDependencyMutation {
382                dependency_id: target_id.to_string(),
383            })
384        }
385        WriteResult::Indeterminate => WriteOutcome::Failed(AdapterError::Internal(
386            "write outcome is indeterminate because the group committer shut down".into(),
387        )),
388    }
389}
390
391/// Ends the implicit transaction that a statement which cannot run in a
392/// transaction block opened for itself.
393///
394/// A read-then-write that reads persisted state is refused inside a transaction,
395/// so it must not quietly become part of one. Clearing the ops it staged leaves
396/// [`crate::session::TransactionStatus::may_span_pipeline`] false, so pgwire
397/// commits the implicit transaction rather than letting the rest of an
398/// extended-protocol pipeline join it. The write is already durable at this
399/// point, and the caller has established there is nothing else staged to lose.
400fn end_own_transaction(session: &mut Session, stages_rows: bool) {
401    if !stages_rows {
402        session.clear_transaction_ops();
403    }
404}
405
406/// Checks that a read-then-write may read what its selection depends on.
407///
408/// An invalid selection is invalid wherever the statement runs, so a caller with
409/// something contextual to report, such as the transaction state, must ask this
410/// first. Reporting the transaction for a statement that can never work suggests
411/// it would work outside one.
412///
413/// `catalog` must be the snapshot the plan was built against. A missing item
414/// means the caller mixed snapshots, which is reported as a catalog error rather
415/// than treated as a dropped dependency.
416pub(crate) fn validate_selection_dependencies(
417    catalog: &Catalog,
418    depends_on: &BTreeSet<GlobalId>,
419    policy: DependencyPolicy,
420) -> Result<(), AdapterError> {
421    let dependency_ids = depends_on
422        .iter()
423        .copied()
424        .map(|gid| {
425            catalog.try_resolve_item_id(&gid).ok_or_else(|| {
426                AdapterError::Catalog(mz_catalog::memory::error::Error {
427                    kind: ErrorKind::Sql(CatalogError::UnknownItem(gid.to_string())),
428                })
429            })
430        })
431        .collect::<Result<Vec<_>, _>>()?;
432    let max_rw_dependencies = mz_adapter_types::dyncfgs::READ_THEN_WRITE_MAX_DEPENDENCIES
433        .get(catalog.system_config().dyncfgs());
434    validate_read_then_write_dependencies(catalog, dependency_ids, max_rw_dependencies, policy)
435}
436
437/// Validates a read-then-write and resolves the context the rest of the
438/// pipeline runs against.
439///
440/// Rejects `mz_now()` in the selection, the assignments or the returning
441/// clause. `optimize_mir_read_then_write` relies on that rejection by name
442/// when it prepares unmaterializable functions one-shot. Also enforces the
443/// dependency cap, resolves the target cluster and requires it to have a
444/// live replica, honors the session's replica pin, computes the read side's
445/// `TimelineContext`, and fetches the target table's descriptor.
446///
447/// `catalog` must be the snapshot the plan was built against. One snapshot
448/// serves planning, validation and optimization, so items the plan names
449/// cannot disappear from it, and the missing-entry branches below are
450/// failsafes rather than a live concurrent-DDL path.
451///
452/// `dependency_policy` decides which relations the selection may read. Both
453/// policies reject `mz_now()` anywhere in the transitive dependencies.
454fn validate_read_then_write(
455    catalog: &Arc<Catalog>,
456    session: &Session,
457    plan: &plan::ReadThenWritePlan,
458    target_cluster: TargetCluster,
459    dependency_policy: DependencyPolicy,
460) -> Result<ValidationResult, AdapterError> {
461    if contains_mz_now(plan) {
462        return Err(AdapterError::Unsupported(
463            "calls to mz_now in write statements",
464        ));
465    }
466
467    // One walk of the selection serves both the dependency check and the
468    // timeline validation below.
469    let depends_on = plan.selection.depends_on();
470
471    validate_selection_dependencies(catalog, &depends_on, dependency_policy)?;
472
473    let cluster = catalog.resolve_target_cluster(target_cluster, session)?;
474    let cluster_id = cluster.id;
475
476    if cluster.replicas().next().is_none() {
477        return Err(AdapterError::NoClusterReplicasAvailable {
478            name: cluster.name.clone(),
479            is_managed: cluster.is_managed(),
480        });
481    }
482
483    let replica_id = session
484        .vars()
485        .cluster_replica()
486        .map(|name| {
487            cluster
488                .replica_id(name)
489                .ok_or(AdapterError::UnknownClusterReplica {
490                    cluster_name: cluster.name.clone(),
491                    replica_name: name.to_string(),
492                })
493        })
494        .transpose()?;
495
496    let timeline = catalog.validate_timeline_context(depends_on.iter().copied())?;
497
498    // The loop waits for the subscribe's frontier to reach a target timestamp
499    // taken from the `EpochMilliseconds` oracle, and it is the target table's
500    // upper the write then competes for. A selection in another timeline
501    // counts something else, transactions rather than milliseconds for a CDCv2
502    // source, so its frontier is not comparable with that target and the
503    // statement would burn until `statement_timeout`. Refuse it up front
504    // instead.
505    //
506    // Only `INSERT ... SELECT` reaches this. A DELETE or UPDATE selection
507    // includes the target table, so a foreign timeline already fails above
508    // as a mixed-timeline query.
509    if let TimelineContext::TimelineDependent(t) = &timeline {
510        if t != &Timeline::EpochMilliseconds {
511            return Err(AdapterError::Unsupported(
512                "read-then-write on a selection outside the EpochMilliseconds timeline",
513            ));
514        }
515    }
516
517    // Get the table descriptor for constraint validation. As above, a
518    // missing entry would mean the snapshot contract was broken.
519    let table_desc = match catalog.try_get_entry(&plan.id) {
520        Some(entry) => entry
521            .relation_desc_latest()
522            .expect("table has desc")
523            .into_owned(),
524        None => {
525            return Err(AdapterError::Catalog(mz_catalog::memory::error::Error {
526                kind: ErrorKind::Sql(CatalogError::UnknownItem(plan.id.to_string())),
527            }));
528        }
529    };
530
531    Ok(ValidationResult {
532        cluster_id,
533        replica_id,
534        timeline,
535        depends_on,
536        table_desc,
537    })
538}
539
540/// Builds the response for a write that is about to be submitted.
541///
542/// This runs before the write, so the result-size checks in here reject the
543/// statement without having written anything.
544fn build_success_response(
545    kind: &MutationKind,
546    returning: &[MirScalarExpr],
547    diffs: &[(Row, Diff)],
548    max_result_size: u64,
549    max_query_result_size: u64,
550    row_set_finishing_seconds: &Histogram,
551) -> Result<ExecuteResponse, AdapterError> {
552    if returning.is_empty() {
553        // For UPDATE each changed row produces a retraction (-1) and an
554        // insertion (+1), so we divide by 2 below.
555        let row_count = diffs
556            .iter()
557            .map(|(_, diff)| diff.into_inner().unsigned_abs())
558            .sum::<u64>();
559        let row_count = usize::try_from(row_count).expect("positive row count must fit in usize");
560
561        return Ok(match kind {
562            MutationKind::Delete => ExecuteResponse::Deleted(row_count),
563            MutationKind::Update => ExecuteResponse::Updated(row_count / 2),
564            MutationKind::Insert => ExecuteResponse::Inserted(row_count),
565        });
566    }
567
568    let mut returning_rows = Vec::new();
569    let arena = RowArena::new();
570    // RETURNING expressions are evaluated row-by-row in this loop, so an
571    // expression like `RETURNING repeat('x', 10_000_000)` will allocate
572    // unbounded data unless we bail mid-loop. The post-loop
573    // `RowSetFinishing::finish` below would also reject this, but only
574    // after we've materialized everything. The early-bail caps the
575    // temporary allocation. We pick the lower of the two configured caps,
576    // whichever fires first wins.
577    let mut projected_byte_size: u64 = 0;
578    let early_cap = std::cmp::min(max_result_size, max_query_result_size);
579
580    for (row, diff) in diffs {
581        let include = match kind {
582            MutationKind::Delete => diff.is_negative(),
583            MutationKind::Update | MutationKind::Insert => diff.is_positive(),
584        };
585
586        if !include {
587            continue;
588        }
589
590        let mut returning_row = Row::with_capacity(returning.len());
591        let mut packer = returning_row.packer();
592        let datums: Vec<_> = row.iter().collect();
593
594        for expr in returning {
595            match expr.eval(&datums, &arena) {
596                Ok(datum) => packer.push(datum),
597                Err(err) => return Err(err.into()),
598            }
599        }
600
601        let multiplicity = NonZeroUsize::try_from(
602            NonZeroI64::try_from(diff.into_inner().abs()).expect("diff is non-zero"),
603        )
604        .map_err(AdapterError::from)?;
605
606        let row_bytes = u64::cast_from(returning_row.byte_len())
607            .saturating_mul(u64::cast_from(multiplicity.get()));
608        projected_byte_size = projected_byte_size.saturating_add(row_bytes);
609        if projected_byte_size > early_cap {
610            return Err(AdapterError::ResultSize(format!(
611                "result exceeds max size of {}",
612                ByteSize::b(early_cap)
613            )));
614        }
615
616        returning_rows.push((returning_row, multiplicity));
617    }
618
619    // Run the canonical finish to enforce both caps with full precision
620    // (including the sorted-view memory overhead) and to register the
621    // row-set-finishing duration histogram.
622    let finishing = RowSetFinishing {
623        order_by: Vec::new(),
624        limit: None,
625        offset: 0,
626        project: (0..returning.len()).collect(),
627    };
628    match finishing.finish(
629        RowCollection::new(returning_rows, &finishing.order_by),
630        max_result_size,
631        Some(max_query_result_size),
632        row_set_finishing_seconds,
633    ) {
634        Ok((rows, _size_bytes)) => Ok(ExecuteResponse::SendingRowsImmediate {
635            rows: Box::new(rows),
636        }),
637        Err(e) => Err(AdapterError::ResultSize(e)),
638    }
639}
640
641/// Whether a read-then-write mentions `mz_now()` anywhere.
642///
643/// Read time and write time differ on this path, so `mz_now()` has no single
644/// answer and is refused in every position.
645pub(crate) fn contains_mz_now(plan: &plan::ReadThenWritePlan) -> bool {
646    plan.selection.contains_temporal()
647        || plan.assignments.values().any(|e| e.contains_temporal())
648        || plan.returning.iter().any(|e| e.contains_temporal())
649}
650
651/// The timeline whose oracle governs a read-then-write with the given read-side
652/// [`TimelineContext`].
653///
654/// The write target is always a table on `EpochMilliseconds`, so a read side
655/// that pins no timeline (`TimestampDependent`) still maps to
656/// `EpochMilliseconds`. `None` only for timestamp-independent selections, which
657/// need no oracle at all.
658fn governing_timeline(timeline: &TimelineContext) -> Option<Timeline> {
659    <Coordinator as TimestampProvider>::get_timeline(timeline)
660}
661
662/// A handle to an internal subscribe, meaning one that writes no
663/// `mz_subscriptions` row. A `Drop` impl ensures the subscribe's dataflow is
664/// cleaned up when dropped.
665struct SubscribeHandle {
666    rx: mpsc::UnboundedReceiver<PeekResponseUnary>,
667    sink_id: GlobalId,
668    /// Wrapped in `Option` so we can move it out in `Drop`.
669    client: Option<CoordinatorClient>,
670}
671
672impl SubscribeHandle {
673    /// Receive the next message from the subscribe, waiting if necessary.
674    pub async fn recv(&mut self) -> Option<PeekResponseUnary> {
675        self.rx.recv().await
676    }
677
678    /// Try to receive a message without waiting.
679    pub fn try_recv(&mut self) -> Result<PeekResponseUnary, mpsc::error::TryRecvError> {
680        self.rx.try_recv()
681    }
682}
683
684impl Drop for SubscribeHandle {
685    fn drop(&mut self) {
686        if let Some(client) = self.client.take() {
687            // Fire-and-forget: if the coordinator is gone, the subscribe will
688            // be cleaned up when the process exits anyway.
689            client.try_send(Command::DropInternalSubscribe {
690                sink_id: self.sink_id,
691            });
692        }
693    }
694}
695
696impl PeekClient {
697    /// Execute a read-then-write operation using frontend sequencing.
698    ///
699    /// The caller owns the end-of-execution logging for
700    /// `statement_logging_id` and verified and planned the portal against
701    /// `catalog`, which stays in force through optimization and write-target
702    /// generation capture.
703    pub(crate) async fn frontend_read_then_write(
704        &mut self,
705        session: &mut Session,
706        plan: plan::ReadThenWritePlan,
707        target_cluster: TargetCluster,
708        catalog: &Arc<Catalog>,
709        statement_logging_id: Option<StatementLoggingId>,
710        attempt_state: Arc<FrontendWriteAttemptState>,
711    ) -> Result<ExecuteResponse, AdapterError> {
712        self.read_then_write(
713            session,
714            plan,
715            target_cluster,
716            catalog,
717            statement_logging_id,
718            attempt_state,
719            RtwCaller::Session,
720        )
721        .await
722    }
723
724    /// Executes a coordinator-owned read-then-write against system relations,
725    /// pinned to `replica_id`.
726    ///
727    /// See `RtwCaller::Background` for what the caller takes on by using this.
728    pub(crate) async fn background_read_then_write(
729        &mut self,
730        session: &mut Session,
731        plan: plan::ReadThenWritePlan,
732        cluster_id: ComputeInstanceId,
733        replica_id: ReplicaId,
734        catalog: &Arc<Catalog>,
735    ) -> Result<ExecuteResponse, AdapterError> {
736        let is_system_table = matches!(plan.id, CatalogItemId::System(_))
737            && catalog
738                .try_get_entry(&plan.id)
739                .is_some_and(|entry| matches!(entry.item(), CatalogItem::Table(_)));
740        if !is_system_table {
741            soft_panic_or_log!(
742                "background read-then-write target {} is not a system table",
743                plan.id
744            );
745            return Err(AdapterError::Internal(
746                "background read-then-write target is not a system table".into(),
747            ));
748        }
749
750        self.read_then_write(
751            session,
752            plan,
753            TargetCluster::Transaction(cluster_id),
754            catalog,
755            None,
756            // Nothing cancels a background write, so this state only ever
757            // records that a write was submitted.
758            Arc::new(FrontendWriteAttemptState::new()),
759            RtwCaller::Background { replica_id },
760        )
761        .await
762    }
763
764    #[allow(clippy::too_many_arguments)]
765    async fn read_then_write(
766        &mut self,
767        session: &mut Session,
768        mut plan: plan::ReadThenWritePlan,
769        target_cluster: TargetCluster,
770        catalog: &Arc<Catalog>,
771        statement_logging_id: Option<StatementLoggingId>,
772        attempt_state: Arc<FrontendWriteAttemptState>,
773        caller: RtwCaller,
774    ) -> Result<ExecuteResponse, AdapterError> {
775        // The OCC dataflow emits raw diffs and does not apply top-level
776        // finishing. Silently dropping a LIMIT, OFFSET, projection or ordering
777        // can change the rows written, so this stage requires trivial finishing.
778        if !plan.finishing.is_trivial(plan.selection.arity()) {
779            soft_panic_or_log!("frontend read-then-write received nontrivial row-set finishing");
780            return Err(AdapterError::Internal(
781                "frontend read-then-write requires trivial row-set finishing".into(),
782            ));
783        }
784
785        // A transaction that has taken a timestamped read, was opened READ
786        // ONLY, or is committed to some other kind of operation cannot take a
787        // write. Check up front because the marker op below rejects only
788        // some of those states and reports different errors.
789        //
790        // Both this and the marker op require an open transaction. The
791        // frontends start one before they execute anything, and a `Failed`
792        // transaction only ever admits COMMIT/ROLLBACK, so DML never arrives
793        // in a state where these panic.
794        if !session.transaction().allows_writes() {
795            return Err(AdapterError::ReadOnlyTransaction);
796        }
797
798        let validation_result = validate_read_then_write(
799            catalog,
800            session,
801            &plan,
802            target_cluster,
803            caller.dependency_policy(),
804        )?;
805
806        let ValidationResult {
807            cluster_id,
808            mut replica_id,
809            timeline,
810            depends_on,
811            table_desc,
812        } = validation_result;
813        if let Some(pinned) = caller.replica_override() {
814            replica_id = Some(pinned);
815        }
816
817        // A write that reads no persisted state may join a surrounding
818        // transaction. Its rows do not come from a snapshot, so staging them
819        // and letting the transaction flush them keeps them atomic with
820        // everything else the transaction does.
821        //
822        // A write that does read persisted state may not. We refuse it in an
823        // explicit transaction, and an extended-protocol pipeline is an
824        // implicit transaction, so it must not silently span one either. It
825        // runs as its own transaction instead, which is how PostgreSQL treats
826        // statements that cannot run in a transaction block.
827        let stages_rows = depends_on.is_empty();
828
829        // Snapshot this before the marker op below, which makes the predicate
830        // true unconditionally.
831        let in_transaction = session
832            .transaction()
833            .may_share_transaction_with_other_statements();
834
835        if !stages_rows && in_transaction {
836            // Defense in depth for the gate in
837            // `SessionClient::try_frontend_read_then_write`. Rejecting here,
838            // before we run a dataflow, is the last point where refusing is
839            // still possible: past the OCC loop the write may already be
840            // durable.
841            soft_panic_or_log!(
842                "read-dependent read-then-write reached the OCC path inside a transaction"
843            );
844            return Err(AdapterError::Internal(
845                "read-then-write cannot be run inside a transaction block".into(),
846            ));
847        }
848
849        // Mark this as a write transaction in the session state machine, so
850        // auto-commit treats the statement as a write. The rows follow once we
851        // know them.
852        session.add_transaction_ops(TransactionOps::Writes(vec![]))?;
853
854        // Prepare expressions (resolve unmaterializable functions like
855        // current_user())
856        let style = ExprPrepOneShot {
857            logical_time: EvalTime::NotAvailable, // We already errored out on mz_now above.
858            session,
859            catalog_state: catalog.state(),
860        };
861        for expr in plan
862            .assignments
863            .values_mut()
864            .chain(plan.returning.iter_mut())
865        {
866            style.prep_scalar_expr(expr)?;
867        }
868
869        let (mut optimizer, global_mir_plan) =
870            self.optimize_mir_read_then_write(catalog, session, &plan, cluster_id)?;
871
872        // Acquire the OCC semaphore permit *before* acquiring read holds in
873        // `frontend_determine_timestamp`. Under contention, waiters will
874        // otherwise sit on read holds on the RTW's read dependencies for the
875        // entire time they are queued, pinning compaction on those
876        // collections. Waiting on the permit first keeps queued operations
877        // hold-free. Once we have a permit we proceed to acquire the read holds
878        // needed for the rest of the operation.
879        //
880        // The cost of this ordering is that a permit held by a long-running
881        // operation stalls every read-then-write in the process, including ones
882        // on unrelated tables. We accept that because the
883        // statement timeout in
884        // `SessionClient::try_frontend_read_then_write_with_cancel` covers the
885        // permit wait, so the stall is bounded for everyone but a session that
886        // disabled its own timeout.
887        //
888        // The semaphore is owned by the coordinator and outlives every
889        // session task, so `acquire_owned` cannot return `Err` in practice.
890        //
891        // Background maintenance skips the queue entirely. It is single-flight
892        // by construction, one sweep at a time and one mutation at a time, so it
893        // adds at most one concurrent read-then-write. Taking a permit instead
894        // would let it hold one for as long as a subscribe on a loaded user
895        // replica takes to hydrate, and with `max_concurrent_occ_writes` set low
896        // that stalls user DML behind a background sampler. The bound above does
897        // not apply to it either: it has no statement timeout, only its own much
898        // longer one.
899        let permit = if caller.is_background() {
900            None
901        } else {
902            Some(
903                Arc::clone(&self.occ_write_semaphore)
904                    .acquire_owned()
905                    .await
906                    .expect("semaphore is never closed during coordinator lifetime"),
907            )
908        };
909
910        // Determine timestamp and acquire read holds.
911        let oracle_read_ts = self.oracle_read_ts(&timeline).await?;
912
913        // Real-time recency, on the same terms as the frontend peek path. The
914        // coordinator round trip polls the selection's upstream sources, so we
915        // only pay it when the session actually asked for recency.
916        let vars = session.vars();
917        let real_time_recency_ts: Option<Timestamp> = if vars.real_time_recency()
918            && vars.transaction_isolation() == &IsolationLevel::StrictSerializable
919            && !session.contains_read_timestamp()
920        {
921            let real_time_recency_timeout = *vars.real_time_recency_timeout();
922            self.call_coordinator(|tx| Command::DetermineRealTimeRecentTimestamp {
923                source_ids: depends_on.iter().copied().collect(),
924                real_time_recency_timeout,
925                tx,
926            })
927            .await??
928        } else {
929            None
930        };
931
932        let bundle = global_mir_plan.id_bundle(cluster_id);
933        let (determination, read_holds) = self
934            .frontend_determine_timestamp(
935                session,
936                &bundle,
937                &QueryWhen::FreshestTableWrite,
938                cluster_id,
939                &timeline,
940                oracle_read_ts,
941                real_time_recency_ts,
942            )
943            .await?;
944
945        let as_of = determination.timestamp_context.timestamp_or_default();
946
947        let global_mir_plan = global_mir_plan.resolve(Antichain::from_elem(as_of));
948        let global_lir_plan = optimizer.catch_unwind_optimize(global_mir_plan)?;
949
950        // Log optimization finished
951        if let Some(logging_id) = statement_logging_id {
952            self.log_lifecycle_event(logging_id, StatementLifecycleEvent::OptimizationFinished);
953        }
954
955        let sink_id = global_lir_plan.sink_id();
956        let target_id = plan.id;
957        let target_global_id = catalog.get_entry(&target_id).latest_global_id();
958        let kind = plan.kind.clone();
959        let returning = plan.returning.clone();
960
961        let (df_desc, df_meta) = global_lir_plan.unapply();
962
963        crate::coord::sequencer::emit_optimizer_notices(
964            &**catalog,
965            session,
966            &df_meta.optimizer_notices,
967        );
968        if session.vars().emit_timestamp_notice() {
969            self.add_timestamp_notice(session, cluster_id, bundle, determination)
970                .await?;
971        }
972
973        let arity = df_desc
974            .sink_exports
975            .values()
976            .next()
977            .expect("has sink")
978            .from_desc
979            .arity();
980
981        let conn_id = session.conn_id().clone();
982        let session_uuid = session.uuid();
983        let start_time = (self.statement_logging_frontend.now)();
984        let max_result_size = catalog.system_config().max_result_size();
985        let max_query_result_size = session.vars().max_query_result_size();
986        let row_set_finishing_seconds = session.metrics().row_set_finishing_seconds().clone();
987        let max_occ_retries = usize::cast_from(catalog.system_config().max_occ_retries());
988
989        // Linearize the read BEFORE subscribing or writing: block until
990        // the oracle for this query's timeline has advanced to `as_of`.
991        //
992        // Ordering is load-bearing: this leaves the oracle at or above `as_of`,
993        // which is what makes the loop's target clear `as_of` and so include the
994        // snapshot. A far-future `as_of` parks here until the clock arrives,
995        // bounded by `statement_timeout`.
996        self.ensure_read_linearized(&timeline, as_of).await?;
997
998        // The loop takes its write target from this oracle, and reaching one takes
999        // `&mut self`, which the loop does not have. `None` for a
1000        // timestamp-independent selection, which reads at `Timestamp::maximum()`
1001        // and so always leaves through the blind path rather than reaching a write.
1002        let write_oracle = match governing_timeline(&timeline) {
1003            Some(tl) => Some(Arc::clone(self.ensure_oracle(tl).await?)),
1004            None => None,
1005        };
1006
1007        let subscribe_handle = self
1008            .create_internal_subscribe(
1009                Box::new(df_desc),
1010                cluster_id,
1011                replica_id,
1012                depends_on.clone(),
1013                as_of,
1014                arity,
1015                sink_id,
1016                caller.subscribe_owner(&conn_id, session_uuid),
1017                start_time,
1018                read_holds,
1019            )
1020            .await?;
1021
1022        let (retry_count, result) = self
1023            .run_occ_loop(
1024                subscribe_handle,
1025                target_id,
1026                target_global_id,
1027                kind,
1028                returning,
1029                max_result_size,
1030                max_query_result_size,
1031                row_set_finishing_seconds,
1032                max_occ_retries,
1033                table_desc,
1034                caller.write_conn_id(&conn_id),
1035                statement_logging_id,
1036                as_of,
1037                write_oracle,
1038                &attempt_state,
1039            )
1040            .await;
1041
1042        let caller_label = match caller {
1043            RtwCaller::Session => OCC_CALLER_SESSION,
1044            RtwCaller::Background { .. } => OCC_CALLER_BACKGROUND,
1045        };
1046        self.coordinator_client()
1047            .metrics()
1048            .occ_retry_count
1049            .with_label_values(&[caller_label])
1050            .observe(f64::from(u32::try_from(retry_count).unwrap_or(u32::MAX)));
1051
1052        // Finish the operation, including a blind write's submission, before
1053        // releasing the OCC permit. Holding it for the entire operation is what
1054        // bounds concurrency. An early drop would let a waiter start its
1055        // subscribe while we are still consolidating diffs, retrying, or
1056        // waiting for our write to commit.
1057        //
1058        // The zero-row linearization wait below is the one exception, and hands
1059        // the permit back before it parks.
1060        let mut permit = permit;
1061        let response = match result {
1062            Ok(OccOutcome::Committed { response, write_ts }) => {
1063                // A committed write timestamp for a statement we meant to
1064                // stage means the two predicates disagreed: the syntactic one
1065                // said it reads nothing, the subscribe then read persisted
1066                // state. The write is already durable, so there is nothing to
1067                // refuse, and `apply_write` still has to run to keep the
1068                // session's read timestamps ahead of it.
1069                soft_assert_or_log!(
1070                    !stages_rows,
1071                    "read-then-write committed a write it meant to stage"
1072                );
1073                session.apply_write(write_ts);
1074                end_own_transaction(session, stages_rows);
1075                Ok(response)
1076            }
1077            Ok(OccOutcome::NoRowsMatched {
1078                response,
1079                empty_as_of,
1080            }) => {
1081                // An `empty_as_of` for a statement we meant to stage is the
1082                // same predicate disagreement the `Committed` arm guards
1083                // against: the syntactic answer said it reads nothing, the
1084                // subscribe then read persisted state. Nothing is durable here,
1085                // so there is nothing to undo, but the disagreement itself is
1086                // the bug and it would otherwise park silently.
1087                soft_assert_or_log!(
1088                    !(stages_rows && empty_as_of.is_some()),
1089                    "read-then-write observed a read timestamp for a statement \
1090                     it meant to stage"
1091                );
1092                end_own_transaction(session, stages_rows);
1093                match empty_as_of {
1094                    Some(empty_as_of) => {
1095                        // The wait is a no-op where the oracle is already past
1096                        // `empty_as_of`, which is the common `WHERE <no match>`,
1097                        // and otherwise costs the group commit that
1098                        // `ensure_read_linearized` asks for. Either way the
1099                        // subscribe handle is gone, so the permit guards nothing
1100                        // and holding it would throttle unrelated writes.
1101                        drop(permit.take());
1102                        self.ensure_read_linearized(&timeline, empty_as_of)
1103                            .await
1104                            .map(|()| response)
1105                    }
1106                    None => Ok(response),
1107                }
1108            }
1109            Ok(OccOutcome::Blind { response, diffs }) if stages_rows => {
1110                // Staging rather than writing here is what makes the statement
1111                // atomic with its transaction. An extended-protocol pipeline is
1112                // an implicit transaction, and it may still fail after us, so a
1113                // write of our own would survive a rollback that discards
1114                // everything around it.
1115                //
1116                // NOTE: A staged session write carries no target-generation
1117                // guard the way the immediate path's `target_global_id` does. A
1118                // `WriteOp` only names the `CatalogItemId`, and commit staging
1119                // resolves whatever global id is current then. What keeps it
1120                // safe is a check at the far end: group commit compares the
1121                // arity of the rows each staged write carries against the
1122                // target's latest `RelationDesc` and rolls the transaction back
1123                // with `ConcurrentDependencyMutation` instead of encoding old
1124                // rows against a new schema. So an `ALTER TABLE ... ADD COLUMN`
1125                // landing between here and the commit becomes the same
1126                // retryable failure the immediate path reports as
1127                // `TargetChanged`. The comparison reads one row per staged
1128                // write and looks only at arity, so it stands in for the
1129                // descriptor rather than pinning it.
1130                //
1131                // Missing the pin is true of every staged write. The arity
1132                // check is not: rows staged as a batch, which is how `COPY
1133                // FROM` arrives, carry their schema into persist instead.
1134                session
1135                    .add_transaction_ops(TransactionOps::Writes(vec![WriteOp {
1136                        id: target_id,
1137                        rows: TableData::Rows(diffs),
1138                    }]))
1139                    .map(|()| response)
1140            }
1141            Ok(OccOutcome::Blind { response, diffs }) => {
1142                if caller.is_background() {
1143                    return Err(AdapterError::Internal(
1144                        "background read-then-write unexpectedly had no persisted dependency"
1145                            .into(),
1146                    ));
1147                }
1148                // The subscribe closed on its own even though the selection
1149                // reads persisted state, so the input is sealed and the diffs
1150                // are frontier-independent after all. Staging them would be
1151                // safe for that reason, and we still do not, because it would
1152                // make a statement's transaction semantics depend on whether an
1153                // input happens to be past its last refresh. What the statement
1154                // reads decides, so this commits as its own transaction like
1155                // any other read-dependent write.
1156                match self
1157                    .submit_blind_write(
1158                        conn_id,
1159                        target_id,
1160                        target_global_id,
1161                        diffs,
1162                        statement_logging_id,
1163                        &attempt_state,
1164                    )
1165                    .await
1166                {
1167                    Ok(write_ts) => {
1168                        session.apply_write(write_ts);
1169                        end_own_transaction(session, stages_rows);
1170                        Ok(response)
1171                    }
1172                    Err(err) => Err(err),
1173                }
1174            }
1175            Err(err) => Err(err),
1176        };
1177
1178        drop(permit);
1179
1180        response
1181    }
1182
1183    /// Builds the subscribe optimizer and the unresolved global MIR plan for a
1184    /// read-then-write.
1185    ///
1186    /// The optimized expression is the selection with the mutation already
1187    /// applied, so the subscribe's sink emits ready-to-write table diffs rather
1188    /// than query results. `finishing` and `returning` are deliberately not part
1189    /// of the dataflow, and unmaterializable functions are prepared one-shot.
1190    fn optimize_mir_read_then_write(
1191        &self,
1192        catalog: &Arc<Catalog>,
1193        session: &dyn SessionMetadata,
1194        plan: &plan::ReadThenWritePlan,
1195        cluster_id: ComputeInstanceId,
1196    ) -> Result<
1197        (
1198            optimize::subscribe::Optimizer,
1199            optimize::subscribe::GlobalMirPlan<optimize::subscribe::Unresolved>,
1200        ),
1201        AdapterError,
1202    > {
1203        // `finishing` is unused: the OCC path emits raw diffs and
1204        // `apply_mutation_to_mir` handles update projection.
1205        let plan::ReadThenWritePlan {
1206            id: _,
1207            selection,
1208            finishing: _,
1209            assignments,
1210            kind,
1211            returning: _,
1212        } = plan;
1213
1214        let expr = selection.clone().lower(catalog.system_config(), None)?;
1215        let mut expr = apply_mutation_to_mir(expr, kind, assignments);
1216
1217        // Resolve unmaterializable functions (now(), current_user, ...) before
1218        // the subscribe optimizer sees them: it uses `ExprPrepMaintained`,
1219        // which rejects them, but our subscribe is a one-shot read so we can
1220        // resolve them to constants. `mz_now()` is rejected upstream by
1221        // `validate_read_then_write`.
1222        let style = ExprPrepOneShot {
1223            logical_time: EvalTime::NotAvailable,
1224            session,
1225            catalog_state: catalog.state(),
1226        };
1227        expr.try_visit_scalars_mut(&mut |s| style.prep_scalar_expr(s))?;
1228
1229        let compute_instance = ComputeInstanceSnapshot::new_without_collections(cluster_id);
1230        let (_, view_id) = self.transient_id_gen.allocate_id();
1231        let (_, sink_id) = self.transient_id_gen.allocate_id();
1232        let debug_name = format!("frontend-read-then-write-subscribe-{}", sink_id);
1233        let optimizer_config = optimize::OptimizerConfig::from(catalog.system_config())
1234            .override_from(&catalog.get_cluster(cluster_id).config.features())
1235            .override_from(
1236                &catalog
1237                    .state()
1238                    .cluster_scoped_optimizer_overrides(cluster_id),
1239            );
1240
1241        let mut optimizer = optimize::subscribe::Optimizer::new(
1242            Arc::<Catalog>::clone(catalog),
1243            compute_instance,
1244            view_id,
1245            sink_id,
1246            true, // with_snapshot
1247            None, // up_to
1248            debug_name,
1249            optimizer_config,
1250            self.optimizer_metrics.clone(),
1251        );
1252
1253        let expr_typ = expr.typ();
1254        let sql_typ = mz_repr::SqlRelationType::from_repr(&expr_typ);
1255        let column_names: Vec<String> = (0..sql_typ.column_types.len())
1256            .map(|i| format!("column{}", i))
1257            .collect();
1258        let relation_desc = RelationDesc::new(sql_typ, column_names.iter().map(|s| s.as_str()));
1259
1260        // MIR ⇒ MIR optimization (global). The mutation is already applied in
1261        // MIR, so we hand the expression to the subscribe optimizer directly
1262        // instead of going through the `SubscribePlan` path, which expects HIR.
1263        // An empty `output` makes the sink emit raw diffs.
1264        // `optimize_query` is not an `Optimize` impl, so wrap it by hand.
1265        let global_mir_plan = mz_transform::catch_unwind_optimize(|| {
1266            optimizer.optimize_query(expr, relation_desc, vec![])
1267        })?;
1268
1269        Ok((optimizer, global_mir_plan))
1270    }
1271
1272    /// The governing oracle's read timestamp, used as a lower bound for
1273    /// timestamp selection. `None` when the selection needs no oracle.
1274    async fn oracle_read_ts(
1275        &mut self,
1276        timeline: &TimelineContext,
1277    ) -> Result<Option<Timestamp>, AdapterError> {
1278        match governing_timeline(timeline) {
1279            Some(timeline) => {
1280                let oracle = self.ensure_oracle(timeline).await?;
1281                Ok(Some(oracle.read_ts().await))
1282            }
1283            None => Ok(None),
1284        }
1285    }
1286
1287    /// Block until the oracle for this query's timeline has advanced to
1288    /// `as_of`. Returns immediately if it already has.
1289    ///
1290    /// This implements the strict-serializable read guarantee for RTW:
1291    /// once this returns, any session observing the oracle sees a read
1292    /// timestamp at least as large as `as_of`, so reads at `as_of` (and
1293    /// writes derived from them) cannot appear to "go backwards" relative
1294    /// to subsequent queries.
1295    async fn ensure_read_linearized(
1296        &mut self,
1297        timeline: &TimelineContext,
1298        as_of: Timestamp,
1299    ) -> Result<(), AdapterError> {
1300        // Linearization must target the oracle future readers of the target
1301        // table will consult, which is why this uses `governing_timeline` and
1302        // not `TimelineContext::timeline()`. The latter answers "is there a
1303        // source-forced timeline?" and would skip linearization entirely for a
1304        // read side that pins none.
1305        let tl = match governing_timeline(timeline) {
1306            Some(tl) => tl,
1307            None => return Ok(()),
1308        };
1309
1310        // Cloned before `ensure_oracle` borrows `self` for the rest of this
1311        // function. The handle is an `Arc` internally, so this is cheap.
1312        let group_commit_notifier = self.group_commit_notifier.clone();
1313        let oracle = self.ensure_oracle(tl).await?;
1314
1315        // The oracle advances only when a group commit applies, and an empty
1316        // group commit is already the periodic keepalive. So when we have
1317        // nothing to write ourselves, waiting for the next tick costs up to a
1318        // full `default_timestamp_interval`. We ask for that commit instead of
1319        // waiting for it, which also spares the oracle the ~1ms poll below
1320        // running for the whole interval.
1321        //
1322        // Once per wait rather than once per poll. The committer never
1323        // allocates a write timestamp above wall clock, so a far-future `as_of`
1324        // cannot be reached by asking, and nudging per iteration would spin for
1325        // as long as such a statement legitimately parks. That case pays one
1326        // empty commit, which is what the keepalive would have done anyway.
1327        let mut nudged = false;
1328
1329        loop {
1330            let oracle_ts = oracle.read_ts().await;
1331            if as_of <= oracle_ts {
1332                return Ok(());
1333            }
1334
1335            if !nudged {
1336                group_commit_notifier.notify();
1337                nudged = true;
1338            }
1339
1340            // Sleep for roughly the difference between as_of and the current
1341            // oracle timestamp. Since timestamps are epoch milliseconds, the
1342            // difference is the approximate wall-clock time we need to wait.
1343            // Cap at 1s to avoid very long sleeps if clocks are skewed,
1344            // matching the cap in `message_linearize_reads`.
1345            let wait_ms = u64::from(as_of.saturating_sub(oracle_ts));
1346            let wait = Duration::from_millis(wait_ms).min(Duration::from_secs(1));
1347            tokio::time::sleep(wait).await;
1348        }
1349    }
1350
1351    /// Submits frontier-independent diffs to group commit, which picks the
1352    /// write timestamp, and returns the timestamp the write committed at.
1353    ///
1354    /// Only valid for diffs that do not depend on an observed read frontier:
1355    /// the write lands at a timestamp this caller does not choose.
1356    async fn submit_blind_write(
1357        &self,
1358        conn_id: mz_adapter_types::connection::ConnectionId,
1359        target_id: CatalogItemId,
1360        target_global_id: GlobalId,
1361        diffs: Vec<(Row, Diff)>,
1362        statement_logging_id: Option<StatementLoggingId>,
1363        attempt_state: &FrontendWriteAttemptState,
1364    ) -> Result<Timestamp, AdapterError> {
1365        attempt_state.mark_write_submitted();
1366        let result = self
1367            .call_coordinator(|tx| Command::AttemptWrite {
1368                attempt: WriteAttemptKind::Session {
1369                    conn_id,
1370                    write_ts: None,
1371                },
1372                target_id,
1373                target_global_id,
1374                diffs,
1375                tx,
1376            })
1377            .await?;
1378
1379        // Every outcome here terminates the attempt, so `write_submitted`
1380        // stays set per its contract.
1381        match classify_write_result(result, target_id, attempt_state) {
1382            WriteOutcome::Committed(timestamp) => {
1383                if let Some(id) = statement_logging_id {
1384                    self.log_set_timestamp(id, timestamp);
1385                }
1386                Ok(timestamp)
1387            }
1388            WriteOutcome::Failed(err) => Err(err),
1389            WriteOutcome::Conflict { .. } => {
1390                // Unreachable: a write that requests no timestamp cannot have
1391                // one pass. Group commit resolves it through
1392                // `UserWriteResponder::Internal`, which only reports a conflict
1393                // to a write that asked for a specific timestamp.
1394                soft_panic_or_log!("blind read-then-write unexpectedly got TimestampPassed");
1395                Err(AdapterError::Internal(
1396                    "blind write unexpectedly got TimestampPassed".into(),
1397                ))
1398            }
1399        }
1400    }
1401
1402    /// Creates an internal subscribe, meaning one that writes no
1403    /// `mz_subscriptions` row. Returns a [`SubscribeHandle`] that ensures
1404    /// cleanup on drop.
1405    async fn create_internal_subscribe(
1406        &self,
1407        df_desc: Box<optimize::LirDataflowDescription>,
1408        cluster_id: ComputeInstanceId,
1409        replica_id: Option<ReplicaId>,
1410        depends_on: BTreeSet<GlobalId>,
1411        as_of: Timestamp,
1412        arity: usize,
1413        sink_id: GlobalId,
1414        owner: ActiveSubscribeOwner,
1415        start_time: mz_ore::now::EpochMillis,
1416        read_holds: crate::ReadHolds,
1417    ) -> Result<SubscribeHandle, AdapterError> {
1418        let rx: mpsc::UnboundedReceiver<PeekResponseUnary> = self
1419            .call_coordinator(|tx| Command::CreateInternalSubscribe {
1420                df_desc,
1421                cluster_id,
1422                replica_id,
1423                depends_on,
1424                as_of,
1425                arity,
1426                sink_id,
1427                owner,
1428                start_time,
1429                read_holds,
1430                tx,
1431            })
1432            .await??;
1433
1434        Ok(SubscribeHandle {
1435            rx,
1436            sink_id,
1437            client: Some(self.coordinator_client().clone()),
1438        })
1439    }
1440
1441    /// Run the OCC loop: drain the subscribe at `as_of`, apply the
1442    /// mutation, and submit the resulting diffs as a write.
1443    ///
1444    /// Semantically a SELECT at `target - 1` followed by an INSERT at `target`.
1445    /// `write_oracle` chooses `target`, the subscribe's frontier certifies the
1446    /// payload is complete below it, and a target the target table has moved
1447    /// past comes back as `WriteResult::TimestampPassed`, whose next eligible
1448    /// timestamp the loop adopts. At most `max_occ_retries` attempts.
1449    ///
1450    /// A subscribe that ends on its own has diffs no frontier can change, and
1451    /// those are returned as [`OccOutcome::Blind`] rather than written.
1452    ///
1453    /// Contract on the caller, both ends of the read: the oracle's read
1454    /// timestamp must be at or above `as_of` on entry, and an
1455    /// [`OccOutcome::NoRowsMatched`] must be linearized against its
1456    /// `empty_as_of` before the response goes out.
1457    ///
1458    /// `write_oracle` is `None` only for a timestamp-independent selection. Such
1459    /// a statement reads at `Timestamp::maximum()`, so it observes no progress
1460    /// past its `as_of` and always leaves through the blind path.
1461    ///
1462    /// Returns `(retry_count, result)` so the caller can record OCC retry
1463    /// metrics regardless of whether the operation succeeded or failed.
1464    async fn run_occ_loop(
1465        &self,
1466        mut subscribe_handle: SubscribeHandle,
1467        target_id: CatalogItemId,
1468        target_global_id: GlobalId,
1469        kind: MutationKind,
1470        returning: Vec<MirScalarExpr>,
1471        max_result_size: u64,
1472        max_query_result_size: u64,
1473        row_set_finishing_seconds: Histogram,
1474        max_occ_retries: usize,
1475        table_desc: RelationDesc,
1476        write_conn_id: Option<mz_adapter_types::connection::ConnectionId>,
1477        statement_logging_id: Option<StatementLoggingId>,
1478        as_of: Timestamp,
1479        write_oracle: Option<Arc<dyn TimestampOracle<Timestamp> + Send + Sync>>,
1480        attempt_state: &FrontendWriteAttemptState,
1481    ) -> (usize, Result<OccOutcome, AdapterError>) {
1482        let mut state = OccState::new();
1483
1484        // The timestamp the next attempt writes at, chosen when we are first
1485        // ready to attempt one and replaced only by a conflict. `None` until
1486        // then.
1487        let mut write_target: Option<Timestamp> = None;
1488
1489        // The smallest timestamp an attempt may target. `as_of` itself is out,
1490        // since the payload has to contain the snapshot the subscribe emits
1491        // there.
1492        //
1493        // `as_of` is `Timestamp::MAX` for a selection with no timestamp at all,
1494        // which is the selection whose subscribe closes on its own and leaves
1495        // through the blind path rather than a write. There is no timestamp
1496        // above `MAX`, so saturating keeps this total instead of asserting a
1497        // property of a value the write path never uses.
1498        let min_target = as_of.try_step_forward().unwrap_or(as_of);
1499
1500        // Retry invariant: the payload is the selection consolidated at
1501        // `target - 1`, and the diffs at or above `target` are concurrent with
1502        // the write, so a retry folds them in only once it raises the target.
1503        let result = loop {
1504            if let Some(error) = attempt_state.requested_error() {
1505                break Err(error);
1506            }
1507
1508            // Before a target is chosen, `min_target` is a lower bound on it, so
1509            // folding there consolidates the snapshot without admitting a diff a
1510            // write would have to treat as concurrent.
1511            let fold_target = write_target.unwrap_or(min_target);
1512
1513            // Already certified for the target we hold? Write. Otherwise wait for
1514            // the next subscribe message. Waiting first would hang after a
1515            // conflict, since an input settled until its next refresh sends
1516            // nothing further and does not close the channel either.
1517            //
1518            // Termination: the write arm awaits a round trip and only a conflict
1519            // returns to this one, raising `retry_count` towards
1520            // `max_occ_retries`, so neither arm spins.
1521            let attempt_write = match write_target {
1522                Some(target) if state.current_upper.is_some_and(|upper| upper >= target) => true,
1523                _ => {
1524                    let msg = match subscribe_handle.recv().await {
1525                        Some(msg) => msg,
1526                        None => {
1527                            // The channel closed cleanly, which says the
1528                            // selection can never change again, so these diffs
1529                            // are frontier-independent. It does not say the
1530                            // selection reads nothing persisted, a sealed
1531                            // `REFRESH AT` MV closes cleanly too. Either way no
1532                            // target separates them, and the caller decides
1533                            // where they go.
1534                            state.fold_all();
1535                            if state.payload.is_empty() {
1536                                break Ok(OccOutcome::NoRowsMatched {
1537                                    response: build_no_rows_response(&kind),
1538                                    empty_as_of: None,
1539                                });
1540                            }
1541                            let success_response = match build_success_response(
1542                                &kind,
1543                                &returning,
1544                                &state.payload,
1545                                max_result_size,
1546                                max_query_result_size,
1547                                &row_set_finishing_seconds,
1548                            ) {
1549                                Ok(response) => response,
1550                                Err(e) => break Err(e),
1551                            };
1552
1553                            break Ok(OccOutcome::Blind {
1554                                response: success_response,
1555                                diffs: std::mem::take(&mut state.payload),
1556                            });
1557                        }
1558                    };
1559
1560                    match process_message(
1561                        msg,
1562                        &mut state,
1563                        as_of,
1564                        fold_target,
1565                        max_result_size,
1566                        &table_desc,
1567                    ) {
1568                        ProcessResult::Continue { ready_to_write } => ready_to_write,
1569                        ProcessResult::NoRowsMatched { empty_as_of } => {
1570                            break Ok(OccOutcome::NoRowsMatched {
1571                                response: build_no_rows_response(&kind),
1572                                empty_as_of: Some(empty_as_of),
1573                            });
1574                        }
1575                        ProcessResult::Error(e) => break Err(e),
1576                    }
1577                }
1578            };
1579
1580            if !attempt_write {
1581                continue;
1582            }
1583
1584            // Drain buffered messages before attempting the write.
1585            let drain_err = loop {
1586                match subscribe_handle.try_recv() {
1587                    Ok(msg) => {
1588                        match process_message(
1589                            msg,
1590                            &mut state,
1591                            as_of,
1592                            fold_target,
1593                            max_result_size,
1594                            &table_desc,
1595                        ) {
1596                            ProcessResult::Continue { .. } => {}
1597                            ProcessResult::NoRowsMatched { empty_as_of } => {
1598                                break Some(Ok(OccOutcome::NoRowsMatched {
1599                                    response: build_no_rows_response(&kind),
1600                                    empty_as_of: Some(empty_as_of),
1601                                }));
1602                            }
1603                            ProcessResult::Error(e) => {
1604                                break Some(Err(e));
1605                            }
1606                        }
1607                    }
1608                    Err(mpsc::error::TryRecvError::Empty) => break None,
1609                    // The subscribe can finish (coordinator drops the sender
1610                    // after `process_response` returns true) between our last
1611                    // recv() and this drain. This is benign, all buffered
1612                    // messages have already been consumed via the Ok(msg) arm
1613                    // above.
1614                    Err(mpsc::error::TryRecvError::Disconnected) => break None,
1615                }
1616            };
1617            if let Some(result) = drain_err {
1618                break result;
1619            }
1620
1621            let upper = state
1622                .current_upper
1623                .expect("a write attempt requires an observed frontier");
1624
1625            let target = match write_target {
1626                Some(target) => target,
1627                None => {
1628                    let Some(oracle) = &write_oracle else {
1629                        // Invariant: a statement with no governing timeline
1630                        // reads at `as_of == Timestamp::maximum()`, so it
1631                        // observes no progress past its `as_of` and leaves
1632                        // through the blind arm above rather than reaching a
1633                        // write.
1634                        soft_panic_or_log!(
1635                            "read-then-write reached a write attempt with no governing timeline"
1636                        );
1637                        break Err(AdapterError::Internal(
1638                            "read-then-write has no oracle to take a write timestamp from".into(),
1639                        ));
1640                    };
1641
1642                    // One step above the oracle's write timestamp is the smallest
1643                    // value `commit_timestamped` accepts.
1644                    let peek_write_ts = oracle.peek_write_ts().await;
1645                    let Some(chosen) = peek_write_ts.try_step_forward() else {
1646                        // A timeline that reached `Timestamp::MAX` is a broken
1647                        // environment, not anything this statement did.
1648                        soft_panic_or_log!(
1649                            "read-then-write cannot target a timestamp above the write \
1650                             timeline's timestamp {peek_write_ts}"
1651                        );
1652                        break Err(AdapterError::Internal(format!(
1653                            "write timeline exhausted at timestamp {peek_write_ts}"
1654                        )));
1655                    };
1656
1657                    // Unreachable while the oracle's read timestamp is at or
1658                    // above `as_of` on entry, and the clamp keeps the payload
1659                    // rule rather than only reporting the violation.
1660                    if chosen < min_target {
1661                        soft_panic_or_log!(
1662                            "read-then-write target {chosen} does not clear the as_of {as_of}, \
1663                             so the payload would miss the snapshot"
1664                        );
1665                    }
1666                    let chosen = std::cmp::max(chosen, min_target);
1667
1668                    write_target = Some(chosen);
1669                    chosen
1670                }
1671            };
1672
1673            // Fold in what the drain picked up, plus anything a target raised
1674            // by the last conflict now admits.
1675            state.fold_below(target);
1676
1677            // A write at `target` needs every diff below it, which is what
1678            // progress at or above `target` certifies. Waiting for the next
1679            // message is bounded by `statement_timeout`, like every wait here.
1680            if upper < target {
1681                continue;
1682            }
1683
1684            if state.payload.is_empty() {
1685                // Everything below `target` cancelled out, so there is nothing to
1686                // write and the answer holds as of `target - 1`. Diffs pending at
1687                // or above `target` are concurrent with the write this would have
1688                // been and do not enter it.
1689                break Ok(OccOutcome::NoRowsMatched {
1690                    response: build_no_rows_response(&kind),
1691                    empty_as_of: Some(empty_as_of(target)),
1692                });
1693            }
1694
1695            let success_response = match build_success_response(
1696                &kind,
1697                &returning,
1698                &state.payload,
1699                max_result_size,
1700                max_query_result_size,
1701                &row_set_finishing_seconds,
1702            ) {
1703                Ok(response) => response,
1704                Err(e) => break Err(e),
1705            };
1706
1707            // Submit write.
1708            //
1709            // TODO(aljoscha): Store `Arc<Row>` in the payload if this shows up
1710            // in profiles. Every attempt clones every row, and we retry up to
1711            // `max_occ_retries` times.
1712            attempt_state.mark_write_submitted();
1713            let result = match self
1714                .call_coordinator(|tx| Command::AttemptWrite {
1715                    attempt: match write_conn_id.clone() {
1716                        Some(conn_id) => WriteAttemptKind::Session {
1717                            conn_id,
1718                            write_ts: Some(target),
1719                        },
1720                        None => WriteAttemptKind::Background { write_ts: target },
1721                    },
1722                    target_id,
1723                    target_global_id,
1724                    diffs: state.payload.clone(),
1725                    tx,
1726                })
1727                .await
1728            {
1729                Ok(result) => result,
1730                Err(error) => break Err(error),
1731            };
1732
1733            match classify_write_result(result, target_id, attempt_state) {
1734                WriteOutcome::Committed(timestamp) => {
1735                    if let Some(id) = statement_logging_id {
1736                        self.log_set_timestamp(id, timestamp);
1737                    }
1738                    // N.B. subscribe_handle is dropped here, which fires off
1739                    // the cleanup message.
1740                    break Ok(OccOutcome::Committed {
1741                        response: success_response,
1742                        write_ts: timestamp,
1743                    });
1744                }
1745                WriteOutcome::Failed(err) => break Err(err),
1746                WriteOutcome::Conflict {
1747                    next_eligible_timestamp,
1748                } => {
1749                    // The write definitively did not land, so the attempt is
1750                    // resolved. Clearing `write_submitted` lets a cancel or
1751                    // statement timeout that fires during the upcoming
1752                    // subscribe wait resolve promptly instead of awaiting a
1753                    // write result.
1754                    attempt_state.mark_write_resolved();
1755                    // Adopt the timestamp the committer reported as next
1756                    // eligible. The accumulated diffs say nothing about it
1757                    // yet, and they do not have to: the readiness check above
1758                    // holds the next attempt until the subscribe has certified
1759                    // everything below the new target, and the fold then moves
1760                    // the diffs in between into the payload.
1761                    write_target = Some(next_eligible_timestamp);
1762                    state.retry_count += 1;
1763                    // Cancellation wins over the retry budget: if both apply,
1764                    // the user asked us to stop and that is the more truthful
1765                    // answer.
1766                    if let Some(error) = attempt_state.requested_error() {
1767                        break Err(error);
1768                    }
1769                    if state.retry_count > max_occ_retries {
1770                        // Contention is a user-visible condition, not an
1771                        // internal invariant violation, and every attempt was
1772                        // refused before anything was appended, so the
1773                        // statement is retryable.
1774                        break Err(AdapterError::ReadThenWriteContention);
1775                    }
1776                    tracing::debug!(
1777                        retry_count = state.retry_count,
1778                        write_ts = %target,
1779                        next_eligible_timestamp = %next_eligible_timestamp,
1780                        "OCC write conflict, retrying"
1781                    );
1782                    continue;
1783                }
1784            }
1785        };
1786
1787        (state.retry_count, result)
1788    }
1789}
1790
1791/// Result of validating a read-then-write operation.
1792struct ValidationResult {
1793    cluster_id: ComputeInstanceId,
1794    replica_id: Option<ReplicaId>,
1795    timeline: TimelineContext,
1796    depends_on: BTreeSet<GlobalId>,
1797    /// The table descriptor, used for constraint validation.
1798    table_desc: RelationDesc,
1799}
1800
1801/// Accumulated state for the OCC loop in `run_occ_loop`.
1802///
1803/// Every diff the subscribe ever sent is kept, split at [`Self::split`]. The
1804/// split only rises, so each diff crosses it once.
1805struct OccState {
1806    /// Consolidated net diffs from strictly below [`Self::split`].
1807    payload: Vec<(Row, Diff)>,
1808    /// Diffs at or above [`Self::split`], consolidated by `(row, timestamp)`.
1809    pending: Vec<(Row, Timestamp, Diff)>,
1810    /// Where the last fold split the diffs, `None` before the first one.
1811    split: Option<Timestamp>,
1812    /// Timestamp of the last progress message, which certifies that no diff
1813    /// will arrive below it.
1814    current_upper: Option<Timestamp>,
1815    retry_count: usize,
1816    /// Row bytes held in `payload` and `pending` together, which is what the
1817    /// `max_result_size` check measures. `pending` is consolidated by
1818    /// `(row, timestamp)`, so a row touched at several timestamps occupies
1819    /// several entries until a rising split folds them together, and the count
1820    /// can exceed the size of the payload that eventually goes out.
1821    byte_size: u64,
1822}
1823
1824impl OccState {
1825    fn new() -> Self {
1826        Self {
1827            payload: Vec::new(),
1828            pending: Vec::new(),
1829            split: None,
1830            current_upper: None,
1831            retry_count: 0,
1832            byte_size: 0,
1833        }
1834    }
1835
1836    /// Raises the split to `split`, moving the diffs below it into the payload.
1837    ///
1838    /// Lowering it is a bug: the payload is consolidated and never re-split, so
1839    /// the diffs above a lowered split would stay in it. We clamp to the old
1840    /// split, which keeps the payload's contract intact.
1841    fn fold_below(&mut self, split: Timestamp) {
1842        let split = match self.split {
1843            Some(previous) if split < previous => {
1844                soft_panic_or_log!(
1845                    "read-then-write folded at {split}, below its previous split {previous}"
1846                );
1847                previous
1848            }
1849            _ => split,
1850        };
1851        self.split = Some(split);
1852        self.fold(Some(split));
1853    }
1854
1855    /// Moves every accumulated diff into the payload, whatever its timestamp.
1856    ///
1857    /// Only valid once the subscribe has run to completion, where the diffs are
1858    /// frontier-independent and no split separates them.
1859    fn fold_all(&mut self) {
1860        self.fold(None);
1861    }
1862
1863    /// Moves the diffs below `split`, or all of them when it is `None`, into the
1864    /// payload, consolidates both halves, and recomputes `byte_size`.
1865    fn fold(&mut self, split: Option<Timestamp>) {
1866        for (row, ts, diff) in std::mem::take(&mut self.pending) {
1867            match split {
1868                Some(split) if ts >= split => self.pending.push((row, ts, diff)),
1869                _ => self.payload.push((row, diff)),
1870            }
1871        }
1872        consolidation::consolidate(&mut self.payload);
1873        consolidation::consolidate_updates(&mut self.pending);
1874        self.byte_size = self
1875            .payload
1876            .iter()
1877            .map(|(row, _)| u64::cast_from(row.byte_len()))
1878            .chain(
1879                self.pending
1880                    .iter()
1881                    .map(|(row, _, _)| u64::cast_from(row.byte_len())),
1882            )
1883            .sum();
1884    }
1885
1886    /// Whether nothing has been accumulated on either side of the split.
1887    fn is_empty(&self) -> bool {
1888        self.payload.is_empty() && self.pending.is_empty()
1889    }
1890}
1891
1892/// Result of processing a single subscribe message in the OCC loop.
1893enum ProcessResult {
1894    Continue {
1895        ready_to_write: bool,
1896    },
1897    /// The consolidated selection is empty, as of the timestamp reported. See
1898    /// [`OccOutcome::NoRowsMatched`].
1899    NoRowsMatched {
1900        empty_as_of: Timestamp,
1901    },
1902    Error(AdapterError),
1903}
1904
1905/// Process one subscribe message, updating `state` in place.
1906///
1907/// Data rows are accumulated into `state` (with per-row constraint and
1908/// max-result-size checks). Progress messages fold everything below
1909/// `fold_target` into the payload and can promote the accumulated diffs to
1910/// "ready to write".
1911///
1912/// `fold_target` must not exceed the timestamp the next write attempt uses, or
1913/// the payload takes in a diff that is concurrent with that write.
1914fn process_message(
1915    response: PeekResponseUnary,
1916    state: &mut OccState,
1917    as_of: Timestamp,
1918    fold_target: Timestamp,
1919    max_result_size: u64,
1920    table_desc: &RelationDesc,
1921) -> ProcessResult {
1922    match response {
1923        PeekResponseUnary::Rows(mut rows) => {
1924            let mut saw_progress = false;
1925
1926            while let Some(row) = rows.next() {
1927                let mut datums = row.iter();
1928
1929                // Extract mz_timestamp (SubscribeOutput::Diffs format:
1930                // mz_timestamp, mz_progressed, mz_diff, ...data columns...).
1931                //
1932                // Format drift would mean we'd silently commit an incorrect
1933                // write, so surface every shape mismatch as an internal
1934                // error rather than panicking the process.
1935                let Some(ts_datum) = datums.next() else {
1936                    return ProcessResult::Error(AdapterError::Internal(
1937                        "missing mz_timestamp in subscribe output".into(),
1938                    ));
1939                };
1940                let ts = match ts_datum {
1941                    mz_repr::Datum::Numeric(n) => match n.0.try_into() {
1942                        Ok(ts_u64) => Timestamp::new(ts_u64),
1943                        Err(_) => {
1944                            return ProcessResult::Error(AdapterError::Internal(format!(
1945                                "mz_timestamp in subscribe output is not a valid u64: {n}"
1946                            )));
1947                        }
1948                    },
1949                    other => {
1950                        return ProcessResult::Error(AdapterError::Internal(format!(
1951                            "unexpected mz_timestamp datum: {other:?}"
1952                        )));
1953                    }
1954                };
1955
1956                let Some(progressed_datum) = datums.next() else {
1957                    return ProcessResult::Error(AdapterError::Internal(
1958                        "missing mz_progressed in subscribe output".into(),
1959                    ));
1960                };
1961                let is_progress = matches!(progressed_datum, mz_repr::Datum::True);
1962
1963                if is_progress {
1964                    state.current_upper = Some(ts);
1965                    saw_progress = true;
1966
1967                    // Fold and consolidate incrementally on each progress
1968                    // message. This keeps memory bounded by the consolidated
1969                    // size and makes the byte_size check below accurate (except
1970                    // for rows received between two progress messages, which is
1971                    // a small window).
1972                    state.fold_below(fold_target);
1973
1974                    // NOTE: The first progress message is always at `as_of`,
1975                    // emitted by `ActiveSubscribe::initialize` before any data
1976                    // batch, so the accumulation is empty there whatever the
1977                    // snapshot holds. Later progress is gated on `batch.upper >
1978                    // as_of` (see `crate::active_compute_sink`), so `ts > as_of`
1979                    // is what distinguishes a real answer from that first one.
1980                    //
1981                    // Nothing accumulated at all, so no write is coming and the
1982                    // loop would otherwise wait for diffs that will not arrive.
1983                    // Our view is complete below `ts` and the payload covers
1984                    // below `fold_target`, so the emptiness holds as of one below
1985                    // the earlier of the two.
1986                    if ts > as_of && state.is_empty() {
1987                        return ProcessResult::NoRowsMatched {
1988                            empty_as_of: empty_as_of(std::cmp::min(ts, fold_target)),
1989                        };
1990                    }
1991                } else {
1992                    let Some(diff_datum) = datums.next() else {
1993                        return ProcessResult::Error(AdapterError::Internal(
1994                            "missing mz_diff in subscribe output".into(),
1995                        ));
1996                    };
1997                    let diff = match diff_datum {
1998                        mz_repr::Datum::Int64(d) => Diff::from(d),
1999                        other => {
2000                            return ProcessResult::Error(AdapterError::Internal(format!(
2001                                "unexpected mz_diff datum while processing read-then-write: {other:?}"
2002                            )));
2003                        }
2004                    };
2005
2006                    let data_row = Row::pack(datums);
2007
2008                    // Validate constraints for rows being added (positive diff)
2009                    if diff.is_positive() {
2010                        for (idx, datum) in data_row.iter().enumerate() {
2011                            if let Err(e) = table_desc.constraints_met(idx, &datum) {
2012                                return ProcessResult::Error(e.into());
2013                            }
2014                        }
2015                    }
2016
2017                    state.byte_size = state
2018                        .byte_size
2019                        .saturating_add(u64::cast_from(data_row.byte_len()));
2020                    if state.byte_size > max_result_size {
2021                        return ProcessResult::Error(AdapterError::ResultSize(format!(
2022                            "result exceeds max size of {}",
2023                            ByteSize::b(max_result_size)
2024                        )));
2025                    }
2026                    state.pending.push((data_row, ts, diff));
2027                }
2028            }
2029
2030            // The complement of the zero-row exit above: something accumulated
2031            // means a write is coming, nothing at all means there is none.
2032            let ready_to_write = saw_progress && !state.is_empty();
2033            ProcessResult::Continue { ready_to_write }
2034        }
2035        PeekResponseUnary::Error(e) => {
2036            ProcessResult::Error(AdapterError::Unstructured(anyhow::anyhow!(e)))
2037        }
2038        // `Unstructured` would misclassify an ordinary concurrent DDL race
2039        // as an internal error (XX000).
2040        PeekResponseUnary::DependencyDropped(dep) => {
2041            ProcessResult::Error(dep.to_concurrent_dependency_drop())
2042        }
2043        PeekResponseUnary::Canceled => ProcessResult::Error(AdapterError::Canceled),
2044    }
2045}
2046
2047/// The timestamp an answer holds as of, given a view complete strictly below
2048/// `complete_below`.
2049///
2050/// Both callers derive `complete_below` from a timestamp strictly above the
2051/// subscribe's `as_of`, so it is never `Timestamp::MIN` and the saturating
2052/// fallback is unreachable.
2053fn empty_as_of(complete_below: Timestamp) -> Timestamp {
2054    complete_below.step_back().unwrap_or(complete_below)
2055}
2056
2057/// Build the response returned when no rows matched the selection.
2058///
2059/// TODO: Return an empty result set for zero-row `INSERT ... RETURNING`, as
2060/// Postgres does, instead of a plain row count.
2061fn build_no_rows_response(kind: &MutationKind) -> ExecuteResponse {
2062    match kind {
2063        MutationKind::Delete => ExecuteResponse::Deleted(0),
2064        MutationKind::Update => ExecuteResponse::Updated(0),
2065        MutationKind::Insert => ExecuteResponse::Inserted(0),
2066    }
2067}
2068
2069/// Transform a MIR expression to produce the appropriate diffs for a mutation.
2070///
2071/// - DELETE: Negates the expression to produce `(row, -1)` diffs
2072/// - UPDATE: Unions negated old rows with mapped new rows to produce both
2073///   `(old_row, -1)` and `(new_row, +1)` diffs
2074fn apply_mutation_to_mir(
2075    expr: MirRelationExpr,
2076    kind: &MutationKind,
2077    assignments: &BTreeMap<usize, MirScalarExpr>,
2078) -> MirRelationExpr {
2079    match kind {
2080        MutationKind::Delete => MirRelationExpr::Negate {
2081            input: Box::new(expr),
2082        },
2083        MutationKind::Update => {
2084            let arity = expr.arity();
2085
2086            // Find a fresh LocalId that won't conflict with any in the expression.
2087            //
2088            // Invariant: `Let` and `LetRec` are the only MIR nodes that *bind*
2089            // LocalIds. `Get` references them but does not introduce new ones.
2090            // So scanning just those two node kinds and picking `max + 1` is
2091            // guaranteed to produce an id unused by the subtree.
2092            let mut max_id = 0_u64;
2093            expr.visit_pre(|e| match e {
2094                MirRelationExpr::Let { id, .. } => {
2095                    max_id = std::cmp::max(max_id, id.into());
2096                }
2097                MirRelationExpr::LetRec { ids, .. } => {
2098                    for id in ids {
2099                        max_id = std::cmp::max(max_id, id.into());
2100                    }
2101                }
2102                _ => {}
2103            });
2104            let binding_id = LocalId::new(max_id + 1);
2105
2106            let get_binding = MirRelationExpr::Get {
2107                id: Id::Local(binding_id),
2108                typ: expr.typ(),
2109                access_strategy: mz_expr::AccessStrategy::UnknownOrLocal,
2110            };
2111
2112            let map_scalars: Vec<MirScalarExpr> = (0..arity)
2113                .map(|i| {
2114                    assignments
2115                        .get(&i)
2116                        .cloned()
2117                        .unwrap_or_else(|| MirScalarExpr::column(i))
2118                })
2119                .collect();
2120
2121            let new_rows = get_binding
2122                .clone()
2123                .map(map_scalars)
2124                .project((arity..2 * arity).collect());
2125
2126            let old_rows = MirRelationExpr::Negate {
2127                input: Box::new(get_binding),
2128            };
2129
2130            let body = new_rows.union(old_rows);
2131
2132            MirRelationExpr::Let {
2133                id: binding_id,
2134                value: Box::new(expr),
2135                body: Box::new(body),
2136            }
2137        }
2138        // INSERT: rows pass through unchanged, the subscribe emits them with
2139        // diff +1.
2140        MutationKind::Insert => expr,
2141    }
2142}
2143
2144#[cfg(test)]
2145mod tests {
2146    use mz_repr::adt::numeric;
2147    use mz_repr::{Datum, IntoRowIterator};
2148
2149    use super::*;
2150
2151    fn row(value: i64) -> Row {
2152        Row::pack_slice(&[Datum::Int64(value)])
2153    }
2154
2155    /// A progress message in the subscribe's `SubscribeOutput::Diffs` shape:
2156    /// `mz_timestamp, mz_progressed, mz_diff, data...`.
2157    fn progress(ts: u64) -> PeekResponseUnary {
2158        let mut row = Row::default();
2159        let mut packer = row.packer();
2160        packer.push(Datum::from(numeric::Numeric::from(ts)));
2161        packer.push(Datum::True);
2162        packer.push(Datum::Null);
2163        PeekResponseUnary::Rows(Box::new(row.into_row_iter()))
2164    }
2165
2166    /// Accumulates `(row value, timestamp, diff)` triples the way
2167    /// `process_message` does, without going through a subscribe.
2168    fn accumulate(diffs: impl IntoIterator<Item = (i64, u64, i64)>) -> OccState {
2169        let mut state = OccState::new();
2170        for (value, ts, diff) in diffs {
2171            state
2172                .pending
2173                .push((row(value), Timestamp::new(ts), Diff::from(diff)));
2174        }
2175        state
2176    }
2177
2178    /// The target is the boundary the write turns on, so the off-by-one is the
2179    /// whole point: a diff at exactly the target is concurrent with the write
2180    /// and must not be in its payload.
2181    #[mz_ore::test]
2182    fn test_fold_below_splits_at_the_target() {
2183        let mut state = accumulate([(1, 9, 1), (2, 10, 1), (3, 11, 1)]);
2184        state.fold_below(Timestamp::new(10));
2185
2186        assert_eq!(state.payload, vec![(row(1), Diff::ONE)]);
2187        assert_eq!(
2188            state.pending,
2189            vec![
2190                (row(2), Timestamp::new(10), Diff::ONE),
2191                (row(3), Timestamp::new(11), Diff::ONE),
2192            ]
2193        );
2194    }
2195
2196    /// A retry raises the target, which is what admits the diffs that were
2197    /// concurrent with the attempt that lost.
2198    #[mz_ore::test]
2199    fn test_fold_below_moves_each_diff_once() {
2200        let mut state = accumulate([(1, 9, 1), (2, 10, 1), (3, 11, 1)]);
2201
2202        state.fold_below(Timestamp::new(10));
2203        assert_eq!(state.payload, vec![(row(1), Diff::ONE)]);
2204
2205        state.fold_below(Timestamp::new(11));
2206        assert_eq!(
2207            state.payload,
2208            vec![(row(1), Diff::ONE), (row(2), Diff::ONE)]
2209        );
2210        assert_eq!(state.pending, vec![(row(3), Timestamp::new(11), Diff::ONE)]);
2211
2212        state.fold_below(Timestamp::new(12));
2213        assert_eq!(
2214            state.payload,
2215            vec![
2216                (row(1), Diff::ONE),
2217                (row(2), Diff::ONE),
2218                (row(3), Diff::ONE),
2219            ]
2220        );
2221        assert!(state.pending.is_empty());
2222    }
2223
2224    /// A row inserted and retracted below the target leaves nothing behind,
2225    /// which is what makes the payload the net change rather than a log.
2226    #[mz_ore::test]
2227    fn test_fold_below_cancels_opposite_diffs() {
2228        let mut state = accumulate([(1, 9, 1), (1, 10, -1), (2, 9, 1)]);
2229        state.fold_below(Timestamp::new(11));
2230
2231        assert_eq!(state.payload, vec![(row(2), Diff::ONE)]);
2232        assert!(state.pending.is_empty());
2233        assert!(!state.is_empty());
2234    }
2235
2236    /// The same rows stay pending or move to the payload depending on the
2237    /// target, so a size check that saw only one half would let a statement
2238    /// past `max_result_size` by picking the other one.
2239    #[mz_ore::test]
2240    fn test_byte_size_counts_payload_and_pending() {
2241        let row_bytes = u64::cast_from(row(1).byte_len());
2242
2243        let mut state = accumulate([(1, 9, 1), (2, 10, 1), (3, 11, 1)]);
2244        state.fold_below(Timestamp::new(10));
2245        assert_eq!(state.payload.len(), 1);
2246        assert_eq!(state.pending.len(), 2);
2247        assert_eq!(state.byte_size, 3 * row_bytes);
2248
2249        state.fold_below(Timestamp::new(12));
2250        assert!(state.pending.is_empty());
2251        assert_eq!(state.byte_size, 3 * row_bytes);
2252    }
2253
2254    /// A subscribe that ran to completion has no target to split on, and
2255    /// cancellation still applies.
2256    #[mz_ore::test]
2257    fn test_fold_all_takes_every_timestamp() {
2258        let mut state = accumulate([(1, 9, 1), (1, 10, -1), (2, u64::MAX, 1)]);
2259        state.fold_all();
2260
2261        assert_eq!(state.payload, vec![(row(2), Diff::ONE)]);
2262        assert!(state.pending.is_empty());
2263    }
2264
2265    /// A zero-row answer holds as of the timestamp the answer was reached at,
2266    /// never an input's frontier. The caller waits for the oracle to reach
2267    /// whatever it gets, and an input settled until its next refresh reports a
2268    /// frontier days out, so reporting that would spend the statement's timeout
2269    /// on an answer of "0 rows".
2270    #[mz_ore::test]
2271    fn test_zero_rows_report_the_answer_not_the_frontier() {
2272        let as_of = Timestamp::new(10);
2273        let desc = RelationDesc::empty();
2274
2275        // First pass, where the fold target is `as_of + 1`. The answer holds at
2276        // `as_of`, which the caller linearized before the subscribe started, so
2277        // it costs no wait however far out the frontier is.
2278        let mut state = OccState::new();
2279        match process_message(
2280            progress(u64::MAX / 2),
2281            &mut state,
2282            as_of,
2283            as_of.step_forward(),
2284            u64::MAX,
2285            &desc,
2286        ) {
2287            ProcessResult::NoRowsMatched { empty_as_of } => assert_eq!(empty_as_of, as_of),
2288            _ => panic!("an empty selection past `as_of` must report no rows matched"),
2289        }
2290
2291        // A target raised by a conflict, with the frontier past it. The answer
2292        // holds at one below the target, the same timestamp a write there would
2293        // have been read at.
2294        let fold_target = Timestamp::new(20);
2295        let mut state = OccState::new();
2296        match process_message(
2297            progress(u64::MAX / 2),
2298            &mut state,
2299            as_of,
2300            fold_target,
2301            u64::MAX,
2302            &desc,
2303        ) {
2304            ProcessResult::NoRowsMatched { empty_as_of } => {
2305                assert_eq!(empty_as_of, Timestamp::new(19))
2306            }
2307            _ => panic!("an empty selection past `as_of` must report no rows matched"),
2308        }
2309
2310        // A frontier below the target certifies less, so the answer holds one
2311        // below the frontier instead.
2312        let mut state = OccState::new();
2313        match process_message(
2314            progress(15),
2315            &mut state,
2316            as_of,
2317            fold_target,
2318            u64::MAX,
2319            &desc,
2320        ) {
2321            ProcessResult::NoRowsMatched { empty_as_of } => {
2322                assert_eq!(empty_as_of, Timestamp::new(14))
2323            }
2324            _ => panic!("an empty selection past `as_of` must report no rows matched"),
2325        }
2326    }
2327
2328    /// Diffs waiting above the target mean a write is still coming, so the
2329    /// answer is not "no rows" yet even with an empty payload.
2330    #[mz_ore::test]
2331    fn test_pending_diffs_are_not_a_zero_row_answer() {
2332        let as_of = Timestamp::new(10);
2333        let desc = RelationDesc::empty();
2334
2335        let mut state = accumulate([(1, 30, 1)]);
2336        match process_message(
2337            progress(20),
2338            &mut state,
2339            as_of,
2340            as_of.step_forward(),
2341            u64::MAX,
2342            &desc,
2343        ) {
2344            ProcessResult::Continue { ready_to_write } => assert!(ready_to_write),
2345            _ => panic!("a selection with diffs above the target must not report no rows"),
2346        }
2347    }
2348}