Skip to main content

mz_adapter/
util.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::{BTreeMap, BTreeSet};
11use std::fmt::Debug;
12
13use itertools::Itertools;
14use mz_catalog::durable::{DurableCatalogError, FenceError};
15use mz_compute_client::controller::error::{
16    CollectionUpdateError, DataflowCreationError, InstanceMissing, PeekError, ReadPolicyError,
17};
18use mz_controller_types::ClusterId;
19use mz_ore::tracing::OpenTelemetryContext;
20use mz_ore::{assert_none, exit, soft_assert_no_log};
21use mz_repr::{RelationDesc, RowIterator, SqlScalarType};
22use mz_sql::names::FullItemName;
23use mz_sql::plan::StatementDesc;
24use mz_sql::session::metadata::SessionMetadata;
25use mz_sql::session::vars::Var;
26use mz_sql_parser::ast::display::AstDisplay;
27use mz_sql_parser::ast::{
28    CreateIndexStatement, Ident, Raw, RawClusterName, RawItemName, Statement,
29};
30use mz_storage_types::controller::StorageError;
31use mz_transform::TransformError;
32use tokio::sync::mpsc::UnboundedSender;
33use tokio::sync::oneshot;
34
35use crate::catalog::{Catalog, CatalogState};
36use crate::command::{Command, Response};
37use crate::coord::{Message, PendingTxnResponse};
38use crate::error::AdapterError;
39use crate::session::{EndTransactionAction, Session};
40use crate::{ExecuteContext, ExecuteResponse};
41
42/// Handles responding to clients.
43#[derive(Debug)]
44pub struct ClientTransmitter<T>
45where
46    T: Transmittable,
47    <T as Transmittable>::Allowed: 'static,
48{
49    tx: Option<oneshot::Sender<Response<T>>>,
50    internal_cmd_tx: UnboundedSender<Message>,
51    /// Expresses an optional soft-assert on the set of values allowed to be
52    /// sent from `self`.
53    allowed: Option<&'static [T::Allowed]>,
54}
55
56impl<T: Transmittable + std::fmt::Debug> ClientTransmitter<T> {
57    /// Creates a new client transmitter.
58    pub fn new(
59        tx: oneshot::Sender<Response<T>>,
60        internal_cmd_tx: UnboundedSender<Message>,
61    ) -> ClientTransmitter<T> {
62        ClientTransmitter {
63            tx: Some(tx),
64            internal_cmd_tx,
65            allowed: None,
66        }
67    }
68
69    /// Transmits `result` to the client, returning ownership of the session
70    /// `session` as well.
71    ///
72    /// # Panics
73    /// - If in `soft_assert`, `result.is_ok()`, `self.allowed.is_some()`, and
74    ///   the result value is not in the set of allowed values.
75    #[mz_ore::instrument(level = "debug")]
76    pub fn send(mut self, result: Result<T, AdapterError>, session: Session) {
77        // Guarantee that the value sent is of an allowed type.
78        soft_assert_no_log!(
79            match (&result, self.allowed.take()) {
80                (Ok(t), Some(allowed)) => allowed.contains(&t.to_allowed()),
81                _ => true,
82            },
83            "tried to send disallowed value {result:?} through ClientTransmitter; \
84            see ClientTransmitter::set_allowed"
85        );
86
87        // If we were not able to send a message, we must clean up the session
88        // ourselves. Return it to the caller for disposal.
89        if let Err(res) = self
90            .tx
91            .take()
92            .expect("tx will always be `Some` unless `self` has been consumed")
93            .send(Response {
94                result,
95                session,
96                otel_ctx: OpenTelemetryContext::obtain(),
97            })
98        {
99            // If the coordinator is gone too, the process is shutting down and there is no
100            // session state left to clean up, so a failed send is fine.
101            let send_res = self.internal_cmd_tx.send(Message::Command(
102                OpenTelemetryContext::obtain(),
103                Command::Terminate {
104                    conn_id: res.session.conn_id().clone(),
105                    tx: None,
106                },
107            ));
108            if send_res.is_err() {
109                tracing::warn!("coordinator gone, could not clean up session");
110            }
111        }
112    }
113
114    pub fn take(mut self) -> oneshot::Sender<Response<T>> {
115        self.tx
116            .take()
117            .expect("tx will always be `Some` unless `self` has been consumed")
118    }
119
120    /// Sets `self` so that the next call to [`Self::send`] will soft-assert
121    /// that, if `Ok`, the value is one of `allowed`, as determined by
122    /// [`Transmittable::to_allowed`].
123    pub fn set_allowed(&mut self, allowed: &'static [T::Allowed]) {
124        self.allowed = Some(allowed);
125    }
126}
127
128/// A helper trait for [`ClientTransmitter`].
129pub trait Transmittable {
130    /// The type of values used to express which set of values are allowed.
131    type Allowed: Eq + PartialEq + std::fmt::Debug;
132    /// The conversion from the [`ClientTransmitter`]'s type to `Allowed`.
133    ///
134    /// The benefit of this style of trait, rather than relying on a bound on
135    /// `Allowed`, are:
136    /// - Not requiring a clone
137    /// - The flexibility for facile implementations that do not plan to make
138    ///   use of the `allowed` feature. Those types can simply implement this
139    ///   trait for `bool`, and return `true`. However, it might not be
140    ///   semantically appropriate to expose `From<&Self> for bool`.
141    fn to_allowed(&self) -> Self::Allowed;
142}
143
144impl Transmittable for () {
145    type Allowed = bool;
146
147    fn to_allowed(&self) -> Self::Allowed {
148        true
149    }
150}
151
152/// A pending write result and the context needed to deliver it.
153///
154/// Dropping it uses the [`ExecuteContext`] retirement backstop.
155#[derive(Debug)]
156pub struct CompletedClientTransmitter {
157    ctx: ExecuteContext,
158    response: Result<PendingTxnResponse, AdapterError>,
159    action: EndTransactionAction,
160}
161
162impl CompletedClientTransmitter {
163    /// Creates a new completed client transmitter.
164    pub fn new(
165        ctx: ExecuteContext,
166        response: Result<PendingTxnResponse, AdapterError>,
167        action: EndTransactionAction,
168    ) -> Self {
169        CompletedClientTransmitter {
170            ctx,
171            response,
172            action,
173        }
174    }
175
176    /// Returns the execute context to be finalized, and the result to send it.
177    pub fn finalize(mut self) -> (ExecuteContext, Result<ExecuteResponse, AdapterError>) {
178        let changed = self
179            .ctx
180            .session_mut()
181            .vars_mut()
182            .end_transaction(self.action);
183
184        // Append any parameters that changed to the response.
185        let response = self.response.map(|mut r| {
186            r.extend_params(changed);
187            ExecuteResponse::from(r)
188        });
189
190        (self.ctx, response)
191    }
192}
193
194impl<T: Transmittable> Drop for ClientTransmitter<T> {
195    fn drop(&mut self) {
196        if self.tx.is_some() {
197            panic!("client transmitter dropped without send")
198        }
199    }
200}
201
202// TODO(benesch): constructing the canonical CREATE INDEX statement should be
203// the responsibility of the SQL package.
204pub fn index_sql(
205    index_name: String,
206    cluster_id: ClusterId,
207    view_name: FullItemName,
208    view_desc: &RelationDesc,
209    keys: &[usize],
210) -> String {
211    use mz_sql::ast::{Expr, Value};
212
213    CreateIndexStatement::<Raw> {
214        name: Some(Ident::new_unchecked(index_name)),
215        on_name: RawItemName::Name(mz_sql::normalize::unresolve(view_name)),
216        in_cluster: Some(RawClusterName::Resolved(cluster_id.to_string())),
217        key_parts: Some(
218            keys.iter()
219                .map(|i| match view_desc.get_unambiguous_name(*i) {
220                    Some(n) => Expr::Identifier(vec![Ident::new_unchecked(n.to_string())]),
221                    _ => Expr::Value(Value::Number((i + 1).to_string())),
222                })
223                .collect(),
224        ),
225        with_options: vec![],
226        if_not_exists: false,
227    }
228    .to_ast_string_stable()
229}
230
231/// Creates a description of the statement `stmt`.
232pub fn describe(
233    catalog: &Catalog,
234    stmt: Statement<Raw>,
235    param_types: &[Option<SqlScalarType>],
236    session: &Session,
237) -> Result<StatementDesc, AdapterError> {
238    let catalog = &catalog.for_session(session);
239    let (stmt, _) = mz_sql::names::resolve(catalog, stmt)?;
240    Ok(mz_sql::plan::describe(
241        session.pcx(),
242        catalog,
243        stmt,
244        param_types,
245    )?)
246}
247
248pub trait ResultExt<T> {
249    /// Like [`Result::expect`], but terminates the process with `halt` or
250    /// exit code 0 instead of `panic` if the error indicates that it should
251    /// cause a halt of graceful termination.
252    fn unwrap_or_terminate(self, context: &str) -> T;
253
254    /// Terminates the process with `halt` or exit code 0 if `self` is an
255    /// error that should halt or cause graceful termination. Otherwise,
256    /// does nothing.
257    fn maybe_terminate(self, context: &str) -> Self;
258}
259
260impl<T, E> ResultExt<T> for Result<T, E>
261where
262    E: ShouldTerminateGracefully + Debug,
263{
264    fn unwrap_or_terminate(self, context: &str) -> T {
265        match self {
266            Ok(t) => t,
267            Err(e) if e.should_terminate_gracefully() => exit!(0, "{context}: {e:?}"),
268            Err(e) => panic!("{context}: {e:?}"),
269        }
270    }
271
272    fn maybe_terminate(self, context: &str) -> Self {
273        if let Err(e) = &self {
274            if e.should_terminate_gracefully() {
275                exit!(0, "{context}: {e:?}");
276            }
277        }
278
279        self
280    }
281}
282
283/// A trait for errors that should terminate gracefully rather than panic
284/// the process.
285trait ShouldTerminateGracefully {
286    /// Reports whether the error should terminate the process gracefully
287    /// rather than panic.
288    fn should_terminate_gracefully(&self) -> bool;
289}
290
291impl ShouldTerminateGracefully for AdapterError {
292    fn should_terminate_gracefully(&self) -> bool {
293        match self {
294            AdapterError::Catalog(e) => e.should_terminate_gracefully(),
295            _ => false,
296        }
297    }
298}
299
300impl ShouldTerminateGracefully for mz_catalog::memory::error::Error {
301    fn should_terminate_gracefully(&self) -> bool {
302        match &self.kind {
303            mz_catalog::memory::error::ErrorKind::Durable(e) => e.should_terminate_gracefully(),
304            _ => false,
305        }
306    }
307}
308
309impl ShouldTerminateGracefully for mz_catalog::durable::CatalogError {
310    fn should_terminate_gracefully(&self) -> bool {
311        match &self {
312            Self::Durable(e) => e.should_terminate_gracefully(),
313            Self::Catalog(_) => false,
314        }
315    }
316}
317
318impl ShouldTerminateGracefully for DurableCatalogError {
319    fn should_terminate_gracefully(&self) -> bool {
320        match self {
321            DurableCatalogError::Fence(err) => err.should_terminate_gracefully(),
322            DurableCatalogError::CatalogOutOfSync { .. } => true,
323            DurableCatalogError::IncompatibleDataVersion { .. }
324            | DurableCatalogError::IncompatiblePersistVersion { .. }
325            | DurableCatalogError::Proto(_)
326            | DurableCatalogError::Uninitialized
327            | DurableCatalogError::NotWritable(_)
328            | DurableCatalogError::DryRunTransaction
329            | DurableCatalogError::DuplicateKey
330            | DurableCatalogError::UniquenessViolation
331            | DurableCatalogError::Storage(_)
332            | DurableCatalogError::Internal(_) => false,
333        }
334    }
335}
336
337impl ShouldTerminateGracefully for FenceError {
338    fn should_terminate_gracefully(&self) -> bool {
339        match self {
340            FenceError::DeployGeneration { .. } => true,
341            FenceError::Epoch { .. } | FenceError::MigrationUpper { .. } => false,
342        }
343    }
344}
345
346impl ShouldTerminateGracefully for StorageError {
347    fn should_terminate_gracefully(&self) -> bool {
348        match self {
349            StorageError::CollectionMetadataAlreadyExists(_)
350            | StorageError::PersistShardAlreadyInUse(_)
351            | StorageError::PersistSchemaEvolveRace { .. }
352            | StorageError::PersistInvalidSchemaEvolve { .. }
353            | StorageError::TxnWalShardAlreadyExists
354            | StorageError::UpdateBeyondUpper(_)
355            | StorageError::ReadBeforeSince(_)
356            | StorageError::InvalidUppers(_)
357            | StorageError::InvalidUsage(_)
358            | StorageError::CollectionIdReused(_)
359            | StorageError::SinkIdReused(_)
360            | StorageError::IdentifierMissing(_)
361            | StorageError::IdentifierInvalid(_)
362            | StorageError::IngestionInstanceMissing { .. }
363            | StorageError::ExportInstanceMissing { .. }
364            | StorageError::Generic(_)
365            | StorageError::ReadOnly
366            | StorageError::DataflowError(_)
367            | StorageError::InvalidAlter { .. }
368            | StorageError::ShuttingDown(_)
369            | StorageError::MissingSubsourceReference { .. }
370            | StorageError::RtrTimeout(_)
371            | StorageError::RtrDropFailure(_) => false,
372        }
373    }
374}
375
376impl ShouldTerminateGracefully for DataflowCreationError {
377    fn should_terminate_gracefully(&self) -> bool {
378        match self {
379            DataflowCreationError::SinceViolation(_)
380            | DataflowCreationError::InstanceMissing(_)
381            | DataflowCreationError::CollectionMissing(_)
382            | DataflowCreationError::ReplicaMissing(_)
383            | DataflowCreationError::MissingAsOf
384            | DataflowCreationError::EmptyAsOfForSubscribe
385            | DataflowCreationError::EmptyAsOfForCopyTo => false,
386        }
387    }
388}
389
390impl ShouldTerminateGracefully for CollectionUpdateError {
391    fn should_terminate_gracefully(&self) -> bool {
392        match self {
393            CollectionUpdateError::InstanceMissing(_)
394            | CollectionUpdateError::CollectionMissing(_) => false,
395        }
396    }
397}
398
399impl ShouldTerminateGracefully for PeekError {
400    fn should_terminate_gracefully(&self) -> bool {
401        match self {
402            PeekError::SinceViolation(_)
403            | PeekError::ReadHoldIdMismatch(_)
404            | PeekError::InstanceMissing(_)
405            | PeekError::CollectionMissing(_)
406            | PeekError::ReplicaMissing(_) => false,
407        }
408    }
409}
410
411impl ShouldTerminateGracefully for ReadPolicyError {
412    fn should_terminate_gracefully(&self) -> bool {
413        match self {
414            ReadPolicyError::InstanceMissing(_)
415            | ReadPolicyError::CollectionMissing(_)
416            | ReadPolicyError::WriteOnlyCollection(_) => false,
417        }
418    }
419}
420
421impl ShouldTerminateGracefully for TransformError {
422    fn should_terminate_gracefully(&self) -> bool {
423        match self {
424            TransformError::Internal(_)
425            | TransformError::IdentifierMissing(_)
426            | TransformError::CallerShouldPanic(_) => false,
427        }
428    }
429}
430
431impl ShouldTerminateGracefully for InstanceMissing {
432    fn should_terminate_gracefully(&self) -> bool {
433        false
434    }
435}
436
437/// Returns the viewable session and system variables.
438pub(crate) fn viewable_variables<'a>(
439    catalog: &'a CatalogState,
440    session: &'a dyn SessionMetadata,
441) -> impl Iterator<Item = &'a dyn Var> {
442    session
443        .vars()
444        .iter()
445        .chain(catalog.system_config().iter())
446        .filter(|v| v.visible(session.user(), catalog.system_config()).is_ok())
447}
448
449/// Verify that the rows in [`RowIterator`] match the expected [`RelationDesc`].
450pub fn verify_datum_desc(
451    desc: &RelationDesc,
452    rows: &mut dyn RowIterator,
453) -> Result<(), AdapterError> {
454    // Verify the first row is of the expected type. This is often good enough to
455    // find problems.
456    //
457    // Notably it failed to find database-issues#1946 when "FETCH 2" was used in a test, instead
458    // we had to use "FETCH 1" twice.
459
460    let Some(row) = rows.peek() else {
461        return Ok(());
462    };
463
464    let datums = row.unpack();
465    let col_types = &desc.typ().column_types;
466    if datums.len() != col_types.len() {
467        let msg = format!(
468            "internal error: row descriptor has {} columns but row has {} columns",
469            col_types.len(),
470            datums.len(),
471        );
472        return Err(AdapterError::Internal(msg));
473    }
474
475    for (i, (d, t)) in datums.iter().zip_eq(col_types).enumerate() {
476        if !d.is_instance_of_sql(t) {
477            let msg = format!(
478                "internal error: column {} is not of expected type {:?}: {:?}",
479                i, t, d
480            );
481            return Err(AdapterError::Internal(msg));
482        }
483    }
484
485    Ok(())
486}
487
488/// Sort items in dependency order using topological sort.
489///
490/// # Panics
491///
492/// Panics if `key_fn` produces non-unique keys for the provided `items`.
493/// Panics if there is a dependency cycle among the provided `items`.
494pub fn sort_topological<T, K, FK, FD>(items: &mut Vec<T>, key_fn: FK, dependencies_fn: FD)
495where
496    T: Debug,
497    K: Debug + Copy + Ord,
498    FK: Fn(&T) -> K,
499    FD: Fn(&T) -> BTreeSet<K>,
500{
501    let mut items_by_key = BTreeMap::new();
502    for item in items.drain(..) {
503        let key = key_fn(&item);
504        let prev = items_by_key.insert(key, item);
505        assert_none!(prev);
506    }
507
508    // For each item, the number of unprocessed dependencies.
509    let mut in_degree = BTreeMap::<K, usize>::new();
510    // For each item, the keys of items depending on it.
511    let mut dependents = BTreeMap::<K, Vec<K>>::new();
512    // Items that have no unprocessed dependencies.
513    let mut ready = Vec::<K>::new();
514
515    // Build the graph.
516    for (&key, item) in &items_by_key {
517        let mut dependencies = dependencies_fn(item);
518        // Remove any dependencies not contained in `items`, as well as self-references.
519        dependencies.retain(|dep| items_by_key.contains_key(dep) && *dep != key);
520
521        in_degree.insert(key, dependencies.len());
522
523        for dep in &dependencies {
524            dependents.entry(*dep).or_default().push(key);
525        }
526
527        if dependencies.is_empty() {
528            ready.push(key);
529        }
530    }
531
532    // Process items in topological order, pushing back into the input Vec.
533    while let Some(id) = ready.pop() {
534        let item = items_by_key.remove(&id).expect("must exist");
535        items.push(item);
536
537        if let Some(depts) = dependents.get(&id) {
538            for dept in depts {
539                let deg = in_degree.get_mut(dept).expect("must exist");
540                *deg -= 1;
541                if *deg == 0 {
542                    ready.push(*dept);
543                }
544            }
545        }
546    }
547
548    // Cycle detection: if we didn't process all items, there's a cycle.
549    if !items_by_key.is_empty() {
550        panic!("dependency cycle: {items_by_key:?}");
551    }
552}