Skip to main content

mz_adapter/
notice.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::fmt;
11
12use chrono::{DateTime, Utc};
13use itertools::Itertools;
14use mz_controller::clusters::ClusterStatus;
15use mz_orchestrator::{OfflineReason, ServiceStatus};
16use mz_ore::str::{StrExt, separated};
17use mz_pgwire_common::{ErrorResponse, Severity};
18use mz_repr::adt::mz_acl_item::AclMode;
19use mz_repr::strconv;
20use mz_sql::ast::NoticeSeverity;
21use mz_sql::catalog::ErrorMessageObjectDescription;
22use mz_sql::plan::PlanNotice;
23use mz_sql::session::vars::IsolationLevel;
24use tokio_postgres::error::SqlState;
25
26use crate::TimestampExplanation;
27
28/// Notices that can occur in the adapter layer.
29///
30/// These are diagnostic warnings or informational messages that are not
31/// severe enough to warrant failing a query entirely.
32#[derive(Clone, Debug)]
33pub enum AdapterNotice {
34    DatabaseAlreadyExists {
35        name: String,
36    },
37    SchemaAlreadyExists {
38        name: String,
39    },
40    TableAlreadyExists {
41        name: String,
42    },
43    ObjectAlreadyExists {
44        name: String,
45        ty: &'static str,
46    },
47    DatabaseDoesNotExist {
48        name: String,
49    },
50    ClusterDoesNotExist {
51        name: String,
52    },
53    DefaultClusterDoesNotExist {
54        name: String,
55        kind: &'static str,
56        suggested_action: String,
57    },
58    NoResolvableSearchPathSchema {
59        search_path: Vec<String>,
60    },
61    ExistingTransactionInProgress,
62    ExplicitTransactionControlInImplicitTransaction,
63    UserRequested {
64        severity: NoticeSeverity,
65    },
66    ClusterReplicaStatusChanged {
67        cluster: String,
68        replica: String,
69        status: ClusterStatus,
70        time: DateTime<Utc>,
71    },
72    CascadeDroppedObject {
73        objects: Vec<String>,
74    },
75    DroppedActiveDatabase {
76        name: String,
77    },
78    DroppedActiveCluster {
79        name: String,
80    },
81    QueryTimestamp {
82        explanation: TimestampExplanation,
83    },
84    EqualSubscribeBounds {
85        bound: mz_repr::Timestamp,
86    },
87    QueryTrace {
88        trace_id: opentelemetry::trace::TraceId,
89    },
90    UnimplementedIsolationLevel {
91        isolation_level: String,
92    },
93    StrongSessionSerializable,
94    BadStartupSetting {
95        name: String,
96        reason: String,
97    },
98    RbacUserDisabled,
99    RoleMembershipAlreadyExists {
100        role_name: String,
101        member_name: String,
102    },
103    RoleMembershipDoesNotExists {
104        role_name: String,
105        member_name: String,
106    },
107    AutoRunOnCatalogServerCluster,
108    AlterIndexOwner {
109        name: String,
110    },
111    CannotRevoke {
112        object_description: ErrorMessageObjectDescription,
113    },
114    NonApplicablePrivilegeTypes {
115        non_applicable_privileges: AclMode,
116        object_description: ErrorMessageObjectDescription,
117    },
118    PlanNotice(PlanNotice),
119    UnknownSessionDatabase(String),
120    OptimizerNotice {
121        notice: String,
122        hint: String,
123    },
124    WebhookSourceCreated {
125        url: url::Url,
126    },
127    DroppedInUseIndex(DroppedInUseIndex),
128    PerReplicaLogRead {
129        log_names: Vec<String>,
130    },
131    VarDefaultUpdated {
132        role: Option<String>,
133        var_name: Option<String>,
134    },
135    /// An `ALTER SYSTEM` statement named a system parameter that
136    /// `environmentd` only reads at startup, so the running process keeps the
137    /// value it sampled at boot.
138    StartupOnlyVarUpdated {
139        var_name: String,
140    },
141    Welcome(String),
142    PlanInsights(String),
143    IntrospectionClusterUsage,
144    AutoRouteIntrospectionQueriesUsage,
145    /// A cluster contains sources that run on only one replica, yet the
146    /// cluster has (or is about to have) more than one replica.
147    SingleReplicaSourcesOnMultiReplicaCluster {
148        cluster: String,
149        sources: Vec<String>,
150    },
151    /// An OIDC group maps to a reserved role name (mz_/pg_ prefix).
152    OidcGroupSyncReservedRole {
153        group: String,
154    },
155    /// OIDC group sync encountered an error (fail-open mode).
156    OidcGroupSyncError {
157        message: String,
158    },
159}
160
161impl AdapterNotice {
162    pub fn into_response(self) -> ErrorResponse {
163        ErrorResponse {
164            severity: self.severity(),
165            code: self.code(),
166            message: self.to_string(),
167            detail: self.detail(),
168            hint: self.hint(),
169            position: None,
170        }
171    }
172
173    /// Returns the severity for a notice.
174    pub fn severity(&self) -> Severity {
175        match self {
176            AdapterNotice::DatabaseAlreadyExists { .. } => Severity::Notice,
177            AdapterNotice::SchemaAlreadyExists { .. } => Severity::Notice,
178            AdapterNotice::TableAlreadyExists { .. } => Severity::Notice,
179            AdapterNotice::ObjectAlreadyExists { .. } => Severity::Notice,
180            AdapterNotice::DatabaseDoesNotExist { .. } => Severity::Notice,
181            AdapterNotice::ClusterDoesNotExist { .. } => Severity::Notice,
182            AdapterNotice::DefaultClusterDoesNotExist { .. } => Severity::Notice,
183            AdapterNotice::NoResolvableSearchPathSchema { .. } => Severity::Notice,
184            AdapterNotice::ExistingTransactionInProgress => Severity::Warning,
185            AdapterNotice::ExplicitTransactionControlInImplicitTransaction => Severity::Warning,
186            AdapterNotice::UserRequested { severity } => match severity {
187                NoticeSeverity::Debug => Severity::Debug,
188                NoticeSeverity::Info => Severity::Info,
189                NoticeSeverity::Log => Severity::Log,
190                NoticeSeverity::Notice => Severity::Notice,
191                NoticeSeverity::Warning => Severity::Warning,
192            },
193            AdapterNotice::ClusterReplicaStatusChanged { .. } => Severity::Notice,
194            AdapterNotice::CascadeDroppedObject { .. } => Severity::Notice,
195            AdapterNotice::DroppedActiveDatabase { .. } => Severity::Notice,
196            AdapterNotice::DroppedActiveCluster { .. } => Severity::Notice,
197            AdapterNotice::QueryTimestamp { .. } => Severity::Notice,
198            AdapterNotice::EqualSubscribeBounds { .. } => Severity::Notice,
199            AdapterNotice::QueryTrace { .. } => Severity::Notice,
200            AdapterNotice::UnimplementedIsolationLevel { .. } => Severity::Notice,
201            AdapterNotice::StrongSessionSerializable => Severity::Notice,
202            AdapterNotice::BadStartupSetting { .. } => Severity::Notice,
203            AdapterNotice::RbacUserDisabled => Severity::Notice,
204            AdapterNotice::RoleMembershipAlreadyExists { .. } => Severity::Notice,
205            AdapterNotice::RoleMembershipDoesNotExists { .. } => Severity::Warning,
206            AdapterNotice::AutoRunOnCatalogServerCluster => Severity::Debug,
207            AdapterNotice::AlterIndexOwner { .. } => Severity::Warning,
208            AdapterNotice::CannotRevoke { .. } => Severity::Warning,
209            AdapterNotice::NonApplicablePrivilegeTypes { .. } => Severity::Notice,
210            AdapterNotice::PlanNotice(notice) => match notice {
211                PlanNotice::ObjectDoesNotExist { .. } => Severity::Notice,
212                PlanNotice::ColumnAlreadyExists { .. } => Severity::Notice,
213                PlanNotice::UpsertSinkKeyNotEnforced { .. } => Severity::Warning,
214                PlanNotice::ReplicaDiskOptionDeprecated { .. } => Severity::Notice,
215            },
216            AdapterNotice::UnknownSessionDatabase(_) => Severity::Notice,
217            AdapterNotice::OptimizerNotice { .. } => Severity::Notice,
218            AdapterNotice::WebhookSourceCreated { .. } => Severity::Notice,
219            AdapterNotice::DroppedInUseIndex { .. } => Severity::Notice,
220            AdapterNotice::PerReplicaLogRead { .. } => Severity::Notice,
221            AdapterNotice::VarDefaultUpdated { .. } => Severity::Notice,
222            AdapterNotice::StartupOnlyVarUpdated { .. } => Severity::Warning,
223            AdapterNotice::Welcome(_) => Severity::Notice,
224            AdapterNotice::PlanInsights(_) => Severity::Notice,
225            AdapterNotice::IntrospectionClusterUsage => Severity::Warning,
226            AdapterNotice::AutoRouteIntrospectionQueriesUsage => Severity::Warning,
227            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { .. } => Severity::Warning,
228            AdapterNotice::OidcGroupSyncReservedRole { .. } => Severity::Warning,
229            AdapterNotice::OidcGroupSyncError { .. } => Severity::Warning,
230        }
231    }
232
233    /// Reports additional details about the notice, if any are available.
234    pub fn detail(&self) -> Option<String> {
235        match self {
236            AdapterNotice::PlanNotice(notice) => notice.detail(),
237            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { .. } => Some(
238                "Adding replicas to the cluster does not make these sources more fault tolerant \
239                 and does not increase their ingestion throughput."
240                    .into(),
241            ),
242            AdapterNotice::QueryTimestamp { explanation } => Some(format!("\n{explanation}")),
243            AdapterNotice::CascadeDroppedObject { objects } => Some(
244                objects
245                    .iter()
246                    .map(|obj_info| format!("drop cascades to {}", obj_info))
247                    .join("\n"),
248            ),
249            _ => None,
250        }
251    }
252
253    /// Reports a hint for the user about how the notice could be addressed.
254    pub fn hint(&self) -> Option<String> {
255        match self {
256            AdapterNotice::DatabaseDoesNotExist { name: _ } => Some("Create the database with CREATE DATABASE or pick an extant database with SET DATABASE = name. List available databases with SHOW DATABASES.".into()),
257            AdapterNotice::ClusterDoesNotExist { name: _ } => Some("Create the cluster with CREATE CLUSTER or pick an extant cluster with SET CLUSTER = name. List available clusters with SHOW CLUSTERS.".into()),
258            AdapterNotice::DefaultClusterDoesNotExist {
259                name: _,
260                kind: _,
261                suggested_action,
262            } => Some(suggested_action.clone()),
263            AdapterNotice::NoResolvableSearchPathSchema { search_path: _ } => Some("Create a schema with CREATE SCHEMA or pick an extant schema with SET SCHEMA = name. List available schemas with SHOW SCHEMAS.".into()),
264            AdapterNotice::DroppedActiveDatabase { name: _ } => Some("Choose a new active database by executing SET DATABASE = <name>.".into()),
265            AdapterNotice::DroppedActiveCluster { name: _ } => Some("Choose a new active cluster by executing SET CLUSTER = <name>.".into()),
266            AdapterNotice::ClusterReplicaStatusChanged { status, .. } => {
267                match status {
268                    ServiceStatus::Offline(None)
269                    | ServiceStatus::Offline(Some(OfflineReason::Initializing)) => Some("The cluster replica may be restarting or going offline.".into()),
270                    ServiceStatus::Offline(Some(OfflineReason::OomKilled)) => Some("The cluster replica may have run out of memory and been killed.".into()),
271                    ServiceStatus::Online => None,
272                }
273            },
274            AdapterNotice::RbacUserDisabled => Some("To enable RBAC globally run `ALTER SYSTEM SET enable_rbac_checks TO TRUE` as a superuser. TO enable RBAC for just this session run `SET enable_session_rbac_checks TO TRUE`.".into()),
275            AdapterNotice::AlterIndexOwner {name: _} => Some("Change the ownership of the index's relation, instead.".into()),
276            AdapterNotice::UnknownSessionDatabase(_) => Some(
277                "Create the database with CREATE DATABASE \
278                 or pick an extant database with SET DATABASE = name. \
279                 List available databases with SHOW DATABASES."
280                    .into(),
281            ),
282            AdapterNotice::OptimizerNotice { notice: _, hint } => Some(hint.clone()),
283            AdapterNotice::DroppedInUseIndex(..) => Some("To free up the resources used by the index, recreate all the above-mentioned objects.".into()),
284            AdapterNotice::IntrospectionClusterUsage => Some("Use the new name instead.".into()),
285            AdapterNotice::AutoRouteIntrospectionQueriesUsage => Some("Use the new name instead.".into()),
286            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { .. } => Some(
287                "To achieve fault tolerance for other objects in the cluster, consider moving \
288                 these sources to a separate cluster with a single replica."
289                    .into(),
290            ),
291            _ => None
292        }
293    }
294
295    /// Reports the error code.
296    pub fn code(&self) -> SqlState {
297        match self {
298            AdapterNotice::DatabaseAlreadyExists { .. } => SqlState::DUPLICATE_DATABASE,
299            AdapterNotice::SchemaAlreadyExists { .. } => SqlState::DUPLICATE_SCHEMA,
300            AdapterNotice::TableAlreadyExists { .. } => SqlState::DUPLICATE_TABLE,
301            AdapterNotice::ObjectAlreadyExists { .. } => SqlState::DUPLICATE_OBJECT,
302            AdapterNotice::DatabaseDoesNotExist { .. } => SqlState::from_code("MZ006"),
303            AdapterNotice::ClusterDoesNotExist { .. } => SqlState::from_code("MZ007"),
304            AdapterNotice::NoResolvableSearchPathSchema { .. } => SqlState::from_code("MZ008"),
305            AdapterNotice::ExistingTransactionInProgress => SqlState::ACTIVE_SQL_TRANSACTION,
306            AdapterNotice::ExplicitTransactionControlInImplicitTransaction => {
307                SqlState::NO_ACTIVE_SQL_TRANSACTION
308            }
309            AdapterNotice::UserRequested { severity } => match severity {
310                NoticeSeverity::Warning => SqlState::WARNING,
311                _ => SqlState::SUCCESSFUL_COMPLETION,
312            },
313            AdapterNotice::ClusterReplicaStatusChanged { .. } => SqlState::SUCCESSFUL_COMPLETION,
314            AdapterNotice::CascadeDroppedObject { .. } => SqlState::SUCCESSFUL_COMPLETION,
315            AdapterNotice::DroppedActiveDatabase { .. } => SqlState::from_code("MZ002"),
316            AdapterNotice::DroppedActiveCluster { .. } => SqlState::from_code("MZ003"),
317            AdapterNotice::QueryTimestamp { .. } => SqlState::SUCCESSFUL_COMPLETION,
318            AdapterNotice::EqualSubscribeBounds { .. } => SqlState::SUCCESSFUL_COMPLETION,
319            AdapterNotice::QueryTrace { .. } => SqlState::SUCCESSFUL_COMPLETION,
320            AdapterNotice::UnimplementedIsolationLevel { .. } => SqlState::SUCCESSFUL_COMPLETION,
321            AdapterNotice::StrongSessionSerializable => SqlState::SUCCESSFUL_COMPLETION,
322            AdapterNotice::BadStartupSetting { .. } => SqlState::SUCCESSFUL_COMPLETION,
323            AdapterNotice::RbacUserDisabled => SqlState::SUCCESSFUL_COMPLETION,
324            AdapterNotice::RoleMembershipAlreadyExists { .. } => SqlState::SUCCESSFUL_COMPLETION,
325            AdapterNotice::RoleMembershipDoesNotExists { .. } => SqlState::WARNING,
326            AdapterNotice::AutoRunOnCatalogServerCluster => SqlState::SUCCESSFUL_COMPLETION,
327            AdapterNotice::AlterIndexOwner { .. } => SqlState::WARNING,
328            AdapterNotice::CannotRevoke { .. } => SqlState::WARNING_PRIVILEGE_NOT_REVOKED,
329            AdapterNotice::NonApplicablePrivilegeTypes { .. } => SqlState::SUCCESSFUL_COMPLETION,
330            AdapterNotice::PlanNotice(plan) => match plan {
331                PlanNotice::ObjectDoesNotExist { .. } => SqlState::UNDEFINED_OBJECT,
332                PlanNotice::ColumnAlreadyExists { .. } => SqlState::DUPLICATE_COLUMN,
333                PlanNotice::UpsertSinkKeyNotEnforced { .. } => SqlState::WARNING,
334                PlanNotice::ReplicaDiskOptionDeprecated { .. } => {
335                    SqlState::WARNING_DEPRECATED_FEATURE
336                }
337            },
338            AdapterNotice::UnknownSessionDatabase(_) => SqlState::from_code("MZ004"),
339            AdapterNotice::DefaultClusterDoesNotExist { .. } => SqlState::from_code("MZ005"),
340            AdapterNotice::OptimizerNotice { .. } => SqlState::SUCCESSFUL_COMPLETION,
341            AdapterNotice::DroppedInUseIndex { .. } => SqlState::SUCCESSFUL_COMPLETION,
342            AdapterNotice::WebhookSourceCreated { .. } => SqlState::SUCCESSFUL_COMPLETION,
343            AdapterNotice::PerReplicaLogRead { .. } => SqlState::SUCCESSFUL_COMPLETION,
344            AdapterNotice::VarDefaultUpdated { .. } => SqlState::SUCCESSFUL_COMPLETION,
345            AdapterNotice::StartupOnlyVarUpdated { .. } => SqlState::WARNING,
346            AdapterNotice::Welcome(_) => SqlState::SUCCESSFUL_COMPLETION,
347            AdapterNotice::PlanInsights(_) => SqlState::from_code("MZ001"),
348            AdapterNotice::IntrospectionClusterUsage => SqlState::WARNING,
349            AdapterNotice::AutoRouteIntrospectionQueriesUsage => SqlState::WARNING,
350            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { .. } => SqlState::WARNING,
351            AdapterNotice::OidcGroupSyncReservedRole { .. } => SqlState::WARNING,
352            AdapterNotice::OidcGroupSyncError { .. } => SqlState::WARNING,
353        }
354    }
355}
356
357impl fmt::Display for AdapterNotice {
358    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
359        match self {
360            AdapterNotice::DatabaseAlreadyExists { name } => {
361                write!(f, "database {} already exists, skipping", name.quoted())
362            }
363            AdapterNotice::SchemaAlreadyExists { name } => {
364                write!(f, "schema {} already exists, skipping", name.quoted())
365            }
366            AdapterNotice::TableAlreadyExists { name } => {
367                write!(f, "table {} already exists, skipping", name.quoted())
368            }
369            AdapterNotice::ObjectAlreadyExists { name, ty } => {
370                write!(f, "{} {} already exists, skipping", ty, name.quoted())
371            }
372            AdapterNotice::DatabaseDoesNotExist { name } => {
373                write!(f, "database {} does not exist", name.quoted())
374            }
375            AdapterNotice::CascadeDroppedObject { objects } => {
376                write!(f, "drop cascades to {} other objects", objects.len())
377            }
378            AdapterNotice::ClusterDoesNotExist { name } => {
379                write!(f, "cluster {} does not exist", name.quoted())
380            }
381            AdapterNotice::DefaultClusterDoesNotExist { kind, name, .. } => {
382                write!(f, "{kind} default cluster {} does not exist", name.quoted())
383            }
384            AdapterNotice::NoResolvableSearchPathSchema { search_path } => {
385                write!(
386                    f,
387                    "no schema on the search path exists: {}",
388                    search_path.join(", ")
389                )
390            }
391            AdapterNotice::ExistingTransactionInProgress => {
392                write!(f, "there is already a transaction in progress")
393            }
394            AdapterNotice::ExplicitTransactionControlInImplicitTransaction => {
395                write!(f, "there is no transaction in progress")
396            }
397            AdapterNotice::UserRequested { severity } => {
398                write!(f, "raised a test {}", severity.to_string().to_lowercase())
399            }
400            AdapterNotice::ClusterReplicaStatusChanged {
401                cluster,
402                replica,
403                status,
404                time,
405            } => {
406                let mut time_buf = String::new();
407                strconv::format_timestamptz(&mut time_buf, time);
408                write!(
409                    f,
410                    "cluster replica {}.{} changed status to {} at {}",
411                    cluster,
412                    replica,
413                    status.as_kebab_case_str().quoted(),
414                    time_buf,
415                )?;
416                Ok(())
417            }
418            AdapterNotice::DroppedActiveDatabase { name } => {
419                write!(f, "active database {} has been dropped", name.quoted())
420            }
421            AdapterNotice::DroppedActiveCluster { name } => {
422                write!(f, "active cluster {} has been dropped", name.quoted())
423            }
424            AdapterNotice::QueryTimestamp { .. } => write!(f, "EXPLAIN TIMESTAMP for query"),
425            AdapterNotice::EqualSubscribeBounds { bound } => {
426                write!(
427                    f,
428                    "subscribe as of {bound} (inclusive) up to the same bound {bound} (exclusive) is guaranteed to be empty"
429                )
430            }
431            AdapterNotice::QueryTrace { trace_id } => {
432                write!(f, "trace id: {}", trace_id)
433            }
434            AdapterNotice::UnimplementedIsolationLevel { isolation_level } => {
435                write!(
436                    f,
437                    "transaction isolation level {isolation_level} is unimplemented, the session will be upgraded to {}",
438                    IsolationLevel::Serializable
439                )
440            }
441            AdapterNotice::StrongSessionSerializable => {
442                write!(
443                    f,
444                    "The Strong Session Serializable isolation level may exhibit consistency violations when reading from catalog objects",
445                )
446            }
447            AdapterNotice::BadStartupSetting { name, reason } => {
448                write!(f, "startup setting {name} not set: {reason}")
449            }
450            AdapterNotice::RbacUserDisabled => {
451                write!(
452                    f,
453                    "RBAC is disabled so no role attributes or object ownership will be considered \
454                    when executing statements"
455                )
456            }
457            AdapterNotice::RoleMembershipAlreadyExists {
458                role_name,
459                member_name,
460            } => write!(
461                f,
462                "role \"{member_name}\" is already a member of role \"{role_name}\""
463            ),
464            AdapterNotice::RoleMembershipDoesNotExists {
465                role_name,
466                member_name,
467            } => write!(
468                f,
469                "role \"{member_name}\" is not a member of role \"{role_name}\""
470            ),
471            AdapterNotice::AutoRunOnCatalogServerCluster => write!(
472                f,
473                "query was automatically run on the \"mz_catalog_server\" cluster"
474            ),
475            AdapterNotice::AlterIndexOwner { name } => {
476                write!(f, "cannot change owner of {}", name.quoted())
477            }
478            AdapterNotice::CannotRevoke { object_description } => {
479                write!(f, "no privileges could be revoked for {object_description}")
480            }
481            AdapterNotice::NonApplicablePrivilegeTypes {
482                non_applicable_privileges,
483                object_description,
484            } => {
485                write!(
486                    f,
487                    "non-applicable privilege types {} for {}",
488                    non_applicable_privileges.to_error_string(),
489                    object_description,
490                )
491            }
492            AdapterNotice::PlanNotice(plan) => plan.fmt(f),
493            AdapterNotice::UnknownSessionDatabase(name) => {
494                write!(f, "session database {} does not exist", name.quoted())
495            }
496            AdapterNotice::OptimizerNotice { notice, hint: _ } => notice.fmt(f),
497            AdapterNotice::WebhookSourceCreated { url } => {
498                write!(f, "URL to POST data is '{url}'")
499            }
500            AdapterNotice::DroppedInUseIndex(DroppedInUseIndex {
501                index_name,
502                dependant_objects,
503            }) => {
504                write!(
505                    f,
506                    "The dropped index {index_name} is being used by the following objects: {}. The index is now dropped from the catalog, but it will continue to be maintained and take up resources until all dependent objects are dropped, altered, or Materialize is restarted!",
507                    separated(", ", dependant_objects)
508                )
509            }
510            AdapterNotice::PerReplicaLogRead { log_names } => {
511                write!(
512                    f,
513                    "Queried introspection relations: {}. Unlike other objects in Materialize, results from querying these objects depend on the current values of the `cluster` and `cluster_replica` session variables.",
514                    log_names.join(", ")
515                )
516            }
517            AdapterNotice::VarDefaultUpdated { role, var_name } => {
518                let vars = match var_name {
519                    Some(name) => format!("variable {} was", name.quoted()),
520                    None => "variables were".to_string(),
521                };
522                let target = match role {
523                    Some(role_name) => role_name.quoted().to_string(),
524                    None => "the system".to_string(),
525                };
526                write!(
527                    f,
528                    "{vars} updated for {target}, this will have no effect on the current session"
529                )
530            }
531            AdapterNotice::StartupOnlyVarUpdated { var_name } => write!(
532                f,
533                "changes to {} only take effect when environmentd restarts",
534                var_name.quoted()
535            ),
536            AdapterNotice::Welcome(message) => message.fmt(f),
537            AdapterNotice::PlanInsights(message) => message.fmt(f),
538            AdapterNotice::IntrospectionClusterUsage => write!(
539                f,
540                "The mz_introspection cluster has been renamed to mz_catalog_server."
541            ),
542            AdapterNotice::AutoRouteIntrospectionQueriesUsage => write!(
543                f,
544                "The auto_route_introspection_queries variable has been renamed to auto_route_catalog_queries."
545            ),
546            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { cluster, sources } => {
547                // Keep the notice readable on clusters with many sources.
548                const MAX_LISTED_SOURCES: usize = 3;
549                write!(
550                    f,
551                    "cluster {} has more than one replica, but the following sources in it always \
552                     run on only the first replica: {}",
553                    cluster.quoted(),
554                    separated(
555                        ", ",
556                        sources.iter().take(MAX_LISTED_SOURCES).map(|n| n.quoted())
557                    ),
558                )?;
559                if sources.len() > MAX_LISTED_SOURCES {
560                    write!(f, ", and {} more", sources.len() - MAX_LISTED_SOURCES)?;
561                }
562                Ok(())
563            }
564            AdapterNotice::OidcGroupSyncReservedRole { group } => {
565                write!(
566                    f,
567                    "OIDC group \"{}\" maps to a reserved role name, skipping",
568                    group
569                )
570            }
571            AdapterNotice::OidcGroupSyncError { message } => {
572                write!(f, "OIDC group-to-role sync failed: {}", message)
573            }
574        }
575    }
576}
577
578#[derive(Clone, Debug)]
579pub struct DroppedInUseIndex {
580    pub index_name: String,
581    pub dependant_objects: Vec<String>,
582}
583
584impl From<PlanNotice> for AdapterNotice {
585    fn from(notice: PlanNotice) -> AdapterNotice {
586        AdapterNotice::PlanNotice(notice)
587    }
588}