Skip to main content

mz_adapter/coord/sequencer/inner/
peek.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
10use std::collections::BTreeSet;
11
12use mz_controller_types::ClusterId;
13use mz_ore::instrument;
14use mz_repr::{GlobalId, Timestamp};
15use mz_sql::plan::QueryWhen;
16use mz_sql::session::metadata::SessionMetadata;
17
18use crate::coord::Coordinator;
19use crate::coord::id_bundle::CollectionIdBundle;
20use crate::coord::timeline::{TimelineContext, timedomain_for};
21use crate::coord::timestamp_selection::{TimestampContext, TimestampDetermination};
22use crate::error::AdapterError;
23use crate::session::{RequireLinearization, Session, TransactionOps, TransactionStatus};
24
25impl Coordinator {
26    /// Determines the query timestamp and acquires read holds on dependent sources
27    /// if necessary.
28    #[instrument]
29    pub(super) fn sequence_peek_timestamp(
30        &mut self,
31        session: &mut Session,
32        when: &QueryWhen,
33        cluster_id: ClusterId,
34        timeline_context: TimelineContext,
35        oracle_read_ts: Option<Timestamp>,
36        source_bundle: &CollectionIdBundle,
37        source_ids: &BTreeSet<GlobalId>,
38        real_time_recency_ts: Option<Timestamp>,
39        requires_linearization: RequireLinearization,
40    ) -> Result<TimestampDetermination, AdapterError> {
41        let in_immediate_multi_stmt_txn = session.transaction().in_immediate_multi_stmt_txn(when);
42        let timedomain_bundle;
43
44        // Fetch or generate a timestamp for this query and what the read holds would be if we need to set
45        // them.
46        let (determination, read_holds) = match session.get_transaction_timestamp_determination() {
47            // Use the transaction's timestamp if it exists and this isn't an AS OF query.
48            Some(
49                determination @ TimestampDetermination {
50                    timestamp_context: TimestampContext::TimelineTimestamp { .. },
51                    ..
52                },
53            ) if in_immediate_multi_stmt_txn => (determination, None),
54            _ => {
55                let determine_bundle = if in_immediate_multi_stmt_txn {
56                    // In a transaction, determine a timestamp that will be valid for anything in
57                    // any schema referenced by the first query.
58                    timedomain_bundle = timedomain_for(
59                        self.catalog(),
60                        &self.index_oracle(cluster_id),
61                        source_ids,
62                        &timeline_context,
63                        session.conn_id(),
64                        cluster_id,
65                    )?;
66
67                    &timedomain_bundle
68                } else {
69                    // If not in a transaction, use the source.
70                    source_bundle
71                };
72                let (determination, read_holds) = self.determine_timestamp(
73                    session,
74                    determine_bundle,
75                    when,
76                    cluster_id,
77                    &timeline_context,
78                    oracle_read_ts,
79                    real_time_recency_ts,
80                )?;
81                // We only need read holds if the read depends on a timestamp.
82                let read_holds = match determination.timestamp_context.timestamp() {
83                    Some(_ts) => Some(read_holds),
84                    None => {
85                        // We don't need the read holds and shouldn't add them
86                        // to the txn.
87                        //
88                        // TODO: Handle this within determine_timestamp.
89                        drop(read_holds);
90                        None
91                    }
92                };
93                (determination, read_holds)
94            }
95        };
96
97        // Always either verify the current statement ids are within the existing
98        // transaction's read hold set (timedomain), or create the read holds if this is the
99        // first statement in a transaction (or this is a single statement transaction).
100        // This must happen even if this is an `AS OF` query as well. There are steps after
101        // this that happen off thread, so no matter the kind of statement or transaction,
102        // we must acquire read holds here so they are held until the off-thread work
103        // returns to the coordinator.
104
105        if let Some(txn_reads) = self.txn_read_holds.get(session.conn_id()) {
106            // Find referenced ids not in the read hold. A reference could be caused by a
107            // user specifying an object in a different schema than the first query. An
108            // index could be caused by a CREATE INDEX after the transaction started.
109            let allowed_id_bundle = txn_reads.id_bundle();
110
111            // We don't need the read holds that determine_timestamp acquired
112            // for us.
113            drop(read_holds);
114
115            let outside = source_bundle.difference(&allowed_id_bundle);
116            // Queries without a timestamp and timeline can belong to any existing timedomain.
117            if determination.timestamp_context.contains_timestamp() && !outside.is_empty() {
118                let valid_names =
119                    allowed_id_bundle.resolve_names(self.catalog(), session.conn_id());
120                let invalid_names = outside.resolve_names(self.catalog(), session.conn_id());
121                return Err(AdapterError::RelationOutsideTimeDomain {
122                    relations: invalid_names,
123                    names: valid_names,
124                });
125            }
126        } else if let Some(read_holds) = read_holds {
127            self.store_transaction_read_holds(session.conn_id().clone(), read_holds);
128        }
129
130        // TODO: Checking for only `InTransaction` and not `Implied` (also `Started`?) seems
131        // arbitrary and we don't recall why we did it (possibly an error!). Change this to always
132        // set the transaction ops. Decide and document what our policy should be on AS OF queries.
133        // Maybe they shouldn't be allowed in transactions at all because it's hard to explain
134        // what's going on there. This should probably get a small design document.
135
136        // We only track the peeks in the session if the query doesn't use AS
137        // OF or we're inside an explicit transaction. The latter case is
138        // necessary to support PG's `BEGIN` semantics, whose behavior can
139        // depend on whether or not reads have occurred in the txn.
140        let mut transaction_determination = determination.clone();
141        if when.is_transactional() {
142            session.add_transaction_ops(TransactionOps::Peeks {
143                determination: transaction_determination,
144                cluster_id,
145                requires_linearization,
146            })?;
147        } else if matches!(session.transaction(), &TransactionStatus::InTransaction(_)) {
148            // If the query uses AS OF, then ignore the timestamp.
149            transaction_determination.timestamp_context = TimestampContext::NoTimestamp;
150            session.add_transaction_ops(TransactionOps::Peeks {
151                determination: transaction_determination,
152                cluster_id,
153                requires_linearization,
154            })?;
155        };
156
157        Ok(determination)
158    }
159}