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