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