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 has no matching Materialize role.
152    OidcGroupSyncUnmatchedGroup {
153        group: String,
154    },
155    /// An OIDC group maps to a reserved role name (mz_/pg_ prefix).
156    OidcGroupSyncReservedRole {
157        group: String,
158    },
159    /// OIDC group sync encountered an error (fail-open mode).
160    OidcGroupSyncError {
161        message: String,
162    },
163}
164
165impl AdapterNotice {
166    pub fn into_response(self) -> ErrorResponse {
167        ErrorResponse {
168            severity: self.severity(),
169            code: self.code(),
170            message: self.to_string(),
171            detail: self.detail(),
172            hint: self.hint(),
173            position: None,
174        }
175    }
176
177    /// Returns the severity for a notice.
178    pub fn severity(&self) -> Severity {
179        match self {
180            AdapterNotice::DatabaseAlreadyExists { .. } => Severity::Notice,
181            AdapterNotice::SchemaAlreadyExists { .. } => Severity::Notice,
182            AdapterNotice::TableAlreadyExists { .. } => Severity::Notice,
183            AdapterNotice::ObjectAlreadyExists { .. } => Severity::Notice,
184            AdapterNotice::DatabaseDoesNotExist { .. } => Severity::Notice,
185            AdapterNotice::ClusterDoesNotExist { .. } => Severity::Notice,
186            AdapterNotice::DefaultClusterDoesNotExist { .. } => Severity::Notice,
187            AdapterNotice::NoResolvableSearchPathSchema { .. } => Severity::Notice,
188            AdapterNotice::ExistingTransactionInProgress => Severity::Warning,
189            AdapterNotice::ExplicitTransactionControlInImplicitTransaction => Severity::Warning,
190            AdapterNotice::UserRequested { severity } => match severity {
191                NoticeSeverity::Debug => Severity::Debug,
192                NoticeSeverity::Info => Severity::Info,
193                NoticeSeverity::Log => Severity::Log,
194                NoticeSeverity::Notice => Severity::Notice,
195                NoticeSeverity::Warning => Severity::Warning,
196            },
197            AdapterNotice::ClusterReplicaStatusChanged { .. } => Severity::Notice,
198            AdapterNotice::CascadeDroppedObject { .. } => Severity::Notice,
199            AdapterNotice::DroppedActiveDatabase { .. } => Severity::Notice,
200            AdapterNotice::DroppedActiveCluster { .. } => Severity::Notice,
201            AdapterNotice::QueryTimestamp { .. } => Severity::Notice,
202            AdapterNotice::EqualSubscribeBounds { .. } => Severity::Notice,
203            AdapterNotice::QueryTrace { .. } => Severity::Notice,
204            AdapterNotice::UnimplementedIsolationLevel { .. } => Severity::Notice,
205            AdapterNotice::StrongSessionSerializable => Severity::Notice,
206            AdapterNotice::BadStartupSetting { .. } => Severity::Notice,
207            AdapterNotice::RbacUserDisabled => Severity::Notice,
208            AdapterNotice::RoleMembershipAlreadyExists { .. } => Severity::Notice,
209            AdapterNotice::RoleMembershipDoesNotExists { .. } => Severity::Warning,
210            AdapterNotice::AutoRunOnCatalogServerCluster => Severity::Debug,
211            AdapterNotice::AlterIndexOwner { .. } => Severity::Warning,
212            AdapterNotice::CannotRevoke { .. } => Severity::Warning,
213            AdapterNotice::NonApplicablePrivilegeTypes { .. } => Severity::Notice,
214            AdapterNotice::PlanNotice(notice) => match notice {
215                PlanNotice::ObjectDoesNotExist { .. } => Severity::Notice,
216                PlanNotice::ColumnAlreadyExists { .. } => Severity::Notice,
217                PlanNotice::UpsertSinkKeyNotEnforced { .. } => Severity::Warning,
218                PlanNotice::ReplicaDiskOptionDeprecated { .. } => Severity::Notice,
219            },
220            AdapterNotice::UnknownSessionDatabase(_) => Severity::Notice,
221            AdapterNotice::OptimizerNotice { .. } => Severity::Notice,
222            AdapterNotice::WebhookSourceCreated { .. } => Severity::Notice,
223            AdapterNotice::DroppedInUseIndex { .. } => Severity::Notice,
224            AdapterNotice::PerReplicaLogRead { .. } => Severity::Notice,
225            AdapterNotice::VarDefaultUpdated { .. } => Severity::Notice,
226            AdapterNotice::StartupOnlyVarUpdated { .. } => Severity::Warning,
227            AdapterNotice::Welcome(_) => Severity::Notice,
228            AdapterNotice::PlanInsights(_) => Severity::Notice,
229            AdapterNotice::IntrospectionClusterUsage => Severity::Warning,
230            AdapterNotice::AutoRouteIntrospectionQueriesUsage => Severity::Warning,
231            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { .. } => Severity::Warning,
232            AdapterNotice::OidcGroupSyncUnmatchedGroup { .. } => Severity::Notice,
233            AdapterNotice::OidcGroupSyncReservedRole { .. } => Severity::Warning,
234            AdapterNotice::OidcGroupSyncError { .. } => Severity::Warning,
235        }
236    }
237
238    /// Reports additional details about the notice, if any are available.
239    pub fn detail(&self) -> Option<String> {
240        match self {
241            AdapterNotice::PlanNotice(notice) => notice.detail(),
242            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { .. } => Some(
243                "Adding replicas to the cluster does not make these sources more fault tolerant \
244                 and does not increase their ingestion throughput."
245                    .into(),
246            ),
247            AdapterNotice::QueryTimestamp { explanation } => Some(format!("\n{explanation}")),
248            AdapterNotice::CascadeDroppedObject { objects } => Some(
249                objects
250                    .iter()
251                    .map(|obj_info| format!("drop cascades to {}", obj_info))
252                    .join("\n"),
253            ),
254            _ => None,
255        }
256    }
257
258    /// Reports a hint for the user about how the notice could be addressed.
259    pub fn hint(&self) -> Option<String> {
260        match self {
261            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()),
262            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()),
263            AdapterNotice::DefaultClusterDoesNotExist {
264                name: _,
265                kind: _,
266                suggested_action,
267            } => Some(suggested_action.clone()),
268            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()),
269            AdapterNotice::DroppedActiveDatabase { name: _ } => Some("Choose a new active database by executing SET DATABASE = <name>.".into()),
270            AdapterNotice::DroppedActiveCluster { name: _ } => Some("Choose a new active cluster by executing SET CLUSTER = <name>.".into()),
271            AdapterNotice::ClusterReplicaStatusChanged { status, .. } => {
272                match status {
273                    ServiceStatus::Offline(None)
274                    | ServiceStatus::Offline(Some(OfflineReason::Initializing)) => Some("The cluster replica may be restarting or going offline.".into()),
275                    ServiceStatus::Offline(Some(OfflineReason::OomKilled)) => Some("The cluster replica may have run out of memory and been killed.".into()),
276                    ServiceStatus::Online => None,
277                }
278            },
279            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()),
280            AdapterNotice::AlterIndexOwner {name: _} => Some("Change the ownership of the index's relation, instead.".into()),
281            AdapterNotice::UnknownSessionDatabase(_) => Some(
282                "Create the database with CREATE DATABASE \
283                 or pick an extant database with SET DATABASE = name. \
284                 List available databases with SHOW DATABASES."
285                    .into(),
286            ),
287            AdapterNotice::OptimizerNotice { notice: _, hint } => Some(hint.clone()),
288            AdapterNotice::DroppedInUseIndex(..) => Some("To free up the resources used by the index, recreate all the above-mentioned objects.".into()),
289            AdapterNotice::IntrospectionClusterUsage => Some("Use the new name instead.".into()),
290            AdapterNotice::AutoRouteIntrospectionQueriesUsage => Some("Use the new name instead.".into()),
291            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { .. } => Some(
292                "To achieve fault tolerance for other objects in the cluster, consider moving \
293                 these sources to a separate cluster with a single replica."
294                    .into(),
295            ),
296            _ => None
297        }
298    }
299
300    /// Reports the error code.
301    pub fn code(&self) -> SqlState {
302        match self {
303            AdapterNotice::DatabaseAlreadyExists { .. } => SqlState::DUPLICATE_DATABASE,
304            AdapterNotice::SchemaAlreadyExists { .. } => SqlState::DUPLICATE_SCHEMA,
305            AdapterNotice::TableAlreadyExists { .. } => SqlState::DUPLICATE_TABLE,
306            AdapterNotice::ObjectAlreadyExists { .. } => SqlState::DUPLICATE_OBJECT,
307            AdapterNotice::DatabaseDoesNotExist { .. } => SqlState::from_code("MZ006"),
308            AdapterNotice::ClusterDoesNotExist { .. } => SqlState::from_code("MZ007"),
309            AdapterNotice::NoResolvableSearchPathSchema { .. } => SqlState::from_code("MZ008"),
310            AdapterNotice::ExistingTransactionInProgress => SqlState::ACTIVE_SQL_TRANSACTION,
311            AdapterNotice::ExplicitTransactionControlInImplicitTransaction => {
312                SqlState::NO_ACTIVE_SQL_TRANSACTION
313            }
314            AdapterNotice::UserRequested { severity } => match severity {
315                NoticeSeverity::Warning => SqlState::WARNING,
316                _ => SqlState::SUCCESSFUL_COMPLETION,
317            },
318            AdapterNotice::ClusterReplicaStatusChanged { .. } => SqlState::SUCCESSFUL_COMPLETION,
319            AdapterNotice::CascadeDroppedObject { .. } => SqlState::SUCCESSFUL_COMPLETION,
320            AdapterNotice::DroppedActiveDatabase { .. } => SqlState::from_code("MZ002"),
321            AdapterNotice::DroppedActiveCluster { .. } => SqlState::from_code("MZ003"),
322            AdapterNotice::QueryTimestamp { .. } => SqlState::SUCCESSFUL_COMPLETION,
323            AdapterNotice::EqualSubscribeBounds { .. } => SqlState::SUCCESSFUL_COMPLETION,
324            AdapterNotice::QueryTrace { .. } => SqlState::SUCCESSFUL_COMPLETION,
325            AdapterNotice::UnimplementedIsolationLevel { .. } => SqlState::SUCCESSFUL_COMPLETION,
326            AdapterNotice::StrongSessionSerializable => SqlState::SUCCESSFUL_COMPLETION,
327            AdapterNotice::BadStartupSetting { .. } => SqlState::SUCCESSFUL_COMPLETION,
328            AdapterNotice::RbacUserDisabled => SqlState::SUCCESSFUL_COMPLETION,
329            AdapterNotice::RoleMembershipAlreadyExists { .. } => SqlState::SUCCESSFUL_COMPLETION,
330            AdapterNotice::RoleMembershipDoesNotExists { .. } => SqlState::WARNING,
331            AdapterNotice::AutoRunOnCatalogServerCluster => SqlState::SUCCESSFUL_COMPLETION,
332            AdapterNotice::AlterIndexOwner { .. } => SqlState::WARNING,
333            AdapterNotice::CannotRevoke { .. } => SqlState::WARNING_PRIVILEGE_NOT_REVOKED,
334            AdapterNotice::NonApplicablePrivilegeTypes { .. } => SqlState::SUCCESSFUL_COMPLETION,
335            AdapterNotice::PlanNotice(plan) => match plan {
336                PlanNotice::ObjectDoesNotExist { .. } => SqlState::UNDEFINED_OBJECT,
337                PlanNotice::ColumnAlreadyExists { .. } => SqlState::DUPLICATE_COLUMN,
338                PlanNotice::UpsertSinkKeyNotEnforced { .. } => SqlState::WARNING,
339                PlanNotice::ReplicaDiskOptionDeprecated { .. } => {
340                    SqlState::WARNING_DEPRECATED_FEATURE
341                }
342            },
343            AdapterNotice::UnknownSessionDatabase(_) => SqlState::from_code("MZ004"),
344            AdapterNotice::DefaultClusterDoesNotExist { .. } => SqlState::from_code("MZ005"),
345            AdapterNotice::OptimizerNotice { .. } => SqlState::SUCCESSFUL_COMPLETION,
346            AdapterNotice::DroppedInUseIndex { .. } => SqlState::SUCCESSFUL_COMPLETION,
347            AdapterNotice::WebhookSourceCreated { .. } => SqlState::SUCCESSFUL_COMPLETION,
348            AdapterNotice::PerReplicaLogRead { .. } => SqlState::SUCCESSFUL_COMPLETION,
349            AdapterNotice::VarDefaultUpdated { .. } => SqlState::SUCCESSFUL_COMPLETION,
350            AdapterNotice::StartupOnlyVarUpdated { .. } => SqlState::WARNING,
351            AdapterNotice::Welcome(_) => SqlState::SUCCESSFUL_COMPLETION,
352            AdapterNotice::PlanInsights(_) => SqlState::from_code("MZ001"),
353            AdapterNotice::IntrospectionClusterUsage => SqlState::WARNING,
354            AdapterNotice::AutoRouteIntrospectionQueriesUsage => SqlState::WARNING,
355            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { .. } => SqlState::WARNING,
356            AdapterNotice::OidcGroupSyncUnmatchedGroup { .. } => SqlState::SUCCESSFUL_COMPLETION,
357            AdapterNotice::OidcGroupSyncReservedRole { .. } => SqlState::WARNING,
358            AdapterNotice::OidcGroupSyncError { .. } => SqlState::WARNING,
359        }
360    }
361}
362
363impl fmt::Display for AdapterNotice {
364    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
365        match self {
366            AdapterNotice::DatabaseAlreadyExists { name } => {
367                write!(f, "database {} already exists, skipping", name.quoted())
368            }
369            AdapterNotice::SchemaAlreadyExists { name } => {
370                write!(f, "schema {} already exists, skipping", name.quoted())
371            }
372            AdapterNotice::TableAlreadyExists { name } => {
373                write!(f, "table {} already exists, skipping", name.quoted())
374            }
375            AdapterNotice::ObjectAlreadyExists { name, ty } => {
376                write!(f, "{} {} already exists, skipping", ty, name.quoted())
377            }
378            AdapterNotice::DatabaseDoesNotExist { name } => {
379                write!(f, "database {} does not exist", name.quoted())
380            }
381            AdapterNotice::CascadeDroppedObject { objects } => {
382                write!(f, "drop cascades to {} other objects", objects.len())
383            }
384            AdapterNotice::ClusterDoesNotExist { name } => {
385                write!(f, "cluster {} does not exist", name.quoted())
386            }
387            AdapterNotice::DefaultClusterDoesNotExist { kind, name, .. } => {
388                write!(f, "{kind} default cluster {} does not exist", name.quoted())
389            }
390            AdapterNotice::NoResolvableSearchPathSchema { search_path } => {
391                write!(
392                    f,
393                    "no schema on the search path exists: {}",
394                    search_path.join(", ")
395                )
396            }
397            AdapterNotice::ExistingTransactionInProgress => {
398                write!(f, "there is already a transaction in progress")
399            }
400            AdapterNotice::ExplicitTransactionControlInImplicitTransaction => {
401                write!(f, "there is no transaction in progress")
402            }
403            AdapterNotice::UserRequested { severity } => {
404                write!(f, "raised a test {}", severity.to_string().to_lowercase())
405            }
406            AdapterNotice::ClusterReplicaStatusChanged {
407                cluster,
408                replica,
409                status,
410                time,
411            } => {
412                let mut time_buf = String::new();
413                strconv::format_timestamptz(&mut time_buf, time);
414                write!(
415                    f,
416                    "cluster replica {}.{} changed status to {} at {}",
417                    cluster,
418                    replica,
419                    status.as_kebab_case_str().quoted(),
420                    time_buf,
421                )?;
422                Ok(())
423            }
424            AdapterNotice::DroppedActiveDatabase { name } => {
425                write!(f, "active database {} has been dropped", name.quoted())
426            }
427            AdapterNotice::DroppedActiveCluster { name } => {
428                write!(f, "active cluster {} has been dropped", name.quoted())
429            }
430            AdapterNotice::QueryTimestamp { .. } => write!(f, "EXPLAIN TIMESTAMP for query"),
431            AdapterNotice::EqualSubscribeBounds { bound } => {
432                write!(
433                    f,
434                    "subscribe as of {bound} (inclusive) up to the same bound {bound} (exclusive) is guaranteed to be empty"
435                )
436            }
437            AdapterNotice::QueryTrace { trace_id } => {
438                write!(f, "trace id: {}", trace_id)
439            }
440            AdapterNotice::UnimplementedIsolationLevel { isolation_level } => {
441                write!(
442                    f,
443                    "transaction isolation level {isolation_level} is unimplemented, the session will be upgraded to {}",
444                    IsolationLevel::Serializable
445                )
446            }
447            AdapterNotice::StrongSessionSerializable => {
448                write!(
449                    f,
450                    "The Strong Session Serializable isolation level may exhibit consistency violations when reading from catalog objects",
451                )
452            }
453            AdapterNotice::BadStartupSetting { name, reason } => {
454                write!(f, "startup setting {name} not set: {reason}")
455            }
456            AdapterNotice::RbacUserDisabled => {
457                write!(
458                    f,
459                    "RBAC is disabled so no role attributes or object ownership will be considered \
460                    when executing statements"
461                )
462            }
463            AdapterNotice::RoleMembershipAlreadyExists {
464                role_name,
465                member_name,
466            } => write!(
467                f,
468                "role \"{member_name}\" is already a member of role \"{role_name}\""
469            ),
470            AdapterNotice::RoleMembershipDoesNotExists {
471                role_name,
472                member_name,
473            } => write!(
474                f,
475                "role \"{member_name}\" is not a member of role \"{role_name}\""
476            ),
477            AdapterNotice::AutoRunOnCatalogServerCluster => write!(
478                f,
479                "query was automatically run on the \"mz_catalog_server\" cluster"
480            ),
481            AdapterNotice::AlterIndexOwner { name } => {
482                write!(f, "cannot change owner of {}", name.quoted())
483            }
484            AdapterNotice::CannotRevoke { object_description } => {
485                write!(f, "no privileges could be revoked for {object_description}")
486            }
487            AdapterNotice::NonApplicablePrivilegeTypes {
488                non_applicable_privileges,
489                object_description,
490            } => {
491                write!(
492                    f,
493                    "non-applicable privilege types {} for {}",
494                    non_applicable_privileges.to_error_string(),
495                    object_description,
496                )
497            }
498            AdapterNotice::PlanNotice(plan) => plan.fmt(f),
499            AdapterNotice::UnknownSessionDatabase(name) => {
500                write!(f, "session database {} does not exist", name.quoted())
501            }
502            AdapterNotice::OptimizerNotice { notice, hint: _ } => notice.fmt(f),
503            AdapterNotice::WebhookSourceCreated { url } => {
504                write!(f, "URL to POST data is '{url}'")
505            }
506            AdapterNotice::DroppedInUseIndex(DroppedInUseIndex {
507                index_name,
508                dependant_objects,
509            }) => {
510                write!(
511                    f,
512                    "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!",
513                    separated(", ", dependant_objects)
514                )
515            }
516            AdapterNotice::PerReplicaLogRead { log_names } => {
517                write!(
518                    f,
519                    "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.",
520                    log_names.join(", ")
521                )
522            }
523            AdapterNotice::VarDefaultUpdated { role, var_name } => {
524                let vars = match var_name {
525                    Some(name) => format!("variable {} was", name.quoted()),
526                    None => "variables were".to_string(),
527                };
528                let target = match role {
529                    Some(role_name) => role_name.quoted().to_string(),
530                    None => "the system".to_string(),
531                };
532                write!(
533                    f,
534                    "{vars} updated for {target}, this will have no effect on the current session"
535                )
536            }
537            AdapterNotice::StartupOnlyVarUpdated { var_name } => write!(
538                f,
539                "changes to {} only take effect when environmentd restarts",
540                var_name.quoted()
541            ),
542            AdapterNotice::Welcome(message) => message.fmt(f),
543            AdapterNotice::PlanInsights(message) => message.fmt(f),
544            AdapterNotice::IntrospectionClusterUsage => write!(
545                f,
546                "The mz_introspection cluster has been renamed to mz_catalog_server."
547            ),
548            AdapterNotice::AutoRouteIntrospectionQueriesUsage => write!(
549                f,
550                "The auto_route_introspection_queries variable has been renamed to auto_route_catalog_queries."
551            ),
552            AdapterNotice::SingleReplicaSourcesOnMultiReplicaCluster { cluster, sources } => {
553                // Keep the notice readable on clusters with many sources.
554                const MAX_LISTED_SOURCES: usize = 3;
555                write!(
556                    f,
557                    "cluster {} has more than one replica, but the following sources in it always \
558                     run on only the first replica: {}",
559                    cluster.quoted(),
560                    separated(
561                        ", ",
562                        sources.iter().take(MAX_LISTED_SOURCES).map(|n| n.quoted())
563                    ),
564                )?;
565                if sources.len() > MAX_LISTED_SOURCES {
566                    write!(f, ", and {} more", sources.len() - MAX_LISTED_SOURCES)?;
567                }
568                Ok(())
569            }
570            AdapterNotice::OidcGroupSyncUnmatchedGroup { group } => {
571                write!(
572                    f,
573                    "OIDC group \"{}\" has no matching Materialize role, skipping",
574                    group
575                )
576            }
577            AdapterNotice::OidcGroupSyncReservedRole { group } => {
578                write!(
579                    f,
580                    "OIDC group \"{}\" maps to a reserved role name, skipping",
581                    group
582                )
583            }
584            AdapterNotice::OidcGroupSyncError { message } => {
585                write!(f, "OIDC group-to-role sync failed: {}", message)
586            }
587        }
588    }
589}
590
591#[derive(Clone, Debug)]
592pub struct DroppedInUseIndex {
593    pub index_name: String,
594    pub dependant_objects: Vec<String>,
595}
596
597impl From<PlanNotice> for AdapterNotice {
598    fn from(notice: PlanNotice) -> AdapterNotice {
599        AdapterNotice::PlanNotice(notice)
600    }
601}