Skip to main content

mz_adapter/
error.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::error::Error;
12use std::fmt;
13use std::num::TryFromIntError;
14
15use dec::TryFromDecimalError;
16use itertools::Itertools;
17use mz_catalog::builtin::MZ_CATALOG_SERVER_CLUSTER;
18use mz_compute_client::controller::error as compute_error;
19
20use mz_compute_types::ComputeInstanceId;
21use mz_controller_types::ClusterId;
22use mz_expr::EvalError;
23use mz_ore::cast::CastFrom;
24use mz_ore::error::ErrorExt;
25use mz_ore::stack::RecursionLimitError;
26use mz_ore::str::StrExt;
27use mz_pgwire_common::{ErrorResponse, Severity};
28use mz_repr::adt::array::InvalidArrayError;
29use mz_repr::adt::range::InvalidRangeError;
30use mz_repr::adt::timestamp::TimestampError;
31use mz_repr::explain::ExplainError;
32use mz_repr::strconv::{ParseError, ParseErrorKind};
33use mz_repr::{ColumnDiff, ColumnName, KeyDiff, NotNullViolation, RelationDescDiff, Timestamp};
34use mz_sql::plan::PlanError;
35use mz_sql::rbac;
36use mz_sql::session::vars::VarError;
37use mz_storage_types::connections::ConnectionValidationError;
38use mz_storage_types::controller::StorageError;
39use mz_storage_types::errors::{CollectionMissing, DataflowError};
40use smallvec::SmallVec;
41use timely::progress::Antichain;
42use tokio::sync::oneshot;
43use tokio_postgres::error::SqlState;
44
45use crate::coord::NetworkPolicyError;
46use crate::optimize::OptimizerError;
47use crate::peek_client::CollectionLookupError;
48
49/// Errors that can occur in the coordinator.
50#[derive(Debug)]
51pub enum AdapterError {
52    /// A `SUBSCRIBE` was requested whose `UP TO` bound precedes its `as_of` timestamp
53    AbsurdSubscribeBounds {
54        as_of: mz_repr::Timestamp,
55        up_to: mz_repr::Timestamp,
56    },
57    /// Attempted to use a potentially ambiguous column reference expression with a system table.
58    // We don't allow this until https://github.com/MaterializeInc/database-issues/issues/4824 is
59    // resolved because it prevents us from adding columns to system tables.
60    AmbiguousSystemColumnReference,
61    /// An error occurred in a catalog operation.
62    Catalog(mz_catalog::memory::error::Error),
63    /// 1. The cached plan or descriptor changed,
64    /// 2. or some dependency of a statement disappeared during sequencing.
65    /// TODO(ggevay): we should refactor 2. usages to use `ConcurrentDependencyDrop` instead
66    /// (e.g., in MV sequencing)
67    ChangedPlan(String),
68    /// The cursor already exists.
69    DuplicateCursor(String),
70    /// An error while evaluating an expression.
71    Eval(EvalError),
72    /// An error produced while executing a dataflow.
73    Dataflow(Box<DataflowError>),
74    /// An error occurred while planning the statement.
75    Explain(ExplainError),
76    /// The ID allocator exhausted all valid IDs.
77    IdExhaustionError,
78    /// Unexpected internal state was encountered.
79    Internal(String),
80    /// Attempted to read from log sources of a replica with disabled introspection.
81    IntrospectionDisabled {
82        log_names: Vec<String>,
83    },
84    /// Attempted to create an object dependent on log sources that doesn't support
85    /// log dependencies.
86    InvalidLogDependency {
87        object_type: String,
88        log_names: Vec<String>,
89    },
90    /// No such cluster replica size has been configured.
91    InvalidClusterReplicaAz {
92        az: String,
93        expected: Vec<String>,
94    },
95    /// SET TRANSACTION ISOLATION LEVEL was called in the middle of a transaction.
96    InvalidSetIsolationLevel,
97    /// SET cluster was called in the middle of a transaction.
98    InvalidSetCluster,
99    /// No such storage instance size has been configured.
100    InvalidStorageClusterSize {
101        size: String,
102        expected: Vec<String>,
103    },
104    /// Creating a source or sink without specifying its size is forbidden.
105    SourceOrSinkSizeRequired {
106        expected: Vec<String>,
107    },
108    /// The selection value for a table mutation operation refers to an invalid object.
109    InvalidTableMutationSelection {
110        /// The full name of the problematic object, such as a source or source-backed table.
111        object_name: String,
112        /// Human-readable type of the object, such as "source-backed table".
113        object_type: String,
114    },
115    /// A read-then-write statement's read set has more transitive dependencies
116    /// than validation is willing to walk.
117    ReadThenWriteDependencyLimitExceeded {
118        /// The configured maximum number of dependencies.
119        max_rw_dependencies: usize,
120    },
121    /// Expression violated a column's constraint
122    ConstraintViolation(NotNullViolation),
123    /// An error occurred while decoding COPY data.
124    CopyFormatError(String),
125    /// Transaction cluster was dropped in the middle of a transaction.
126    ConcurrentClusterDrop,
127    /// A dependency was dropped while sequencing a statement.
128    ConcurrentDependencyDrop {
129        dependency_kind: &'static str,
130        dependency_id: String,
131    },
132    /// A dependency's definition changed while a statement was being sequenced.
133    /// Raised by `PlanValidity::check` when a dependency's `create_sql` hash no
134    /// longer matches what we captured at plan time, and by
135    /// `Coordinator::stage_group_commit` when a table's `RelationDesc` no longer
136    /// matches the rows a staged write packed against it.
137    ConcurrentDependencyMutation {
138        dependency_id: String,
139    },
140    /// A frontend read-then-write exhausted its OCC retry budget.
141    ///
142    /// The statement is retryable: every attempt was refused before anything
143    /// was appended, so nothing it intended has been committed.
144    ReadThenWriteContention,
145    /// The write timestamp ran past what the write timeline may be advanced to,
146    /// so nothing was appended. See `coord::timeline::write_ts_upper_bound` for
147    /// the bound and why exceeding it is not recoverable.
148    ///
149    /// The timestamp comes from the timeline's oracle, so reaching this means the
150    /// oracle has run away from the wall clock rather than that the statement
151    /// asked for anything unusual.
152    ReadThenWriteTimestampTooFarAhead {
153        target_timestamp: mz_repr::Timestamp,
154        limit: mz_repr::Timestamp,
155    },
156    CollectionUnreadable {
157        id: String,
158    },
159    /// Target cluster has no replicas to service query.
160    NoClusterReplicasAvailable {
161        name: String,
162        is_managed: bool,
163    },
164    /// The named operation cannot be run in a transaction.
165    OperationProhibitsTransaction(String),
166    /// The named operation requires an active transaction.
167    OperationRequiresTransaction(String),
168    /// An error occurred while planning the statement.
169    PlanError(PlanError),
170    /// The named prepared statement already exists.
171    PreparedStatementExists(String),
172    /// Wrapper around parsing error
173    ParseError(mz_sql_parser::parser::ParserStatementError),
174    /// The transaction is in read-only mode.
175    ReadOnlyTransaction,
176    /// The transaction in in read-only mode and a read already occurred.
177    ReadWriteUnavailable,
178    /// The recursion limit of some operation was exceeded.
179    RecursionLimit(RecursionLimitError),
180    /// A query in a transaction referenced a relation outside the first query's
181    /// time domain.
182    RelationOutsideTimeDomain {
183        relations: Vec<String>,
184        names: Vec<String>,
185    },
186    /// A query tried to create more resources than is allowed in the system configuration.
187    ResourceExhaustion {
188        resource_type: String,
189        limit_name: String,
190        desired: String,
191        limit: String,
192        current: String,
193    },
194    /// Result size of a query is too large.
195    ResultSize(String),
196    /// A `SUBSCRIBE` (or `COPY (SUBSCRIBE ...) TO STDOUT`) buffered more bytes in
197    /// environmentd than its budget allows because the client was not reading
198    /// fast enough. The subscribe is retired rather than let one slow client
199    /// grow the shared process's memory without bound.
200    SubscribeFellBehind {
201        /// Bytes buffered when the budget was exceeded.
202        buffered_bytes: usize,
203        /// The `subscribe_max_buffered_bytes` budget that was exceeded.
204        max_buffered_bytes: usize,
205    },
206    /// A query exceeded the configured compute peek row iteration limit.
207    PeekRowIterationLimitExceeded {
208        /// The configured per-worker limit.
209        limit: usize,
210    },
211    /// The specified feature is not permitted in safe mode.
212    SafeModeViolation(String),
213    /// Waiting on a query timed out.
214    ///
215    /// Note this differs slightly from PG's implementation/semantics.
216    StatementTimeout,
217    /// The user canceled the query
218    Canceled,
219    /// An idle session in a transaction has timed out.
220    IdleInTransactionSessionTimeout,
221    /// The transaction is in single-subscribe mode.
222    SubscribeOnlyTransaction,
223    /// An error occurred in the optimizer.
224    Optimizer(OptimizerError),
225    /// A query depends on items which are not allowed to be referenced from the current cluster.
226    UnallowedOnCluster {
227        depends_on: SmallVec<[String; 2]>,
228        cluster: String,
229    },
230    /// A user tried to perform an action that they were unauthorized to do.
231    Unauthorized(rbac::UnauthorizedError),
232    /// The named cursor does not exist.
233    UnknownCursor(String),
234    /// The named role does not exist.
235    UnknownLoginRole(String),
236    UnknownPreparedStatement(String),
237    /// The named cluster replica does not exist.
238    UnknownClusterReplica {
239        cluster_name: String,
240        replica_name: String,
241    },
242    /// The named setting does not exist.
243    UnrecognizedConfigurationParam(String),
244    /// A generic error occurred.
245    //
246    // TODO(benesch): convert all those errors to structured errors.
247    Unstructured(anyhow::Error),
248    /// The named feature is not supported and will (probably) not be.
249    Unsupported(&'static str),
250    /// Some feature isn't available for a (potentially opaque) reason.
251    /// For example, in cloud Self-Managed auth features aren't available,
252    /// but we don't want to mention self managed auth.
253    UnavailableFeature {
254        feature: String,
255        docs: Option<String>,
256    },
257    /// Attempted to read from log sources without selecting a target replica.
258    UntargetedLogRead {
259        log_names: Vec<String>,
260    },
261    /// The transaction is in write-only mode.
262    WriteOnlyTransaction,
263    /// The transaction can only execute a single statement.
264    SingleStatementTransaction,
265    /// The transaction can only execute simple DDL.
266    DDLOnlyTransaction,
267    /// Final execution would change an outcome already reported to the client.
268    DDLTransactionOutcomeChanged,
269    /// A conditional cluster-config write failed its precondition: the cluster's
270    /// managed config changed between when the write was conditioned and when it
271    /// was applied. Produced by `Op::CheckClusterState`, never by SQL DDL, so it
272    /// does not reach a SQL client.
273    ClusterStateChanged {
274        cluster_id: ClusterId,
275    },
276    /// An error occurred in the storage layer
277    Storage(mz_storage_types::controller::StorageError),
278    /// An error occurred in the compute layer
279    Compute(anyhow::Error),
280    /// An error in the orchestrator layer
281    Orchestrator(anyhow::Error),
282    /// A statement tried to drop a role that had dependent objects.
283    ///
284    /// The map keys are role names and values are detailed error messages.
285    DependentObject(BTreeMap<String, Vec<String>>),
286    /// A statement tried to drop indexes while materialized views, indexes, or metric sinks
287    /// read from them.
288    ///
289    /// `blocked` is never empty. Each entry pairs a blocked index's name with its readers as
290    /// `(object type, name)` pairs.
291    IndexInUse {
292        blocked: Vec<(String, Vec<(String, String)>)>,
293    },
294    /// When performing an `ALTER` of some variety, re-planning the statement
295    /// errored.
296    InvalidAlter(&'static str, PlanError),
297    /// An error occurred while validating a connection.
298    ConnectionValidation(ConnectionValidationError),
299    /// We refuse to create the materialized view, because it would never be refreshed, so it would
300    /// never be queryable. This can happen when the only specified refreshes are further back in
301    /// the past than the initial compaction window of the materialized view.
302    MaterializedViewWouldNeverRefresh(Timestamp, Timestamp),
303    /// A CREATE MATERIALIZED VIEW statement tried to acquire a read hold at a REFRESH AT time,
304    /// but was unable to get a precise read hold.
305    InputNotReadableAtRefreshAtTime(Timestamp, Antichain<Timestamp>),
306    /// A humanized version of [`StorageError::RtrTimeout`].
307    RtrTimeout(String),
308    /// A humanized version of [`StorageError::RtrDropFailure`].
309    RtrDropFailure(String),
310    /// The collection requested to be sinked cannot be read at any timestamp
311    UnreadableSinkCollection,
312    /// User sessions have been blocked.
313    UserSessionsDisallowed,
314    /// This use session has been denied by a NetworkPolicy.
315    NetworkPolicyDenied(NetworkPolicyError),
316    /// Something attempted a write (to catalog, storage, tables, etc.) while in
317    /// read-only mode.
318    ReadOnly,
319    AlterClusterTimeout,
320    /// Another `ALTER CLUSTER` replaced the target this statement was waiting
321    /// for before it became realized.
322    AlterClusterSuperseded,
323    /// The cluster controller could not provision a reconfiguration target
324    /// within the environment's resource limits.
325    AlterClusterResourceExhausted,
326    /// Attempt to convert a cluster to unmanaged while a graceful
327    /// reconfiguration is in progress.
328    AlterClusterUnmanagedWhileReconfiguring,
329    /// Attempt to convert a cluster to unmanaged while a hydration burst is in
330    /// flight.
331    AlterClusterUnmanagedWhileBursting,
332    /// Attempt to change a cluster's schedule while a graceful reconfiguration
333    /// is in progress.
334    AlterClusterScheduleWhileReconfiguring,
335    /// Attempt to use `WAIT` on an `ALTER` of a scheduled (non-`MANUAL`)
336    /// cluster, whose replica set the controller reshapes in the background.
337    AlterClusterWaitOnScheduledCluster,
338    AuthenticationError(AuthenticationError),
339    /// Schema of a replacement is incompatible with the target.
340    ReplacementSchemaMismatch(RelationDescDiff),
341    /// Attempt to replace a sealed materialized view.
342    ReplaceMaterializedViewSealed {
343        name: String,
344    },
345    /// Could not find a valid timestamp satisfying all constraints.
346    ImpossibleTimestampConstraints {
347        constraints: String,
348    },
349    /// OIDC group-to-role sync failed and strict mode is enabled.
350    OidcGroupSyncFailed(String),
351    /// Returned when bounded staleness was selected but the input frontiers lag
352    /// further than the bound permits, so no timestamp in the no-wait window is
353    /// at most `bound` stale.
354    BoundedStalenessExceeded {
355        bound: std::time::Duration,
356        gap_ms: u64,
357        slowest_input: Option<mz_repr::GlobalId>,
358    },
359    /// A write was attempted in a session whose isolation level is bounded
360    /// staleness. Bounded staleness is read-only.
361    BoundedStalenessReadOnly,
362    /// `real_time_recency = on` and bounded staleness were both requested in
363    /// the same session; they are mutually exclusive.
364    BoundedStalenessRealTimeRecencyConflict,
365    /// A bounded-staleness query touched a timeline whose timestamps are not
366    /// the `EpochMilliseconds` wall-clock timeline.
367    BoundedStalenessTimelineUnsupported,
368}
369
370#[derive(Debug, thiserror::Error)]
371pub enum AuthenticationError {
372    #[error("invalid credentials")]
373    InvalidCredentials,
374    #[error("role is not allowed to login")]
375    NonLogin,
376    #[error("role does not exist")]
377    RoleNotFound,
378    #[error("password is required")]
379    PasswordRequired,
380}
381
382/// Maps a parse/cast [`ParseError`] to the appropriate `DATA_EXCEPTION` (class
383/// 22) SQLSTATE. Out-of-range values map to overflow codes and malformed input
384/// maps to invalid-representation codes, in both cases distinguishing datetime
385/// types from everything else, matching PostgreSQL.
386fn parse_error_code(err: &ParseError) -> SqlState {
387    let is_datetime = matches!(
388        &*err.type_name,
389        "date" | "time" | "timestamp" | "timestamp with time zone" | "interval"
390    );
391    match (err.kind, is_datetime) {
392        (ParseErrorKind::OutOfRange, true) => SqlState::DATETIME_FIELD_OVERFLOW,
393        (ParseErrorKind::OutOfRange, false) => SqlState::NUMERIC_VALUE_OUT_OF_RANGE,
394        (ParseErrorKind::InvalidInputSyntax, true) => SqlState::INVALID_DATETIME_FORMAT,
395        (ParseErrorKind::InvalidInputSyntax, false) => SqlState::INVALID_TEXT_REPRESENTATION,
396    }
397}
398
399/// Maps an [`EvalError`] to the appropriate SQLSTATE.
400///
401/// Historically every `EvalError` fell through to `INTERNAL_ERROR` (`XX000`),
402/// but the overwhelming majority are user-facing data exceptions (class 22) or
403/// other well-defined conditions, not internal errors. This match is
404/// deliberately exhaustive — with no wildcard — so that adding a new
405/// `EvalError` variant is a compile error until it is assigned a code, and we
406/// never silently regress to `XX000`. Codes are chosen to match PostgreSQL
407/// where an equivalent error exists.
408fn eval_error_code(err: &EvalError) -> SqlState {
409    match err {
410        // Division and mathematical domain errors.
411        EvalError::DivisionByZero => SqlState::DIVISION_BY_ZERO,
412        EvalError::NegSqrt | EvalError::ComplexOutOfRange(_) => {
413            SqlState::INVALID_ARGUMENT_FOR_POWER_FUNCTION
414        }
415        EvalError::InfinityOutOfDomain(_)
416        | EvalError::NegativeOutOfDomain(_)
417        | EvalError::ZeroOutOfDomain(_)
418        | EvalError::OutOfDomain(..)
419        | EvalError::Undefined(_) => SqlState::INVALID_PARAMETER_VALUE,
420
421        // Out-of-range numeric, integer, and float values.
422        EvalError::FloatOverflow
423        | EvalError::FloatUnderflow
424        | EvalError::NumericFieldOverflow
425        | EvalError::Float32OutOfRange(_)
426        | EvalError::Float64OutOfRange(_)
427        | EvalError::Int16OutOfRange(_)
428        | EvalError::Int32OutOfRange(_)
429        | EvalError::Int64OutOfRange(_)
430        | EvalError::UInt16OutOfRange(_)
431        | EvalError::UInt32OutOfRange(_)
432        | EvalError::UInt64OutOfRange(_)
433        | EvalError::OidOutOfRange(_)
434        | EvalError::MzTimestampOutOfRange(_)
435        | EvalError::MzTimestampStepOverflow
436        | EvalError::CharOutOfRange => SqlState::NUMERIC_VALUE_OUT_OF_RANGE,
437
438        // Out-of-range datetime and interval values.
439        EvalError::DateOutOfRange
440        | EvalError::TimestampOutOfRange
441        | EvalError::TimestampCannotBeNan
442        | EvalError::DateBinOutOfRange(_)
443        | EvalError::DateDiffOverflow { .. } => SqlState::DATETIME_FIELD_OVERFLOW,
444        EvalError::IntervalOutOfRange(_) => SqlState::INTERVAL_FIELD_OVERFLOW,
445
446        // Parse/cast failures, plus other malformed textual/encoded input.
447        EvalError::Parse(e) => parse_error_code(e),
448        EvalError::ParseHex(_)
449        | EvalError::InvalidBase64Equals
450        | EvalError::InvalidBase64Symbol(_)
451        | EvalError::InvalidBase64EndSequence => SqlState::INVALID_TEXT_REPRESENTATION,
452        EvalError::InvalidByteSequence { .. } => SqlState::CHARACTER_NOT_IN_REPERTOIRE,
453        EvalError::CharacterNotValidForEncoding(_) | EvalError::CharacterTooLargeForEncoding(_) => {
454            SqlState::PROGRAM_LIMIT_EXCEEDED
455        }
456
457        // Invalid argument values.
458        EvalError::InvalidTimezone(_)
459        | EvalError::InvalidTimezoneInterval
460        | EvalError::InvalidTimezoneConversion
461        | EvalError::InvalidIanaTimezoneId(_)
462        | EvalError::InvalidLayer { .. }
463        | EvalError::InvalidEncodingName(_)
464        | EvalError::InvalidHashAlgorithm(_)
465        | EvalError::InvalidDatePart(_)
466        | EvalError::InvalidParameterValue(_)
467        | EvalError::InvalidJsonbCast { .. }
468        | EvalError::UnknownUnits(_)
469        | EvalError::UnsupportedUnits(..)
470        | EvalError::InvalidIdentifier { .. }
471        | EvalError::InvalidRoleId(_)
472        | EvalError::InvalidPrivileges(_)
473        | EvalError::NegLimit => SqlState::INVALID_PARAMETER_VALUE,
474
475        // Regular expressions.
476        EvalError::InvalidRegex(_) | EvalError::InvalidRegexFlag(_) => {
477            SqlState::INVALID_REGULAR_EXPRESSION
478        }
479
480        // LIKE escape sequences.
481        EvalError::UnterminatedLikeEscapeSequence | EvalError::LikeEscapeTooLong => {
482            SqlState::INVALID_ESCAPE_SEQUENCE
483        }
484
485        // NULL values where they are not permitted.
486        EvalError::KeyCannotBeNull
487        | EvalError::MustNotBeNull(_)
488        | EvalError::AclArrayNullElement
489        | EvalError::MzAclArrayNullElement => SqlState::NULL_VALUE_NOT_ALLOWED,
490
491        // Array subscript/dimension errors.
492        EvalError::IndexOutOfRange { .. }
493        | EvalError::ArrayFillWrongArraySubscripts
494        | EvalError::IncompatibleArrayDimensions { .. } => SqlState::ARRAY_SUBSCRIPT_ERROR,
495        EvalError::InvalidArray(e) => match e {
496            InvalidArrayError::TooManyDimensions(_) => SqlState::PROGRAM_LIMIT_EXCEEDED,
497            InvalidArrayError::WrongCardinality { .. } => SqlState::ARRAY_SUBSCRIPT_ERROR,
498        },
499
500        // Range errors.
501        EvalError::InvalidRange(e) => match e {
502            InvalidRangeError::CanonicalizationOverflow(_) => SqlState::NUMERIC_VALUE_OUT_OF_RANGE,
503            InvalidRangeError::MisorderedRangeBounds
504            | InvalidRangeError::InvalidRangeBoundFlags
505            | InvalidRangeError::NullRangeBoundFlags
506            | InvalidRangeError::DiscontiguousUnion
507            | InvalidRangeError::DiscontiguousDifference
508            | InvalidRangeError::InvalidRangeData => SqlState::DATA_EXCEPTION,
509        },
510
511        // Cardinality violations from scalar subqueries.
512        EvalError::MultipleRowsFromSubquery | EvalError::NegativeRowsFromSubquery => {
513            SqlState::CARDINALITY_VIOLATION
514        }
515
516        // Length, size, and resource limits.
517        EvalError::StringValueTooLong { .. } => SqlState::STRING_DATA_RIGHT_TRUNCATION,
518        EvalError::LikePatternTooLong
519        | EvalError::LengthTooLarge
520        | EvalError::TempStorageBudgetExceeded
521        | EvalError::NullCharacterNotPermitted
522        | EvalError::MaxArraySizeExceeded(_)
523        | EvalError::LetRecLimitExceeded(_) => SqlState::PROGRAM_LIMIT_EXCEEDED,
524
525        // Unsupported features.
526        EvalError::Unsupported { .. }
527        | EvalError::MultidimensionalArrayRemovalNotSupported
528        | EvalError::MultiDimensionalArraySearch => SqlState::FEATURE_NOT_SUPPORTED,
529
530        // User-raised errors (e.g. `error_if_null`).
531        EvalError::IfNullError(_) => SqlState::DATA_EXCEPTION,
532
533        // Genuinely internal errors.
534        EvalError::Internal(_)
535        | EvalError::InvalidCatalogJson(_)
536        | EvalError::TypeFromOid(_)
537        | EvalError::PrettyError(_)
538        | EvalError::RedactError(_) => SqlState::INTERNAL_ERROR,
539    }
540}
541
542fn dataflow_error_code(error: &DataflowError) -> SqlState {
543    match error {
544        DataflowError::EvalError(error) => eval_error_code(error),
545        DataflowError::DecodeError(_)
546        | DataflowError::SourceError(_)
547        | DataflowError::EnvelopeError(_) => SqlState::INTERNAL_ERROR,
548    }
549}
550
551impl AdapterError {
552    pub fn into_response(self, severity: Severity) -> ErrorResponse {
553        ErrorResponse {
554            severity,
555            code: self.code(),
556            message: self.to_string(),
557            detail: self.detail(),
558            hint: self.hint(),
559            position: self.position(),
560        }
561    }
562
563    pub fn position(&self) -> Option<usize> {
564        match self {
565            AdapterError::ParseError(err) => Some(err.error.pos),
566            _ => None,
567        }
568    }
569
570    /// Reports additional details about the error, if any are available.
571    pub fn detail(&self) -> Option<String> {
572        match self {
573            AdapterError::AmbiguousSystemColumnReference => {
574                Some("This is a current limitation in Materialize".into())
575            }
576            AdapterError::Catalog(c) => c.detail(),
577            AdapterError::Eval(e) => e.detail(),
578            AdapterError::Dataflow(e) => match &**e {
579                DataflowError::EvalError(e) => e.detail(),
580                _ => None,
581            },
582            AdapterError::RelationOutsideTimeDomain { relations, names } => Some(format!(
583                "The following relations in the query are outside the transaction's time domain:\n{}\n{}",
584                relations
585                    .iter()
586                    .map(|r| r.quoted().to_string())
587                    .collect::<Vec<_>>()
588                    .join("\n"),
589                match names.is_empty() {
590                    true => "No relations are available.".to_string(),
591                    false => format!(
592                        "Only the following relations are available:\n{}",
593                        names
594                            .iter()
595                            .map(|name| name.quoted().to_string())
596                            .collect::<Vec<_>>()
597                            .join("\n")
598                    ),
599                }
600            )),
601            AdapterError::SourceOrSinkSizeRequired { .. } => Some(
602                "Either specify the cluster that will maintain this object via IN CLUSTER or \
603                specify size via SIZE option."
604                    .into(),
605            ),
606            AdapterError::InvalidTableMutationSelection {
607                object_name,
608                object_type,
609            } => Some(format!(
610                "{object_type} '{}' may not be used in this operation; \
611                     every relation leaf in the selection must be a writable user table",
612                object_name.quoted()
613            )),
614            AdapterError::ReadThenWriteDependencyLimitExceeded {
615                max_rw_dependencies,
616            } => Some(format!(
617                "The read set transitively depends on more than {max_rw_dependencies} \
618                     objects. Reduce the number of dependencies, or raise the \
619                     read_then_write_max_dependencies system parameter."
620            )),
621            AdapterError::PeekRowIterationLimitExceeded { limit } => Some(format!(
622                "The query attempted to examine more than {limit} rows on a single compute \
623                 worker. This limit prevents long-running SELECT queries from delaying other \
624                 work on the cluster."
625            )),
626            AdapterError::SafeModeViolation(_) => Some(
627                "The Materialize server you are connected to is running in \
628                 safe mode, which limits the features that are available."
629                    .into(),
630            ),
631            AdapterError::IntrospectionDisabled { log_names }
632            | AdapterError::UntargetedLogRead { log_names } => Some(format!(
633                "The query references the following log sources:\n    {}",
634                log_names.join("\n    "),
635            )),
636            AdapterError::InvalidLogDependency { log_names, .. } => Some(format!(
637                "The object depends on the following log sources:\n    {}",
638                log_names.join("\n    "),
639            )),
640            AdapterError::PlanError(e) => e.detail(),
641            AdapterError::Unauthorized(unauthorized) => unauthorized.detail(),
642            AdapterError::IndexInUse { blocked } => match &blocked[..] {
643                [(index_name, _)] => Some(format!(
644                    "The dependent objects are live dataflows that read from index {}, so the \
645                     index cannot be dropped while they exist.",
646                    index_name.quoted()
647                )),
648                blocked => {
649                    let per_index = blocked
650                        .iter()
651                        .map(|(index_name, dependents)| {
652                            format!(
653                                "Index {} is read by {}.",
654                                index_name.quoted(),
655                                fmt_dependents(dependents.iter())
656                            )
657                        })
658                        .join(" ");
659                    Some(format!(
660                        "{per_index} The dependent objects are live dataflows that read from \
661                         these indexes, so the indexes cannot be dropped while they exist."
662                    ))
663                }
664            },
665            AdapterError::DependentObject(dependent_objects) => Some(
666                dependent_objects
667                    .iter()
668                    .map(|(role_name, err_msgs)| {
669                        err_msgs
670                            .iter()
671                            .map(|err_msg| format!("{role_name}: {err_msg}"))
672                            .join("\n")
673                    })
674                    .join("\n"),
675            ),
676            AdapterError::Storage(storage_error) => storage_error
677                .source()
678                .map(|source_error| source_error.to_string_with_causes()),
679            AdapterError::ReadOnlyTransaction => Some(
680                "SELECT queries cannot be combined with other query types, including SUBSCRIBE."
681                    .into(),
682            ),
683            AdapterError::InvalidAlter(_, e) => e.detail(),
684            AdapterError::Optimizer(e) => e.detail(),
685            AdapterError::ConnectionValidation(e) => e.detail(),
686            AdapterError::MaterializedViewWouldNeverRefresh(last_refresh, earliest_possible) => {
687                Some(format!(
688                    "The specified last refresh is at {}, while the earliest possible time to compute the materialized \
689                    view is {}.",
690                    last_refresh, earliest_possible,
691                ))
692            }
693            AdapterError::UnallowedOnCluster { cluster, .. } => {
694                (cluster == MZ_CATALOG_SERVER_CLUSTER.name).then(|| {
695                    format!(
696                        "The transaction is executing on the \
697                        {cluster} cluster, maybe having been routed \
698                        there by the first statement in the transaction."
699                    )
700                })
701            }
702            AdapterError::InputNotReadableAtRefreshAtTime(oracle_read_ts, least_valid_read) => {
703                Some(format!(
704                    "The requested REFRESH AT time is {}, \
705                    but not all input collections are readable earlier than [{}].",
706                    oracle_read_ts,
707                    if least_valid_read.len() == 1 {
708                        format!(
709                            "{}",
710                            least_valid_read
711                                .as_option()
712                                .expect("antichain contains exactly 1 timestamp")
713                        )
714                    } else {
715                        // This can't occur currently
716                        format!("{:?}", least_valid_read)
717                    }
718                ))
719            }
720            AdapterError::RtrTimeout(name) => Some(format!(
721                "{name} failed to ingest data up to the real-time recency point"
722            )),
723            AdapterError::RtrDropFailure(name) => Some(format!(
724                "{name} dropped before ingesting data to the real-time recency point"
725            )),
726            AdapterError::UserSessionsDisallowed => {
727                Some("Your organization has been blocked. Please contact support.".to_string())
728            }
729            AdapterError::NetworkPolicyDenied(reason) => Some(format!("{reason}.")),
730            AdapterError::ReplacementSchemaMismatch(diff) => {
731                let mut lines: Vec<_> = diff.column_diffs.iter().map(|(idx, diff)| {
732                    let pos = idx + 1;
733                    match diff {
734                        ColumnDiff::Missing { name } => {
735                            let name = name.as_str().quoted();
736                            format!("missing column {name} at position {pos}")
737                        }
738                        ColumnDiff::Extra { name } => {
739                            let name = name.as_str().quoted();
740                            format!("extra column {name} at position {pos}")
741                        }
742                        ColumnDiff::TypeMismatch { name, left, right } => {
743                            let name = name.as_str().quoted();
744                            format!("column {name} at position {pos}: type mismatch (target: {left:?}, replacement: {right:?})")
745                        }
746                        ColumnDiff::NullabilityMismatch { name, left, right } => {
747                            let name = name.as_str().quoted();
748                            let left = if *left { "NULL" } else { "NOT NULL" };
749                            let right = if *right { "NULL" } else { "NOT NULL" };
750                            format!("column {name} at position {pos}: nullability mismatch (target: {left}, replacement: {right})")
751                        }
752                        ColumnDiff::NameMismatch { left, right } => {
753                            let left = left.as_str().quoted();
754                            let right = right.as_str().quoted();
755                            format!("column at position {pos}: name mismatch (target: {left}, replacement: {right})")
756                        }
757                    }
758                }).collect();
759
760                if let Some(KeyDiff { left, right }) = &diff.key_diff {
761                    let format_keys = |keys: &BTreeSet<Vec<ColumnName>>| {
762                        if keys.is_empty() {
763                            "(none)".to_string()
764                        } else {
765                            keys.iter()
766                                .map(|key| {
767                                    let cols = key.iter().map(|c| c.as_str()).join(", ");
768                                    format!("{{{cols}}}")
769                                })
770                                .join(", ")
771                        }
772                    };
773                    lines.push(format!(
774                        "keys differ (target: {}, replacement: {})",
775                        format_keys(left),
776                        format_keys(right)
777                    ));
778                }
779                Some(lines.join("\n"))
780            }
781            AdapterError::ReplaceMaterializedViewSealed { .. } => Some(
782                "The materialized view's contents are final for all future times, so they can \
783                 no longer be changed. See \
784                 https://materialize.com/docs/sql/alter-materialized-view/#troubleshooting"
785                    .into(),
786            ),
787            AdapterError::ImpossibleTimestampConstraints { constraints } => {
788                Some(format!("Constraints:\n{}", constraints))
789            }
790            AdapterError::BoundedStalenessExceeded {
791                gap_ms,
792                slowest_input,
793                ..
794            } => {
795                let mut detail = format!(
796                    "Freshest available timestamp is {}ms older than the bound.",
797                    gap_ms,
798                );
799                if let Some(id) = slowest_input {
800                    detail.push_str(&format!(" Slowest input: {}.", id));
801                }
802                Some(detail)
803            }
804            AdapterError::BoundedStalenessTimelineUnsupported => Some(
805                "This query touches a timeline other than the EpochMilliseconds wall-clock \
806                 timeline."
807                    .into(),
808            ),
809            _ => None,
810        }
811    }
812
813    /// Reports a hint for the user about how the error could be fixed.
814    pub fn hint(&self) -> Option<String> {
815        match self {
816            AdapterError::AmbiguousSystemColumnReference => Some(
817                "Rewrite the view to refer to all columns by name. Expand all wildcards and \
818                convert all NATURAL JOINs to USING joins."
819                    .to_string(),
820            ),
821            AdapterError::Catalog(c) => c.hint(),
822            AdapterError::Eval(e) => e.hint(),
823            AdapterError::IndexInUse { .. } => Some(
824                "Drop the dependent objects first, then drop the index and recreate the \
825                 dependents. To replace them in production without downtime, use a blue/green \
826                 deployment."
827                    .to_string(),
828            ),
829            AdapterError::SubscribeFellBehind { .. } => Some(
830                "The client is not reading results fast enough. Use a client that reads output \
831                without buffering, or raise the subscribe_max_buffered_bytes system variable."
832                    .to_string(),
833            ),
834            AdapterError::Dataflow(e) => match &**e {
835                DataflowError::EvalError(e) => e.hint(),
836                DataflowError::SourceError(e) => e.hint.as_ref().map(|hint| hint.to_string()),
837                _ => None,
838            },
839            AdapterError::AlterClusterUnmanagedWhileReconfiguring => Some(
840                "Cancel the reconfiguration by altering the cluster back to its current \
841                configuration, or wait for it to settle, then convert."
842                    .to_string(),
843            ),
844            AdapterError::AlterClusterUnmanagedWhileBursting => Some(
845                "Remove the strategy with ALTER CLUSTER ... RESET (AUTO SCALING STRATEGY), \
846                or wait for the burst to wind down, then convert."
847                    .to_string(),
848            ),
849            AdapterError::AlterClusterScheduleWhileReconfiguring => Some(
850                "Cancel the reconfiguration by altering the cluster back to its current \
851                configuration, or wait for it to settle, then change the schedule."
852                    .to_string(),
853            ),
854            AdapterError::AlterClusterWaitOnScheduledCluster => Some(
855                "The scheduler reshapes a scheduled cluster's replicas in the background. \
856                Run the ALTER without a WAIT option, or set SCHEDULE = MANUAL first."
857                    .to_string(),
858            ),
859            AdapterError::InvalidClusterReplicaAz { expected, az: _ } => {
860                Some(if expected.is_empty() {
861                    "No availability zones configured; do not specify AVAILABILITY ZONE".into()
862                } else {
863                    format!("Valid availability zones are: {}", expected.join(", "))
864                })
865            }
866            AdapterError::InvalidStorageClusterSize { expected, .. } => {
867                Some(format!("Valid sizes are: {}", expected.join(", ")))
868            }
869            AdapterError::SourceOrSinkSizeRequired { expected } => Some(format!(
870                "Try choosing one of the smaller sizes to start. Available sizes: {}",
871                expected.join(", ")
872            )),
873            AdapterError::NoClusterReplicasAvailable { is_managed, .. } => {
874                Some(if *is_managed {
875                    "Use ALTER CLUSTER to adjust the replication factor of the cluster. \
876                    Example:`ALTER CLUSTER <cluster-name> SET (REPLICATION FACTOR 1)`".into()
877                } else {
878                    "Use CREATE CLUSTER REPLICA to attach cluster replicas to the cluster".into()
879                })
880            }
881            AdapterError::UntargetedLogRead { .. } => Some(
882                "Use `SET cluster_replica = <replica-name>` to target a specific replica in the \
883                 active cluster. Note that subsequent queries will only be answered by \
884                 the selected replica, which might reduce availability. To undo the replica \
885                 selection, use `RESET cluster_replica`."
886                    .into(),
887            ),
888            AdapterError::ResourceExhaustion { resource_type, .. } => Some(format!(
889                "Drop an existing {resource_type} or contact support to request a limit increase."
890            )),
891            AdapterError::StatementTimeout => Some(
892                "Consider increasing the maximum allowed statement duration for this session by \
893                 setting the statement_timeout session variable. For example, `SET \
894                 statement_timeout = '120s'`."
895                    .into(),
896            ),
897            AdapterError::PeekRowIterationLimitExceeded { .. } => Some(
898                "Reduce the number of rows the query must examine, for example by querying an \
899                 indexed, more selective result. Queries with `LIMIT` and no `ORDER BY` can also \
900                 stop early. To permit this query, increase \
901                 `compute_peek_row_iteration_limit`."
902                    .into(),
903            ),
904            AdapterError::PlanError(e) => e.hint(),
905            AdapterError::UnallowedOnCluster { cluster, .. } => {
906                (cluster != MZ_CATALOG_SERVER_CLUSTER.name).then(||
907                    "Use `SET CLUSTER = <cluster-name>` to change your cluster and re-run the query."
908                    .to_string()
909                )
910            }
911            AdapterError::InvalidAlter(_, e) => e.hint(),
912            AdapterError::Optimizer(e) => e.hint(),
913            AdapterError::ConnectionValidation(e) => e.hint(),
914            AdapterError::InputNotReadableAtRefreshAtTime(_, _) => Some(
915                "You can use `REFRESH AT greatest(mz_now(), <explicit timestamp>)` to refresh \
916                 either at the explicitly specified timestamp, or now if the given timestamp would \
917                 be in the past.".to_string()
918            ),
919            AdapterError::AlterClusterTimeout => Some(
920                "Consider increasing the timeout duration in the alter cluster statement.".into(),
921            ),
922            AdapterError::AlterClusterSuperseded => Some(
923                "Retry the ALTER CLUSTER statement if its target is still desired.".into(),
924            ),
925            AdapterError::AlterClusterResourceExhausted => Some(
926                "Reduce the target resource use, or inspect \
927                 mz_internal.mz_cluster_reconfigurations for the retained outcome."
928                    .into(),
929            ),
930            AdapterError::DDLTransactionOutcomeChanged => Some(
931                "Retry the transaction to execute its statements against the current catalog.".into()
932            ),
933            AdapterError::ConcurrentDependencyMutation { .. } => Some(
934                "Another session modified one of this statement's dependencies before \
935                 it could commit. Retry the statement.".into()
936            ),
937            AdapterError::ReadThenWriteContention => Some(
938                "Concurrent writes to the target table kept this statement from \
939                 committing. Retry the statement, or lower the write concurrency.".into()
940            ),
941            AdapterError::ReadThenWriteTimestampTooFarAhead { .. } => Some(
942                "The write timestamp this statement would have committed at is far ahead of \
943                 the wall clock. Committing there would keep writes and strict-serializable \
944                 reads on this timeline from proceeding until the clock caught up.".into()
945            ),
946            AdapterError::CollectionUnreadable { .. } => Some(
947                "This could be because the collection has recently been dropped.".into()
948            ),
949            AdapterError::ReplaceMaterializedViewSealed { .. } => Some(
950                "Create a new materialized view with the new definition instead.".into()
951            ),
952            _ => None,
953        }
954    }
955
956    pub fn code(&self) -> SqlState {
957        // We define this up here to make sure `AdapterError::` and `OptimizerError::` act the same way.
958        const RECURSION_LIMIT_ERROR_CODE: SqlState = SqlState::INTERNAL_ERROR;
959
960        // TODO(benesch): we should only use `SqlState::INTERNAL_ERROR` for
961        // those errors that are truly internal errors. At the moment we have
962        // a various classes of uncategorized errors that use this error code
963        // inappropriately.
964        match self {
965            // DATA_EXCEPTION to match what Postgres returns for degenerate
966            // range bounds
967            AdapterError::AbsurdSubscribeBounds { .. } => SqlState::DATA_EXCEPTION,
968            AdapterError::AmbiguousSystemColumnReference => SqlState::FEATURE_NOT_SUPPORTED,
969            AdapterError::Catalog(e) => match &e.kind {
970                mz_catalog::memory::error::ErrorKind::VarError(e) => match e {
971                    VarError::ConstrainedParameter { .. } => SqlState::INVALID_PARAMETER_VALUE,
972                    VarError::FixedValueParameter { .. } => SqlState::INVALID_PARAMETER_VALUE,
973                    VarError::InvalidParameterType { .. } => SqlState::INVALID_PARAMETER_VALUE,
974                    VarError::InvalidParameterValue { .. } => SqlState::INVALID_PARAMETER_VALUE,
975                    VarError::ReadOnlyParameter(_) => SqlState::CANT_CHANGE_RUNTIME_PARAM,
976                    VarError::UnknownParameter(_) => SqlState::UNDEFINED_OBJECT,
977                    VarError::RequiresUnsafeMode { .. } => SqlState::CANT_CHANGE_RUNTIME_PARAM,
978                    VarError::RequiresFeatureFlag { .. } => SqlState::CANT_CHANGE_RUNTIME_PARAM,
979                },
980                _ => SqlState::INTERNAL_ERROR,
981            },
982            AdapterError::ChangedPlan(_) => SqlState::FEATURE_NOT_SUPPORTED,
983            AdapterError::DuplicateCursor(_) => SqlState::DUPLICATE_CURSOR,
984            // Evaluation errors are almost all user-facing data exceptions, not
985            // internal errors. `eval_error_code` matches every variant
986            // exhaustively so the catch-all `INTERNAL_ERROR` no longer applies
987            // to errors that are really the user's fault. See SQL-326.
988            AdapterError::Eval(e) => eval_error_code(e),
989            AdapterError::Dataflow(e) => dataflow_error_code(e),
990            AdapterError::Explain(_) => SqlState::INTERNAL_ERROR,
991            AdapterError::IdExhaustionError => SqlState::INTERNAL_ERROR,
992            AdapterError::Internal(_) => SqlState::INTERNAL_ERROR,
993            AdapterError::IntrospectionDisabled { .. } => SqlState::FEATURE_NOT_SUPPORTED,
994            AdapterError::InvalidLogDependency { .. } => SqlState::FEATURE_NOT_SUPPORTED,
995            AdapterError::InvalidClusterReplicaAz { .. } => SqlState::FEATURE_NOT_SUPPORTED,
996            AdapterError::InvalidSetIsolationLevel => SqlState::ACTIVE_SQL_TRANSACTION,
997            AdapterError::InvalidSetCluster => SqlState::ACTIVE_SQL_TRANSACTION,
998            AdapterError::InvalidStorageClusterSize { .. } => SqlState::FEATURE_NOT_SUPPORTED,
999            AdapterError::SourceOrSinkSizeRequired { .. } => SqlState::FEATURE_NOT_SUPPORTED,
1000            AdapterError::InvalidTableMutationSelection { .. } => {
1001                SqlState::INVALID_TRANSACTION_STATE
1002            }
1003            AdapterError::ReadThenWriteDependencyLimitExceeded { .. } => {
1004                SqlState::PROGRAM_LIMIT_EXCEEDED
1005            }
1006            AdapterError::ConstraintViolation(NotNullViolation(_)) => SqlState::NOT_NULL_VIOLATION,
1007            AdapterError::CopyFormatError(_) => SqlState::BAD_COPY_FILE_FORMAT,
1008            AdapterError::ConcurrentClusterDrop => SqlState::INVALID_TRANSACTION_STATE,
1009            AdapterError::ConcurrentDependencyDrop { .. } => SqlState::UNDEFINED_OBJECT,
1010            AdapterError::ConcurrentDependencyMutation { .. } => {
1011                SqlState::T_R_SERIALIZATION_FAILURE
1012            }
1013            AdapterError::ReadThenWriteContention => SqlState::T_R_SERIALIZATION_FAILURE,
1014            AdapterError::ReadThenWriteTimestampTooFarAhead { .. } => {
1015                // An invariant violation in the environment rather than a property of
1016                // the statement: the write timeline has run away from the wall clock.
1017                // Nothing the client sends can produce or avoid it.
1018                SqlState::INTERNAL_ERROR
1019            }
1020            AdapterError::CollectionUnreadable { .. } => SqlState::NO_DATA_FOUND,
1021            AdapterError::NoClusterReplicasAvailable { .. } => SqlState::FEATURE_NOT_SUPPORTED,
1022            AdapterError::OperationProhibitsTransaction(_) => SqlState::ACTIVE_SQL_TRANSACTION,
1023            AdapterError::OperationRequiresTransaction(_) => SqlState::NO_ACTIVE_SQL_TRANSACTION,
1024            AdapterError::ParseError(_) => SqlState::SYNTAX_ERROR,
1025            AdapterError::PlanError(PlanError::InvalidSchemaName) => SqlState::INVALID_SCHEMA_NAME,
1026            AdapterError::PlanError(PlanError::ColumnAlreadyExists { .. }) => {
1027                SqlState::DUPLICATE_COLUMN
1028            }
1029            AdapterError::PlanError(PlanError::UnknownParameter(_)) => {
1030                SqlState::UNDEFINED_PARAMETER
1031            }
1032            AdapterError::PlanError(PlanError::ParameterNotAllowed(_)) => {
1033                SqlState::UNDEFINED_PARAMETER
1034            }
1035            // `PlanError::Unsupported` is raised (via `bail_unsupported!`) only for
1036            // genuinely unsupported features, so it maps to PostgreSQL's
1037            // feature-not-supported code rather than internal-error. See SQL-326.
1038            AdapterError::PlanError(PlanError::Unsupported { .. }) => {
1039                SqlState::FEATURE_NOT_SUPPORTED
1040            }
1041            AdapterError::PlanError(_) => SqlState::INTERNAL_ERROR,
1042            AdapterError::PreparedStatementExists(_) => SqlState::DUPLICATE_PSTATEMENT,
1043            AdapterError::ReadOnlyTransaction => SqlState::READ_ONLY_SQL_TRANSACTION,
1044            AdapterError::ReadWriteUnavailable => SqlState::INVALID_TRANSACTION_STATE,
1045            AdapterError::SingleStatementTransaction => SqlState::INVALID_TRANSACTION_STATE,
1046            AdapterError::StatementTimeout => SqlState::QUERY_CANCELED,
1047            AdapterError::Canceled => SqlState::QUERY_CANCELED,
1048            AdapterError::IdleInTransactionSessionTimeout => {
1049                SqlState::IDLE_IN_TRANSACTION_SESSION_TIMEOUT
1050            }
1051            AdapterError::RecursionLimit(_) => RECURSION_LIMIT_ERROR_CODE,
1052            AdapterError::RelationOutsideTimeDomain { .. } => SqlState::INVALID_TRANSACTION_STATE,
1053            AdapterError::ResourceExhaustion { .. } => SqlState::INSUFFICIENT_RESOURCES,
1054            AdapterError::ResultSize(_) => SqlState::OUT_OF_MEMORY,
1055            AdapterError::SubscribeFellBehind { .. } => SqlState::OUT_OF_MEMORY,
1056            AdapterError::PeekRowIterationLimitExceeded { .. } => SqlState::PROGRAM_LIMIT_EXCEEDED,
1057            AdapterError::SafeModeViolation(_) => SqlState::INTERNAL_ERROR,
1058            AdapterError::SubscribeOnlyTransaction => SqlState::INVALID_TRANSACTION_STATE,
1059            AdapterError::Optimizer(e) => match e {
1060                OptimizerError::PlanError(PlanError::InvalidSchemaName) => {
1061                    SqlState::INVALID_SCHEMA_NAME
1062                }
1063                OptimizerError::PlanError(PlanError::ColumnAlreadyExists { .. }) => {
1064                    SqlState::DUPLICATE_COLUMN
1065                }
1066                OptimizerError::PlanError(PlanError::UnknownParameter(_)) => {
1067                    SqlState::UNDEFINED_PARAMETER
1068                }
1069                OptimizerError::PlanError(PlanError::ParameterNotAllowed(_)) => {
1070                    SqlState::UNDEFINED_PARAMETER
1071                }
1072                OptimizerError::PlanError(PlanError::Unsupported { .. }) => {
1073                    SqlState::FEATURE_NOT_SUPPORTED
1074                }
1075                OptimizerError::PlanError(_) => SqlState::INTERNAL_ERROR,
1076                OptimizerError::RecursionLimitError(_) => RECURSION_LIMIT_ERROR_CODE,
1077                OptimizerError::Internal(s) => {
1078                    AdapterError::Internal(s.clone()).code() // Delegate to outer
1079                }
1080                OptimizerError::EvalError(e) => {
1081                    AdapterError::Eval(e.clone()).code() // Delegate to outer
1082                }
1083                OptimizerError::TransformError(_) => SqlState::INTERNAL_ERROR,
1084                OptimizerError::UnmaterializableFunction(_) => SqlState::FEATURE_NOT_SUPPORTED,
1085                OptimizerError::UncallableFunction { .. } => SqlState::FEATURE_NOT_SUPPORTED,
1086                OptimizerError::UnsupportedTemporalExpression(_) => SqlState::FEATURE_NOT_SUPPORTED,
1087                OptimizerError::RestrictedFunction(_) => SqlState::INSUFFICIENT_PRIVILEGE,
1088                // This should be handled by peek optimization, so it's an internal error if it
1089                // reaches the user.
1090                OptimizerError::InternalUnsafeMfpPlan(_) => SqlState::INTERNAL_ERROR,
1091            },
1092            AdapterError::UnallowedOnCluster { .. } => {
1093                SqlState::S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED
1094            }
1095            AdapterError::Unauthorized(_) => SqlState::INSUFFICIENT_PRIVILEGE,
1096            AdapterError::UnknownCursor(_) => SqlState::INVALID_CURSOR_NAME,
1097            AdapterError::UnknownPreparedStatement(_) => SqlState::UNDEFINED_PSTATEMENT,
1098            AdapterError::UnknownLoginRole(_) => SqlState::INVALID_AUTHORIZATION_SPECIFICATION,
1099            AdapterError::UnknownClusterReplica { .. } => SqlState::UNDEFINED_OBJECT,
1100            AdapterError::UnrecognizedConfigurationParam(_) => SqlState::UNDEFINED_OBJECT,
1101            AdapterError::Unsupported(..) => SqlState::FEATURE_NOT_SUPPORTED,
1102            AdapterError::UnavailableFeature { .. } => SqlState::FEATURE_NOT_SUPPORTED,
1103            AdapterError::Unstructured(_) => SqlState::INTERNAL_ERROR,
1104            AdapterError::UntargetedLogRead { .. } => SqlState::FEATURE_NOT_SUPPORTED,
1105            AdapterError::DDLTransactionOutcomeChanged => SqlState::T_R_SERIALIZATION_FAILURE,
1106            AdapterError::ClusterStateChanged { .. } => SqlState::T_R_SERIALIZATION_FAILURE,
1107            // It's not immediately clear which error code to use here because a
1108            // "write-only transaction", "single table write transaction", or "ddl only
1109            // transaction" are not things in Postgres. This error code is the generic "bad txn
1110            // thing" code, so it's probably the best choice.
1111            AdapterError::WriteOnlyTransaction => SqlState::INVALID_TRANSACTION_STATE,
1112            AdapterError::DDLOnlyTransaction => SqlState::INVALID_TRANSACTION_STATE,
1113            AdapterError::Storage(_) | AdapterError::Compute(_) | AdapterError::Orchestrator(_) => {
1114                SqlState::INTERNAL_ERROR
1115            }
1116            AdapterError::DependentObject(_) => SqlState::DEPENDENT_OBJECTS_STILL_EXIST,
1117            AdapterError::IndexInUse { .. } => SqlState::DEPENDENT_OBJECTS_STILL_EXIST,
1118            AdapterError::InvalidAlter(_, _) => SqlState::FEATURE_NOT_SUPPORTED,
1119            AdapterError::ConnectionValidation(_) => SqlState::SYSTEM_ERROR,
1120            // `DATA_EXCEPTION`, similarly to `AbsurdSubscribeBounds`.
1121            AdapterError::MaterializedViewWouldNeverRefresh(_, _) => SqlState::DATA_EXCEPTION,
1122            AdapterError::InputNotReadableAtRefreshAtTime(_, _) => SqlState::DATA_EXCEPTION,
1123            AdapterError::RtrTimeout(_) => SqlState::QUERY_CANCELED,
1124            AdapterError::RtrDropFailure(_) => SqlState::UNDEFINED_OBJECT,
1125            AdapterError::UnreadableSinkCollection => SqlState::from_code("MZ009"),
1126            AdapterError::UserSessionsDisallowed => SqlState::from_code("MZ010"),
1127            AdapterError::NetworkPolicyDenied(_) => SqlState::from_code("MZ011"),
1128            // In read-only mode all transactions are implicitly read-only
1129            // transactions.
1130            AdapterError::ReadOnly => SqlState::READ_ONLY_SQL_TRANSACTION,
1131            AdapterError::AlterClusterTimeout => SqlState::QUERY_CANCELED,
1132            AdapterError::AlterClusterSuperseded => SqlState::QUERY_CANCELED,
1133            AdapterError::AlterClusterResourceExhausted => SqlState::INSUFFICIENT_RESOURCES,
1134            AdapterError::AlterClusterUnmanagedWhileReconfiguring => SqlState::OBJECT_IN_USE,
1135            AdapterError::AlterClusterUnmanagedWhileBursting => SqlState::OBJECT_IN_USE,
1136            AdapterError::AlterClusterScheduleWhileReconfiguring => SqlState::OBJECT_IN_USE,
1137            AdapterError::AlterClusterWaitOnScheduledCluster => SqlState::FEATURE_NOT_SUPPORTED,
1138            AdapterError::ReplacementSchemaMismatch(_) => SqlState::FEATURE_NOT_SUPPORTED,
1139            AdapterError::AuthenticationError(AuthenticationError::InvalidCredentials) => {
1140                SqlState::INVALID_PASSWORD
1141            }
1142            AdapterError::AuthenticationError(_) => SqlState::INVALID_AUTHORIZATION_SPECIFICATION,
1143            AdapterError::ReplaceMaterializedViewSealed { .. } => {
1144                SqlState::OBJECT_NOT_IN_PREREQUISITE_STATE
1145            }
1146            // similar to AbsurdSubscribeBounds
1147            AdapterError::ImpossibleTimestampConstraints { .. } => SqlState::DATA_EXCEPTION,
1148            AdapterError::OidcGroupSyncFailed(_) => SqlState::INTERNAL_ERROR,
1149            AdapterError::BoundedStalenessExceeded { .. } => SqlState::T_R_SERIALIZATION_FAILURE,
1150            // Matches ReadOnlyTransaction/ReadOnly: a write was rejected
1151            // because the session is effectively read-only.
1152            AdapterError::BoundedStalenessReadOnly => SqlState::READ_ONLY_SQL_TRANSACTION,
1153            AdapterError::BoundedStalenessRealTimeRecencyConflict => {
1154                SqlState::FEATURE_NOT_SUPPORTED
1155            }
1156            AdapterError::BoundedStalenessTimelineUnsupported => SqlState::FEATURE_NOT_SUPPORTED,
1157        }
1158    }
1159
1160    pub fn internal<E: std::fmt::Display>(context: &str, e: E) -> AdapterError {
1161        AdapterError::Internal(format!("{context}: {e}"))
1162    }
1163
1164    // We don't want the following error conversions to `ConcurrentDependencyDrop` to happen
1165    // automatically, because it might depend on the context whether `ConcurrentDependencyDrop`
1166    // is appropriate, so we want to make the conversion target explicit at the call site.
1167    // For example, maybe we get an `InstanceMissing` if the user specifies a non-existing cluster,
1168    // in which case `ConcurrentDependencyDrop` would not be appropriate.
1169
1170    pub fn concurrent_dependency_drop_from_collection_missing(e: CollectionMissing) -> Self {
1171        AdapterError::ConcurrentDependencyDrop {
1172            dependency_kind: "collection",
1173            dependency_id: e.0.to_string(),
1174        }
1175    }
1176
1177    /// Maps a lookup error of the session task's compute instance clients.
1178    pub fn concurrent_dependency_drop_from_collection_lookup_error(
1179        e: CollectionLookupError,
1180        compute_instance: ComputeInstanceId,
1181    ) -> Self {
1182        match e {
1183            CollectionLookupError::InstanceMissing(id) => AdapterError::ConcurrentDependencyDrop {
1184                dependency_kind: "cluster",
1185                dependency_id: id.to_string(),
1186            },
1187            CollectionLookupError::CollectionMissing(id) => {
1188                AdapterError::ConcurrentDependencyDrop {
1189                    dependency_kind: "collection",
1190                    dependency_id: id.to_string(),
1191                }
1192            }
1193            CollectionLookupError::InstanceShutDown => AdapterError::ConcurrentDependencyDrop {
1194                dependency_kind: "cluster",
1195                dependency_id: compute_instance.to_string(),
1196            },
1197        }
1198    }
1199
1200    /// Maps a collection lookup error of the compute controller.
1201    pub fn concurrent_dependency_drop_from_compute_lookup_error(
1202        e: compute_error::CollectionLookupError,
1203    ) -> Self {
1204        match e {
1205            compute_error::CollectionLookupError::InstanceMissing(id) => {
1206                AdapterError::ConcurrentDependencyDrop {
1207                    dependency_kind: "cluster",
1208                    dependency_id: id.to_string(),
1209                }
1210            }
1211            compute_error::CollectionLookupError::CollectionMissing(id) => {
1212                AdapterError::ConcurrentDependencyDrop {
1213                    dependency_kind: "collection",
1214                    dependency_id: id.to_string(),
1215                }
1216            }
1217        }
1218    }
1219
1220    pub fn concurrent_dependency_drop_from_instance_peek_error(
1221        e: mz_compute_client::controller::instance_client::PeekError,
1222        compute_instance: ComputeInstanceId,
1223    ) -> AdapterError {
1224        use mz_compute_client::controller::instance_client::PeekError::*;
1225        match e {
1226            ReplicaMissing(id) => AdapterError::ConcurrentDependencyDrop {
1227                dependency_kind: "replica",
1228                dependency_id: id.to_string(),
1229            },
1230            InstanceShutDown => AdapterError::ConcurrentDependencyDrop {
1231                dependency_kind: "cluster",
1232                dependency_id: compute_instance.to_string(),
1233            },
1234            e @ ReadHoldIdMismatch(_) => AdapterError::internal("instance peek error", e),
1235            e @ ReadHoldInsufficient(_) => AdapterError::internal("instance peek error", e),
1236        }
1237    }
1238
1239    pub fn concurrent_dependency_drop_from_collection_update_error(
1240        e: compute_error::CollectionUpdateError,
1241    ) -> Self {
1242        use compute_error::CollectionUpdateError::*;
1243        match e {
1244            InstanceMissing(id) => AdapterError::ConcurrentDependencyDrop {
1245                dependency_kind: "cluster",
1246                dependency_id: id.to_string(),
1247            },
1248            CollectionMissing(id) => AdapterError::ConcurrentDependencyDrop {
1249                dependency_kind: "collection",
1250                dependency_id: id.to_string(),
1251            },
1252        }
1253    }
1254
1255    pub fn concurrent_dependency_drop_from_peek_error(
1256        e: mz_compute_client::controller::error::PeekError,
1257    ) -> AdapterError {
1258        use mz_compute_client::controller::error::PeekError::*;
1259        match e {
1260            InstanceMissing(id) => AdapterError::ConcurrentDependencyDrop {
1261                dependency_kind: "cluster",
1262                dependency_id: id.to_string(),
1263            },
1264            CollectionMissing(id) => AdapterError::ConcurrentDependencyDrop {
1265                dependency_kind: "collection",
1266                dependency_id: id.to_string(),
1267            },
1268            ReplicaMissing(id) => AdapterError::ConcurrentDependencyDrop {
1269                dependency_kind: "replica",
1270                dependency_id: id.to_string(),
1271            },
1272            e @ (ReadHoldIdMismatch(_) | SinceViolation(_)) => {
1273                AdapterError::internal("peek error", e)
1274            }
1275        }
1276    }
1277
1278    pub fn concurrent_dependency_drop_from_dataflow_creation_error(
1279        e: compute_error::DataflowCreationError,
1280    ) -> Self {
1281        use compute_error::DataflowCreationError::*;
1282        match e {
1283            InstanceMissing(id) => AdapterError::ConcurrentDependencyDrop {
1284                dependency_kind: "cluster",
1285                dependency_id: id.to_string(),
1286            },
1287            CollectionMissing(id) => AdapterError::ConcurrentDependencyDrop {
1288                dependency_kind: "collection",
1289                dependency_id: id.to_string(),
1290            },
1291            ReplicaMissing(id) => AdapterError::ConcurrentDependencyDrop {
1292                dependency_kind: "replica",
1293                dependency_id: id.to_string(),
1294            },
1295            MissingAsOf | SinceViolation(..) | EmptyAsOfForSubscribe | EmptyAsOfForCopyTo => {
1296                AdapterError::internal("dataflow creation error", e)
1297            }
1298        }
1299    }
1300}
1301
1302impl fmt::Display for AdapterError {
1303    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1304        match self {
1305            AdapterError::AbsurdSubscribeBounds { as_of, up_to } => {
1306                write!(
1307                    f,
1308                    "subscription lower bound (`AS OF`) is greater than its upper bound (`UP TO`): \
1309                     {as_of} > {up_to}",
1310                )
1311            }
1312            AdapterError::AmbiguousSystemColumnReference => {
1313                write!(
1314                    f,
1315                    "cannot use wildcard expansions or NATURAL JOINs in a view that depends on \
1316                    system objects"
1317                )
1318            }
1319            AdapterError::ChangedPlan(e) => write!(f, "{}", e),
1320            AdapterError::Catalog(e) => e.fmt(f),
1321            AdapterError::DuplicateCursor(name) => {
1322                write!(f, "cursor {} already exists", name.quoted())
1323            }
1324            AdapterError::Eval(e) => e.fmt(f),
1325            AdapterError::Dataflow(e) => e.fmt(f),
1326            AdapterError::Explain(e) => e.fmt(f),
1327            AdapterError::IdExhaustionError => f.write_str("ID allocator exhausted all valid IDs"),
1328            AdapterError::Internal(e) => write!(f, "internal error: {}", e),
1329            AdapterError::IntrospectionDisabled { .. } => write!(
1330                f,
1331                "cannot read log sources of replica with disabled introspection"
1332            ),
1333            AdapterError::InvalidLogDependency { object_type, .. } => {
1334                write!(f, "{object_type} objects cannot depend on log sources")
1335            }
1336            AdapterError::InvalidClusterReplicaAz { az, expected: _ } => {
1337                write!(f, "unknown cluster replica availability zone {az}",)
1338            }
1339            AdapterError::InvalidSetIsolationLevel => write!(
1340                f,
1341                "SET TRANSACTION ISOLATION LEVEL must be called before any query"
1342            ),
1343            AdapterError::InvalidSetCluster => {
1344                write!(f, "SET cluster cannot be called in an active transaction")
1345            }
1346            AdapterError::InvalidStorageClusterSize { size, .. } => {
1347                write!(f, "unknown source size {size}")
1348            }
1349            AdapterError::SourceOrSinkSizeRequired { .. } => {
1350                write!(f, "must specify either cluster or size option")
1351            }
1352            AdapterError::InvalidTableMutationSelection { .. } => {
1353                write!(
1354                    f,
1355                    "invalid selection: every relation leaf must be a writable user table"
1356                )
1357            }
1358            AdapterError::ReadThenWriteDependencyLimitExceeded {
1359                max_rw_dependencies,
1360            } => {
1361                write!(
1362                    f,
1363                    "selection has too many transitive dependencies to validate (limit {max_rw_dependencies})"
1364                )
1365            }
1366            AdapterError::PeekRowIterationLimitExceeded { limit } => {
1367                write!(
1368                    f,
1369                    "query exceeded the configured row iteration limit of {limit} rows"
1370                )
1371            }
1372            AdapterError::ReplaceMaterializedViewSealed { name } => {
1373                write!(
1374                    f,
1375                    "materialized view {name} is sealed and thus cannot be replaced"
1376                )
1377            }
1378            AdapterError::ConstraintViolation(not_null_violation) => {
1379                write!(f, "{}", not_null_violation)
1380            }
1381            AdapterError::CopyFormatError(e) => write!(f, "{e}"),
1382            AdapterError::ConcurrentClusterDrop => {
1383                write!(f, "the transaction's active cluster has been dropped")
1384            }
1385            AdapterError::ConcurrentDependencyDrop {
1386                dependency_kind,
1387                dependency_id,
1388            } => {
1389                write!(f, "{dependency_kind} '{dependency_id}' was dropped")
1390            }
1391            AdapterError::ConcurrentDependencyMutation { dependency_id } => {
1392                write!(
1393                    f,
1394                    "catalog item '{dependency_id}' was concurrently modified"
1395                )
1396            }
1397            AdapterError::ReadThenWriteContention => {
1398                write!(
1399                    f,
1400                    "read-then-write exceeded maximum retry attempts under contention"
1401                )
1402            }
1403            AdapterError::ReadThenWriteTimestampTooFarAhead {
1404                target_timestamp,
1405                limit,
1406            } => {
1407                write!(
1408                    f,
1409                    "read-then-write would have to commit at {target_timestamp}, past the \
1410                     highest timestamp the write timeline may be advanced to ({limit})"
1411                )
1412            }
1413            AdapterError::CollectionUnreadable { id } => {
1414                write!(f, "collection '{id}' is not readable at any timestamp")
1415            }
1416            AdapterError::NoClusterReplicasAvailable { name, .. } => {
1417                write!(
1418                    f,
1419                    "CLUSTER {} has no replicas available to service request",
1420                    name.quoted()
1421                )
1422            }
1423            AdapterError::OperationProhibitsTransaction(op) => {
1424                write!(f, "{} cannot be run inside a transaction block", op)
1425            }
1426            AdapterError::OperationRequiresTransaction(op) => {
1427                write!(f, "{} can only be used in transaction blocks", op)
1428            }
1429            AdapterError::ParseError(e) => e.fmt(f),
1430            AdapterError::PlanError(e) => e.fmt(f),
1431            AdapterError::PreparedStatementExists(name) => {
1432                write!(f, "prepared statement {} already exists", name.quoted())
1433            }
1434            AdapterError::ReadOnlyTransaction => f.write_str("transaction in read-only mode"),
1435            AdapterError::SingleStatementTransaction => {
1436                f.write_str("this transaction can only execute a single statement")
1437            }
1438            AdapterError::ReadWriteUnavailable => {
1439                f.write_str("transaction read-write mode must be set before any query")
1440            }
1441            AdapterError::StatementTimeout => {
1442                write!(f, "canceling statement due to statement timeout")
1443            }
1444            AdapterError::Canceled => {
1445                write!(f, "canceling statement due to user request")
1446            }
1447            AdapterError::IdleInTransactionSessionTimeout => {
1448                write!(
1449                    f,
1450                    "terminating connection due to idle-in-transaction timeout"
1451                )
1452            }
1453            AdapterError::RecursionLimit(e) => e.fmt(f),
1454            AdapterError::RelationOutsideTimeDomain { .. } => {
1455                write!(
1456                    f,
1457                    "Transactions can only reference objects in the same timedomain. \
1458                     See https://materialize.com/docs/sql/begin/#same-timedomain-error",
1459                )
1460            }
1461            AdapterError::ResourceExhaustion {
1462                resource_type,
1463                limit_name,
1464                desired,
1465                limit,
1466                current,
1467            } => {
1468                write!(
1469                    f,
1470                    "creating {resource_type} would violate {limit_name} limit (desired: {desired}, limit: {limit}, current: {current})"
1471                )
1472            }
1473            AdapterError::ResultSize(e) => write!(f, "{e}"),
1474            AdapterError::SubscribeFellBehind {
1475                buffered_bytes,
1476                max_buffered_bytes,
1477            } => {
1478                use bytesize::ByteSize;
1479                let buffered = u64::cast_from(*buffered_bytes);
1480                let max = u64::cast_from(*max_buffered_bytes);
1481                write!(
1482                    f,
1483                    "SUBSCRIBE fell behind: the client did not read results fast enough, so its backlog reached {}, exceeding the {} budget",
1484                    ByteSize::b(buffered),
1485                    ByteSize::b(max),
1486                )
1487            }
1488            AdapterError::SafeModeViolation(feature) => {
1489                write!(f, "cannot create {} in safe mode", feature)
1490            }
1491            AdapterError::SubscribeOnlyTransaction => {
1492                f.write_str("SUBSCRIBE in transactions must be the only read statement")
1493            }
1494            AdapterError::Optimizer(e) => e.fmt(f),
1495            AdapterError::UnallowedOnCluster {
1496                depends_on,
1497                cluster,
1498            } => {
1499                let items = depends_on.into_iter().map(|item| item.quoted()).join(", ");
1500                write!(
1501                    f,
1502                    "querying the following items {items} is not allowed from the {} cluster",
1503                    cluster.quoted()
1504                )
1505            }
1506            AdapterError::Unauthorized(unauthorized) => {
1507                write!(f, "{unauthorized}")
1508            }
1509            AdapterError::UnknownCursor(name) => {
1510                write!(f, "cursor {} does not exist", name.quoted())
1511            }
1512            AdapterError::UnknownLoginRole(name) => {
1513                write!(f, "role {} does not exist", name.quoted())
1514            }
1515            AdapterError::Unsupported(features) => write!(f, "{} are not supported", features),
1516            AdapterError::Unstructured(e) => write!(f, "{}", e.display_with_causes()),
1517            AdapterError::WriteOnlyTransaction => f.write_str("transaction in write-only mode"),
1518            AdapterError::UnknownPreparedStatement(name) => {
1519                write!(f, "prepared statement {} does not exist", name.quoted())
1520            }
1521            AdapterError::UnknownClusterReplica {
1522                cluster_name,
1523                replica_name,
1524            } => write!(
1525                f,
1526                "cluster replica '{cluster_name}.{replica_name}' does not exist"
1527            ),
1528            AdapterError::UnrecognizedConfigurationParam(setting_name) => write!(
1529                f,
1530                "unrecognized configuration parameter {}",
1531                setting_name.quoted()
1532            ),
1533            AdapterError::UntargetedLogRead { .. } => {
1534                f.write_str("log source reads must target a replica")
1535            }
1536            AdapterError::DDLOnlyTransaction => f.write_str(
1537                "transactions which modify objects are restricted to just modifying objects",
1538            ),
1539            AdapterError::DDLTransactionOutcomeChanged => f.write_str(
1540                "a statement in this transaction would produce a different result at commit",
1541            ),
1542            AdapterError::ClusterStateChanged { cluster_id } => {
1543                write!(f, "cluster {cluster_id} was concurrently modified")
1544            }
1545            AdapterError::Storage(e) => e.fmt(f),
1546            AdapterError::Compute(e) => e.fmt(f),
1547            AdapterError::Orchestrator(e) => e.fmt(f),
1548            AdapterError::DependentObject(dependent_objects) => {
1549                let role_str = if dependent_objects.keys().count() == 1 {
1550                    "role"
1551                } else {
1552                    "roles"
1553                };
1554                write!(
1555                    f,
1556                    "{role_str} \"{}\" cannot be dropped because some objects depend on it",
1557                    dependent_objects.keys().join(", ")
1558                )
1559            }
1560            AdapterError::IndexInUse { blocked } => {
1561                // Mirrors `PlanError::DependentObjectsStillExist` so clients see one shape of
1562                // RESTRICT failure regardless of which layer detected the dependency.
1563                let dependents = fmt_dependents(blocked.iter().flat_map(|(_, deps)| deps).unique());
1564                match &blocked[..] {
1565                    [(index_name, _)] => write!(
1566                        f,
1567                        "cannot drop index {}: still depended upon by {dependents}",
1568                        index_name.quoted()
1569                    ),
1570                    blocked => write!(
1571                        f,
1572                        "cannot drop indexes {}: still depended upon by {dependents}",
1573                        blocked.iter().map(|(name, _)| name.quoted()).join(", ")
1574                    ),
1575                }
1576            }
1577            AdapterError::InvalidAlter(t, e) => {
1578                write!(f, "invalid ALTER {t}: {e}")
1579            }
1580            AdapterError::ConnectionValidation(e) => e.fmt(f),
1581            AdapterError::MaterializedViewWouldNeverRefresh(_, _) => {
1582                write!(
1583                    f,
1584                    "all the specified refreshes of the materialized view would be too far in the past, and thus they \
1585                    would never happen"
1586                )
1587            }
1588            AdapterError::InputNotReadableAtRefreshAtTime(_, _) => {
1589                write!(
1590                    f,
1591                    "REFRESH AT requested for a time where not all the inputs are readable"
1592                )
1593            }
1594            AdapterError::RtrTimeout(_) => {
1595                write!(
1596                    f,
1597                    "timed out before ingesting the source's visible frontier when real-time-recency query issued"
1598                )
1599            }
1600            AdapterError::RtrDropFailure(_) => write!(
1601                f,
1602                "real-time source dropped before ingesting the upstream system's visible frontier"
1603            ),
1604            AdapterError::UnreadableSinkCollection => {
1605                write!(f, "collection is not readable at any time")
1606            }
1607            AdapterError::UserSessionsDisallowed => write!(f, "login blocked"),
1608            AdapterError::NetworkPolicyDenied(_) => write!(f, "session denied"),
1609            AdapterError::ReadOnly => write!(f, "cannot write in read-only mode"),
1610            AdapterError::AlterClusterTimeout => {
1611                write!(f, "canceling statement, provided timeout lapsed")
1612            }
1613            AdapterError::AlterClusterSuperseded => {
1614                write!(
1615                    f,
1616                    "cluster reconfiguration was superseded by another ALTER CLUSTER"
1617                )
1618            }
1619            AdapterError::AlterClusterResourceExhausted => {
1620                write!(
1621                    f,
1622                    "cluster reconfiguration could not provision its target within the available resource limits"
1623                )
1624            }
1625            AdapterError::AuthenticationError(e) => {
1626                write!(f, "authentication error {e}")
1627            }
1628            AdapterError::UnavailableFeature { feature, docs } => {
1629                write!(f, "{} is not supported in this environment.", feature)?;
1630                if let Some(docs) = docs {
1631                    write!(
1632                        f,
1633                        " For more information consult the documentation at {docs}"
1634                    )?;
1635                }
1636                Ok(())
1637            }
1638            AdapterError::AlterClusterUnmanagedWhileReconfiguring => {
1639                write!(
1640                    f,
1641                    "cannot convert cluster to unmanaged while a reconfiguration is in progress"
1642                )
1643            }
1644            AdapterError::AlterClusterUnmanagedWhileBursting => {
1645                write!(
1646                    f,
1647                    "cannot convert cluster to unmanaged while a hydration burst is in progress"
1648                )
1649            }
1650            AdapterError::AlterClusterScheduleWhileReconfiguring => {
1651                write!(
1652                    f,
1653                    "cannot change the cluster schedule while a reconfiguration is in progress"
1654                )
1655            }
1656            AdapterError::AlterClusterWaitOnScheduledCluster => {
1657                write!(
1658                    f,
1659                    "WAIT is not supported when the cluster SCHEDULE is not MANUAL"
1660                )
1661            }
1662            AdapterError::ReplacementSchemaMismatch(_) => {
1663                write!(f, "replacement schema differs from target schema")
1664            }
1665            AdapterError::ImpossibleTimestampConstraints { .. } => {
1666                write!(f, "could not find a valid timestamp for the query")
1667            }
1668            AdapterError::OidcGroupSyncFailed(msg) => {
1669                write!(f, "OIDC group-to-role sync failed: {msg}")
1670            }
1671            AdapterError::BoundedStalenessExceeded { bound, .. } => {
1672                write!(
1673                    f,
1674                    "cannot serve query under bounded staleness {}",
1675                    humantime::format_duration(*bound),
1676                )
1677            }
1678            AdapterError::BoundedStalenessReadOnly => {
1679                f.write_str("writes are not permitted under bounded staleness isolation")
1680            }
1681            AdapterError::BoundedStalenessRealTimeRecencyConflict => {
1682                f.write_str("real_time_recency cannot be combined with bounded staleness isolation")
1683            }
1684            AdapterError::BoundedStalenessTimelineUnsupported => {
1685                f.write_str("bounded staleness isolation requires the EpochMilliseconds timeline")
1686            }
1687        }
1688    }
1689}
1690
1691impl From<anyhow::Error> for AdapterError {
1692    fn from(e: anyhow::Error) -> AdapterError {
1693        match e.downcast::<PlanError>() {
1694            Ok(plan_error) => AdapterError::PlanError(plan_error),
1695            Err(e) => AdapterError::Unstructured(e),
1696        }
1697    }
1698}
1699
1700impl From<TryFromIntError> for AdapterError {
1701    fn from(e: TryFromIntError) -> AdapterError {
1702        AdapterError::Unstructured(e.into())
1703    }
1704}
1705
1706impl From<TryFromDecimalError> for AdapterError {
1707    fn from(e: TryFromDecimalError) -> AdapterError {
1708        AdapterError::Unstructured(e.into())
1709    }
1710}
1711
1712impl From<mz_catalog::memory::error::Error> for AdapterError {
1713    fn from(e: mz_catalog::memory::error::Error) -> AdapterError {
1714        AdapterError::Catalog(e)
1715    }
1716}
1717
1718impl From<mz_catalog::durable::CatalogError> for AdapterError {
1719    fn from(e: mz_catalog::durable::CatalogError) -> Self {
1720        mz_catalog::memory::error::Error::from(e).into()
1721    }
1722}
1723
1724impl From<mz_catalog::durable::DurableCatalogError> for AdapterError {
1725    fn from(e: mz_catalog::durable::DurableCatalogError) -> Self {
1726        mz_catalog::durable::CatalogError::from(e).into()
1727    }
1728}
1729
1730impl From<EvalError> for AdapterError {
1731    fn from(e: EvalError) -> AdapterError {
1732        AdapterError::Eval(e)
1733    }
1734}
1735
1736impl From<mz_compute_client::protocol::response::PeekError> for AdapterError {
1737    fn from(error: mz_compute_client::protocol::response::PeekError) -> Self {
1738        use mz_compute_client::protocol::response::PeekError;
1739
1740        match error {
1741            PeekError::Dataflow(error) => AdapterError::Dataflow(error),
1742            PeekError::Unstructured(error) => AdapterError::Unstructured(anyhow::Error::msg(error)),
1743            PeekError::RowIterationLimitExceeded { limit } => {
1744                AdapterError::PeekRowIterationLimitExceeded { limit }
1745            }
1746            error @ PeekError::ResultExceedsMaxSize { .. } => {
1747                AdapterError::ResultSize(error.to_string())
1748            }
1749        }
1750    }
1751}
1752
1753impl From<ExplainError> for AdapterError {
1754    fn from(e: ExplainError) -> AdapterError {
1755        match e {
1756            ExplainError::RecursionLimitError(e) => AdapterError::RecursionLimit(e),
1757            e => AdapterError::Explain(e),
1758        }
1759    }
1760}
1761
1762impl From<mz_sql::catalog::CatalogError> for AdapterError {
1763    fn from(e: mz_sql::catalog::CatalogError) -> AdapterError {
1764        AdapterError::Catalog(mz_catalog::memory::error::Error::from(e))
1765    }
1766}
1767
1768impl From<PlanError> for AdapterError {
1769    fn from(e: PlanError) -> AdapterError {
1770        match e {
1771            PlanError::UnknownCursor(name) => AdapterError::UnknownCursor(name),
1772            _ => AdapterError::PlanError(e),
1773        }
1774    }
1775}
1776
1777impl From<OptimizerError> for AdapterError {
1778    fn from(e: OptimizerError) -> AdapterError {
1779        use OptimizerError::*;
1780        match e {
1781            PlanError(e) => Self::PlanError(e),
1782            RecursionLimitError(e) => Self::RecursionLimit(e),
1783            EvalError(e) => Self::Eval(e),
1784            InternalUnsafeMfpPlan(e) => Self::Internal(e),
1785            Internal(e) => Self::Internal(e),
1786            RestrictedFunction(func) => {
1787                Self::Unauthorized(mz_sql::rbac::UnauthorizedError::RestrictedSystemObject {
1788                    object_name: format!("function {func}"),
1789                })
1790            }
1791            e => Self::Optimizer(e),
1792        }
1793    }
1794}
1795
1796impl From<NotNullViolation> for AdapterError {
1797    fn from(e: NotNullViolation) -> AdapterError {
1798        AdapterError::ConstraintViolation(e)
1799    }
1800}
1801
1802impl From<RecursionLimitError> for AdapterError {
1803    fn from(e: RecursionLimitError) -> AdapterError {
1804        AdapterError::RecursionLimit(e)
1805    }
1806}
1807
1808impl From<oneshot::error::RecvError> for AdapterError {
1809    fn from(e: oneshot::error::RecvError) -> AdapterError {
1810        AdapterError::Unstructured(e.into())
1811    }
1812}
1813
1814impl From<StorageError> for AdapterError {
1815    fn from(e: StorageError) -> Self {
1816        AdapterError::Storage(e)
1817    }
1818}
1819
1820impl From<compute_error::InstanceExists> for AdapterError {
1821    fn from(e: compute_error::InstanceExists) -> Self {
1822        AdapterError::Compute(e.into())
1823    }
1824}
1825
1826impl From<TimestampError> for AdapterError {
1827    fn from(e: TimestampError) -> Self {
1828        let e: EvalError = e.into();
1829        e.into()
1830    }
1831}
1832
1833impl From<mz_sql_parser::parser::ParserStatementError> for AdapterError {
1834    fn from(e: mz_sql_parser::parser::ParserStatementError) -> Self {
1835        AdapterError::ParseError(e)
1836    }
1837}
1838
1839impl From<VarError> for AdapterError {
1840    fn from(e: VarError) -> Self {
1841        let e: mz_catalog::memory::error::Error = e.into();
1842        e.into()
1843    }
1844}
1845
1846impl From<rbac::UnauthorizedError> for AdapterError {
1847    fn from(e: rbac::UnauthorizedError) -> Self {
1848        AdapterError::Unauthorized(e)
1849    }
1850}
1851
1852impl From<mz_sql_parser::ast::IdentError> for AdapterError {
1853    fn from(value: mz_sql_parser::ast::IdentError) -> Self {
1854        AdapterError::PlanError(PlanError::InvalidIdent(value))
1855    }
1856}
1857
1858impl From<mz_pgwire_common::ConnectionError> for AdapterError {
1859    fn from(value: mz_pgwire_common::ConnectionError) -> Self {
1860        match value {
1861            mz_pgwire_common::ConnectionError::TooManyConnections { current, limit } => {
1862                AdapterError::ResourceExhaustion {
1863                    resource_type: "connection".into(),
1864                    limit_name: "max_connections".into(),
1865                    desired: (current + 1).to_string(),
1866                    limit: limit.to_string(),
1867                    current: current.to_string(),
1868                }
1869            }
1870        }
1871    }
1872}
1873
1874impl From<NetworkPolicyError> for AdapterError {
1875    fn from(value: NetworkPolicyError) -> Self {
1876        AdapterError::NetworkPolicyDenied(value)
1877    }
1878}
1879
1880impl From<ConnectionValidationError> for AdapterError {
1881    fn from(e: ConnectionValidationError) -> AdapterError {
1882        AdapterError::ConnectionValidation(e)
1883    }
1884}
1885
1886impl Error for AdapterError {}
1887
1888#[cfg(test)]
1889mod tests {
1890    use super::*;
1891
1892    #[mz_ore::test]
1893    fn alter_cluster_resource_exhausted_is_specific() {
1894        let error = AdapterError::AlterClusterResourceExhausted;
1895        assert_eq!(error.code(), SqlState::INSUFFICIENT_RESOURCES);
1896        assert_eq!(
1897            error.hint().as_deref(),
1898            Some(
1899                "Reduce the target resource use, or inspect \
1900                 mz_internal.mz_cluster_reconfigurations for the retained outcome."
1901            )
1902        );
1903        assert_eq!(
1904            error.to_string(),
1905            "cluster reconfiguration could not provision its target within the available resource limits"
1906        );
1907    }
1908
1909    #[mz_ore::test]
1910    fn peek_row_iteration_limit_error_is_user_facing() {
1911        let response = AdapterError::PeekRowIterationLimitExceeded { limit: 1000 }
1912            .into_response(Severity::Error);
1913
1914        assert_eq!(response.code, SqlState::PROGRAM_LIMIT_EXCEEDED);
1915        assert_eq!(
1916            response.message,
1917            "query exceeded the configured row iteration limit of 1000 rows"
1918        );
1919        assert_eq!(
1920            response.detail.as_deref(),
1921            Some(
1922                "The query attempted to examine more than 1000 rows on a single compute worker. \
1923                 This limit prevents long-running SELECT queries from delaying other work on the \
1924                 cluster."
1925            )
1926        );
1927        assert!(
1928            response
1929                .hint
1930                .as_deref()
1931                .is_some_and(|hint| hint.contains("compute_peek_row_iteration_limit"))
1932        );
1933    }
1934
1935    #[mz_ore::test]
1936    fn structured_dataflow_error_preserves_message_and_code() {
1937        use mz_compute_client::protocol::response::PeekError;
1938
1939        let dataflow_error = DataflowError::from(EvalError::DivisionByZero);
1940        let expected_message = dataflow_error.to_string();
1941        let response =
1942            AdapterError::from(PeekError::from(dataflow_error)).into_response(Severity::Error);
1943
1944        assert_eq!(response.code, SqlState::DIVISION_BY_ZERO);
1945        assert_eq!(response.message, expected_message);
1946    }
1947}
1948
1949/// Renders `(object type, name)` pairs as `materialized view "mv", index "i"`.
1950fn fmt_dependents<'a>(dependents: impl Iterator<Item = &'a (String, String)>) -> String {
1951    dependents
1952        .map(|(dependent_type, dependent_name)| {
1953            format!("{} {}", dependent_type, dependent_name.quoted())
1954        })
1955        .join(", ")
1956}