1use std::collections::{BTreeMap, BTreeSet};
16use std::fmt::Write;
17use std::iter;
18use std::num::NonZeroU32;
19use std::time::Duration;
20
21use chrono::DateTime;
22use itertools::Itertools;
23use mz_adapter_types::compaction::{CompactionWindow, DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION};
24use mz_arrow_util::builder::ArrowBuilder;
25use mz_auth::password::Password;
26use mz_controller_types::{ClusterId, DEFAULT_REPLICA_LOGGING_INTERVAL, ReplicaId};
27use mz_expr::{CollectionPlan, UnmaterializableFunc};
28use mz_interchange::avro::{AvroSchemaGenerator, DocTarget};
29use mz_ore::cast::{CastFrom, TryCastFrom};
30use mz_ore::collections::{CollectionExt, HashSet};
31use mz_ore::num::NonNeg;
32use mz_ore::str::StrExt;
33use mz_ore::{soft_assert_or_log, soft_panic_or_log};
34use mz_proto::RustType;
35use mz_repr::adt::interval::Interval;
36use mz_repr::adt::mz_acl_item::{MzAclItem, PrivilegeMap};
37use mz_repr::adt::timestamp::CheckedTimestamp;
38use mz_repr::network_policy_id::NetworkPolicyId;
39use mz_repr::optimize::OptimizerFeatureOverrides;
40use mz_repr::refresh_schedule::{RefreshEvery, RefreshSchedule};
41use mz_repr::role_id::RoleId;
42use mz_repr::{
43 CatalogItemId, ColumnName, RelationDesc, RelationVersion, RelationVersionSelector,
44 SqlColumnType, SqlRelationType, SqlScalarType, Timestamp, VersionedRelationDesc,
45 preserves_order, strconv,
46};
47use mz_sql_parser::ast::{
48 self, AlterClusterAction, AlterClusterStatement, AlterConnectionAction, AlterConnectionOption,
49 AlterConnectionOptionName, AlterConnectionStatement, AlterIndexAction, AlterIndexStatement,
50 AlterMaterializedViewApplyReplacementStatement, AlterNetworkPolicyStatement,
51 AlterObjectRenameStatement, AlterObjectSwapStatement, AlterRetainHistoryStatement,
52 AlterRoleOption, AlterRoleStatement, AlterSecretStatement, AlterSetClusterStatement,
53 AlterSinkAction, AlterSinkStatement, AlterSourceAction, AlterSourceAddSubsourceOption,
54 AlterSourceAddSubsourceOptionName, AlterSourceStatement, AlterSystemResetAllStatement,
55 AlterSystemResetStatement, AlterSystemSetStatement, AlterTableAddColumnStatement, AvroSchema,
56 AvroSchemaOption, AvroSchemaOptionName, ClusterAlterOption, ClusterAlterOptionName,
57 ClusterAlterOptionValue, ClusterAlterUntilReadyOption, ClusterAlterUntilReadyOptionName,
58 ClusterAutoScalingStrategyOptionValue, ClusterFeature, ClusterFeatureName, ClusterOption,
59 ClusterOptionName, ClusterScheduleOptionValue, ColumnDef, ColumnOption, CommentObjectType,
60 CommentStatement, ConnectionOption, ConnectionOptionName, CreateClusterReplicaStatement,
61 CreateClusterStatement, CreateConnectionOption, CreateConnectionOptionName,
62 CreateConnectionStatement, CreateConnectionType, CreateDatabaseStatement, CreateIndexStatement,
63 CreateMaterializedViewStatement, CreateMetricSinkOption, CreateMetricSinkOptionName,
64 CreateMetricSinkStatement, CreateNetworkPolicyStatement, CreateRoleStatement,
65 CreateSchemaStatement, CreateSecretStatement, CreateSinkConnection, CreateSinkOption,
66 CreateSinkOptionName, CreateSinkStatement, CreateSourceConnection, CreateSourceOption,
67 CreateSourceOptionName, CreateSourceStatement, CreateSubsourceOption,
68 CreateSubsourceOptionName, CreateSubsourceStatement, CreateTableFromSourceStatement,
69 CreateTableStatement, CreateTypeAs, CreateTypeListOption, CreateTypeListOptionName,
70 CreateTypeMapOption, CreateTypeMapOptionName, CreateTypeStatement, CreateViewStatement,
71 CreateWebhookSourceStatement, CsrConfigOption, CsrConfigOptionName, CsrConnection,
72 CsrConnectionAvro, CsrConnectionProtobuf, CsrSeedProtobuf, CsvColumns, DeferredItemName,
73 DocOnIdentifier, DocOnSchema, DropObjectsStatement, DropOwnedStatement, Expr, Format,
74 FormatSpecifier, GlueAvroOption, GlueAvroOptionName, IcebergSinkConfigOption, Ident,
75 IfExistsBehavior, IndexOption, IndexOptionName, KafkaSinkConfigOption, KeyConstraint,
76 LoadGeneratorOption, LoadGeneratorOptionName, MaterializedViewOption,
77 MaterializedViewOptionName, MySqlConfigOption, MySqlConfigOptionName, NetworkPolicyOption,
78 NetworkPolicyOptionName, NetworkPolicyRuleDefinition, NetworkPolicyRuleOption,
79 NetworkPolicyRuleOptionName, OnHydrationOptionValue, PgConfigOption, PgConfigOptionName,
80 ProtobufSchema, QualifiedReplica, RefreshAtOptionValue, RefreshEveryOptionValue,
81 RefreshOptionValue, ReplicaDefinition, ReplicaOption, ReplicaOptionName, RoleAttribute,
82 SetRoleVar, SourceErrorPolicy, SourceIncludeMetadata, SqlServerConfigOption,
83 SqlServerConfigOptionName, Statement, TableConstraint, TableFromSourceColumns,
84 TableFromSourceOption, TableFromSourceOptionName, TableOption, TableOptionName,
85 UnresolvedDatabaseName, UnresolvedItemName, UnresolvedObjectName, UnresolvedSchemaName, Value,
86 ViewDefinition, WithOptionValue,
87};
88use mz_sql_parser::ident;
89use mz_sql_parser::parser::StatementParseResult;
90use mz_storage_types::connections::inline::ReferencedConnection;
91use mz_storage_types::connections::{Connection, KafkaTopicOptions};
92use mz_storage_types::sinks::{
93 IcebergSinkConnection, KafkaIdStyle, KafkaSinkConnection, KafkaSinkFormat, KafkaSinkFormatType,
94 SinkEnvelope, StorageSinkConnection, iceberg_type_overrides,
95};
96use mz_storage_types::sources::encoding::{
97 AvroEncoding, ColumnSpec, CsvEncoding, DataEncoding, ProtobufEncoding, RegexEncoding,
98 SourceDataEncoding, included_column_desc,
99};
100use mz_storage_types::sources::envelope::{
101 KeyEnvelope, NoneEnvelope, SourceEnvelope, UnplannedSourceEnvelope, UpsertStyle,
102};
103use mz_storage_types::sources::kafka::{
104 KafkaMetadataKind, KafkaSourceConnection, KafkaSourceExportDetails, kafka_metadata_columns_desc,
105};
106use mz_storage_types::sources::load_generator::{
107 KeyValueLoadGenerator, LOAD_GENERATOR_KEY_VALUE_OFFSET_DEFAULT, LoadGenerator,
108 LoadGeneratorOutput, LoadGeneratorSourceConnection, LoadGeneratorSourceExportDetails,
109};
110use mz_storage_types::sources::mysql::{
111 MySqlSourceConnection, MySqlSourceDetails, ProtoMySqlSourceDetails,
112};
113use mz_storage_types::sources::postgres::{
114 PostgresSourceConnection, PostgresSourcePublicationDetails,
115 ProtoPostgresSourcePublicationDetails,
116};
117use mz_storage_types::sources::sql_server::{
118 ProtoSqlServerSourceExtras, SqlServerSourceExportDetails,
119};
120use mz_storage_types::sources::{
121 GenericSourceConnection, MySqlSourceExportDetails, PostgresSourceExportDetails,
122 ProtoSourceExportStatementDetails, SourceConnection, SourceDesc, SourceExportDataConfig,
123 SourceExportDetails, SourceExportStatementDetails, SqlServerSourceConnection,
124 SqlServerSourceExtras, Timeline,
125};
126use mz_storage_types::wire_format::WireFormat;
127use prost::Message;
128
129use crate::ast::display::AstDisplay;
130use crate::catalog::{
131 CatalogCluster, CatalogDatabase, CatalogError, CatalogItem, CatalogItemType,
132 CatalogRecordField, CatalogType, CatalogTypeDetails, ObjectType, SystemObjectType,
133};
134use crate::iceberg::IcebergSinkConfigOptionExtracted;
135use crate::kafka_util::{KafkaSinkConfigOptionExtracted, KafkaSourceConfigOptionExtracted};
136use crate::names::{
137 Aug, CommentObjectId, DatabaseId, DependencyIds, ObjectId, PartialItemName, QualifiedItemName,
138 ResolvedClusterName, ResolvedColumnReference, ResolvedDataType, ResolvedDatabaseSpecifier,
139 ResolvedItemName, ResolvedNetworkPolicyName, SchemaSpecifier, SystemObjectId,
140};
141use crate::normalize::{self, ident};
142use crate::plan::error::PlanError;
143use crate::plan::query::{
144 ExprContext, QueryLifetime, TypeResolutionBudget, plan_expr, scalar_type_from_sql,
145};
146use crate::plan::scope::Scope;
147use crate::plan::statement::ddl::connection::{INALTERABLE_OPTIONS, MUTUALLY_EXCLUSIVE_SETS};
148use crate::plan::statement::{StatementContext, StatementDesc, scl};
149use crate::plan::typeconv::CastContext;
150use crate::plan::with_options::{OptionalDuration, OptionalString, TryFromValue};
151use crate::plan::{
152 AlterClusterPlan, AlterClusterPlanStrategy, AlterClusterRenamePlan,
153 AlterClusterReplicaRenamePlan, AlterClusterSwapPlan, AlterConnectionPlan, AlterItemRenamePlan,
154 AlterMaterializedViewApplyReplacementPlan, AlterNetworkPolicyPlan, AlterNoopPlan,
155 AlterOptionParameter, AlterRetainHistoryPlan, AlterRolePlan, AlterSchemaRenamePlan,
156 AlterSchemaSwapPlan, AlterSecretPlan, AlterSetClusterPlan, AlterSinkPlan,
157 AlterSourceTimestampIntervalPlan, AlterSystemResetAllPlan, AlterSystemResetPlan,
158 AlterSystemSetPlan, AlterTablePlan, AutoScalingStrategy, ClusterSchedule, CommentPlan,
159 ComputeReplicaConfig, ComputeReplicaIntrospectionConfig, ConnectionDetails,
160 CreateClusterManagedPlan, CreateClusterPlan, CreateClusterReplicaPlan,
161 CreateClusterUnmanagedPlan, CreateClusterVariant, CreateConnectionPlan, CreateDatabasePlan,
162 CreateIndexPlan, CreateMaterializedViewPlan, CreateMetricSinkPlan, CreateNetworkPolicyPlan,
163 CreateRolePlan, CreateSchemaPlan, CreateSecretPlan, CreateSinkPlan, CreateSourcePlan,
164 CreateTablePlan, CreateTypePlan, CreateViewPlan, DataSourceDesc, DropObjectsPlan,
165 DropOwnedPlan, HirRelationExpr, Index, MaterializedView, MetricSink, NetworkPolicyRule,
166 NetworkPolicyRuleAction, NetworkPolicyRuleDirection, OnHydration, Plan, PlanClusterOption,
167 PlanNotice, PolicyAddress, QueryContext, ReplicaConfig, Secret, Sink, Source, Table,
168 TableDataSource, Type, VariableValue, View, WebhookBodyFormat, WebhookHeaderFilters,
169 WebhookHeaders, WebhookValidation, literal, plan_utils, query, transform_ast,
170};
171use crate::session::vars::{
172 self, ENABLE_AUTO_SCALING_STRATEGY, ENABLE_CLUSTER_SCHEDULE_REFRESH,
173 ENABLE_COLLECTION_PARTITION_BY, ENABLE_CREATE_TABLE_FROM_SOURCE, ENABLE_KAFKA_SINK_HEADERS,
174 ENABLE_METRIC_SINK, ENABLE_REFRESH_EVERY_MVS, ENABLE_REPLICA_TARGETED_MATERIALIZED_VIEWS,
175 VarInput,
176};
177use crate::{names, parse};
178
179mod connection;
180
181const MAX_NUM_COLUMNS: usize = 256;
186
187const MAX_KAFKA_TOPIC_METADATA_REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60);
188const MIN_KAFKA_TOPIC_METADATA_REFRESH_INTERVAL: Duration = Duration::from_secs(1);
189
190static MANAGED_REPLICA_PATTERN: std::sync::LazyLock<regex::Regex> =
191 std::sync::LazyLock::new(|| regex::Regex::new(r"^r(\d)+$").unwrap());
192
193fn check_partition_by(desc: &RelationDesc, mut partition_by: Vec<Ident>) -> Result<(), PlanError> {
197 if partition_by.len() > desc.len() {
198 tracing::error!(
199 "PARTITION BY contains more columns than the relation. (expected at most {}, got {})",
200 desc.len(),
201 partition_by.len()
202 );
203 partition_by.truncate(desc.len());
204 }
205
206 let desc_prefix = desc.iter().take(partition_by.len());
207 for (idx, ((desc_name, desc_type), partition_name)) in
208 desc_prefix.zip_eq(partition_by).enumerate()
209 {
210 let partition_name = normalize::column_name(partition_name);
211 if *desc_name != partition_name {
212 sql_bail!(
213 "PARTITION BY columns should be a prefix of the relation's columns (expected {desc_name} at index {idx}, got {partition_name})"
214 );
215 }
216 if !preserves_order(&desc_type.scalar_type) {
217 sql_bail!("PARTITION BY column {partition_name} has unsupported type");
218 }
219 }
220 Ok(())
221}
222
223pub fn describe_create_database(
224 _: &StatementContext,
225 _: CreateDatabaseStatement,
226) -> Result<StatementDesc, PlanError> {
227 Ok(StatementDesc::new(None))
228}
229
230pub fn plan_create_database(
231 _: &StatementContext,
232 CreateDatabaseStatement {
233 name,
234 if_not_exists,
235 }: CreateDatabaseStatement,
236) -> Result<Plan, PlanError> {
237 Ok(Plan::CreateDatabase(CreateDatabasePlan {
238 name: normalize::ident(name.0),
239 if_not_exists,
240 }))
241}
242
243pub fn describe_create_schema(
244 _: &StatementContext,
245 _: CreateSchemaStatement,
246) -> Result<StatementDesc, PlanError> {
247 Ok(StatementDesc::new(None))
248}
249
250pub fn plan_create_schema(
251 scx: &StatementContext,
252 CreateSchemaStatement {
253 mut name,
254 if_not_exists,
255 }: CreateSchemaStatement,
256) -> Result<Plan, PlanError> {
257 if name.0.len() > 2 {
258 sql_bail!("schema name {} has more than two components", name);
259 }
260 let schema_name = normalize::ident(
261 name.0
262 .pop()
263 .expect("names always have at least one component"),
264 );
265 let database_spec = match name.0.pop() {
266 None => match scx.catalog.active_database() {
267 Some(id) => ResolvedDatabaseSpecifier::Id(id.clone()),
268 None => sql_bail!("no database specified and no active database"),
269 },
270 Some(n) => match scx.resolve_database(&UnresolvedDatabaseName(n.clone())) {
271 Ok(database) => ResolvedDatabaseSpecifier::Id(database.id()),
272 Err(_) => sql_bail!("invalid database {}", n.as_str()),
273 },
274 };
275 Ok(Plan::CreateSchema(CreateSchemaPlan {
276 database_spec,
277 schema_name,
278 if_not_exists,
279 }))
280}
281
282pub fn describe_create_table(
283 _: &StatementContext,
284 _: CreateTableStatement<Aug>,
285) -> Result<StatementDesc, PlanError> {
286 Ok(StatementDesc::new(None))
287}
288
289pub fn plan_create_table(
290 scx: &StatementContext,
291 stmt: CreateTableStatement<Aug>,
292) -> Result<Plan, PlanError> {
293 let CreateTableStatement {
294 name,
295 columns,
296 constraints,
297 if_not_exists,
298 temporary,
299 with_options,
300 } = &stmt;
301
302 let names: Vec<_> = columns
303 .iter()
304 .filter(|c| {
305 let is_versioned = c
309 .options
310 .iter()
311 .any(|o| matches!(o.option, ColumnOption::Versioned { .. }));
312 !is_versioned
313 })
314 .map(|c| normalize::column_name(c.name.clone()))
315 .collect();
316
317 if let Some(dup) = names.iter().duplicates().next() {
318 sql_bail!("column {} specified more than once", dup.quoted());
319 }
320
321 let mut column_types = Vec::with_capacity(columns.len());
324 let mut defaults = Vec::with_capacity(columns.len());
325 let mut changes = BTreeMap::new();
326 let mut keys = Vec::new();
327
328 for (i, c) in columns.into_iter().enumerate() {
329 if c.collation
330 .as_ref()
331 .is_some_and(|collation| !query::is_supported_collation(collation))
332 {
333 bail_unsupported!("COLLATE");
334 }
335 let aug_data_type = &c.data_type;
336 let ty = query::scalar_type_from_sql(scx, aug_data_type)?;
337 let mut nullable = true;
338 let mut default = Expr::null();
339 let mut versioned = false;
340 for option in &c.options {
341 match &option.option {
342 ColumnOption::NotNull => nullable = false,
343 ColumnOption::Default(expr) => {
344 let mut expr = expr.clone();
347 transform_ast::transform(scx, &mut expr)?;
348 let _ = query::plan_default_expr(scx, &expr, &ty)?;
349 default = expr.clone();
350 }
351 ColumnOption::Unique { is_primary } => {
352 keys.push(vec![i]);
353 if *is_primary {
354 nullable = false;
355 }
356 }
357 ColumnOption::Versioned { action, version } => {
358 let version = normalize::relation_version(*version);
359 versioned = true;
360
361 let name = normalize::column_name(c.name.clone());
362 let typ = ty.clone().nullable(nullable);
363
364 changes.insert(version, (action.clone(), name, typ));
365 }
366 other => {
367 bail_unsupported!(format!("CREATE TABLE with column constraint: {}", other))
368 }
369 }
370 }
371 if !versioned {
374 column_types.push(ty.nullable(nullable));
375 }
376 defaults.push(default);
377 }
378
379 let mut seen_primary = false;
380 'c: for constraint in constraints {
381 match constraint {
382 TableConstraint::Unique {
383 name: _,
384 columns,
385 is_primary,
386 nulls_not_distinct,
387 } => {
388 if seen_primary && *is_primary {
389 sql_bail!(
390 "multiple primary keys for table {} are not allowed",
391 name.to_ast_string_stable()
392 );
393 }
394 seen_primary = *is_primary || seen_primary;
395
396 let mut key = vec![];
397 for column in columns {
398 let column = normalize::column_name(column.clone());
399 match names.iter().position(|name| *name == column) {
400 None => sql_bail!("unknown column in constraint: {}", column),
401 Some(i) => {
402 let nullable = &mut column_types[i].nullable;
403 if *is_primary {
404 if *nulls_not_distinct {
405 sql_bail!(
406 "[internal error] PRIMARY KEY does not support NULLS NOT DISTINCT"
407 );
408 }
409
410 *nullable = false;
411 } else if !(*nulls_not_distinct || !*nullable) {
412 break 'c;
415 }
416
417 key.push(i);
418 }
419 }
420 }
421
422 if *is_primary {
423 keys.insert(0, key);
424 } else {
425 keys.push(key);
426 }
427 }
428 TableConstraint::ForeignKey { .. } => {
429 scx.require_feature_flag(&vars::UNSAFE_ENABLE_TABLE_FOREIGN_KEY)?
432 }
433 TableConstraint::Check { .. } => {
434 scx.require_feature_flag(&vars::UNSAFE_ENABLE_TABLE_CHECK_CONSTRAINT)?
437 }
438 }
439 }
440
441 if !keys.is_empty() {
442 scx.require_feature_flag(&vars::UNSAFE_ENABLE_TABLE_KEYS)?
445 }
446
447 let typ = SqlRelationType::new(column_types).with_keys(keys);
448
449 let temporary = *temporary;
450 let name = if temporary {
451 scx.allocate_temporary_qualified_name(normalize::unresolved_item_name(name.to_owned())?)?
452 } else {
453 scx.allocate_qualified_name(normalize::unresolved_item_name(name.to_owned())?)?
454 };
455
456 let full_name = scx.catalog.resolve_full_name(&name);
458 let partial_name = PartialItemName::from(full_name.clone());
459 if let (false, Ok(item)) = (
462 if_not_exists,
463 scx.catalog.resolve_item_or_type(&partial_name),
464 ) {
465 return Err(PlanError::ItemAlreadyExists {
466 name: full_name.to_string(),
467 item_type: item.item_type(),
468 });
469 }
470
471 let desc = RelationDesc::new(typ, names);
472 let mut desc = VersionedRelationDesc::new(desc);
473 for (version, (_action, name, typ)) in changes.into_iter() {
474 let new_version = desc.add_column(name, typ);
475 if version != new_version {
476 return Err(PlanError::InvalidTable {
477 name: full_name.item,
478 });
479 }
480 }
481
482 let create_sql = normalize::create_statement(scx, Statement::CreateTable(stmt.clone()))?;
483
484 let original_desc = desc.at_version(RelationVersionSelector::Specific(RelationVersion::root()));
490 let options = plan_table_options(scx, &original_desc, with_options.clone())?;
491
492 let compaction_window = options.iter().find_map(|o| {
493 #[allow(irrefutable_let_patterns)]
494 if let crate::plan::TableOption::RetainHistory(lcw) = o {
495 Some(lcw.clone())
496 } else {
497 None
498 }
499 });
500
501 let table = Table {
502 create_sql,
503 desc,
504 temporary,
505 compaction_window,
506 data_source: TableDataSource::TableWrites { defaults },
507 };
508 Ok(Plan::CreateTable(CreateTablePlan {
509 name,
510 table,
511 if_not_exists: *if_not_exists,
512 }))
513}
514
515pub fn describe_create_table_from_source(
516 _: &StatementContext,
517 _: CreateTableFromSourceStatement<Aug>,
518) -> Result<StatementDesc, PlanError> {
519 Ok(StatementDesc::new(None))
520}
521
522pub fn describe_create_webhook_source(
523 _: &StatementContext,
524 _: CreateWebhookSourceStatement<Aug>,
525) -> Result<StatementDesc, PlanError> {
526 Ok(StatementDesc::new(None))
527}
528
529pub fn describe_create_source(
530 _: &StatementContext,
531 _: CreateSourceStatement<Aug>,
532) -> Result<StatementDesc, PlanError> {
533 Ok(StatementDesc::new(None))
534}
535
536pub fn describe_create_subsource(
537 _: &StatementContext,
538 _: CreateSubsourceStatement<Aug>,
539) -> Result<StatementDesc, PlanError> {
540 Ok(StatementDesc::new(None))
541}
542
543generate_extracted_config!(
544 CreateSourceOption,
545 (TimestampInterval, Duration),
546 (RetainHistory, OptionalDuration)
547);
548
549generate_extracted_config!(
550 PgConfigOption,
551 (Details, String),
552 (Publication, String),
553 (TextColumns, Vec::<UnresolvedItemName>, Default(vec![])),
554 (ExcludeColumns, Vec::<UnresolvedItemName>, Default(vec![]))
555);
556
557generate_extracted_config!(
558 MySqlConfigOption,
559 (Details, String),
560 (TextColumns, Vec::<UnresolvedItemName>, Default(vec![])),
561 (ExcludeColumns, Vec::<UnresolvedItemName>, Default(vec![]))
562);
563
564generate_extracted_config!(
565 SqlServerConfigOption,
566 (Details, String),
567 (TextColumns, Vec::<UnresolvedItemName>, Default(vec![])),
568 (ExcludeColumns, Vec::<UnresolvedItemName>, Default(vec![]))
569);
570
571pub fn plan_create_webhook_source(
572 scx: &StatementContext,
573 mut stmt: CreateWebhookSourceStatement<Aug>,
574) -> Result<Plan, PlanError> {
575 if stmt.is_table {
576 scx.require_feature_flag(&ENABLE_CREATE_TABLE_FROM_SOURCE)?;
577 }
578
579 let in_cluster = source_sink_cluster_config(scx, &mut stmt.in_cluster)?;
582 let create_sql =
583 normalize::create_statement(scx, Statement::CreateWebhookSource(stmt.clone()))?;
584
585 let CreateWebhookSourceStatement {
586 name,
587 if_not_exists,
588 body_format,
589 include_headers,
590 validate_using,
591 is_table,
592 in_cluster: _,
594 } = stmt;
595
596 let validate_using = validate_using
597 .map(|stmt| query::plan_webhook_validate_using(scx, stmt))
598 .transpose()?;
599 if let Some(WebhookValidation { expression, .. }) = &validate_using {
600 if !expression.contains_column() {
603 return Err(PlanError::WebhookValidationDoesNotUseColumns);
604 }
605 if expression.contains_unmaterializable_except(&[UnmaterializableFunc::CurrentTimestamp]) {
609 return Err(PlanError::WebhookValidationNonDeterministic);
610 }
611 }
612
613 let body_format = match body_format {
614 Format::Bytes => WebhookBodyFormat::Bytes,
615 Format::Json { array } => WebhookBodyFormat::Json { array },
616 Format::Text => WebhookBodyFormat::Text,
617 ty => {
619 return Err(PlanError::Unsupported {
620 feature: format!("{ty} is not a valid BODY FORMAT for a WEBHOOK source"),
621 discussion_no: None,
622 });
623 }
624 };
625
626 let mut column_ty = vec![
627 SqlColumnType {
629 scalar_type: SqlScalarType::from(body_format),
630 nullable: false,
631 },
632 ];
633 let mut column_names = vec!["body".to_string()];
634
635 let mut headers = WebhookHeaders::default();
636
637 if let Some(filters) = include_headers.column {
639 column_ty.push(SqlColumnType {
640 scalar_type: SqlScalarType::Map {
641 value_type: Box::new(SqlScalarType::String),
642 custom_id: None,
643 },
644 nullable: false,
645 });
646 column_names.push("headers".to_string());
647
648 let (allow, block): (BTreeSet<_>, BTreeSet<_>) =
649 filters.into_iter().partition_map(|filter| {
650 if filter.block {
651 itertools::Either::Right(filter.header_name)
652 } else {
653 itertools::Either::Left(filter.header_name)
654 }
655 });
656 headers.header_column = Some(WebhookHeaderFilters { allow, block });
657 }
658
659 for header in include_headers.mappings {
661 let scalar_type = header
662 .use_bytes
663 .then_some(SqlScalarType::Bytes)
664 .unwrap_or(SqlScalarType::String);
665 column_ty.push(SqlColumnType {
666 scalar_type,
667 nullable: true,
668 });
669 column_names.push(header.column_name.into_string());
670
671 let column_idx = column_ty.len() - 1;
672 assert_eq!(
674 column_idx,
675 column_names.len() - 1,
676 "header column names and types don't match"
677 );
678 headers
679 .mapped_headers
680 .insert(column_idx, (header.header_name, header.use_bytes));
681 }
682
683 let mut unique_check = HashSet::with_capacity(column_names.len());
685 for name in &column_names {
686 if !unique_check.insert(name) {
687 return Err(PlanError::AmbiguousColumn(name.clone().into()));
688 }
689 }
690 if column_names.len() > MAX_NUM_COLUMNS {
691 return Err(PlanError::TooManyColumns {
692 max_num_columns: MAX_NUM_COLUMNS,
693 req_num_columns: column_names.len(),
694 });
695 }
696
697 let typ = SqlRelationType::new(column_ty);
698 let desc = RelationDesc::new(typ, column_names);
699
700 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
702 let full_name = scx.catalog.resolve_full_name(&name);
703 let partial_name = PartialItemName::from(full_name.clone());
704 if let (false, Ok(item)) = (if_not_exists, scx.catalog.resolve_item(&partial_name)) {
705 return Err(PlanError::ItemAlreadyExists {
706 name: full_name.to_string(),
707 item_type: item.item_type(),
708 });
709 }
710
711 let timeline = Timeline::EpochMilliseconds;
714
715 let plan = if is_table {
716 let data_source = DataSourceDesc::Webhook {
717 validate_using,
718 body_format,
719 headers,
720 cluster_id: Some(in_cluster.id()),
721 };
722 let data_source = TableDataSource::DataSource {
723 desc: data_source,
724 timeline,
725 };
726 Plan::CreateTable(CreateTablePlan {
727 name,
728 if_not_exists,
729 table: Table {
730 create_sql,
731 desc: VersionedRelationDesc::new(desc),
732 temporary: false,
733 compaction_window: None,
734 data_source,
735 },
736 })
737 } else {
738 let data_source = DataSourceDesc::Webhook {
739 validate_using,
740 body_format,
741 headers,
742 cluster_id: None,
744 };
745 Plan::CreateSource(CreateSourcePlan {
746 name,
747 source: Source {
748 create_sql,
749 data_source,
750 desc,
751 compaction_window: None,
752 },
753 if_not_exists,
754 timeline,
755 in_cluster: Some(in_cluster.id()),
756 })
757 };
758
759 Ok(plan)
760}
761
762pub fn plan_create_source(
763 scx: &StatementContext,
764 mut stmt: CreateSourceStatement<Aug>,
765) -> Result<Plan, PlanError> {
766 let CreateSourceStatement {
767 name,
768 in_cluster: _,
769 col_names,
770 connection: source_connection,
771 envelope,
772 if_not_exists,
773 format,
774 key_constraint,
775 include_metadata,
776 with_options,
777 external_references: referenced_subsources,
778 progress_subsource,
779 } = &stmt;
780
781 mz_ore::soft_assert_or_log!(
782 referenced_subsources.is_none(),
783 "referenced subsources must be cleared in purification"
784 );
785
786 let force_source_table_syntax = scx.catalog.system_vars().enable_create_table_from_source()
787 && scx.catalog.system_vars().force_source_table_syntax();
788
789 if force_source_table_syntax {
792 if envelope.is_some() || format.is_some() || !include_metadata.is_empty() {
793 Err(PlanError::UseTablesForSources(
794 "CREATE SOURCE (ENVELOPE|FORMAT|INCLUDE)".to_string(),
795 ))?;
796 }
797 }
798
799 let envelope = envelope.clone().unwrap_or(ast::SourceEnvelope::None);
800
801 if !matches!(source_connection, CreateSourceConnection::Kafka { .. })
802 && include_metadata
803 .iter()
804 .any(|sic| matches!(sic, SourceIncludeMetadata::Headers { .. }))
805 {
806 sql_bail!("INCLUDE HEADERS with non-Kafka sources not supported");
808 }
809 if !matches!(
810 source_connection,
811 CreateSourceConnection::Kafka { .. } | CreateSourceConnection::LoadGenerator { .. }
812 ) && !include_metadata.is_empty()
813 {
814 bail_unsupported!("INCLUDE metadata with non-Kafka sources");
815 }
816
817 if !include_metadata.is_empty()
818 && !matches!(
819 envelope,
820 ast::SourceEnvelope::Upsert { .. }
821 | ast::SourceEnvelope::None
822 | ast::SourceEnvelope::Debezium
823 )
824 {
825 sql_bail!("INCLUDE <metadata> requires ENVELOPE (NONE|UPSERT|DEBEZIUM)");
826 }
827
828 let external_connection =
829 plan_generic_source_connection(scx, source_connection, include_metadata)?;
830
831 let CreateSourceOptionExtracted {
832 timestamp_interval,
833 retain_history,
834 seen: _,
835 } = CreateSourceOptionExtracted::try_from(with_options.clone())?;
836
837 let metadata_columns_desc = match external_connection {
838 GenericSourceConnection::Kafka(KafkaSourceConnection {
839 ref metadata_columns,
840 ..
841 }) => kafka_metadata_columns_desc(metadata_columns),
842 _ => vec![],
843 };
844
845 let (mut desc, envelope, encoding) = apply_source_envelope_encoding(
847 scx,
848 &envelope,
849 format,
850 Some(external_connection.default_key_desc()),
851 external_connection.default_value_desc(),
852 include_metadata,
853 metadata_columns_desc,
854 &external_connection,
855 )?;
856 plan_utils::maybe_rename_columns(format!("source {}", name), &mut desc, col_names)?;
857
858 let names: Vec<_> = desc.iter_names().cloned().collect();
859 if let Some(dup) = names.iter().duplicates().next() {
860 sql_bail!("column {} specified more than once", dup.quoted());
861 }
862
863 if let Some(KeyConstraint::PrimaryKeyNotEnforced { columns }) = key_constraint.clone() {
865 scx.require_feature_flag(&vars::ENABLE_PRIMARY_KEY_NOT_ENFORCED)?;
868
869 let key_columns = columns
870 .into_iter()
871 .map(normalize::column_name)
872 .collect::<Vec<_>>();
873
874 let mut uniq = BTreeSet::new();
875 for col in key_columns.iter() {
876 if !uniq.insert(col) {
877 sql_bail!("Repeated column name in source key constraint: {}", col);
878 }
879 }
880
881 let key_indices = key_columns
882 .iter()
883 .map(|col| {
884 let name_idx = desc
885 .get_by_name(col)
886 .map(|(idx, _type)| idx)
887 .ok_or_else(|| sql_err!("No such column in source key constraint: {}", col))?;
888 if desc.get_unambiguous_name(name_idx).is_none() {
889 sql_bail!("Ambiguous column in source key constraint: {}", col);
890 }
891 Ok(name_idx)
892 })
893 .collect::<Result<Vec<_>, _>>()?;
894
895 if !desc.typ().keys.is_empty() {
896 return Err(key_constraint_err(&desc, &key_columns));
897 } else {
898 desc = desc.with_key(key_indices);
899 }
900 }
901
902 let timestamp_interval = match timestamp_interval {
903 Some(duration) => {
904 if scx.pcx.is_some() {
908 let min = scx.catalog.system_vars().min_timestamp_interval();
909 let max = scx.catalog.system_vars().max_timestamp_interval();
910 if duration < min || duration > max {
911 return Err(PlanError::InvalidTimestampInterval {
912 min,
913 max,
914 requested: duration,
915 });
916 }
917 }
918 duration
919 }
920 None => scx.catalog.system_vars().default_timestamp_interval(),
921 };
922
923 let (desc, data_source) = match progress_subsource {
924 Some(name) => {
925 let DeferredItemName::Named(name) = name else {
926 sql_bail!("[internal error] progress subsource must be named during purification");
927 };
928 let ResolvedItemName::Item { id, .. } = name else {
929 sql_bail!("[internal error] invalid target id");
930 };
931
932 let details = match external_connection {
933 GenericSourceConnection::Kafka(ref c) => {
934 SourceExportDetails::Kafka(KafkaSourceExportDetails {
935 metadata_columns: c.metadata_columns.clone(),
936 })
937 }
938 GenericSourceConnection::LoadGenerator(ref c) => match c.load_generator {
939 LoadGenerator::Auction
940 | LoadGenerator::Marketing
941 | LoadGenerator::Tpch { .. } => SourceExportDetails::None,
942 LoadGenerator::Counter { .. }
943 | LoadGenerator::Clock
944 | LoadGenerator::Datums
945 | LoadGenerator::KeyValue(_) => {
946 SourceExportDetails::LoadGenerator(LoadGeneratorSourceExportDetails {
947 output: LoadGeneratorOutput::Default,
948 })
949 }
950 },
951 GenericSourceConnection::Postgres(_)
952 | GenericSourceConnection::MySql(_)
953 | GenericSourceConnection::SqlServer(_) => SourceExportDetails::None,
954 };
955
956 let data_source = DataSourceDesc::OldSyntaxIngestion {
957 desc: SourceDesc {
958 connection: external_connection,
959 timestamp_interval,
960 },
961 progress_subsource: *id,
962 data_config: SourceExportDataConfig {
963 encoding,
964 envelope: envelope.clone(),
965 },
966 details,
967 };
968 (desc, data_source)
969 }
970 None => {
971 let desc = external_connection.timestamp_desc();
972 let data_source = DataSourceDesc::Ingestion(SourceDesc {
973 connection: external_connection,
974 timestamp_interval,
975 });
976 (desc, data_source)
977 }
978 };
979
980 let if_not_exists = *if_not_exists;
981 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name.clone())?)?;
982
983 let full_name = scx.catalog.resolve_full_name(&name);
985 let partial_name = PartialItemName::from(full_name.clone());
986 if let (false, Ok(item)) = (
989 if_not_exists,
990 scx.catalog.resolve_item_or_type(&partial_name),
991 ) {
992 return Err(PlanError::ItemAlreadyExists {
993 name: full_name.to_string(),
994 item_type: item.item_type(),
995 });
996 }
997
998 let in_cluster = source_sink_cluster_config(scx, &mut stmt.in_cluster)?;
1002
1003 let create_sql = normalize::create_statement(scx, Statement::CreateSource(stmt))?;
1004
1005 let timeline = match envelope {
1007 SourceEnvelope::CdcV2 => {
1008 Timeline::External(scx.catalog.resolve_full_name(&name).to_string())
1009 }
1010 _ => Timeline::EpochMilliseconds,
1011 };
1012
1013 let compaction_window = plan_retain_history_option(scx, retain_history)?;
1014
1015 let source = Source {
1016 create_sql,
1017 data_source,
1018 desc,
1019 compaction_window,
1020 };
1021
1022 Ok(Plan::CreateSource(CreateSourcePlan {
1023 name,
1024 source,
1025 if_not_exists,
1026 timeline,
1027 in_cluster: Some(in_cluster.id()),
1028 }))
1029}
1030
1031pub fn plan_generic_source_connection(
1032 scx: &StatementContext<'_>,
1033 source_connection: &CreateSourceConnection<Aug>,
1034 include_metadata: &Vec<SourceIncludeMetadata>,
1035) -> Result<GenericSourceConnection<ReferencedConnection>, PlanError> {
1036 Ok(match source_connection {
1037 CreateSourceConnection::Kafka {
1038 connection,
1039 options,
1040 } => GenericSourceConnection::Kafka(plan_kafka_source_connection(
1041 scx,
1042 connection,
1043 options,
1044 include_metadata,
1045 )?),
1046 CreateSourceConnection::Postgres {
1047 connection,
1048 options,
1049 } => GenericSourceConnection::Postgres(plan_postgres_source_connection(
1050 scx, connection, options,
1051 )?),
1052 CreateSourceConnection::SqlServer {
1053 connection,
1054 options,
1055 } => GenericSourceConnection::SqlServer(plan_sqlserver_source_connection(
1056 scx, connection, options,
1057 )?),
1058 CreateSourceConnection::MySql {
1059 connection,
1060 options,
1061 } => {
1062 GenericSourceConnection::MySql(plan_mysql_source_connection(scx, connection, options)?)
1063 }
1064 CreateSourceConnection::LoadGenerator { generator, options } => {
1065 GenericSourceConnection::LoadGenerator(plan_load_generator_source_connection(
1066 scx,
1067 generator,
1068 options,
1069 include_metadata,
1070 )?)
1071 }
1072 })
1073}
1074
1075fn plan_load_generator_source_connection(
1076 scx: &StatementContext<'_>,
1077 generator: &ast::LoadGenerator,
1078 options: &Vec<LoadGeneratorOption<Aug>>,
1079 include_metadata: &Vec<SourceIncludeMetadata>,
1080) -> Result<LoadGeneratorSourceConnection, PlanError> {
1081 let load_generator =
1082 load_generator_ast_to_generator(scx, generator, options, include_metadata)?;
1083 let LoadGeneratorOptionExtracted {
1084 tick_interval,
1085 as_of,
1086 up_to,
1087 ..
1088 } = options.clone().try_into()?;
1089 let tick_micros = match tick_interval {
1090 Some(interval) => Some(interval.as_micros().try_into()?),
1091 None => None,
1092 };
1093 if up_to < as_of {
1094 sql_bail!("UP TO cannot be less than AS OF");
1095 }
1096 Ok(LoadGeneratorSourceConnection {
1097 load_generator,
1098 tick_micros,
1099 as_of,
1100 up_to,
1101 })
1102}
1103
1104fn plan_mysql_source_connection(
1105 scx: &StatementContext<'_>,
1106 connection: &ResolvedItemName,
1107 options: &Vec<MySqlConfigOption<Aug>>,
1108) -> Result<MySqlSourceConnection<ReferencedConnection>, PlanError> {
1109 let connection_item = scx.get_item_by_resolved_name(connection)?;
1110 match connection_item.connection()? {
1111 Connection::MySql(connection) => connection,
1112 _ => sql_bail!(
1113 "{} is not a MySQL connection",
1114 scx.catalog.resolve_full_name(connection_item.name())
1115 ),
1116 };
1117 let MySqlConfigOptionExtracted {
1118 details,
1119 text_columns: _,
1123 exclude_columns: _,
1124 seen: _,
1125 } = options.clone().try_into()?;
1126 let details = details
1127 .as_ref()
1128 .ok_or_else(|| internal_err!("MySQL source missing details"))?;
1129 let details = hex::decode(details).map_err(|e| sql_err!("{}", e))?;
1130 let details = ProtoMySqlSourceDetails::decode(&*details).map_err(|e| sql_err!("{}", e))?;
1131 let details = MySqlSourceDetails::from_proto(details).map_err(|e| sql_err!("{}", e))?;
1132 Ok(MySqlSourceConnection {
1133 connection: connection_item.id(),
1134 connection_id: connection_item.id(),
1135 details,
1136 })
1137}
1138
1139fn plan_sqlserver_source_connection(
1140 scx: &StatementContext<'_>,
1141 connection: &ResolvedItemName,
1142 options: &Vec<SqlServerConfigOption<Aug>>,
1143) -> Result<SqlServerSourceConnection<ReferencedConnection>, PlanError> {
1144 let connection_item = scx.get_item_by_resolved_name(connection)?;
1145 match connection_item.connection()? {
1146 Connection::SqlServer(connection) => connection,
1147 _ => sql_bail!(
1148 "{} is not a SQL Server connection",
1149 scx.catalog.resolve_full_name(connection_item.name())
1150 ),
1151 };
1152 let SqlServerConfigOptionExtracted { details, .. } = options.clone().try_into()?;
1153 let details = details
1154 .as_ref()
1155 .ok_or_else(|| internal_err!("SQL Server source missing details"))?;
1156 let extras = hex::decode(details)
1157 .map_err(|e| sql_err!("{e}"))
1158 .and_then(|raw| ProtoSqlServerSourceExtras::decode(&*raw).map_err(|e| sql_err!("{e}")))
1159 .and_then(|proto| SqlServerSourceExtras::from_proto(proto).map_err(|e| sql_err!("{e}")))?;
1160 Ok(SqlServerSourceConnection {
1161 connection_id: connection_item.id(),
1162 connection: connection_item.id(),
1163 extras,
1164 })
1165}
1166
1167fn plan_postgres_source_connection(
1168 scx: &StatementContext<'_>,
1169 connection: &ResolvedItemName,
1170 options: &Vec<PgConfigOption<Aug>>,
1171) -> Result<PostgresSourceConnection<ReferencedConnection>, PlanError> {
1172 let connection_item = scx.get_item_by_resolved_name(connection)?;
1173 let PgConfigOptionExtracted {
1174 details,
1175 publication,
1176 text_columns: _,
1180 exclude_columns: _,
1184 seen: _,
1185 } = options.clone().try_into()?;
1186 let details = details
1187 .as_ref()
1188 .ok_or_else(|| internal_err!("Postgres source missing details"))?;
1189 let details = hex::decode(details).map_err(|e| sql_err!("{}", e))?;
1190 let details =
1191 ProtoPostgresSourcePublicationDetails::decode(&*details).map_err(|e| sql_err!("{}", e))?;
1192 let publication_details =
1193 PostgresSourcePublicationDetails::from_proto(details).map_err(|e| sql_err!("{}", e))?;
1194 Ok(PostgresSourceConnection {
1195 connection: connection_item.id(),
1196 connection_id: connection_item.id(),
1197 publication: publication.ok_or_else(|| internal_err!("PUBLICATION option is required"))?,
1199 publication_details,
1200 })
1201}
1202
1203fn plan_kafka_source_connection(
1204 scx: &StatementContext<'_>,
1205 connection_name: &ResolvedItemName,
1206 options: &Vec<ast::KafkaSourceConfigOption<Aug>>,
1207 include_metadata: &Vec<SourceIncludeMetadata>,
1208) -> Result<KafkaSourceConnection<ReferencedConnection>, PlanError> {
1209 let connection_item = scx.get_item_by_resolved_name(connection_name)?;
1210 if !matches!(connection_item.connection()?, Connection::Kafka(_)) {
1211 sql_bail!(
1212 "{} is not a kafka connection",
1213 scx.catalog.resolve_full_name(connection_item.name())
1214 )
1215 }
1216 let KafkaSourceConfigOptionExtracted {
1217 group_id_prefix,
1218 topic,
1219 topic_metadata_refresh_interval,
1220 start_timestamp: _, start_offset,
1222 seen: _,
1223 }: KafkaSourceConfigOptionExtracted = options.clone().try_into()?;
1224 let topic = topic.ok_or_else(|| internal_err!("TOPIC option is required"))?;
1226 let mut start_offsets = BTreeMap::new();
1227 if let Some(offsets) = start_offset {
1228 for (part, offset) in offsets.iter().enumerate() {
1229 if *offset < 0 {
1230 sql_bail!("START OFFSET must be a nonnegative integer");
1231 }
1232 start_offsets.insert(i32::try_from(part)?, *offset);
1233 }
1234 }
1235 if topic_metadata_refresh_interval > Duration::from_secs(60 * 60) {
1236 sql_bail!("TOPIC METADATA REFRESH INTERVAL cannot be greater than 1 hour");
1239 }
1240 if topic_metadata_refresh_interval < Duration::from_secs(1) {
1241 sql_bail!("TOPIC METADATA REFRESH INTERVAL must be at least 1 second");
1244 }
1245 let metadata_columns = include_metadata
1246 .into_iter()
1247 .flat_map(|item| match item {
1248 SourceIncludeMetadata::Timestamp { alias } => {
1249 let name = match alias {
1250 Some(name) => name.to_string(),
1251 None => "timestamp".to_owned(),
1252 };
1253 Some((name, KafkaMetadataKind::Timestamp))
1254 }
1255 SourceIncludeMetadata::Partition { alias } => {
1256 let name = match alias {
1257 Some(name) => name.to_string(),
1258 None => "partition".to_owned(),
1259 };
1260 Some((name, KafkaMetadataKind::Partition))
1261 }
1262 SourceIncludeMetadata::Offset { alias } => {
1263 let name = match alias {
1264 Some(name) => name.to_string(),
1265 None => "offset".to_owned(),
1266 };
1267 Some((name, KafkaMetadataKind::Offset))
1268 }
1269 SourceIncludeMetadata::Headers { alias } => {
1270 let name = match alias {
1271 Some(name) => name.to_string(),
1272 None => "headers".to_owned(),
1273 };
1274 Some((name, KafkaMetadataKind::Headers))
1275 }
1276 SourceIncludeMetadata::Header {
1277 alias,
1278 key,
1279 use_bytes,
1280 } => Some((
1281 alias.to_string(),
1282 KafkaMetadataKind::Header {
1283 key: key.clone(),
1284 use_bytes: *use_bytes,
1285 },
1286 )),
1287 SourceIncludeMetadata::Key { .. } => {
1288 None
1290 }
1291 })
1292 .collect();
1293 Ok(KafkaSourceConnection {
1294 connection: connection_item.id(),
1295 connection_id: connection_item.id(),
1296 topic,
1297 start_offsets,
1298 group_id_prefix,
1299 topic_metadata_refresh_interval,
1300 metadata_columns,
1301 })
1302}
1303
1304fn apply_source_envelope_encoding(
1305 scx: &StatementContext,
1306 envelope: &ast::SourceEnvelope,
1307 format: &Option<FormatSpecifier<Aug>>,
1308 key_desc: Option<RelationDesc>,
1309 value_desc: RelationDesc,
1310 include_metadata: &[SourceIncludeMetadata],
1311 metadata_columns_desc: Vec<(&str, SqlColumnType)>,
1312 source_connection: &GenericSourceConnection<ReferencedConnection>,
1313) -> Result<
1314 (
1315 RelationDesc,
1316 SourceEnvelope,
1317 Option<SourceDataEncoding<ReferencedConnection>>,
1318 ),
1319 PlanError,
1320> {
1321 let encoding = match format {
1322 Some(format) => Some(get_encoding(scx, format, envelope)?),
1323 None => None,
1324 };
1325
1326 let (key_desc, value_desc) = match &encoding {
1327 Some(encoding) => {
1328 match value_desc.typ().columns() {
1331 [typ] => match typ.scalar_type {
1332 SqlScalarType::Bytes => {}
1333 _ => sql_bail!(
1334 "The schema produced by the source is incompatible with format decoding"
1335 ),
1336 },
1337 _ => sql_bail!(
1338 "The schema produced by the source is incompatible with format decoding"
1339 ),
1340 }
1341
1342 let (key_desc, value_desc) = encoding.desc()?;
1343
1344 let key_desc = key_desc.map(|desc| {
1351 let is_kafka = matches!(source_connection, GenericSourceConnection::Kafka(_));
1352 let is_envelope_none = matches!(envelope, ast::SourceEnvelope::None);
1353 if is_kafka && is_envelope_none {
1354 RelationDesc::from_names_and_types(
1355 desc.into_iter()
1356 .map(|(name, typ)| (name, typ.nullable(true))),
1357 )
1358 } else {
1359 desc
1360 }
1361 });
1362 (key_desc, value_desc)
1363 }
1364 None => (key_desc, value_desc),
1365 };
1366
1367 let key_envelope_no_encoding = matches!(
1380 source_connection,
1381 GenericSourceConnection::LoadGenerator(LoadGeneratorSourceConnection {
1382 load_generator: LoadGenerator::KeyValue(_),
1383 ..
1384 })
1385 );
1386 let mut key_envelope = get_key_envelope(
1387 include_metadata,
1388 encoding.as_ref(),
1389 key_envelope_no_encoding,
1390 )?;
1391
1392 match (&envelope, &key_envelope) {
1393 (ast::SourceEnvelope::Debezium, KeyEnvelope::None) => {}
1394 (ast::SourceEnvelope::Debezium, _) => sql_bail!(
1395 "Cannot use INCLUDE KEY with ENVELOPE DEBEZIUM: Debezium values include all keys."
1396 ),
1397 _ => {}
1398 };
1399
1400 let envelope = match &envelope {
1409 ast::SourceEnvelope::None => UnplannedSourceEnvelope::None(key_envelope),
1411 ast::SourceEnvelope::Debezium => {
1412 let after_idx = match typecheck_debezium(&value_desc) {
1414 Ok((_before_idx, after_idx)) => Ok(after_idx),
1415 Err(type_err) => match encoding.as_ref().map(|e| &e.value) {
1416 Some(DataEncoding::Avro(_)) => Err(type_err),
1417 _ => Err(sql_err!(
1418 "ENVELOPE DEBEZIUM requires that VALUE FORMAT is set to AVRO"
1419 )),
1420 },
1421 }?;
1422
1423 UnplannedSourceEnvelope::Upsert {
1424 style: UpsertStyle::Debezium { after_idx },
1425 }
1426 }
1427 ast::SourceEnvelope::Upsert {
1428 value_decode_err_policy,
1429 } => {
1430 let key_encoding = match encoding.as_ref().and_then(|e| e.key.as_ref()) {
1431 None => {
1432 if !key_envelope_no_encoding {
1433 bail_unsupported!(format!(
1434 "UPSERT requires a key/value format: {:?}",
1435 format
1436 ))
1437 }
1438 None
1439 }
1440 Some(key_encoding) => Some(key_encoding),
1441 };
1442 if key_envelope == KeyEnvelope::None {
1445 key_envelope = get_unnamed_key_envelope(key_encoding)?;
1446 }
1447 let style = match value_decode_err_policy.as_slice() {
1449 [] => UpsertStyle::Default(key_envelope),
1450 [SourceErrorPolicy::Inline { alias }] => {
1451 scx.require_feature_flag(&vars::ENABLE_ENVELOPE_UPSERT_INLINE_ERRORS)?;
1452 UpsertStyle::ValueErrInline {
1453 key_envelope,
1454 error_column: alias
1455 .as_ref()
1456 .map_or_else(|| "error".to_string(), |a| a.to_string()),
1457 }
1458 }
1459 _ => {
1460 bail_unsupported!("ENVELOPE UPSERT with unsupported value decode error policy")
1461 }
1462 };
1463
1464 UnplannedSourceEnvelope::Upsert { style }
1465 }
1466 ast::SourceEnvelope::CdcV2 => {
1467 scx.require_feature_flag(&vars::ENABLE_ENVELOPE_MATERIALIZE)?;
1468 match format {
1470 Some(FormatSpecifier::Bare(Format::Avro(_))) => {}
1471 _ => bail_unsupported!("non-Avro-encoded ENVELOPE MATERIALIZE"),
1472 }
1473 UnplannedSourceEnvelope::CdcV2
1474 }
1475 };
1476
1477 let metadata_desc = included_column_desc(metadata_columns_desc);
1478 let (envelope, desc) = envelope.desc(key_desc, value_desc, metadata_desc)?;
1479
1480 Ok((desc, envelope, encoding))
1481}
1482
1483fn plan_source_export_desc(
1486 scx: &StatementContext,
1487 name: &UnresolvedItemName,
1488 columns: &Vec<ColumnDef<Aug>>,
1489 constraints: &Vec<TableConstraint<Aug>>,
1490) -> Result<RelationDesc, PlanError> {
1491 let names: Vec<_> = columns
1492 .iter()
1493 .map(|c| normalize::column_name(c.name.clone()))
1494 .collect();
1495
1496 if let Some(dup) = names.iter().duplicates().next() {
1497 sql_bail!("column {} specified more than once", dup.quoted());
1498 }
1499
1500 let mut column_types = Vec::with_capacity(columns.len());
1503 let mut keys = Vec::new();
1504
1505 for (i, c) in columns.into_iter().enumerate() {
1506 let aug_data_type = &c.data_type;
1507 let ty = query::scalar_type_from_sql(scx, aug_data_type)?;
1508 let mut nullable = true;
1509 for option in &c.options {
1510 match &option.option {
1511 ColumnOption::NotNull => nullable = false,
1512 ColumnOption::Default(_) => {
1513 bail_unsupported!("Source export with default value")
1514 }
1515 ColumnOption::Unique { is_primary } => {
1516 keys.push(vec![i]);
1517 if *is_primary {
1518 nullable = false;
1519 }
1520 }
1521 other => {
1522 bail_unsupported!(format!("Source export with column constraint: {}", other))
1523 }
1524 }
1525 }
1526 column_types.push(ty.nullable(nullable));
1527 }
1528
1529 let mut seen_primary = false;
1530 'c: for constraint in constraints {
1531 match constraint {
1532 TableConstraint::Unique {
1533 name: _,
1534 columns,
1535 is_primary,
1536 nulls_not_distinct,
1537 } => {
1538 if seen_primary && *is_primary {
1539 sql_bail!(
1540 "multiple primary keys for source export {} are not allowed",
1541 name.to_ast_string_stable()
1542 );
1543 }
1544 seen_primary = *is_primary || seen_primary;
1545
1546 let mut key = vec![];
1547 for column in columns {
1548 let column = normalize::column_name(column.clone());
1549 match names.iter().position(|name| *name == column) {
1550 None => sql_bail!("unknown column in constraint: {}", column),
1551 Some(i) => {
1552 let nullable = &mut column_types[i].nullable;
1553 if *is_primary {
1554 if *nulls_not_distinct {
1555 sql_bail!(
1556 "[internal error] PRIMARY KEY does not support NULLS NOT DISTINCT"
1557 );
1558 }
1559 *nullable = false;
1560 } else if !(*nulls_not_distinct || !*nullable) {
1561 break 'c;
1564 }
1565
1566 key.push(i);
1567 }
1568 }
1569 }
1570
1571 if *is_primary {
1572 keys.insert(0, key);
1573 } else {
1574 keys.push(key);
1575 }
1576 }
1577 TableConstraint::ForeignKey { .. } => {
1578 bail_unsupported!("Source export with a foreign key")
1579 }
1580 TableConstraint::Check { .. } => {
1581 bail_unsupported!("Source export with a check constraint")
1582 }
1583 }
1584 }
1585
1586 let typ = SqlRelationType::new(column_types).with_keys(keys);
1587 let desc = RelationDesc::new(typ, names);
1588 Ok(desc)
1589}
1590
1591generate_extracted_config!(
1592 CreateSubsourceOption,
1593 (Progress, bool, Default(false)),
1594 (ExternalReference, UnresolvedItemName),
1595 (RetainHistory, OptionalDuration),
1596 (TextColumns, Vec::<Ident>, Default(vec![])),
1597 (ExcludeColumns, Vec::<Ident>, Default(vec![])),
1598 (Details, String)
1599);
1600
1601pub fn plan_create_subsource(
1602 scx: &StatementContext,
1603 stmt: CreateSubsourceStatement<Aug>,
1604) -> Result<Plan, PlanError> {
1605 let CreateSubsourceStatement {
1606 name,
1607 columns,
1608 of_source,
1609 constraints,
1610 if_not_exists,
1611 with_options,
1612 } = &stmt;
1613
1614 let CreateSubsourceOptionExtracted {
1615 progress,
1616 retain_history,
1617 external_reference,
1618 text_columns,
1619 exclude_columns,
1620 details,
1621 seen: _,
1622 } = with_options.clone().try_into()?;
1623
1624 if !(progress ^ (external_reference.is_some() && of_source.is_some())) {
1629 bail_internal!(
1630 "CREATE SUBSOURCE statement must specify either PROGRESS or REFERENCES option"
1631 );
1632 }
1633
1634 let desc = plan_source_export_desc(scx, name, columns, constraints)?;
1635
1636 let data_source = if let Some(source_reference) = of_source {
1637 if scx.catalog.system_vars().enable_create_table_from_source()
1640 && scx.catalog.system_vars().force_source_table_syntax()
1641 {
1642 Err(PlanError::UseTablesForSources(
1643 "CREATE SUBSOURCE".to_string(),
1644 ))?;
1645 }
1646
1647 let ingestion_id = *source_reference.item_id();
1650 let external_reference = external_reference.ok_or_else(|| {
1651 sql_err!("CREATE SUBSOURCE with REFERENCES requires EXTERNAL REFERENCE option")
1652 })?;
1653
1654 let details = details
1657 .as_ref()
1658 .ok_or_else(|| internal_err!("source-export subsource missing details"))?;
1659 let details = hex::decode(details).map_err(|e| sql_err!("{}", e))?;
1660 let details =
1661 ProtoSourceExportStatementDetails::decode(&*details).map_err(|e| sql_err!("{}", e))?;
1662 let details =
1663 SourceExportStatementDetails::from_proto(details).map_err(|e| sql_err!("{}", e))?;
1664 let details = match details {
1665 SourceExportStatementDetails::Postgres {
1666 table,
1667 cast_oid_full_range,
1668 initial_lsn,
1669 } => SourceExportDetails::Postgres(PostgresSourceExportDetails {
1670 column_casts: crate::pure::postgres::generate_column_casts(
1671 scx,
1672 &table,
1673 &text_columns,
1674 cast_oid_full_range,
1675 )?,
1676 table,
1677 initial_lsn,
1678 }),
1679 SourceExportStatementDetails::MySql {
1680 table,
1681 initial_gtid_set,
1682 binlog_full_metadata,
1683 } => SourceExportDetails::MySql(MySqlSourceExportDetails {
1684 table,
1685 initial_gtid_set,
1686 text_columns: text_columns.into_iter().map(|c| c.into_string()).collect(),
1687 exclude_columns: exclude_columns
1688 .into_iter()
1689 .map(|c| c.into_string())
1690 .collect(),
1691 binlog_full_metadata,
1692 }),
1693 SourceExportStatementDetails::SqlServer {
1694 table,
1695 capture_instance,
1696 initial_lsn,
1697 } => SourceExportDetails::SqlServer(SqlServerSourceExportDetails {
1698 capture_instance,
1699 table,
1700 initial_lsn,
1701 text_columns: text_columns.into_iter().map(|c| c.into_string()).collect(),
1702 exclude_columns: exclude_columns
1703 .into_iter()
1704 .map(|c| c.into_string())
1705 .collect(),
1706 }),
1707 SourceExportStatementDetails::LoadGenerator { output } => {
1708 SourceExportDetails::LoadGenerator(LoadGeneratorSourceExportDetails { output })
1709 }
1710 SourceExportStatementDetails::Kafka {} => {
1711 bail_unsupported!("subsources cannot reference Kafka sources")
1712 }
1713 };
1714 DataSourceDesc::IngestionExport {
1715 ingestion_id,
1716 external_reference,
1717 details,
1718 data_config: SourceExportDataConfig {
1720 envelope: SourceEnvelope::None(NoneEnvelope {
1721 key_envelope: KeyEnvelope::None,
1722 key_arity: 0,
1723 }),
1724 encoding: None,
1725 },
1726 }
1727 } else if progress {
1728 DataSourceDesc::Progress
1729 } else {
1730 sql_bail!("CREATE SUBSOURCE must specify one of PROGRESS or REFERENCES option")
1731 };
1732
1733 let if_not_exists = *if_not_exists;
1734 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name.clone())?)?;
1735
1736 let create_sql = normalize::create_statement(scx, Statement::CreateSubsource(stmt))?;
1737
1738 let compaction_window = plan_retain_history_option(scx, retain_history)?;
1739 let source = Source {
1740 create_sql,
1741 data_source,
1742 desc,
1743 compaction_window,
1744 };
1745
1746 Ok(Plan::CreateSource(CreateSourcePlan {
1747 name,
1748 source,
1749 if_not_exists,
1750 timeline: Timeline::EpochMilliseconds,
1751 in_cluster: None,
1752 }))
1753}
1754
1755generate_extracted_config!(
1756 TableFromSourceOption,
1757 (TextColumns, Vec::<Ident>, Default(vec![])),
1758 (ExcludeColumns, Vec::<Ident>, Default(vec![])),
1759 (ExcludeConstraints, Vec::<String>, Default(vec![])),
1760 (ExcludeAllConstraints, bool, Default(false)),
1761 (ExcludeAllNotNullConstraints, bool, Default(false)),
1762 (PartitionBy, Vec<Ident>),
1763 (RetainHistory, OptionalDuration),
1764 (Details, String)
1765);
1766
1767pub fn plan_create_table_from_source(
1768 scx: &StatementContext,
1769 stmt: CreateTableFromSourceStatement<Aug>,
1770) -> Result<Plan, PlanError> {
1771 if !scx.catalog.system_vars().enable_create_table_from_source() {
1772 sql_bail!("CREATE TABLE ... FROM SOURCE is not supported");
1773 }
1774
1775 let CreateTableFromSourceStatement {
1776 name,
1777 columns,
1778 constraints,
1779 if_not_exists,
1780 source,
1781 external_reference,
1782 envelope,
1783 format,
1784 include_metadata,
1785 with_options,
1786 } = &stmt;
1787
1788 let envelope = envelope.clone().unwrap_or(ast::SourceEnvelope::None);
1789
1790 let TableFromSourceOptionExtracted {
1791 text_columns,
1792 exclude_columns,
1793 exclude_constraints: _,
1794 exclude_all_constraints: _,
1795 exclude_all_not_null_constraints: _,
1796 retain_history,
1797 partition_by,
1798 details,
1799 seen: _,
1800 } = with_options.clone().try_into()?;
1801
1802 let source_item = scx.get_item_by_resolved_name(source)?;
1803 let ingestion_id = source_item.id();
1804
1805 let details = details
1808 .as_ref()
1809 .ok_or_else(|| internal_err!("source-export missing details"))?;
1810 let details = hex::decode(details).map_err(|e| sql_err!("{}", e))?;
1811 let details =
1812 ProtoSourceExportStatementDetails::decode(&*details).map_err(|e| sql_err!("{}", e))?;
1813 let details =
1814 SourceExportStatementDetails::from_proto(details).map_err(|e| sql_err!("{}", e))?;
1815
1816 if !matches!(details, SourceExportStatementDetails::Kafka { .. })
1817 && include_metadata
1818 .iter()
1819 .any(|sic| matches!(sic, SourceIncludeMetadata::Headers { .. }))
1820 {
1821 sql_bail!("INCLUDE HEADERS with non-Kafka source table not supported");
1823 }
1824 if !matches!(
1825 details,
1826 SourceExportStatementDetails::Kafka { .. }
1827 | SourceExportStatementDetails::LoadGenerator { .. }
1828 ) && !include_metadata.is_empty()
1829 {
1830 bail_unsupported!("INCLUDE metadata with non-Kafka source table");
1831 }
1832
1833 let details = match details {
1834 SourceExportStatementDetails::Postgres {
1835 table,
1836 cast_oid_full_range,
1837 initial_lsn,
1838 } => SourceExportDetails::Postgres(PostgresSourceExportDetails {
1839 column_casts: crate::pure::postgres::generate_column_casts(
1840 scx,
1841 &table,
1842 &text_columns,
1843 cast_oid_full_range,
1844 )?,
1845 table,
1846 initial_lsn,
1847 }),
1848 SourceExportStatementDetails::MySql {
1849 table,
1850 initial_gtid_set,
1851 binlog_full_metadata,
1852 } => SourceExportDetails::MySql(MySqlSourceExportDetails {
1853 table,
1854 initial_gtid_set,
1855 text_columns: text_columns.into_iter().map(|c| c.into_string()).collect(),
1856 exclude_columns: exclude_columns
1857 .into_iter()
1858 .map(|c| c.into_string())
1859 .collect(),
1860 binlog_full_metadata,
1861 }),
1862 SourceExportStatementDetails::SqlServer {
1863 table,
1864 capture_instance,
1865 initial_lsn,
1866 } => SourceExportDetails::SqlServer(SqlServerSourceExportDetails {
1867 table,
1868 capture_instance,
1869 initial_lsn,
1870 text_columns: text_columns.into_iter().map(|c| c.into_string()).collect(),
1871 exclude_columns: exclude_columns
1872 .into_iter()
1873 .map(|c| c.into_string())
1874 .collect(),
1875 }),
1876 SourceExportStatementDetails::LoadGenerator { output } => {
1877 SourceExportDetails::LoadGenerator(LoadGeneratorSourceExportDetails { output })
1878 }
1879 SourceExportStatementDetails::Kafka {} => {
1880 if !include_metadata.is_empty()
1881 && !matches!(
1882 envelope,
1883 ast::SourceEnvelope::Upsert { .. }
1884 | ast::SourceEnvelope::None
1885 | ast::SourceEnvelope::Debezium
1886 )
1887 {
1888 sql_bail!("INCLUDE <metadata> requires ENVELOPE (NONE|UPSERT|DEBEZIUM)");
1890 }
1891
1892 let metadata_columns = include_metadata
1893 .into_iter()
1894 .flat_map(|item| match item {
1895 SourceIncludeMetadata::Timestamp { alias } => {
1896 let name = match alias {
1897 Some(name) => name.to_string(),
1898 None => "timestamp".to_owned(),
1899 };
1900 Some((name, KafkaMetadataKind::Timestamp))
1901 }
1902 SourceIncludeMetadata::Partition { alias } => {
1903 let name = match alias {
1904 Some(name) => name.to_string(),
1905 None => "partition".to_owned(),
1906 };
1907 Some((name, KafkaMetadataKind::Partition))
1908 }
1909 SourceIncludeMetadata::Offset { alias } => {
1910 let name = match alias {
1911 Some(name) => name.to_string(),
1912 None => "offset".to_owned(),
1913 };
1914 Some((name, KafkaMetadataKind::Offset))
1915 }
1916 SourceIncludeMetadata::Headers { alias } => {
1917 let name = match alias {
1918 Some(name) => name.to_string(),
1919 None => "headers".to_owned(),
1920 };
1921 Some((name, KafkaMetadataKind::Headers))
1922 }
1923 SourceIncludeMetadata::Header {
1924 alias,
1925 key,
1926 use_bytes,
1927 } => Some((
1928 alias.to_string(),
1929 KafkaMetadataKind::Header {
1930 key: key.clone(),
1931 use_bytes: *use_bytes,
1932 },
1933 )),
1934 SourceIncludeMetadata::Key { .. } => {
1935 None
1937 }
1938 })
1939 .collect();
1940
1941 SourceExportDetails::Kafka(KafkaSourceExportDetails { metadata_columns })
1942 }
1943 };
1944
1945 let source_connection = &source_item
1946 .source_desc()?
1947 .ok_or_else(|| sql_err!("item is not a source"))?
1948 .connection;
1949
1950 let (key_desc, value_desc) =
1955 if matches!(columns, TableFromSourceColumns::Defined(_)) || !constraints.is_empty() {
1956 let columns = match columns {
1957 TableFromSourceColumns::Defined(columns) => columns,
1958 _ => bail_internal!("expected column definitions to be present"),
1959 };
1960 let desc = plan_source_export_desc(scx, name, columns, constraints)?;
1961 (None, desc)
1962 } else {
1963 let key_desc = source_connection.default_key_desc();
1964 let value_desc = source_connection.default_value_desc();
1965 (Some(key_desc), value_desc)
1966 };
1967
1968 let metadata_columns_desc = match &details {
1969 SourceExportDetails::Kafka(KafkaSourceExportDetails {
1970 metadata_columns, ..
1971 }) => kafka_metadata_columns_desc(metadata_columns),
1972 _ => vec![],
1973 };
1974
1975 let (mut desc, envelope, encoding) = apply_source_envelope_encoding(
1976 scx,
1977 &envelope,
1978 format,
1979 key_desc,
1980 value_desc,
1981 include_metadata,
1982 metadata_columns_desc,
1983 source_connection,
1984 )?;
1985 if let TableFromSourceColumns::Named(col_names) = columns {
1986 plan_utils::maybe_rename_columns(format!("source table {}", name), &mut desc, col_names)?;
1987 }
1988
1989 let names: Vec<_> = desc.iter_names().cloned().collect();
1990 if let Some(dup) = names.iter().duplicates().next() {
1991 sql_bail!("column {} specified more than once", dup.quoted());
1992 }
1993
1994 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name.clone())?)?;
1995
1996 let timeline = match envelope {
1999 SourceEnvelope::CdcV2 => {
2000 Timeline::External(scx.catalog.resolve_full_name(&name).to_string())
2001 }
2002 _ => Timeline::EpochMilliseconds,
2003 };
2004
2005 if let Some(partition_by) = partition_by {
2006 scx.require_feature_flag(&ENABLE_COLLECTION_PARTITION_BY)?;
2007 check_partition_by(&desc, partition_by)?;
2008 }
2009
2010 let data_source = DataSourceDesc::IngestionExport {
2011 ingestion_id,
2012 external_reference: external_reference
2014 .as_ref()
2015 .ok_or_else(|| sql_err!("EXTERNAL REFERENCE is required"))?
2016 .clone(),
2017 details,
2018 data_config: SourceExportDataConfig { envelope, encoding },
2019 };
2020
2021 let if_not_exists = *if_not_exists;
2022
2023 let create_sql = normalize::create_statement(scx, Statement::CreateTableFromSource(stmt))?;
2024
2025 let compaction_window = plan_retain_history_option(scx, retain_history)?;
2026 let table = Table {
2027 create_sql,
2028 desc: VersionedRelationDesc::new(desc),
2029 temporary: false,
2030 compaction_window,
2031 data_source: TableDataSource::DataSource {
2032 desc: data_source,
2033 timeline,
2034 },
2035 };
2036
2037 Ok(Plan::CreateTable(CreateTablePlan {
2038 name,
2039 table,
2040 if_not_exists,
2041 }))
2042}
2043
2044generate_extracted_config!(
2045 LoadGeneratorOption,
2046 (TickInterval, Duration),
2047 (AsOf, u64, Default(0_u64)),
2048 (UpTo, u64, Default(u64::MAX)),
2049 (ScaleFactor, f64),
2050 (MaxCardinality, u64),
2051 (Keys, u64),
2052 (SnapshotRounds, u64),
2053 (TransactionalSnapshot, bool),
2054 (ValueSize, u64),
2055 (Seed, u64),
2056 (Partitions, u64),
2057 (BatchSize, u64)
2058);
2059
2060impl LoadGeneratorOptionExtracted {
2061 pub(super) fn ensure_only_valid_options(
2062 &self,
2063 loadgen: &ast::LoadGenerator,
2064 ) -> Result<(), PlanError> {
2065 use mz_sql_parser::ast::LoadGeneratorOptionName::*;
2066
2067 let mut options = self.seen.clone();
2068
2069 let permitted_options: &[_] = match loadgen {
2070 ast::LoadGenerator::Auction => &[TickInterval, AsOf, UpTo],
2071 ast::LoadGenerator::Clock => &[TickInterval, AsOf, UpTo],
2072 ast::LoadGenerator::Counter => &[TickInterval, AsOf, UpTo, MaxCardinality],
2073 ast::LoadGenerator::Marketing => &[TickInterval, AsOf, UpTo],
2074 ast::LoadGenerator::Datums => &[TickInterval, AsOf, UpTo],
2075 ast::LoadGenerator::Tpch => &[TickInterval, AsOf, UpTo, ScaleFactor],
2076 ast::LoadGenerator::KeyValue => &[
2077 TickInterval,
2078 Keys,
2079 SnapshotRounds,
2080 TransactionalSnapshot,
2081 ValueSize,
2082 Seed,
2083 Partitions,
2084 BatchSize,
2085 ],
2086 };
2087
2088 for o in permitted_options {
2089 options.remove(o);
2090 }
2091
2092 if !options.is_empty() {
2093 sql_bail!(
2094 "{} load generators do not support {} values",
2095 loadgen,
2096 options.iter().join(", ")
2097 )
2098 }
2099
2100 Ok(())
2101 }
2102}
2103
2104pub(crate) fn load_generator_ast_to_generator(
2105 scx: &StatementContext,
2106 loadgen: &ast::LoadGenerator,
2107 options: &[LoadGeneratorOption<Aug>],
2108 include_metadata: &[SourceIncludeMetadata],
2109) -> Result<LoadGenerator, PlanError> {
2110 let extracted: LoadGeneratorOptionExtracted = options.to_vec().try_into()?;
2111 extracted.ensure_only_valid_options(loadgen)?;
2112
2113 if loadgen != &ast::LoadGenerator::KeyValue && !include_metadata.is_empty() {
2114 sql_bail!("INCLUDE metadata only supported with `KEY VALUE` load generators");
2115 }
2116
2117 let load_generator = match loadgen {
2118 ast::LoadGenerator::Auction => LoadGenerator::Auction,
2119 ast::LoadGenerator::Clock => {
2120 scx.require_feature_flag(&vars::ENABLE_LOAD_GENERATOR_CLOCK)?;
2121 LoadGenerator::Clock
2122 }
2123 ast::LoadGenerator::Counter => {
2124 scx.require_feature_flag(&vars::ENABLE_LOAD_GENERATOR_COUNTER)?;
2125 let LoadGeneratorOptionExtracted {
2126 max_cardinality, ..
2127 } = extracted;
2128 LoadGenerator::Counter { max_cardinality }
2129 }
2130 ast::LoadGenerator::Marketing => LoadGenerator::Marketing,
2131 ast::LoadGenerator::Datums => {
2132 scx.require_feature_flag(&vars::ENABLE_LOAD_GENERATOR_DATUMS)?;
2133 LoadGenerator::Datums
2134 }
2135 ast::LoadGenerator::Tpch => {
2136 let LoadGeneratorOptionExtracted { scale_factor, .. } = extracted;
2137
2138 let sf: f64 = scale_factor.unwrap_or(0.01);
2140 if !sf.is_finite() || sf < 0.0 {
2141 sql_bail!("unsupported scale factor {sf}");
2142 }
2143
2144 let f_to_i = |multiplier: f64| -> Result<i64, PlanError> {
2145 let total = (sf * multiplier).floor();
2146 let mut i = i64::try_cast_from(total)
2147 .ok_or_else(|| sql_err!("unsupported scale factor {sf}"))?;
2148 if i < 1 {
2149 i = 1;
2150 }
2151 Ok(i)
2152 };
2153
2154 let count_supplier = f_to_i(10_000f64)?;
2157 let count_part = f_to_i(200_000f64)?;
2158 let count_customer = f_to_i(150_000f64)?;
2159 let count_orders = f_to_i(150_000f64 * 10f64)?;
2160 let count_clerk = f_to_i(1_000f64)?;
2161
2162 LoadGenerator::Tpch {
2163 count_supplier,
2164 count_part,
2165 count_customer,
2166 count_orders,
2167 count_clerk,
2168 }
2169 }
2170 mz_sql_parser::ast::LoadGenerator::KeyValue => {
2171 scx.require_feature_flag(&vars::ENABLE_LOAD_GENERATOR_KEY_VALUE)?;
2172 let LoadGeneratorOptionExtracted {
2173 keys,
2174 snapshot_rounds,
2175 transactional_snapshot,
2176 value_size,
2177 tick_interval,
2178 seed,
2179 partitions,
2180 batch_size,
2181 ..
2182 } = extracted;
2183
2184 let mut include_offset = None;
2185 for im in include_metadata {
2186 match im {
2187 SourceIncludeMetadata::Offset { alias } => {
2188 include_offset = match alias {
2189 Some(alias) => Some(alias.to_string()),
2190 None => Some(LOAD_GENERATOR_KEY_VALUE_OFFSET_DEFAULT.to_string()),
2191 }
2192 }
2193 SourceIncludeMetadata::Key { .. } => continue,
2194
2195 _ => {
2196 sql_bail!("only `INCLUDE OFFSET` and `INCLUDE KEY` is supported");
2197 }
2198 };
2199 }
2200
2201 let lgkv = KeyValueLoadGenerator {
2202 keys: keys.ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires KEYS"))?,
2203 snapshot_rounds: snapshot_rounds
2204 .ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires SNAPSHOT ROUNDS"))?,
2205 transactional_snapshot: transactional_snapshot.unwrap_or(true),
2207 value_size: value_size
2208 .ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires VALUE SIZE"))?,
2209 partitions: partitions
2210 .ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires PARTITIONS"))?,
2211 tick_interval,
2212 batch_size: batch_size
2213 .ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires BATCH SIZE"))?,
2214 seed: seed.ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires SEED"))?,
2215 include_offset,
2216 };
2217
2218 if lgkv.keys == 0
2219 || lgkv.partitions == 0
2220 || lgkv.value_size == 0
2221 || lgkv.batch_size == 0
2222 {
2223 sql_bail!("LOAD GENERATOR KEY VALUE options must be non-zero")
2224 }
2225
2226 if lgkv.keys % lgkv.partitions != 0 {
2227 sql_bail!("KEYS must be a multiple of PARTITIONS")
2228 }
2229
2230 if lgkv.batch_size > lgkv.keys {
2231 sql_bail!("KEYS must be larger than BATCH SIZE")
2232 }
2233
2234 if (lgkv.keys / lgkv.partitions) % lgkv.batch_size != 0 {
2237 sql_bail!("PARTITIONS * BATCH SIZE must be a divisor of KEYS")
2238 }
2239
2240 if lgkv.snapshot_rounds == 0 {
2241 sql_bail!("SNAPSHOT ROUNDS must be larger than 0")
2242 }
2243
2244 LoadGenerator::KeyValue(lgkv)
2245 }
2246 };
2247
2248 Ok(load_generator)
2249}
2250
2251fn typecheck_debezium(value_desc: &RelationDesc) -> Result<(Option<usize>, usize), PlanError> {
2252 let before = value_desc.get_by_name(&"before".into());
2253 let (after_idx, after_ty) = value_desc
2254 .get_by_name(&"after".into())
2255 .ok_or_else(|| sql_err!("'after' column missing from debezium input"))?;
2256 let before_idx = if let Some((before_idx, before_ty)) = before {
2257 if !matches!(before_ty.scalar_type, SqlScalarType::Record { .. }) {
2258 sql_bail!("'before' column must be of type record");
2259 }
2260 if before_ty != after_ty {
2261 sql_bail!("'before' type differs from 'after' column");
2262 }
2263 Some(before_idx)
2264 } else {
2265 None
2266 };
2267 Ok((before_idx, after_idx))
2268}
2269
2270fn get_encoding(
2271 scx: &StatementContext,
2272 format: &FormatSpecifier<Aug>,
2273 envelope: &ast::SourceEnvelope,
2274) -> Result<SourceDataEncoding<ReferencedConnection>, PlanError> {
2275 let encoding = match format {
2276 FormatSpecifier::Bare(format) => get_encoding_inner(scx, format)?,
2277 FormatSpecifier::KeyValue { key, value } => {
2278 let key = {
2279 let encoding = get_encoding_inner(scx, key)?;
2280 Some(encoding.key.unwrap_or(encoding.value))
2281 };
2282 let value = get_encoding_inner(scx, value)?.value;
2283 SourceDataEncoding { key, value }
2284 }
2285 };
2286
2287 let requires_keyvalue = matches!(
2288 envelope,
2289 ast::SourceEnvelope::Debezium | ast::SourceEnvelope::Upsert { .. }
2290 );
2291 let is_keyvalue = encoding.key.is_some();
2292 if requires_keyvalue && !is_keyvalue {
2293 sql_bail!("ENVELOPE [DEBEZIUM] UPSERT requires that KEY FORMAT be specified");
2294 };
2295
2296 Ok(encoding)
2297}
2298
2299fn source_sink_cluster_config<'a, 'ctx>(
2305 scx: &'a StatementContext<'ctx>,
2306 in_cluster: &mut Option<ResolvedClusterName>,
2307) -> Result<&'a dyn CatalogCluster<'ctx>, PlanError> {
2308 let cluster = match in_cluster {
2309 None => {
2310 let cluster = scx.catalog.resolve_cluster(None)?;
2311 *in_cluster = Some(ResolvedClusterName {
2312 id: cluster.id(),
2313 print_name: None,
2314 });
2315 cluster
2316 }
2317 Some(in_cluster) => scx.catalog.get_cluster(in_cluster.id),
2318 };
2319
2320 Ok(cluster)
2321}
2322
2323generate_extracted_config!(AvroSchemaOption, (ConfluentWireFormat, bool, Default(true)));
2324
2325generate_extracted_config!(
2329 GlueAvroOption,
2330 (SchemaName, String),
2331 (KeySchemaName, String),
2332 (ValueSchemaName, String),
2333 (KeyCompatibilityLevel, String),
2334 (ValueCompatibilityLevel, String)
2335);
2336
2337#[derive(Debug)]
2338pub struct Schema {
2339 pub key_schema: Option<String>,
2340 pub value_schema: String,
2341 pub key_reference_schemas: Vec<String>,
2343 pub value_reference_schemas: Vec<String>,
2345 pub wire_format: WireFormat<ReferencedConnection>,
2349}
2350
2351fn get_encoding_inner(
2352 scx: &StatementContext,
2353 format: &Format<Aug>,
2354) -> Result<SourceDataEncoding<ReferencedConnection>, PlanError> {
2355 let value = match format {
2356 Format::Bytes => DataEncoding::Bytes,
2357 Format::Avro(schema) => {
2358 let Schema {
2359 key_schema,
2360 value_schema,
2361 key_reference_schemas,
2362 value_reference_schemas,
2363 wire_format,
2364 } = match schema {
2365 AvroSchema::InlineSchema {
2368 schema: ast::Schema { schema },
2369 with_options,
2370 } => {
2371 let AvroSchemaOptionExtracted {
2372 confluent_wire_format,
2373 ..
2374 } = with_options.clone().try_into()?;
2375 let wire_format = if confluent_wire_format {
2376 WireFormat::Confluent { registry: None }
2377 } else {
2378 WireFormat::None
2379 };
2380 Schema {
2381 key_schema: None,
2382 value_schema: schema.clone(),
2383 key_reference_schemas: vec![],
2384 value_reference_schemas: vec![],
2385 wire_format,
2386 }
2387 }
2388 AvroSchema::Csr {
2389 csr_connection:
2390 CsrConnectionAvro {
2391 connection,
2392 seed,
2393 key_strategy: _,
2394 value_strategy: _,
2395 },
2396 } => {
2397 let item = scx.get_item_by_resolved_name(&connection.connection)?;
2398 let csr_connection = match item.connection()? {
2399 Connection::Csr(_) => item.id(),
2400 _ => {
2401 sql_bail!(
2402 "{} is not a Confluent Schema Registry connection",
2403 scx.catalog
2404 .resolve_full_name(item.name())
2405 .to_string()
2406 .quoted()
2407 )
2408 }
2409 };
2410
2411 if let Some(seed) = seed {
2412 Schema {
2413 key_schema: seed.key_schema.clone(),
2414 value_schema: seed.value_schema.clone(),
2415 key_reference_schemas: seed.key_reference_schemas.clone(),
2416 value_reference_schemas: seed.value_reference_schemas.clone(),
2417 wire_format: WireFormat::Confluent {
2418 registry: Some(csr_connection),
2419 },
2420 }
2421 } else {
2422 sql_bail!("Avro CSR seed resolution has not been performed")
2423 }
2424 }
2425 AvroSchema::Glue {
2426 connection,
2427 with_options: _,
2428 seed,
2429 } => {
2430 let item = scx.get_item_by_resolved_name(connection)?;
2431 let glue_connection = match item.connection()? {
2432 Connection::GlueSchemaRegistry(_) => item.id(),
2433 _ => {
2434 sql_bail!(
2435 "{} is not an AWS Glue Schema Registry connection",
2436 scx.catalog
2437 .resolve_full_name(item.name())
2438 .to_string()
2439 .quoted()
2440 )
2441 }
2442 };
2443
2444 let Some(seed) = seed else {
2448 sql_bail!("Avro Glue seed resolution has not been performed");
2449 };
2450
2451 Schema {
2452 key_schema: None,
2453 value_schema: seed.value_schema.clone(),
2454 key_reference_schemas: vec![],
2455 value_reference_schemas: vec![],
2456 wire_format: WireFormat::Glue {
2457 registry: Some(glue_connection),
2458 },
2459 }
2460 }
2461 };
2462
2463 if let Some(key_schema) = key_schema {
2464 return Ok(SourceDataEncoding {
2465 key: Some(DataEncoding::Avro(AvroEncoding {
2466 schema: key_schema,
2467 reference_schemas: key_reference_schemas,
2468 wire_format: wire_format.clone(),
2469 })),
2470 value: DataEncoding::Avro(AvroEncoding {
2471 schema: value_schema,
2472 reference_schemas: value_reference_schemas,
2473 wire_format,
2474 }),
2475 });
2476 } else {
2477 DataEncoding::Avro(AvroEncoding {
2478 schema: value_schema,
2479 reference_schemas: value_reference_schemas,
2480 wire_format,
2481 })
2482 }
2483 }
2484 Format::Protobuf(schema) => match schema {
2485 ProtobufSchema::Csr {
2486 csr_connection:
2487 CsrConnectionProtobuf {
2488 connection:
2489 CsrConnection {
2490 connection,
2491 options,
2492 },
2493 seed,
2494 },
2495 } => {
2496 if let Some(CsrSeedProtobuf { key, value }) = seed {
2497 let item = scx.get_item_by_resolved_name(connection)?;
2498 let _ = match item.connection()? {
2499 Connection::Csr(connection) => connection,
2500 _ => {
2501 sql_bail!(
2502 "{} is not a schema registry connection",
2503 scx.catalog
2504 .resolve_full_name(item.name())
2505 .to_string()
2506 .quoted()
2507 )
2508 }
2509 };
2510
2511 if !options.is_empty() {
2512 sql_bail!("Protobuf CSR connections do not support any options");
2513 }
2514
2515 let value = DataEncoding::Protobuf(ProtobufEncoding {
2516 descriptors: strconv::parse_bytes(&value.schema)?,
2517 message_name: value.message_name.clone(),
2518 confluent_wire_format: true,
2519 });
2520 if let Some(key) = key {
2521 return Ok(SourceDataEncoding {
2522 key: Some(DataEncoding::Protobuf(ProtobufEncoding {
2523 descriptors: strconv::parse_bytes(&key.schema)?,
2524 message_name: key.message_name.clone(),
2525 confluent_wire_format: true,
2526 })),
2527 value,
2528 });
2529 }
2530 value
2531 } else {
2532 sql_bail!("Protobuf CSR seed resolution has not been performed")
2533 }
2534 }
2535 ProtobufSchema::InlineSchema {
2536 message_name,
2537 schema: ast::Schema { schema },
2538 } => {
2539 let descriptors = strconv::parse_bytes(schema)?;
2540
2541 DataEncoding::Protobuf(ProtobufEncoding {
2542 descriptors,
2543 message_name: message_name.to_owned(),
2544 confluent_wire_format: false,
2545 })
2546 }
2547 },
2548 Format::Regex(regex) => DataEncoding::Regex(RegexEncoding {
2549 regex: mz_repr::adt::regex::Regex::new(regex, false)
2550 .map_err(|e| sql_err!("parsing regex: {e}"))?,
2551 }),
2552 Format::Csv { columns, delimiter } => {
2553 let columns = match columns {
2554 CsvColumns::Header { names } => {
2555 if names.is_empty() {
2556 sql_bail!("[internal error] column spec should get names in purify")
2557 }
2558 ColumnSpec::Header {
2559 names: names.iter().cloned().map(|n| n.into_string()).collect(),
2560 }
2561 }
2562 CsvColumns::Count(n) => ColumnSpec::Count(usize::cast_from(*n)),
2563 };
2564 DataEncoding::Csv(CsvEncoding {
2565 columns,
2566 delimiter: u8::try_from(*delimiter)
2567 .map_err(|_| sql_err!("CSV delimiter must be an ASCII character"))?,
2568 })
2569 }
2570 Format::Json { array: false } => DataEncoding::Json,
2571 Format::Json { array: true } => bail_unsupported!("JSON ARRAY format in sources"),
2572 Format::Text => DataEncoding::Text,
2573 };
2574 Ok(SourceDataEncoding { key: None, value })
2575}
2576
2577fn get_key_envelope(
2579 included_items: &[SourceIncludeMetadata],
2580 encoding: Option<&SourceDataEncoding<ReferencedConnection>>,
2581 key_envelope_no_encoding: bool,
2582) -> Result<KeyEnvelope, PlanError> {
2583 let key_definition = included_items
2584 .iter()
2585 .find(|i| matches!(i, SourceIncludeMetadata::Key { .. }));
2586 if let Some(SourceIncludeMetadata::Key { alias }) = key_definition {
2587 match (alias, encoding.and_then(|e| e.key.as_ref())) {
2588 (Some(name), Some(_)) => Ok(KeyEnvelope::Named(name.as_str().to_string())),
2589 (None, Some(key)) => get_unnamed_key_envelope(Some(key)),
2590 (Some(name), _) if key_envelope_no_encoding => {
2591 Ok(KeyEnvelope::Named(name.as_str().to_string()))
2592 }
2593 (None, _) if key_envelope_no_encoding => get_unnamed_key_envelope(None),
2594 (_, None) => {
2595 sql_bail!(
2599 "INCLUDE KEY requires specifying KEY FORMAT .. VALUE FORMAT, \
2600 got bare FORMAT"
2601 );
2602 }
2603 }
2604 } else {
2605 Ok(KeyEnvelope::None)
2606 }
2607}
2608
2609fn get_unnamed_key_envelope(
2612 key: Option<&DataEncoding<ReferencedConnection>>,
2613) -> Result<KeyEnvelope, PlanError> {
2614 let is_composite = match key {
2618 Some(DataEncoding::Bytes | DataEncoding::Json | DataEncoding::Text) => false,
2619 Some(
2620 DataEncoding::Avro(_)
2621 | DataEncoding::Csv(_)
2622 | DataEncoding::Protobuf(_)
2623 | DataEncoding::Regex { .. },
2624 ) => true,
2625 None => false,
2626 };
2627
2628 if is_composite {
2629 Ok(KeyEnvelope::Flattened)
2630 } else {
2631 Ok(KeyEnvelope::Named("key".to_string()))
2632 }
2633}
2634
2635pub fn describe_create_view(
2636 _: &StatementContext,
2637 _: CreateViewStatement<Aug>,
2638) -> Result<StatementDesc, PlanError> {
2639 Ok(StatementDesc::new(None))
2640}
2641
2642pub fn plan_view(
2643 scx: &StatementContext,
2644 def: &mut ViewDefinition<Aug>,
2645 temporary: bool,
2646) -> Result<(QualifiedItemName, View), PlanError> {
2647 let create_sql = normalize::create_statement(
2648 scx,
2649 Statement::CreateView(CreateViewStatement {
2650 if_exists: IfExistsBehavior::Error,
2651 temporary,
2652 definition: def.clone(),
2653 }),
2654 )?;
2655
2656 let ViewDefinition {
2657 name,
2658 columns,
2659 query,
2660 } = def;
2661
2662 let query::PlannedRootQuery {
2663 expr,
2664 mut desc,
2665 finishing,
2666 scope: _,
2667 } = query::plan_root_query(scx, query.clone(), QueryLifetime::View)?;
2668 assert!(HirRelationExpr::is_trivial_row_set_finishing_hir(
2674 &finishing,
2675 expr.arity()
2676 ));
2677 if expr.contains_parameters()? {
2678 return Err(PlanError::ParameterNotAllowed("views".to_string()));
2679 }
2680
2681 let dependencies = expr
2682 .depends_on()
2683 .into_iter()
2684 .map(|gid| scx.catalog.resolve_item_id(&gid))
2685 .collect();
2686
2687 let name = if temporary {
2688 scx.allocate_temporary_qualified_name(normalize::unresolved_item_name(name.to_owned())?)?
2689 } else {
2690 scx.allocate_qualified_name(normalize::unresolved_item_name(name.to_owned())?)?
2691 };
2692
2693 plan_utils::maybe_rename_columns_exact(
2694 scx.catalog,
2695 format!("view {}", scx.catalog.resolve_full_name(&name)),
2696 &mut desc,
2697 columns,
2698 )?;
2699 let names: Vec<ColumnName> = desc.iter_names().cloned().collect();
2700
2701 if let Some(dup) = names.iter().duplicates().next() {
2702 sql_bail!("column {} specified more than once", dup.quoted());
2703 }
2704
2705 let view = View {
2706 create_sql,
2707 expr,
2708 dependencies,
2709 column_names: names,
2710 temporary,
2711 };
2712
2713 Ok((name, view))
2714}
2715
2716pub fn plan_create_view(
2717 scx: &StatementContext,
2718 mut stmt: CreateViewStatement<Aug>,
2719) -> Result<Plan, PlanError> {
2720 let CreateViewStatement {
2721 temporary,
2722 if_exists,
2723 definition,
2724 } = &mut stmt;
2725 let (name, view) = plan_view(scx, definition, *temporary)?;
2726
2727 let ignore_if_exists_errors = scx.pcx().map_or(false, |pcx| pcx.ignore_if_exists_errors);
2730
2731 let replace = if *if_exists == IfExistsBehavior::Replace && !ignore_if_exists_errors {
2732 let if_exists = true;
2733 let cascade = false;
2734 let maybe_item_to_drop = plan_drop_item(
2735 scx,
2736 ObjectType::View,
2737 if_exists,
2738 definition.name.clone(),
2739 cascade,
2740 )?;
2741
2742 if let Some(id) = maybe_item_to_drop {
2744 let dependencies = view.expr.depends_on();
2745 let invalid_drop = scx
2746 .get_item(&id)
2747 .global_ids()
2748 .any(|gid| dependencies.contains(&gid));
2749 if invalid_drop {
2750 let item = scx.catalog.get_item(&id);
2751 sql_bail!(
2752 "cannot replace view {0}: depended upon by new {0} definition",
2753 scx.catalog.resolve_full_name(item.name())
2754 );
2755 }
2756
2757 Some(id)
2758 } else {
2759 None
2760 }
2761 } else {
2762 None
2763 };
2764 let drop_ids = replace
2765 .map(|id| {
2766 scx.catalog
2767 .item_dependents(id)
2768 .into_iter()
2769 .map(|id| id.unwrap_item_id())
2770 .collect()
2771 })
2772 .unwrap_or_default();
2773
2774 validate_view_dependencies(scx, &view.dependencies.0)?;
2775
2776 let full_name = scx.catalog.resolve_full_name(&name);
2778 let partial_name = PartialItemName::from(full_name.clone());
2779 if let (Ok(item), IfExistsBehavior::Error, false) = (
2782 scx.catalog.resolve_item_or_type(&partial_name),
2783 *if_exists,
2784 ignore_if_exists_errors,
2785 ) {
2786 return Err(PlanError::ItemAlreadyExists {
2787 name: full_name.to_string(),
2788 item_type: item.item_type(),
2789 });
2790 }
2791
2792 Ok(Plan::CreateView(CreateViewPlan {
2793 name,
2794 view,
2795 replace,
2796 drop_ids,
2797 if_not_exists: *if_exists == IfExistsBehavior::Skip,
2798 ambiguous_columns: *scx.ambiguous_columns.borrow(),
2799 }))
2800}
2801
2802fn validate_view_dependencies(
2804 scx: &StatementContext,
2805 dependencies: &BTreeSet<CatalogItemId>,
2806) -> Result<(), PlanError> {
2807 for id in dependencies {
2808 let item = scx.catalog.get_item(id);
2809 if item.replacement_target().is_some() {
2810 let name = scx.catalog.minimal_qualification(item.name());
2811 return Err(PlanError::InvalidDependency {
2812 name: name.to_string(),
2813 item_type: format!("replacement {}", item.item_type()),
2814 });
2815 }
2816 }
2817
2818 Ok(())
2819}
2820
2821pub fn describe_create_materialized_view(
2822 _: &StatementContext,
2823 _: CreateMaterializedViewStatement<Aug>,
2824) -> Result<StatementDesc, PlanError> {
2825 Ok(StatementDesc::new(None))
2826}
2827
2828pub fn describe_create_network_policy(
2829 _: &StatementContext,
2830 _: CreateNetworkPolicyStatement<Aug>,
2831) -> Result<StatementDesc, PlanError> {
2832 Ok(StatementDesc::new(None))
2833}
2834
2835pub fn describe_alter_network_policy(
2836 _: &StatementContext,
2837 _: AlterNetworkPolicyStatement<Aug>,
2838) -> Result<StatementDesc, PlanError> {
2839 Ok(StatementDesc::new(None))
2840}
2841
2842fn check_refresh_time(option: &str, ts: Timestamp) -> Result<(), PlanError> {
2844 let renderable = i64::try_from(ts)
2845 .ok()
2846 .and_then(DateTime::from_timestamp_millis)
2847 .is_some_and(|dt| CheckedTimestamp::try_from(dt).is_ok());
2848 if !renderable {
2849 sql_bail!("{option} time too large: {ts}");
2850 }
2851 Ok(())
2852}
2853
2854pub fn plan_create_materialized_view(
2855 scx: &StatementContext,
2856 mut stmt: CreateMaterializedViewStatement<Aug>,
2857) -> Result<Plan, PlanError> {
2858 let cluster_id =
2859 crate::plan::statement::resolve_cluster_for_materialized_view(scx.catalog, &stmt)?;
2860 stmt.in_cluster = Some(ResolvedClusterName {
2861 id: cluster_id,
2862 print_name: None,
2863 });
2864
2865 let target_replica = match &stmt.in_cluster_replica {
2866 Some(replica_name) => {
2867 scx.require_feature_flag(&ENABLE_REPLICA_TARGETED_MATERIALIZED_VIEWS)?;
2868
2869 let cluster = scx.catalog.get_cluster(cluster_id);
2870 let replica_id = cluster
2871 .replica_ids()
2872 .get(replica_name.as_str())
2873 .copied()
2874 .ok_or_else(|| {
2875 CatalogError::UnknownClusterReplica(replica_name.as_str().to_string())
2876 })?;
2877 Some(replica_id)
2878 }
2879 None => None,
2880 };
2881
2882 let create_sql =
2883 normalize::create_statement(scx, Statement::CreateMaterializedView(stmt.clone()))?;
2884
2885 let partial_name = normalize::unresolved_item_name(stmt.name)?;
2886 let name = scx.allocate_qualified_name(partial_name.clone())?;
2887
2888 let query::PlannedRootQuery {
2889 expr,
2890 mut desc,
2891 finishing,
2892 scope: _,
2893 } = query::plan_root_query(scx, stmt.query, QueryLifetime::MaterializedView)?;
2894 assert!(HirRelationExpr::is_trivial_row_set_finishing_hir(
2896 &finishing,
2897 expr.arity()
2898 ));
2899 if expr.contains_parameters()? {
2900 return Err(PlanError::ParameterNotAllowed(
2901 "materialized views".to_string(),
2902 ));
2903 }
2904
2905 plan_utils::maybe_rename_columns_exact(
2906 scx.catalog,
2907 format!("materialized view {}", scx.catalog.resolve_full_name(&name)),
2908 &mut desc,
2909 &stmt.columns,
2910 )?;
2911 let column_names: Vec<ColumnName> = desc.iter_names().cloned().collect();
2912
2913 let MaterializedViewOptionExtracted {
2914 assert_not_null,
2915 partition_by,
2916 retain_history,
2917 refresh,
2918 seen: _,
2919 }: MaterializedViewOptionExtracted = stmt.with_options.try_into()?;
2920
2921 if let Some(partition_by) = partition_by {
2922 scx.require_feature_flag(&ENABLE_COLLECTION_PARTITION_BY)?;
2923 check_partition_by(&desc, partition_by)?;
2924 }
2925
2926 let refresh_schedule = {
2927 let mut refresh_schedule = RefreshSchedule::default();
2928 let mut on_commits_seen = 0;
2929 for refresh_option_value in refresh {
2930 if !matches!(refresh_option_value, RefreshOptionValue::OnCommit) {
2931 scx.require_feature_flag(&ENABLE_REFRESH_EVERY_MVS)?;
2932 }
2933 match refresh_option_value {
2934 RefreshOptionValue::OnCommit => {
2935 on_commits_seen += 1;
2936 }
2937 RefreshOptionValue::AtCreation => {
2938 soft_panic_or_log!("REFRESH AT CREATION should have been purified away");
2939 bail_internal!("REFRESH AT CREATION should have been purified away")
2940 }
2941 RefreshOptionValue::At(RefreshAtOptionValue { mut time }) => {
2942 transform_ast::transform(scx, &mut time)?; let ecx = &ExprContext {
2944 qcx: &QueryContext::root(scx, QueryLifetime::OneShot),
2945 name: "REFRESH AT",
2946 scope: &Scope::empty(),
2947 relation_type: &SqlRelationType::empty(),
2948 allow_aggregates: false,
2949 allow_subqueries: false,
2950 allow_parameters: false,
2951 allow_windows: false,
2952 };
2953 let hir = plan_expr(ecx, &time)?.cast_to(
2954 ecx,
2955 CastContext::Assignment,
2956 &SqlScalarType::MzTimestamp,
2957 )?;
2958 let timestamp = hir
2960 .into_literal_mz_timestamp()
2961 .ok_or_else(|| PlanError::InvalidRefreshAt)?;
2962 check_refresh_time("REFRESH AT", timestamp)?;
2963 refresh_schedule.ats.push(timestamp);
2964 }
2965 RefreshOptionValue::Every(RefreshEveryOptionValue {
2966 interval,
2967 aligned_to,
2968 }) => {
2969 let interval = Interval::try_from_value(Value::Interval(interval))?;
2970 if interval.as_microseconds() <= 0 {
2971 sql_bail!("REFRESH interval must be positive; got: {}", interval);
2972 }
2973 if interval.months != 0 {
2974 sql_bail!("REFRESH interval must not involve units larger than days");
2979 }
2980 let interval = interval.duration()?;
2981 if u64::try_from(interval.as_millis()).is_err()
2986 || Interval::from_duration(&interval).is_err()
2987 {
2988 sql_bail!("REFRESH interval too large");
2989 }
2990 if interval.as_micros() < 1000 {
2991 sql_bail!("REFRESH interval must be at least 1 ms")
2992 }
2993
2994 let mut aligned_to = match aligned_to {
2995 Some(aligned_to) => aligned_to,
2996 None => {
2997 soft_panic_or_log!(
2998 "ALIGNED TO should have been filled in by purification"
2999 );
3000 sql_bail!(
3001 "INTERNAL ERROR: ALIGNED TO should have been filled in by purification"
3002 )
3003 }
3004 };
3005
3006 transform_ast::transform(scx, &mut aligned_to)?;
3008
3009 let ecx = &ExprContext {
3010 qcx: &QueryContext::root(scx, QueryLifetime::OneShot),
3011 name: "REFRESH EVERY ... ALIGNED TO",
3012 scope: &Scope::empty(),
3013 relation_type: &SqlRelationType::empty(),
3014 allow_aggregates: false,
3015 allow_subqueries: false,
3016 allow_parameters: false,
3017 allow_windows: false,
3018 };
3019 let aligned_to_hir = plan_expr(ecx, &aligned_to)?.cast_to(
3020 ecx,
3021 CastContext::Assignment,
3022 &SqlScalarType::MzTimestamp,
3023 )?;
3024 let aligned_to_const = aligned_to_hir
3026 .into_literal_mz_timestamp()
3027 .ok_or_else(|| PlanError::InvalidRefreshEveryAlignedTo)?;
3028 check_refresh_time("REFRESH EVERY ... ALIGNED TO", aligned_to_const)?;
3029
3030 refresh_schedule.everies.push(RefreshEvery {
3031 interval,
3032 aligned_to: aligned_to_const,
3033 });
3034 }
3035 }
3036 }
3037
3038 if on_commits_seen > 1 {
3039 sql_bail!("REFRESH ON COMMIT cannot be specified multiple times");
3040 }
3041 if on_commits_seen > 0 && refresh_schedule != RefreshSchedule::default() {
3042 sql_bail!("REFRESH ON COMMIT is not compatible with any of the other REFRESH options");
3043 }
3044
3045 if refresh_schedule == RefreshSchedule::default() {
3046 None
3047 } else {
3048 Some(refresh_schedule)
3049 }
3050 };
3051
3052 let as_of = stmt.as_of.map(Timestamp::from);
3053 let compaction_window = plan_retain_history_option(scx, retain_history)?;
3054 let mut non_null_assertions = assert_not_null
3055 .into_iter()
3056 .map(normalize::column_name)
3057 .map(|assertion_name| {
3058 column_names
3059 .iter()
3060 .position(|col| col == &assertion_name)
3061 .ok_or_else(|| {
3062 sql_err!(
3063 "column {} in ASSERT NOT NULL option not found",
3064 assertion_name.quoted()
3065 )
3066 })
3067 })
3068 .collect::<Result<Vec<_>, _>>()?;
3069 non_null_assertions.sort();
3070 if let Some(dup) = non_null_assertions.iter().duplicates().next() {
3071 let dup = &column_names[*dup];
3072 sql_bail!("duplicate column {} in non-null assertions", dup.quoted());
3073 }
3074
3075 if let Some(dup) = column_names.iter().duplicates().next() {
3076 sql_bail!("column {} specified more than once", dup.quoted());
3077 }
3078
3079 let if_exists = match scx.pcx().map(|pcx| pcx.ignore_if_exists_errors) {
3082 Ok(true) => IfExistsBehavior::Skip,
3083 _ => stmt.if_exists,
3084 };
3085
3086 let mut replace = None;
3087 let mut if_not_exists = false;
3088 match if_exists {
3089 IfExistsBehavior::Replace => {
3090 let if_exists = true;
3091 let cascade = false;
3092 let replace_id = plan_drop_item(
3093 scx,
3094 ObjectType::MaterializedView,
3095 if_exists,
3096 partial_name.clone().into(),
3097 cascade,
3098 )?;
3099
3100 if let Some(id) = replace_id {
3102 let dependencies = expr.depends_on();
3103 let invalid_drop = scx
3104 .get_item(&id)
3105 .global_ids()
3106 .any(|gid| dependencies.contains(&gid));
3107 if invalid_drop {
3108 let item = scx.catalog.get_item(&id);
3109 sql_bail!(
3110 "cannot replace materialized view {0}: depended upon by new {0} definition",
3111 scx.catalog.resolve_full_name(item.name())
3112 );
3113 }
3114 replace = Some(id);
3115 }
3116 }
3117 IfExistsBehavior::Skip => if_not_exists = true,
3118 IfExistsBehavior::Error => (),
3119 }
3120 let drop_ids = replace
3121 .map(|id| {
3122 scx.catalog
3123 .item_dependents(id)
3124 .into_iter()
3125 .map(|id| id.unwrap_item_id())
3126 .collect()
3127 })
3128 .unwrap_or_default();
3129 let mut dependencies: BTreeSet<_> = expr
3130 .depends_on()
3131 .into_iter()
3132 .map(|gid| scx.catalog.resolve_item_id(&gid))
3133 .collect();
3134
3135 let mut replacement_target = None;
3137 if let Some(target_name) = &stmt.replacement_for {
3138 scx.require_feature_flag(&vars::ENABLE_REPLACEMENT_MATERIALIZED_VIEWS)?;
3139
3140 let target = scx.get_item_by_resolved_name(target_name)?;
3141 if target.item_type() != CatalogItemType::MaterializedView {
3142 return Err(PlanError::InvalidReplacement {
3143 item_type: target.item_type(),
3144 item_name: scx.catalog.minimal_qualification(target.name()),
3145 replacement_type: CatalogItemType::MaterializedView,
3146 replacement_name: partial_name,
3147 });
3148 }
3149 if target.id().is_system() {
3150 sql_bail!(
3151 "cannot replace {} because it is required by the database system",
3152 scx.catalog.minimal_qualification(target.name()),
3153 );
3154 }
3155
3156 for dependent in scx.catalog.item_dependents(target.id()) {
3158 if let ObjectId::Item(id) = dependent
3159 && dependencies.contains(&id)
3160 {
3161 sql_bail!(
3162 "replacement would cause {} to depend on itself",
3163 scx.catalog.minimal_qualification(target.name()),
3164 );
3165 }
3166 }
3167
3168 dependencies.insert(target.id());
3169
3170 for use_id in target.used_by() {
3171 let use_item = scx.get_item(use_id);
3172 if use_item.replacement_target() == Some(target.id()) {
3173 sql_bail!(
3174 "cannot replace {} because it already has a replacement: {}",
3175 scx.catalog.minimal_qualification(target.name()),
3176 scx.catalog.minimal_qualification(use_item.name()),
3177 );
3178 }
3179 }
3180
3181 replacement_target = Some(target.id());
3182 }
3183
3184 validate_view_dependencies(scx, &dependencies)?;
3185
3186 let full_name = scx.catalog.resolve_full_name(&name);
3188 let partial_name = PartialItemName::from(full_name.clone());
3189 if let (IfExistsBehavior::Error, Ok(item)) =
3192 (if_exists, scx.catalog.resolve_item_or_type(&partial_name))
3193 {
3194 return Err(PlanError::ItemAlreadyExists {
3195 name: full_name.to_string(),
3196 item_type: item.item_type(),
3197 });
3198 }
3199
3200 Ok(Plan::CreateMaterializedView(CreateMaterializedViewPlan {
3201 name,
3202 materialized_view: MaterializedView {
3203 create_sql,
3204 expr,
3205 dependencies: DependencyIds(dependencies),
3206 column_names,
3207 replacement_target,
3208 cluster_id,
3209 target_replica,
3210 non_null_assertions,
3211 compaction_window,
3212 refresh_schedule,
3213 as_of,
3214 },
3215 replace,
3216 drop_ids,
3217 if_not_exists,
3218 ambiguous_columns: *scx.ambiguous_columns.borrow(),
3219 }))
3220}
3221
3222generate_extracted_config!(
3223 MaterializedViewOption,
3224 (AssertNotNull, Ident, AllowMultiple),
3225 (PartitionBy, Vec<Ident>),
3226 (RetainHistory, OptionalDuration),
3227 (Refresh, RefreshOptionValue<Aug>, AllowMultiple)
3228);
3229
3230pub fn describe_create_sink(
3231 _: &StatementContext,
3232 _: CreateSinkStatement<Aug>,
3233) -> Result<StatementDesc, PlanError> {
3234 Ok(StatementDesc::new(None))
3235}
3236
3237generate_extracted_config!(
3238 CreateSinkOption,
3239 (Snapshot, bool),
3240 (PartitionStrategy, String),
3241 (Version, u64),
3242 (CommitInterval, Duration)
3243);
3244
3245pub fn plan_create_sink(
3246 scx: &StatementContext,
3247 stmt: CreateSinkStatement<Aug>,
3248) -> Result<Plan, PlanError> {
3249 let Some(name) = stmt.name.clone() else {
3251 return Err(PlanError::MissingName(CatalogItemType::Sink));
3252 };
3253 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
3254 let full_name = scx.catalog.resolve_full_name(&name);
3255 let partial_name = PartialItemName::from(full_name.clone());
3256 if let (false, Ok(item)) = (stmt.if_not_exists, scx.catalog.resolve_item(&partial_name)) {
3257 return Err(PlanError::ItemAlreadyExists {
3258 name: full_name.to_string(),
3259 item_type: item.item_type(),
3260 });
3261 }
3262
3263 plan_sink(scx, stmt)
3264}
3265
3266fn plan_sink(
3271 scx: &StatementContext,
3272 mut stmt: CreateSinkStatement<Aug>,
3273) -> Result<Plan, PlanError> {
3274 let CreateSinkStatement {
3275 name,
3276 in_cluster: _,
3277 from,
3278 connection,
3279 format,
3280 envelope,
3281 mode,
3282 if_not_exists,
3283 with_options,
3284 } = stmt.clone();
3285
3286 let Some(name) = name else {
3287 return Err(PlanError::MissingName(CatalogItemType::Sink));
3288 };
3289 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
3290
3291 let envelope = match (&connection, envelope, mode) {
3292 (CreateSinkConnection::Kafka { .. }, Some(ast::SinkEnvelope::Upsert), None) => {
3294 SinkEnvelope::Upsert
3295 }
3296 (CreateSinkConnection::Kafka { .. }, Some(ast::SinkEnvelope::Debezium), None) => {
3297 SinkEnvelope::Debezium
3298 }
3299 (CreateSinkConnection::Kafka { .. }, None, None) => {
3300 sql_bail!("ENVELOPE clause is required")
3301 }
3302 (CreateSinkConnection::Kafka { .. }, _, Some(_)) => {
3303 sql_bail!("MODE is not supported for Kafka sinks, use ENVELOPE instead")
3304 }
3305 (CreateSinkConnection::Iceberg { .. }, None, Some(ast::IcebergSinkMode::Upsert)) => {
3307 SinkEnvelope::Upsert
3308 }
3309 (CreateSinkConnection::Iceberg { .. }, None, Some(ast::IcebergSinkMode::Append)) => {
3310 SinkEnvelope::Append
3311 }
3312 (CreateSinkConnection::Iceberg { .. }, None, None) => {
3313 sql_bail!("MODE clause is required")
3314 }
3315 (CreateSinkConnection::Iceberg { .. }, Some(_), _) => {
3316 sql_bail!("ENVELOPE is not supported for Iceberg sinks, use MODE instead")
3317 }
3318 };
3319
3320 let from_name = &from;
3321 let from = scx.get_item_by_resolved_name(&from)?;
3322
3323 {
3324 use CatalogItemType::*;
3325 match from.item_type() {
3326 Table | Source | MaterializedView => {
3327 if from.replacement_target().is_some() {
3328 let name = scx.catalog.minimal_qualification(from.name());
3329 return Err(PlanError::InvalidSinkFrom {
3330 name: name.to_string(),
3331 item_type: format!("replacement {}", from.item_type()),
3332 });
3333 }
3334 }
3335 Sink | MetricSink | View | Index | Type | Func | Secret | Connection => {
3336 let name = scx.catalog.minimal_qualification(from.name());
3337 return Err(PlanError::InvalidSinkFrom {
3338 name: name.to_string(),
3339 item_type: from.item_type().to_string(),
3340 });
3341 }
3342 }
3343 }
3344
3345 if from.id().is_system() {
3346 bail_unsupported!("creating a sink directly on a catalog object");
3347 }
3348
3349 let desc = from
3350 .relation_desc()
3351 .ok_or_else(|| sql_err!("item does not have a relation description"))?;
3352 let key_indices = match &connection {
3353 CreateSinkConnection::Kafka { key: Some(key), .. }
3354 | CreateSinkConnection::Iceberg { key: Some(key), .. } => {
3355 let key_columns = key
3356 .key_columns
3357 .clone()
3358 .into_iter()
3359 .map(normalize::column_name)
3360 .collect::<Vec<_>>();
3361 let mut uniq = BTreeSet::new();
3362 for col in key_columns.iter() {
3363 if !uniq.insert(col) {
3364 sql_bail!("duplicate column referenced in KEY: {}", col);
3365 }
3366 }
3367 let indices = key_columns
3368 .iter()
3369 .map(|col| {
3370 let name_idx =
3371 desc.get_by_name(col)
3372 .map(|(idx, _type)| idx)
3373 .ok_or_else(|| {
3374 sql_err!("column referenced in KEY does not exist: {}", col)
3375 })?;
3376 if desc.get_unambiguous_name(name_idx).is_none() {
3377 sql_bail!("column referenced in KEY is ambiguous: {}", col);
3378 }
3379 Ok(name_idx)
3380 })
3381 .collect::<Result<Vec<_>, _>>()?;
3382
3383 if matches!(&connection, CreateSinkConnection::Iceberg { .. }) {
3386 let cols: Vec<_> = desc.iter().collect();
3387 for &idx in &indices {
3388 let (col_name, col_type) = cols[idx];
3389 let scalar = &col_type.scalar_type;
3390 let is_valid = matches!(
3391 scalar,
3392 SqlScalarType::Bool
3394 | SqlScalarType::Int16
3395 | SqlScalarType::Int32
3396 | SqlScalarType::Int64
3397 | SqlScalarType::UInt16
3398 | SqlScalarType::UInt32
3399 | SqlScalarType::UInt64
3400 | SqlScalarType::Numeric { .. }
3402 | SqlScalarType::Date
3404 | SqlScalarType::Time
3405 | SqlScalarType::Timestamp { .. }
3406 | SqlScalarType::TimestampTz { .. }
3407 | SqlScalarType::Interval
3408 | SqlScalarType::MzTimestamp
3409 | SqlScalarType::String
3411 | SqlScalarType::Char { .. }
3412 | SqlScalarType::VarChar { .. }
3413 | SqlScalarType::PgLegacyChar
3414 | SqlScalarType::PgLegacyName
3415 | SqlScalarType::Bytes
3416 | SqlScalarType::Jsonb
3417 | SqlScalarType::Uuid
3419 | SqlScalarType::Oid
3420 | SqlScalarType::RegProc
3421 | SqlScalarType::RegType
3422 | SqlScalarType::RegClass
3423 | SqlScalarType::MzAclItem
3424 | SqlScalarType::AclItem
3425 | SqlScalarType::Int2Vector
3426 );
3427 if !is_valid {
3428 return Err(PlanError::IcebergSinkUnsupportedKeyType {
3429 column: col_name.to_string(),
3430 column_type: format!("{:?}", scalar),
3431 });
3432 }
3433 }
3434 }
3435
3436 let is_valid_key = desc
3437 .typ()
3438 .keys
3439 .iter()
3440 .any(|key_columns| key_columns.iter().all(|column| indices.contains(column)));
3441
3442 if !is_valid_key && envelope == SinkEnvelope::Upsert {
3443 if key.not_enforced || scx.pcx.is_none() {
3448 scx.catalog
3449 .add_notice(PlanNotice::UpsertSinkKeyNotEnforced {
3450 key: key_columns.clone(),
3451 name: name.item.clone(),
3452 })
3453 } else {
3454 return Err(PlanError::UpsertSinkWithInvalidKey {
3455 name: from_name.full_name_str(),
3456 desired_key: key_columns.iter().map(|c| c.to_string()).collect(),
3457 valid_keys: desc
3458 .typ()
3459 .keys
3460 .iter()
3461 .map(|key| {
3462 key.iter()
3463 .map(|col| desc.get_name(*col).as_str().into())
3464 .collect()
3465 })
3466 .collect(),
3467 });
3468 }
3469 }
3470 Some(indices)
3471 }
3472 CreateSinkConnection::Kafka { key: None, .. }
3473 | CreateSinkConnection::Iceberg { key: None, .. } => None,
3474 };
3475
3476 if key_indices.is_some() && envelope == SinkEnvelope::Append {
3477 sql_bail!("KEY is not supported for MODE APPEND Iceberg sinks");
3478 }
3479
3480 if envelope == SinkEnvelope::Append {
3482 if let CreateSinkConnection::Iceberg { .. } = &connection {
3483 use mz_storage_types::sinks::{
3484 ICEBERG_APPEND_DIFF_COLUMN, ICEBERG_APPEND_TIMESTAMP_COLUMN,
3485 };
3486 for (col_name, _) in desc.iter() {
3487 if col_name.as_str() == ICEBERG_APPEND_DIFF_COLUMN
3488 || col_name.as_str() == ICEBERG_APPEND_TIMESTAMP_COLUMN
3489 {
3490 sql_bail!(
3491 "column {} conflicts with the system column that MODE APPEND \
3492 adds to the Iceberg table",
3493 col_name.quoted()
3494 );
3495 }
3496 }
3497 }
3498 }
3499
3500 let headers_index = match &connection {
3501 CreateSinkConnection::Kafka {
3502 headers: Some(headers),
3503 ..
3504 } => {
3505 scx.require_feature_flag(&ENABLE_KAFKA_SINK_HEADERS)?;
3506
3507 match envelope {
3508 SinkEnvelope::Upsert | SinkEnvelope::Append => (),
3509 SinkEnvelope::Debezium => {
3510 sql_bail!("HEADERS option is not supported with ENVELOPE DEBEZIUM")
3511 }
3512 };
3513
3514 let headers = normalize::column_name(headers.clone());
3515 let (idx, ty) = desc
3516 .get_by_name(&headers)
3517 .ok_or_else(|| sql_err!("HEADERS column ({}) is unknown", headers))?;
3518
3519 if desc.get_unambiguous_name(idx).is_none() {
3520 sql_bail!("HEADERS column ({}) is ambiguous", headers);
3521 }
3522
3523 match &ty.scalar_type {
3524 SqlScalarType::Map { value_type, .. }
3525 if matches!(&**value_type, SqlScalarType::String | SqlScalarType::Bytes) => {}
3526 _ => sql_bail!(
3527 "HEADERS column must have type map[text => text] or map[text => bytea]"
3528 ),
3529 }
3530
3531 Some(idx)
3532 }
3533 _ => None,
3534 };
3535
3536 let relation_key_indices = desc.typ().keys.get(0).cloned();
3538
3539 let key_desc_and_indices = key_indices.map(|key_indices| {
3540 let cols = desc
3541 .iter()
3542 .map(|(name, ty)| (name.clone(), ty.clone()))
3543 .collect::<Vec<_>>();
3544 let (names, types): (Vec<_>, Vec<_>) =
3545 key_indices.iter().map(|&idx| cols[idx].clone()).unzip();
3546 let typ = SqlRelationType::new(types);
3547 (RelationDesc::new(typ, names), key_indices)
3548 });
3549
3550 if key_desc_and_indices.is_none() && envelope == SinkEnvelope::Upsert {
3551 return Err(PlanError::UpsertSinkWithoutKey);
3552 }
3553
3554 let CreateSinkOptionExtracted {
3555 snapshot,
3556 version,
3557 partition_strategy: _,
3558 seen: _,
3559 commit_interval,
3560 } = with_options.try_into()?;
3561
3562 let connection_builder = match connection {
3563 CreateSinkConnection::Kafka {
3564 connection,
3565 options,
3566 ..
3567 } => kafka_sink_builder(
3568 scx,
3569 connection,
3570 options,
3571 format,
3572 relation_key_indices,
3573 key_desc_and_indices,
3574 headers_index,
3575 desc.into_owned(),
3576 envelope,
3577 from.id(),
3578 commit_interval,
3579 )?,
3580 CreateSinkConnection::Iceberg {
3581 catalog_connection,
3582 aws_connection,
3583 options,
3584 ..
3585 } => iceberg_sink_builder(
3586 scx,
3587 catalog_connection,
3588 aws_connection,
3589 options,
3590 relation_key_indices,
3591 key_desc_and_indices,
3592 commit_interval,
3593 &desc,
3594 )?,
3595 };
3596
3597 let with_snapshot = snapshot.unwrap_or(true);
3599 let version = version.unwrap_or(0);
3601
3602 let in_cluster = source_sink_cluster_config(scx, &mut stmt.in_cluster)?;
3606 let create_sql = normalize::create_statement(scx, Statement::CreateSink(stmt))?;
3607
3608 Ok(Plan::CreateSink(CreateSinkPlan {
3609 name,
3610 sink: Sink {
3611 create_sql,
3612 from: from.global_id(),
3613 connection: connection_builder,
3614 envelope,
3615 version,
3616 commit_interval,
3617 },
3618 with_snapshot,
3619 if_not_exists,
3620 in_cluster: in_cluster.id(),
3621 }))
3622}
3623
3624fn key_constraint_err(desc: &RelationDesc, user_keys: &[ColumnName]) -> PlanError {
3625 let user_keys = user_keys.iter().map(|column| column.as_str()).join(", ");
3626
3627 let existing_keys = desc
3628 .typ()
3629 .keys
3630 .iter()
3631 .map(|key_columns| {
3632 key_columns
3633 .iter()
3634 .map(|col| desc.get_name(*col).as_str())
3635 .join(", ")
3636 })
3637 .join(", ");
3638
3639 sql_err!(
3640 "Key constraint ({}) conflicts with existing key ({})",
3641 user_keys,
3642 existing_keys
3643 )
3644}
3645
3646#[derive(Debug, Default, PartialEq, Clone)]
3649pub struct CsrConfigOptionExtracted {
3650 seen: ::std::collections::BTreeSet<CsrConfigOptionName<Aug>>,
3651 pub(crate) avro_key_fullname: Option<String>,
3652 pub(crate) avro_value_fullname: Option<String>,
3653 pub(crate) null_defaults: bool,
3654 pub(crate) value_doc_options: BTreeMap<DocTarget, String>,
3655 pub(crate) key_doc_options: BTreeMap<DocTarget, String>,
3656 pub(crate) key_compatibility_level: Option<mz_ccsr::CompatibilityLevel>,
3657 pub(crate) value_compatibility_level: Option<mz_ccsr::CompatibilityLevel>,
3658}
3659
3660impl std::convert::TryFrom<Vec<CsrConfigOption<Aug>>> for CsrConfigOptionExtracted {
3661 type Error = crate::plan::PlanError;
3662 fn try_from(v: Vec<CsrConfigOption<Aug>>) -> Result<CsrConfigOptionExtracted, Self::Error> {
3663 let mut extracted = CsrConfigOptionExtracted::default();
3664 let mut common_doc_comments = BTreeMap::new();
3665 for option in v {
3666 if !extracted.seen.insert(option.name.clone()) {
3667 return Err(PlanError::Unstructured({
3668 format!("{} specified more than once", option.name)
3669 }));
3670 }
3671 let option_name = option.name.clone();
3672 let option_name_str = option_name.to_ast_string_simple();
3673 let better_error = |e: PlanError| PlanError::InvalidOptionValue {
3674 option_name: option_name.to_ast_string_simple(),
3675 err: e.into(),
3676 };
3677 let to_compatibility_level = |val: Option<WithOptionValue<Aug>>| {
3678 val.map(|s| match s {
3679 WithOptionValue::Value(Value::String(s)) => {
3680 mz_ccsr::CompatibilityLevel::try_from(s.to_uppercase().as_str())
3681 }
3682 _ => Err("must be a string".to_string()),
3683 })
3684 .transpose()
3685 .map_err(PlanError::Unstructured)
3686 .map_err(better_error)
3687 };
3688 match option.name {
3689 CsrConfigOptionName::AvroKeyFullname => {
3690 extracted.avro_key_fullname =
3691 <Option<String>>::try_from_value(option.value).map_err(better_error)?;
3692 }
3693 CsrConfigOptionName::AvroValueFullname => {
3694 extracted.avro_value_fullname =
3695 <Option<String>>::try_from_value(option.value).map_err(better_error)?;
3696 }
3697 CsrConfigOptionName::NullDefaults => {
3698 extracted.null_defaults =
3699 <bool>::try_from_value(option.value).map_err(better_error)?;
3700 }
3701 CsrConfigOptionName::AvroDocOn(doc_on) => {
3702 let value = String::try_from_value(option.value.ok_or_else(|| {
3703 PlanError::InvalidOptionValue {
3704 option_name: option_name_str,
3705 err: Box::new(PlanError::Unstructured("cannot be empty".to_string())),
3706 }
3707 })?)
3708 .map_err(better_error)?;
3709 let key = match doc_on.identifier {
3710 DocOnIdentifier::Column(ast::ColumnName {
3711 relation: ResolvedItemName::Item { id, .. },
3712 column: ResolvedColumnReference::Column { name, index: _ },
3713 }) => DocTarget::Field {
3714 object_id: id,
3715 column_name: name,
3716 },
3717 DocOnIdentifier::Type(ResolvedItemName::Item { id, .. }) => {
3718 DocTarget::Type(id)
3719 }
3720 _ => sql_bail!("invalid DOC ON identifier"),
3721 };
3722
3723 match doc_on.for_schema {
3724 DocOnSchema::KeyOnly => {
3725 extracted.key_doc_options.insert(key, value);
3726 }
3727 DocOnSchema::ValueOnly => {
3728 extracted.value_doc_options.insert(key, value);
3729 }
3730 DocOnSchema::All => {
3731 common_doc_comments.insert(key, value);
3732 }
3733 }
3734 }
3735 CsrConfigOptionName::KeyCompatibilityLevel => {
3736 extracted.key_compatibility_level = to_compatibility_level(option.value)?;
3737 }
3738 CsrConfigOptionName::ValueCompatibilityLevel => {
3739 extracted.value_compatibility_level = to_compatibility_level(option.value)?;
3740 }
3741 }
3742 }
3743
3744 for (key, value) in common_doc_comments {
3745 if !extracted.key_doc_options.contains_key(&key) {
3746 extracted.key_doc_options.insert(key.clone(), value.clone());
3747 }
3748 if !extracted.value_doc_options.contains_key(&key) {
3749 extracted.value_doc_options.insert(key, value);
3750 }
3751 }
3752 Ok(extracted)
3753 }
3754}
3755
3756fn iceberg_sink_builder(
3757 scx: &StatementContext,
3758 catalog_connection: ResolvedItemName,
3759 storage_connection: Option<ResolvedItemName>,
3760 options: Vec<IcebergSinkConfigOption<Aug>>,
3761 relation_key_indices: Option<Vec<usize>>,
3762 key_desc_and_indices: Option<(RelationDesc, Vec<usize>)>,
3763 commit_interval: Option<Duration>,
3764 desc: &RelationDesc,
3765) -> Result<StorageSinkConnection<ReferencedConnection>, PlanError> {
3766 ArrowBuilder::validate_desc_for_parquet(desc, iceberg_type_overrides)
3770 .map_err(|e| sql_err!("{}", e))?;
3771
3772 let catalog_connection_item = scx.get_item_by_resolved_name(&catalog_connection)?;
3773 let catalog_connection_id = catalog_connection_item.id();
3774 if !matches!(
3775 catalog_connection_item.connection()?,
3776 Connection::IcebergCatalog(_)
3777 ) {
3778 sql_bail!(
3779 "{} is not an iceberg catalog connection",
3780 scx.catalog
3781 .resolve_full_name(catalog_connection_item.name())
3782 .to_string()
3783 .quoted()
3784 );
3785 };
3786
3787 let storage_connection_item = storage_connection
3788 .map(|c| scx.get_item_by_resolved_name(&c))
3789 .transpose()?;
3790 let storage_connection_id = storage_connection_item.as_ref().map(|c| c.id());
3791 if let Some(c) = &storage_connection_item
3792 && !matches!(c.connection()?, Connection::Aws(_))
3793 {
3794 sql_bail!(
3795 "{} is not an AWS connection",
3796 scx.catalog.resolve_full_name(c.name()).to_string().quoted()
3797 );
3798 }
3799
3800 let IcebergSinkConfigOptionExtracted {
3801 table,
3802 namespace,
3803 seen: _,
3804 }: IcebergSinkConfigOptionExtracted = options.try_into()?;
3805
3806 let Some(table) = table else {
3807 sql_bail!("Iceberg sink must specify TABLE");
3808 };
3809 let Some(namespace) = namespace else {
3810 sql_bail!("Iceberg sink must specify NAMESPACE");
3811 };
3812 match commit_interval {
3813 None => sql_bail!("Iceberg sink must specify COMMIT INTERVAL"),
3814 Some(interval) if interval < Duration::from_secs(1) => {
3820 sql_bail!("COMMIT INTERVAL must be at least 1 second")
3821 }
3822 Some(_) => {}
3823 }
3824
3825 Ok(StorageSinkConnection::Iceberg(IcebergSinkConnection {
3826 catalog_connection_id,
3827 catalog_connection: catalog_connection_id,
3828 storage_connection_id,
3829 storage_connection: storage_connection_id,
3830 table,
3831 namespace,
3832 relation_key_indices,
3833 key_desc_and_indices,
3834 }))
3835}
3836
3837fn kafka_sink_builder(
3838 scx: &StatementContext,
3839 connection: ResolvedItemName,
3840 options: Vec<KafkaSinkConfigOption<Aug>>,
3841 format: Option<FormatSpecifier<Aug>>,
3842 relation_key_indices: Option<Vec<usize>>,
3843 key_desc_and_indices: Option<(RelationDesc, Vec<usize>)>,
3844 headers_index: Option<usize>,
3845 value_desc: RelationDesc,
3846 envelope: SinkEnvelope,
3847 sink_from: CatalogItemId,
3848 commit_interval: Option<Duration>,
3849) -> Result<StorageSinkConnection<ReferencedConnection>, PlanError> {
3850 let connection_item = scx.get_item_by_resolved_name(&connection)?;
3852 let connection_id = connection_item.id();
3853 match connection_item.connection()? {
3854 Connection::Kafka(_) => (),
3855 _ => sql_bail!(
3856 "{} is not a kafka connection",
3857 scx.catalog.resolve_full_name(connection_item.name())
3858 ),
3859 };
3860
3861 if commit_interval.is_some() {
3862 sql_bail!("COMMIT INTERVAL option is not supported with KAFKA sinks");
3863 }
3864
3865 let KafkaSinkConfigOptionExtracted {
3866 topic,
3867 compression_type,
3868 partition_by,
3869 progress_group_id_prefix,
3870 transactional_id_prefix,
3871 legacy_ids,
3872 topic_config,
3873 topic_metadata_refresh_interval,
3874 topic_partition_count,
3875 topic_replication_factor,
3876 seen: _,
3877 }: KafkaSinkConfigOptionExtracted = options.try_into()?;
3878
3879 let transactional_id = match (transactional_id_prefix, legacy_ids) {
3880 (Some(_), Some(true)) => {
3881 sql_bail!("LEGACY IDS cannot be used at the same time as TRANSACTIONAL ID PREFIX")
3882 }
3883 (None, Some(true)) => KafkaIdStyle::Legacy,
3884 (prefix, _) => KafkaIdStyle::Prefix(prefix),
3885 };
3886
3887 let progress_group_id = match (progress_group_id_prefix, legacy_ids) {
3888 (Some(_), Some(true)) => {
3889 sql_bail!("LEGACY IDS cannot be used at the same time as PROGRESS GROUP ID PREFIX")
3890 }
3891 (None, Some(true)) => KafkaIdStyle::Legacy,
3892 (prefix, _) => KafkaIdStyle::Prefix(prefix),
3893 };
3894
3895 let topic_name = topic.ok_or_else(|| sql_err!("KAFKA CONNECTION must specify TOPIC"))?;
3896
3897 if topic_metadata_refresh_interval > MAX_KAFKA_TOPIC_METADATA_REFRESH_INTERVAL {
3898 sql_bail!("TOPIC METADATA REFRESH INTERVAL cannot be greater than 1 hour");
3901 } else if topic_metadata_refresh_interval < MIN_KAFKA_TOPIC_METADATA_REFRESH_INTERVAL {
3902 sql_bail!("TOPIC METADATA REFRESH INTERVAL must be at least 1 second");
3905 }
3906
3907 let assert_positive = |val: Option<i32>, name: &str| {
3908 if let Some(val) = val {
3909 if val <= 0 {
3910 sql_bail!("{} must be a positive integer", name);
3911 }
3912 }
3913 val.map(NonNeg::try_from)
3914 .transpose()
3915 .map_err(|_| PlanError::Unstructured(format!("{} must be a positive integer", name)))
3916 };
3917 let topic_partition_count = assert_positive(topic_partition_count, "TOPIC PARTITION COUNT")?;
3918 let topic_replication_factor =
3919 assert_positive(topic_replication_factor, "TOPIC REPLICATION FACTOR")?;
3920
3921 let gen_avro_schema_options = |conn| {
3924 let CsrConnectionAvro {
3925 connection:
3926 CsrConnection {
3927 connection,
3928 options,
3929 },
3930 seed,
3931 key_strategy,
3932 value_strategy,
3933 } = conn;
3934 if seed.is_some() {
3935 sql_bail!("SEED option does not make sense with sinks");
3936 }
3937 if key_strategy.is_some() {
3938 sql_bail!("KEY STRATEGY option does not make sense with sinks");
3939 }
3940 if value_strategy.is_some() {
3941 sql_bail!("VALUE STRATEGY option does not make sense with sinks");
3942 }
3943
3944 let item = scx.get_item_by_resolved_name(&connection)?;
3945 let csr_connection = match item.connection()? {
3946 Connection::Csr(_) => item.id(),
3947 _ => {
3948 sql_bail!(
3949 "{} is not a schema registry connection",
3950 scx.catalog
3951 .resolve_full_name(item.name())
3952 .to_string()
3953 .quoted()
3954 )
3955 }
3956 };
3957 let extracted_options: CsrConfigOptionExtracted = options.try_into()?;
3958
3959 if key_desc_and_indices.is_none() && extracted_options.avro_key_fullname.is_some() {
3960 sql_bail!("Cannot specify AVRO KEY FULLNAME without a corresponding KEY field");
3961 }
3962
3963 if key_desc_and_indices.is_some()
3964 && (extracted_options.avro_key_fullname.is_some()
3965 ^ extracted_options.avro_value_fullname.is_some())
3966 {
3967 sql_bail!(
3968 "Must specify both AVRO KEY FULLNAME and AVRO VALUE FULLNAME when specifying generated schema names"
3969 );
3970 }
3971
3972 Ok((csr_connection, extracted_options))
3973 };
3974
3975 let map_format = |format: Format<Aug>, desc: &RelationDesc, is_key: bool| match format {
3976 Format::Json { array: false } => Ok::<_, PlanError>(KafkaSinkFormatType::Json),
3977 Format::Bytes if desc.arity() == 1 => {
3978 let col_type = &desc.typ().column_types[0].scalar_type;
3979 if !mz_pgrepr::Value::can_encode_binary(col_type) {
3980 bail_unsupported!(format!(
3981 "BYTES format with non-encodable type: {:?}",
3982 col_type
3983 ));
3984 }
3985
3986 Ok(KafkaSinkFormatType::Bytes)
3987 }
3988 Format::Text if desc.arity() == 1 => Ok(KafkaSinkFormatType::Text),
3989 Format::Bytes | Format::Text => {
3990 bail_unsupported!("BYTES or TEXT format with multiple columns")
3991 }
3992 Format::Json { array: true } => bail_unsupported!("JSON ARRAY format in sinks"),
3993 Format::Avro(AvroSchema::Csr { csr_connection }) => {
3994 let (csr_connection, options) = gen_avro_schema_options(csr_connection)?;
3995 let schema = if is_key {
3996 AvroSchemaGenerator::new(
3997 desc.clone(),
3998 false,
3999 options.key_doc_options,
4000 options.avro_key_fullname.as_deref().unwrap_or("row"),
4001 options.null_defaults,
4002 Some(sink_from),
4003 false,
4004 )?
4005 .schema()
4006 .to_string()
4007 } else {
4008 AvroSchemaGenerator::new(
4009 desc.clone(),
4010 matches!(envelope, SinkEnvelope::Debezium),
4011 options.value_doc_options,
4012 options.avro_value_fullname.as_deref().unwrap_or("envelope"),
4013 options.null_defaults,
4014 Some(sink_from),
4015 true,
4016 )?
4017 .schema()
4018 .to_string()
4019 };
4020 Ok(KafkaSinkFormatType::Avro {
4021 schema,
4022 compatibility_level: if is_key {
4023 options.key_compatibility_level
4024 } else {
4025 options.value_compatibility_level
4026 },
4027 schema_name: None,
4029 wire_format: WireFormat::Confluent {
4030 registry: Some(csr_connection),
4031 },
4032 })
4033 }
4034 Format::Avro(AvroSchema::Glue {
4035 connection,
4036 with_options,
4037 seed,
4038 }) => {
4039 if seed.is_some() {
4040 sql_bail!("SEED option does not make sense with sinks");
4041 }
4042
4043 let extracted: GlueAvroOptionExtracted = with_options.try_into()?;
4044 let GlueAvroOptionExtracted {
4045 schema_name,
4046 key_schema_name,
4047 value_schema_name,
4048 key_compatibility_level,
4049 value_compatibility_level,
4050 seen: _,
4051 } = extracted;
4052
4053 if schema_name.is_some() {
4058 sql_bail!(
4059 "SCHEMA NAME is not supported for AWS Glue Schema Registry sinks, \
4060 use KEY SCHEMA NAME and VALUE SCHEMA NAME instead"
4061 );
4062 }
4063
4064 if key_desc_and_indices.is_none()
4070 && (key_schema_name.is_some() || key_compatibility_level.is_some())
4071 {
4072 sql_bail!(
4073 "KEY SCHEMA NAME and KEY COMPATIBILITY LEVEL require a corresponding KEY field"
4074 );
4075 }
4076
4077 let item = scx.get_item_by_resolved_name(&connection)?;
4078 let glue_connection = match item.connection()? {
4079 Connection::GlueSchemaRegistry(_) => item.id(),
4080 _ => {
4081 sql_bail!(
4082 "{} is not an AWS Glue Schema Registry connection",
4083 scx.catalog
4084 .resolve_full_name(item.name())
4085 .to_string()
4086 .quoted()
4087 )
4088 }
4089 };
4090
4091 let compatibility_level = {
4095 let raw = if is_key {
4096 key_compatibility_level
4097 } else {
4098 value_compatibility_level
4099 };
4100 raw.map(|s| {
4101 mz_ccsr::CompatibilityLevel::try_from(s.to_uppercase().as_str())
4102 .map_err(PlanError::Unstructured)
4103 })
4104 .transpose()?
4105 };
4106
4107 let schema_name = if is_key {
4111 key_schema_name
4112 } else {
4113 value_schema_name
4114 };
4115
4116 let schema = if is_key {
4119 AvroSchemaGenerator::new(
4120 desc.clone(),
4121 false,
4122 Default::default(),
4123 "row",
4124 false,
4125 Some(sink_from),
4126 false,
4127 )?
4128 .schema()
4129 .to_string()
4130 } else {
4131 AvroSchemaGenerator::new(
4132 desc.clone(),
4133 matches!(envelope, SinkEnvelope::Debezium),
4134 Default::default(),
4135 "envelope",
4136 false,
4137 Some(sink_from),
4138 true,
4139 )?
4140 .schema()
4141 .to_string()
4142 };
4143 Ok(KafkaSinkFormatType::Avro {
4144 schema,
4145 compatibility_level,
4146 schema_name,
4147 wire_format: WireFormat::Glue {
4148 registry: Some(glue_connection),
4149 },
4150 })
4151 }
4152 format => bail_unsupported!(format!("sink format {:?}", format)),
4153 };
4154
4155 let partition_by = match &partition_by {
4156 Some(partition_by) => {
4157 let mut scope = Scope::from_source(None, value_desc.iter_names());
4158
4159 match envelope {
4160 SinkEnvelope::Upsert | SinkEnvelope::Append => (),
4161 SinkEnvelope::Debezium => {
4162 let key_indices: HashSet<_> = key_desc_and_indices
4163 .as_ref()
4164 .map(|(_desc, indices)| indices.as_slice())
4165 .unwrap_or_default()
4166 .into_iter()
4167 .collect();
4168 for (i, item) in scope.items.iter_mut().enumerate() {
4169 if !key_indices.contains(&i) {
4170 item.error_if_referenced = Some(|_table, column| {
4171 PlanError::InvalidPartitionByEnvelopeDebezium {
4172 column_name: column.to_string(),
4173 }
4174 });
4175 }
4176 }
4177 }
4178 };
4179
4180 let ecx = &ExprContext {
4181 qcx: &QueryContext::root(scx, QueryLifetime::OneShot),
4182 name: "PARTITION BY",
4183 scope: &scope,
4184 relation_type: value_desc.typ(),
4185 allow_aggregates: false,
4186 allow_subqueries: false,
4187 allow_parameters: false,
4188 allow_windows: false,
4189 };
4190 let expr = plan_expr(ecx, partition_by)?.cast_to(
4191 ecx,
4192 CastContext::Assignment,
4193 &SqlScalarType::UInt64,
4194 )?;
4195 let expr = expr.lower_uncorrelated(scx.catalog.system_vars())?;
4196
4197 Some(expr)
4198 }
4199 _ => None,
4200 };
4201
4202 let format = match format {
4204 Some(FormatSpecifier::KeyValue { key, value }) => {
4205 let key_format = match key_desc_and_indices.as_ref() {
4206 Some((desc, _indices)) => Some(map_format(key, desc, true)?),
4207 None => None,
4208 };
4209 KafkaSinkFormat {
4210 value_format: map_format(value, &value_desc, false)?,
4211 key_format,
4212 }
4213 }
4214 Some(FormatSpecifier::Bare(format)) => {
4215 let key_format = match key_desc_and_indices.as_ref() {
4216 Some((desc, _indices)) => Some(map_format(format.clone(), desc, true)?),
4217 None => None,
4218 };
4219 KafkaSinkFormat {
4220 value_format: map_format(format, &value_desc, false)?,
4221 key_format,
4222 }
4223 }
4224 None => bail_unsupported!("sink without format"),
4225 };
4226
4227 Ok(StorageSinkConnection::Kafka(KafkaSinkConnection {
4228 connection_id,
4229 connection: connection_id,
4230 format,
4231 topic: topic_name,
4232 relation_key_indices,
4233 key_desc_and_indices,
4234 headers_index,
4235 value_desc,
4236 partition_by,
4237 compression_type,
4238 progress_group_id,
4239 transactional_id,
4240 topic_options: KafkaTopicOptions {
4241 partition_count: topic_partition_count,
4242 replication_factor: topic_replication_factor,
4243 topic_config: topic_config.unwrap_or_default(),
4244 },
4245 topic_metadata_refresh_interval,
4246 }))
4247}
4248
4249pub fn describe_create_metric_sink(
4250 _: &StatementContext,
4251 _: CreateMetricSinkStatement<Aug>,
4252) -> Result<StatementDesc, PlanError> {
4253 Ok(StatementDesc::new(None))
4254}
4255
4256const METRIC_SINK_SOURCE_COLUMNS: &[(&str, fn(&SqlScalarType) -> bool)] = &[
4260 ("metric_name", |t| matches!(t, SqlScalarType::String)),
4261 ("metric_type", |t| matches!(t, SqlScalarType::String)),
4262 (
4263 "labels",
4264 |t| matches!(t, SqlScalarType::Map { value_type, .. } if matches!(**value_type, SqlScalarType::String)),
4265 ),
4266 ("value", |t| matches!(t, SqlScalarType::Float64)),
4267 ("help", |t| matches!(t, SqlScalarType::String)),
4268];
4269
4270generate_extracted_config!(CreateMetricSinkOption, (Prefix, String));
4271
4272const METRIC_SINK_PREFIX_MARKER: &str = "mz_metric_sink_";
4279
4280pub const METRIC_SINK_CURATED_PREFIX_MARKER: &str = "mz_metric_sink_curated_";
4283
4284pub fn validate_metric_sink_prefix(prefix: &str) -> Result<(), PlanError> {
4289 if prefix.is_empty() {
4290 return Err(sql_err!("metric sink prefix must not be empty"));
4291 }
4292 let mut chars = prefix.chars();
4293 let valid = match chars.next() {
4294 Some(c) if c.is_ascii_alphabetic() || c == '_' || c == ':' => {
4295 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':')
4296 }
4297 _ => false,
4298 };
4299 if !valid {
4300 return Err(sql_err!(
4301 "metric sink prefix {:?} is not a valid start of a Prometheus metric name",
4302 prefix
4303 ));
4304 }
4305 if !prefix.starts_with(METRIC_SINK_PREFIX_MARKER) {
4306 return Err(sql_err!(
4307 "metric sink prefix {:?} must start with {:?}",
4308 prefix,
4309 METRIC_SINK_PREFIX_MARKER
4310 ));
4311 }
4312 Ok(())
4313}
4314
4315pub fn validate_user_metric_sink_prefix(prefix: &str) -> Result<(), PlanError> {
4324 validate_metric_sink_prefix(prefix)?;
4325 if prefix.starts_with(METRIC_SINK_CURATED_PREFIX_MARKER)
4326 || METRIC_SINK_CURATED_PREFIX_MARKER.starts_with(prefix)
4327 {
4328 return Err(sql_err!(
4329 "metric sink prefix {:?} overlaps {:?}, which is reserved for the built-in metric sinks",
4330 prefix,
4331 METRIC_SINK_CURATED_PREFIX_MARKER
4332 ));
4333 }
4334 Ok(())
4335}
4336
4337pub fn validate_metric_sink_desc(desc: &RelationDesc) -> Result<(), PlanError> {
4343 for (name, type_ok) in METRIC_SINK_SOURCE_COLUMNS {
4344 let col = ColumnName::from(*name);
4345 let (_, column_type) = desc
4346 .get_by_name(&col)
4347 .ok_or_else(|| sql_err!("metric sink source must expose column {:?}", name))?;
4348 if !type_ok(&column_type.scalar_type) {
4349 return Err(sql_err!(
4350 "metric sink source column {:?} is not of the required type",
4351 name
4352 ));
4353 }
4354 }
4355 Ok(())
4356}
4357
4358pub fn plan_create_metric_sink(
4366 scx: &StatementContext,
4367 mut stmt: CreateMetricSinkStatement<Aug>,
4368) -> Result<Plan, PlanError> {
4369 scx.require_feature_flag(&ENABLE_METRIC_SINK)?;
4370
4371 let CreateMetricSinkStatement {
4372 name,
4373 in_cluster,
4374 if_not_exists,
4375 from,
4376 with_options,
4377 } = &mut stmt;
4378
4379 let if_not_exists = *if_not_exists;
4380 let CreateMetricSinkOptionExtracted { prefix, seen: _ } = with_options.clone().try_into()?;
4381 let Some(prefix) = prefix else {
4384 sql_bail!("CREATE METRIC SINK requires a PREFIX option");
4385 };
4386 validate_user_metric_sink_prefix(&prefix)?;
4387 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name.clone())?)?;
4388 let full_name = scx.catalog.resolve_full_name(&name);
4389 let partial_name = PartialItemName::from(full_name.clone());
4390 if let (false, Ok(item)) = (if_not_exists, scx.catalog.resolve_item(&partial_name)) {
4391 return Err(PlanError::ItemAlreadyExists {
4392 name: full_name.to_string(),
4393 item_type: item.item_type(),
4394 });
4395 }
4396
4397 let from_item = scx.get_item_by_resolved_name(from)?;
4398 let desc = from_item.relation_desc().ok_or_else(|| {
4402 sql_err!(
4403 "cannot create metric sink from {} because it is a {}",
4404 scx.catalog.minimal_qualification(from_item.name()),
4405 from_item.item_type(),
4406 )
4407 })?;
4408 validate_metric_sink_desc(&desc)?;
4409
4410 let cluster_id = match in_cluster {
4411 None => scx.resolve_cluster(None)?.id(),
4412 Some(in_cluster) => in_cluster.id,
4413 };
4414 *in_cluster = Some(ResolvedClusterName {
4415 id: cluster_id,
4416 print_name: None,
4417 });
4418 let from_global_id = from_item.global_id();
4419
4420 let create_sql = normalize::create_statement(scx, Statement::CreateMetricSink(stmt))?;
4421
4422 Ok(Plan::CreateMetricSink(CreateMetricSinkPlan {
4423 name,
4424 metric_sink: MetricSink {
4425 create_sql,
4426 from: from_global_id,
4427 cluster_id,
4428 prefix,
4429 },
4430 if_not_exists,
4431 }))
4432}
4433
4434pub fn describe_create_index(
4435 _: &StatementContext,
4436 _: CreateIndexStatement<Aug>,
4437) -> Result<StatementDesc, PlanError> {
4438 Ok(StatementDesc::new(None))
4439}
4440
4441pub fn plan_create_index(
4442 scx: &StatementContext,
4443 mut stmt: CreateIndexStatement<Aug>,
4444) -> Result<Plan, PlanError> {
4445 let CreateIndexStatement {
4446 name,
4447 on_name,
4448 in_cluster,
4449 key_parts,
4450 with_options,
4451 if_not_exists,
4452 } = &mut stmt;
4453 let on = scx.get_item_by_resolved_name(on_name)?;
4454
4455 {
4456 use CatalogItemType::*;
4457 match on.item_type() {
4458 Table | Source | View | MaterializedView => {
4459 if on.replacement_target().is_some() {
4460 sql_bail!(
4461 "index cannot be created on {} because it is a replacement {}",
4462 on_name.full_name_str(),
4463 on.item_type(),
4464 );
4465 }
4466 }
4467 Sink | MetricSink | Index | Type | Func | Secret | Connection => {
4468 sql_bail!(
4469 "index cannot be created on {} because it is a {}",
4470 on_name.full_name_str(),
4471 on.item_type(),
4472 );
4473 }
4474 }
4475 }
4476
4477 let on_desc = on
4478 .relation_desc()
4479 .ok_or_else(|| sql_err!("item does not have a relation description"))?;
4480
4481 let filled_key_parts = match key_parts {
4482 Some(kp) => kp.to_vec(),
4483 None => {
4484 let mut name_counts = BTreeMap::new();
4489 for name in on_desc.iter_names() {
4490 *name_counts.entry(name).or_insert(0usize) += 1;
4491 }
4492 let key = on_desc.typ().default_key();
4493 key.iter()
4494 .map(|i| {
4495 let name = on_desc.get_name(*i);
4496 if name_counts.get(name).copied() == Some(1) {
4497 Expr::Identifier(vec![normalize::column_name_ident(name)])
4498 } else {
4499 Expr::Value(Value::Number((i + 1).to_string()))
4500 }
4501 })
4502 .collect()
4503 }
4504 };
4505 let keys = query::plan_index_exprs(scx, &on_desc, filled_key_parts.clone())?;
4506
4507 let index_name = if let Some(name) = name {
4508 QualifiedItemName {
4509 qualifiers: on.name().qualifiers.clone(),
4510 item: normalize::ident(name.clone()),
4511 }
4512 } else {
4513 let mut idx_name = QualifiedItemName {
4514 qualifiers: on.name().qualifiers.clone(),
4515 item: on.name().item.clone(),
4516 };
4517 if key_parts.is_none() {
4518 idx_name.item += "_primary_idx";
4520 } else {
4521 let index_name_col_suffix = keys
4524 .iter()
4525 .map(|k| match k {
4526 mz_expr::MirScalarExpr::Column(i, name) => {
4527 match (on_desc.get_unambiguous_name(*i), &name.0) {
4528 (Some(col_name), _) => col_name.to_string(),
4529 (None, Some(name)) => name.to_string(),
4530 (None, None) => format!("{}", i + 1),
4531 }
4532 }
4533 _ => "expr".to_string(),
4534 })
4535 .join("_");
4536 write!(idx_name.item, "_{index_name_col_suffix}_idx")
4537 .expect("write on strings cannot fail");
4538 idx_name.item = normalize::ident(Ident::new(&idx_name.item)?)
4539 }
4540
4541 if !*if_not_exists {
4542 scx.catalog.find_available_name(idx_name)
4543 } else {
4544 idx_name
4545 }
4546 };
4547
4548 let full_name = scx.catalog.resolve_full_name(&index_name);
4550 let partial_name = PartialItemName::from(full_name.clone());
4551 if let (Ok(item), false, false) = (
4559 scx.catalog.resolve_item_or_type(&partial_name),
4560 *if_not_exists,
4561 scx.pcx().map_or(false, |pcx| pcx.ignore_if_exists_errors),
4562 ) {
4563 return Err(PlanError::ItemAlreadyExists {
4564 name: full_name.to_string(),
4565 item_type: item.item_type(),
4566 });
4567 }
4568
4569 let options = plan_index_options(scx, with_options.clone())?;
4570 let cluster_id = match in_cluster {
4571 None => scx.resolve_cluster(None)?.id(),
4572 Some(in_cluster) => in_cluster.id,
4573 };
4574
4575 *in_cluster = Some(ResolvedClusterName {
4576 id: cluster_id,
4577 print_name: None,
4578 });
4579
4580 *name = Some(Ident::new(index_name.item.clone())?);
4582 *key_parts = Some(filled_key_parts);
4583 let if_not_exists = *if_not_exists;
4584
4585 let create_sql = normalize::create_statement(scx, Statement::CreateIndex(stmt))?;
4586 let compaction_window = options.iter().find_map(|o| {
4587 #[allow(irrefutable_let_patterns)]
4588 if let crate::plan::IndexOption::RetainHistory(lcw) = o {
4589 Some(lcw.clone())
4590 } else {
4591 None
4592 }
4593 });
4594
4595 Ok(Plan::CreateIndex(CreateIndexPlan {
4596 name: index_name,
4597 index: Index {
4598 create_sql,
4599 on: on.global_id(),
4600 keys,
4601 cluster_id,
4602 compaction_window,
4603 },
4604 if_not_exists,
4605 }))
4606}
4607
4608pub fn describe_create_type(
4609 _: &StatementContext,
4610 _: CreateTypeStatement<Aug>,
4611) -> Result<StatementDesc, PlanError> {
4612 Ok(StatementDesc::new(None))
4613}
4614
4615pub fn plan_create_type(
4616 scx: &StatementContext,
4617 stmt: CreateTypeStatement<Aug>,
4618) -> Result<Plan, PlanError> {
4619 let create_sql = normalize::create_statement(scx, Statement::CreateType(stmt.clone()))?;
4620 let CreateTypeStatement { name, as_type, .. } = stmt;
4621
4622 fn validate_data_type(
4630 scx: &StatementContext,
4631 data_type: ResolvedDataType,
4632 as_type: &str,
4633 key: &str,
4634 budget: &mut TypeResolutionBudget,
4635 ) -> Result<(CatalogItemId, Vec<i64>), PlanError> {
4636 let (id, modifiers) = match data_type {
4637 ResolvedDataType::Named { id, modifiers, .. } => (id, modifiers),
4638 _ => sql_bail!(
4639 "CREATE TYPE ... AS {}option {} can only use named data types, but \
4640 found unnamed data type {}. Use CREATE TYPE to create a named type first",
4641 as_type,
4642 key,
4643 data_type.human_readable_name(),
4644 ),
4645 };
4646
4647 let item = scx.catalog.get_item(&id);
4648 match item.type_details() {
4649 None => sql_bail!(
4650 "{} must be of class type, but received {} which is of class {}",
4651 key,
4652 scx.catalog.resolve_full_name(item.name()),
4653 item.item_type()
4654 ),
4655 Some(CatalogTypeDetails {
4656 typ: CatalogType::Char,
4657 ..
4658 }) => {
4659 bail_unsupported!("embedding char type in a list or map")
4660 }
4661 _ => {
4662 budget.resolve_child(scx.catalog, id, &modifiers)?;
4665
4666 Ok((id, modifiers))
4667 }
4668 }
4669 }
4670
4671 let mut budget = TypeResolutionBudget::for_root(scx.catalog);
4672 let inner = match as_type {
4673 CreateTypeAs::List { options } => {
4674 let CreateTypeListOptionExtracted {
4675 element_type,
4676 seen: _,
4677 } = CreateTypeListOptionExtracted::try_from(options)?;
4678 let element_type =
4679 element_type.ok_or_else(|| sql_err!("ELEMENT TYPE option is required"))?;
4680 let (id, modifiers) =
4681 validate_data_type(scx, element_type, "LIST ", "ELEMENT TYPE", &mut budget)?;
4682 CatalogType::List {
4683 element_reference: id,
4684 element_modifiers: modifiers,
4685 }
4686 }
4687 CreateTypeAs::Map { options } => {
4688 let CreateTypeMapOptionExtracted {
4689 key_type,
4690 value_type,
4691 seen: _,
4692 } = CreateTypeMapOptionExtracted::try_from(options)?;
4693 let key_type = key_type.ok_or_else(|| sql_err!("KEY TYPE option is required"))?;
4694 let value_type = value_type.ok_or_else(|| sql_err!("VALUE TYPE option is required"))?;
4695 let (key_id, key_modifiers) = validate_data_type(
4699 scx,
4700 key_type,
4701 "MAP ",
4702 "KEY TYPE",
4703 &mut TypeResolutionBudget::for_root(scx.catalog),
4704 )?;
4705 let (value_id, value_modifiers) =
4706 validate_data_type(scx, value_type, "MAP ", "VALUE TYPE", &mut budget)?;
4707 CatalogType::Map {
4708 key_reference: key_id,
4709 key_modifiers,
4710 value_reference: value_id,
4711 value_modifiers,
4712 }
4713 }
4714 CreateTypeAs::Record { column_defs } => {
4715 let mut fields = vec![];
4716 for column_def in column_defs {
4717 let data_type = column_def.data_type;
4718 let key = ident(column_def.name.clone());
4719 let (id, modifiers) = validate_data_type(scx, data_type, "", &key, &mut budget)?;
4720 fields.push(CatalogRecordField {
4721 name: ColumnName::from(key.clone()),
4722 type_reference: id,
4723 type_modifiers: modifiers,
4724 });
4725 }
4726 CatalogType::Record { fields }
4727 }
4728 };
4729
4730 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
4731
4732 let full_name = scx.catalog.resolve_full_name(&name);
4734 let partial_name = PartialItemName::from(full_name.clone());
4735 if let Ok(item) = scx.catalog.resolve_item_or_type(&partial_name) {
4738 if item.item_type().conflicts_with_type() {
4739 return Err(PlanError::ItemAlreadyExists {
4740 name: full_name.to_string(),
4741 item_type: item.item_type(),
4742 });
4743 }
4744 }
4745
4746 Ok(Plan::CreateType(CreateTypePlan {
4747 name,
4748 typ: Type { create_sql, inner },
4749 }))
4750}
4751
4752generate_extracted_config!(CreateTypeListOption, (ElementType, ResolvedDataType));
4753
4754generate_extracted_config!(
4755 CreateTypeMapOption,
4756 (KeyType, ResolvedDataType),
4757 (ValueType, ResolvedDataType)
4758);
4759
4760#[derive(Debug)]
4761pub enum PlannedAlterRoleOption {
4762 Attributes(PlannedRoleAttributes),
4763 Variable(PlannedRoleVariable),
4764}
4765
4766#[derive(Debug, Clone)]
4767pub struct PlannedRoleAttributes {
4768 pub inherit: Option<bool>,
4769 pub password: Option<Password>,
4770 pub scram_iterations: Option<NonZeroU32>,
4771 pub nopassword: Option<bool>,
4775 pub superuser: Option<bool>,
4776 pub login: Option<bool>,
4777}
4778
4779fn plan_role_attributes(
4780 options: Vec<RoleAttribute>,
4781 scx: &StatementContext,
4782) -> Result<PlannedRoleAttributes, PlanError> {
4783 let mut planned_attributes = PlannedRoleAttributes {
4784 inherit: None,
4785 password: None,
4786 scram_iterations: None,
4787 superuser: None,
4788 login: None,
4789 nopassword: None,
4790 };
4791
4792 for option in options {
4793 match option {
4794 RoleAttribute::Inherit | RoleAttribute::NoInherit
4795 if planned_attributes.inherit.is_some() =>
4796 {
4797 sql_bail!("conflicting or redundant options");
4798 }
4799 RoleAttribute::CreateCluster | RoleAttribute::NoCreateCluster => {
4800 bail_never_supported!(
4801 "CREATECLUSTER attribute",
4802 "sql/create-role/#details",
4803 "Use system privileges instead."
4804 );
4805 }
4806 RoleAttribute::CreateDB | RoleAttribute::NoCreateDB => {
4807 bail_never_supported!(
4808 "CREATEDB attribute",
4809 "sql/create-role/#details",
4810 "Use system privileges instead."
4811 );
4812 }
4813 RoleAttribute::CreateRole | RoleAttribute::NoCreateRole => {
4814 bail_never_supported!(
4815 "CREATEROLE attribute",
4816 "sql/create-role/#details",
4817 "Use system privileges instead."
4818 );
4819 }
4820 RoleAttribute::Password(_) if planned_attributes.password.is_some() => {
4821 sql_bail!("conflicting or redundant options");
4822 }
4823
4824 RoleAttribute::Inherit => planned_attributes.inherit = Some(true),
4825 RoleAttribute::NoInherit => planned_attributes.inherit = Some(false),
4826 RoleAttribute::Password(password) => {
4827 if let Some(password) = password {
4828 planned_attributes.password = Some(password.into());
4829 planned_attributes.scram_iterations =
4830 Some(scx.catalog.system_vars().scram_iterations())
4831 } else {
4832 planned_attributes.nopassword = Some(true);
4833 }
4834 }
4835 RoleAttribute::SuperUser => {
4836 if planned_attributes.superuser == Some(false) {
4837 sql_bail!("conflicting or redundant options");
4838 }
4839 planned_attributes.superuser = Some(true);
4840 }
4841 RoleAttribute::NoSuperUser => {
4842 if planned_attributes.superuser == Some(true) {
4843 sql_bail!("conflicting or redundant options");
4844 }
4845 planned_attributes.superuser = Some(false);
4846 }
4847 RoleAttribute::Login => {
4848 if planned_attributes.login == Some(false) {
4849 sql_bail!("conflicting or redundant options");
4850 }
4851 planned_attributes.login = Some(true);
4852 }
4853 RoleAttribute::NoLogin => {
4854 if planned_attributes.login == Some(true) {
4855 sql_bail!("conflicting or redundant options");
4856 }
4857 planned_attributes.login = Some(false);
4858 }
4859 }
4860 }
4861 if planned_attributes.inherit == Some(false) {
4862 bail_unsupported!("non inherit roles");
4863 }
4864
4865 Ok(planned_attributes)
4866}
4867
4868#[derive(Debug)]
4869pub enum PlannedRoleVariable {
4870 Set { name: String, value: VariableValue },
4871 Reset { name: String },
4872}
4873
4874impl PlannedRoleVariable {
4875 pub fn name(&self) -> &str {
4876 match self {
4877 PlannedRoleVariable::Set { name, .. } => name,
4878 PlannedRoleVariable::Reset { name } => name,
4879 }
4880 }
4881}
4882
4883fn plan_role_variable(
4884 scx: &StatementContext,
4885 variable: SetRoleVar,
4886) -> Result<PlannedRoleVariable, PlanError> {
4887 let plan = match variable {
4888 SetRoleVar::Set { name, value } => {
4889 let name = name.to_string();
4890 let value = scl::plan_set_variable_to(value)?;
4891 if let VariableValue::Values(values) = &value {
4894 vars::check_transaction_isolation_feature_flag(
4895 &name,
4896 VarInput::SqlSet(values),
4897 scx.catalog.system_vars(),
4898 )?;
4899 }
4900 PlannedRoleVariable::Set { name, value }
4901 }
4902 SetRoleVar::Reset { name } => PlannedRoleVariable::Reset {
4903 name: name.to_string(),
4904 },
4905 };
4906 Ok(plan)
4907}
4908
4909pub fn describe_create_role(
4910 _: &StatementContext,
4911 _: CreateRoleStatement,
4912) -> Result<StatementDesc, PlanError> {
4913 Ok(StatementDesc::new(None))
4914}
4915
4916pub fn plan_create_role(
4917 scx: &StatementContext,
4918 CreateRoleStatement { name, options }: CreateRoleStatement,
4919) -> Result<Plan, PlanError> {
4920 let attributes = plan_role_attributes(options, scx)?;
4921 Ok(Plan::CreateRole(CreateRolePlan {
4922 name: normalize::ident(name),
4923 attributes: attributes.into(),
4924 }))
4925}
4926
4927pub fn plan_create_network_policy(
4928 ctx: &StatementContext,
4929 CreateNetworkPolicyStatement { name, options }: CreateNetworkPolicyStatement<Aug>,
4930) -> Result<Plan, PlanError> {
4931 ctx.require_feature_flag(&vars::ENABLE_NETWORK_POLICIES)?;
4932 let policy_options: NetworkPolicyOptionExtracted = options.try_into()?;
4933
4934 let Some(rule_defs) = policy_options.rules else {
4935 sql_bail!("RULES must be specified when creating network policies.");
4936 };
4937
4938 let mut rules = vec![];
4939 for NetworkPolicyRuleDefinition { name, options } in rule_defs {
4940 let NetworkPolicyRuleOptionExtracted {
4941 seen: _,
4942 direction,
4943 action,
4944 address,
4945 } = options.try_into()?;
4946 let (direction, action, address) = match (direction, action, address) {
4947 (Some(direction), Some(action), Some(address)) => (
4948 NetworkPolicyRuleDirection::try_from(direction.as_str())?,
4949 NetworkPolicyRuleAction::try_from(action.as_str())?,
4950 PolicyAddress::try_from(address.as_str())?,
4951 ),
4952 (_, _, _) => {
4953 sql_bail!("Direction, Address, and Action must specified when creating a rule")
4954 }
4955 };
4956 rules.push(NetworkPolicyRule {
4957 name: normalize::ident(name),
4958 direction,
4959 action,
4960 address,
4961 });
4962 }
4963
4964 if rules.len()
4965 > ctx
4966 .catalog
4967 .system_vars()
4968 .max_rules_per_network_policy()
4969 .try_into()?
4970 {
4971 sql_bail!("RULES count exceeds max_rules_per_network_policy.")
4972 }
4973
4974 Ok(Plan::CreateNetworkPolicy(CreateNetworkPolicyPlan {
4975 name: normalize::ident(name),
4976 rules,
4977 }))
4978}
4979
4980pub fn plan_alter_network_policy(
4981 ctx: &StatementContext,
4982 AlterNetworkPolicyStatement { name, options }: AlterNetworkPolicyStatement<Aug>,
4983) -> Result<Plan, PlanError> {
4984 ctx.require_feature_flag(&vars::ENABLE_NETWORK_POLICIES)?;
4985
4986 let policy_options: NetworkPolicyOptionExtracted = options.try_into()?;
4987 let policy = ctx
4988 .catalog
4989 .resolve_network_policy(normalize::ident_ref(&name))?;
4990
4991 let Some(rule_defs) = policy_options.rules else {
4992 sql_bail!("RULES must be specified when creating network policies.");
4993 };
4994
4995 let mut rules = vec![];
4996 for NetworkPolicyRuleDefinition { name, options } in rule_defs {
4997 let NetworkPolicyRuleOptionExtracted {
4998 seen: _,
4999 direction,
5000 action,
5001 address,
5002 } = options.try_into()?;
5003
5004 let (direction, action, address) = match (direction, action, address) {
5005 (Some(direction), Some(action), Some(address)) => (
5006 NetworkPolicyRuleDirection::try_from(direction.as_str())?,
5007 NetworkPolicyRuleAction::try_from(action.as_str())?,
5008 PolicyAddress::try_from(address.as_str())?,
5009 ),
5010 (_, _, _) => {
5011 sql_bail!("Direction, Address, and Action must specified when creating a rule")
5012 }
5013 };
5014 rules.push(NetworkPolicyRule {
5015 name: normalize::ident(name),
5016 direction,
5017 action,
5018 address,
5019 });
5020 }
5021 if rules.len()
5022 > ctx
5023 .catalog
5024 .system_vars()
5025 .max_rules_per_network_policy()
5026 .try_into()?
5027 {
5028 sql_bail!("RULES count exceeds max_rules_per_network_policy.")
5029 }
5030
5031 Ok(Plan::AlterNetworkPolicy(AlterNetworkPolicyPlan {
5032 id: policy.id(),
5033 name: normalize::ident(name),
5034 rules,
5035 }))
5036}
5037
5038pub fn describe_create_cluster(
5039 _: &StatementContext,
5040 _: CreateClusterStatement<Aug>,
5041) -> Result<StatementDesc, PlanError> {
5042 Ok(StatementDesc::new(None))
5043}
5044
5045generate_extracted_config!(
5051 ClusterOption,
5052 (AutoScalingStrategy, ClusterAutoScalingStrategyOptionValue),
5053 (AvailabilityZones, Vec<String>),
5054 (Disk, bool),
5055 (ExperimentalArrangementCompression, bool),
5056 (IntrospectionDebugging, bool),
5057 (IntrospectionInterval, OptionalDuration),
5058 (Managed, bool),
5059 (Replicas, Vec<ReplicaDefinition<Aug>>),
5060 (ReplicationFactor, u32),
5061 (Size, String),
5062 (Schedule, ClusterScheduleOptionValue),
5063 (WorkloadClass, OptionalString)
5064);
5065
5066generate_extracted_config!(
5067 NetworkPolicyOption,
5068 (Rules, Vec<NetworkPolicyRuleDefinition<Aug>>)
5069);
5070
5071generate_extracted_config!(
5072 NetworkPolicyRuleOption,
5073 (Direction, String),
5074 (Action, String),
5075 (Address, String)
5076);
5077
5078generate_extracted_config!(ClusterAlterOption, (Wait, ClusterAlterOptionValue<Aug>));
5079
5080generate_extracted_config!(
5081 ClusterAlterUntilReadyOption,
5082 (Timeout, Duration),
5083 (OnTimeout, String)
5084);
5085
5086generate_extracted_config!(
5087 ClusterFeature,
5088 (ReoptimizeImportedViews, Option<bool>, Default(None)),
5089 (EnableEagerDeltaJoins, Option<bool>, Default(None)),
5090 (EnableNewOuterJoinLowering, Option<bool>, Default(None)),
5091 (EnableVariadicLeftJoinLowering, Option<bool>, Default(None)),
5092 (EnableLetrecFixpointAnalysis, Option<bool>, Default(None)),
5093 (EnableJoinPrioritizeArranged, Option<bool>, Default(None)),
5094 (
5095 EnableProjectionPushdownAfterRelationCse,
5096 Option<bool>,
5097 Default(None)
5098 ),
5099 (
5100 EnableUnionCancellationAfterRelationCse,
5101 Option<bool>,
5102 Default(None)
5103 )
5104);
5105
5106pub fn plan_create_cluster(
5110 scx: &StatementContext,
5111 stmt: CreateClusterStatement<Aug>,
5112) -> Result<Plan, PlanError> {
5113 let plan = plan_create_cluster_inner(scx, stmt)?;
5114
5115 if let CreateClusterVariant::Managed(_) = &plan.variant {
5117 let stmt = unplan_create_cluster(scx, plan.clone())
5118 .map_err(|e| PlanError::Replan(e.to_string()))?;
5119 let create_sql = stmt.to_ast_string_stable();
5120 let stmt = parse::parse(&create_sql)
5121 .map_err(|e| PlanError::Replan(e.to_string()))?
5122 .into_element()
5123 .ast;
5124 let (stmt, _resolved_ids) =
5125 names::resolve(scx.catalog, stmt).map_err(|e| PlanError::Replan(e.to_string()))?;
5126 let stmt = match stmt {
5127 Statement::CreateCluster(stmt) => stmt,
5128 stmt => {
5129 return Err(PlanError::Replan(format!(
5130 "replan does not match: plan={plan:?}, create_sql={create_sql:?}, stmt={stmt:?}"
5131 )));
5132 }
5133 };
5134 let replan =
5135 plan_create_cluster_inner(scx, stmt).map_err(|e| PlanError::Replan(e.to_string()))?;
5136 if plan != replan {
5137 return Err(PlanError::Replan(format!(
5138 "replan does not match: plan={plan:?}, replan={replan:?}"
5139 )));
5140 }
5141 }
5142
5143 Ok(Plan::CreateCluster(plan))
5144}
5145
5146pub fn plan_create_cluster_inner(
5147 scx: &StatementContext,
5148 CreateClusterStatement {
5149 name,
5150 options,
5151 features,
5152 if_not_exists,
5153 }: CreateClusterStatement<Aug>,
5154) -> Result<CreateClusterPlan, PlanError> {
5155 let ClusterOptionExtracted {
5156 auto_scaling_strategy,
5157 availability_zones,
5158 experimental_arrangement_compression,
5159 introspection_debugging,
5160 introspection_interval,
5161 managed,
5162 replicas,
5163 replication_factor,
5164 seen: _,
5165 size,
5166 disk,
5167 schedule,
5168 workload_class,
5169 }: ClusterOptionExtracted = options.try_into()?;
5170
5171 let managed = managed.unwrap_or_else(|| replicas.is_none());
5172
5173 if !scx.catalog.active_role_id().is_system() {
5174 if !features.is_empty() {
5175 sql_bail!("FEATURES not supported for non-system users");
5176 }
5177 if workload_class.is_some() {
5178 sql_bail!("WORKLOAD CLASS not supported for non-system users");
5179 }
5180 }
5181
5182 let schedule = schedule.unwrap_or(ClusterScheduleOptionValue::Manual);
5183 let workload_class = workload_class.and_then(|v| v.0);
5184
5185 if managed {
5186 if replicas.is_some() {
5187 sql_bail!("REPLICAS not supported for managed clusters");
5188 }
5189 let Some(size) = size else {
5190 sql_bail!("SIZE must be specified for managed clusters");
5191 };
5192
5193 if disk.is_some() {
5194 if scx.catalog.is_cluster_size_cc(&size) {
5198 sql_bail!(
5199 "DISK option not supported for modern cluster sizes because disk is always enabled"
5200 );
5201 }
5202
5203 scx.catalog
5204 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated);
5205 }
5206
5207 let compute = plan_compute_replica_config(
5208 introspection_interval,
5209 introspection_debugging.unwrap_or(false),
5210 experimental_arrangement_compression.unwrap_or(false),
5211 )?;
5212
5213 let replication_factor = if matches!(schedule, ClusterScheduleOptionValue::Manual) {
5214 replication_factor.unwrap_or_else(|| {
5215 scx.catalog
5216 .system_vars()
5217 .default_cluster_replication_factor()
5218 })
5219 } else {
5220 scx.require_feature_flag(&ENABLE_CLUSTER_SCHEDULE_REFRESH)?;
5221 if replication_factor.is_some() {
5222 sql_bail!(
5223 "REPLICATION FACTOR cannot be given together with any SCHEDULE other than MANUAL"
5224 );
5225 }
5226 0
5230 };
5231 let availability_zones = availability_zones.unwrap_or_default();
5232
5233 if !availability_zones.is_empty() {
5234 scx.require_feature_flag(&vars::ENABLE_MANAGED_CLUSTER_AVAILABILITY_ZONES)?;
5235 }
5236
5237 let ClusterFeatureExtracted {
5239 reoptimize_imported_views,
5240 enable_eager_delta_joins,
5241 enable_new_outer_join_lowering,
5242 enable_variadic_left_join_lowering,
5243 enable_letrec_fixpoint_analysis,
5244 enable_join_prioritize_arranged,
5245 enable_projection_pushdown_after_relation_cse,
5246 enable_union_cancellation_after_relation_cse,
5247 seen: _,
5248 } = ClusterFeatureExtracted::try_from(features)?;
5249 let optimizer_feature_overrides = OptimizerFeatureOverrides {
5250 reoptimize_imported_views,
5251 enable_eager_delta_joins,
5252 enable_new_outer_join_lowering,
5253 enable_variadic_left_join_lowering,
5254 enable_letrec_fixpoint_analysis,
5255 enable_join_prioritize_arranged,
5256 enable_projection_pushdown_after_relation_cse,
5257 enable_union_cancellation_after_relation_cse,
5258 ..Default::default()
5259 };
5260
5261 let auto_scaling_strategy = match auto_scaling_strategy {
5264 Some(value) => {
5265 scx.require_feature_flag(&ENABLE_AUTO_SCALING_STRATEGY)?;
5266 let strategy = plan_auto_scaling_strategy(value)?;
5267 if let Some(strategy) = &strategy {
5268 let schedule_non_manual =
5269 !matches!(schedule, ClusterScheduleOptionValue::Manual);
5270 validate_auto_scaling_strategy(strategy, Some(&size), schedule_non_manual)?;
5271 }
5272 strategy
5273 }
5274 None => None,
5275 };
5276
5277 let schedule = plan_cluster_schedule(schedule)?;
5278
5279 Ok(CreateClusterPlan {
5280 name: normalize::ident(name),
5281 variant: CreateClusterVariant::Managed(CreateClusterManagedPlan {
5282 replication_factor,
5283 size,
5284 availability_zones,
5285 compute,
5286 optimizer_feature_overrides,
5287 schedule,
5288 auto_scaling_strategy,
5289 }),
5290 workload_class,
5291 if_not_exists,
5292 })
5293 } else {
5294 let Some(replica_defs) = replicas else {
5295 sql_bail!("REPLICAS must be specified for unmanaged clusters");
5296 };
5297 if auto_scaling_strategy.is_some() {
5298 sql_bail!("AUTO SCALING STRATEGY not supported for unmanaged clusters");
5299 }
5300 if availability_zones.is_some() {
5301 sql_bail!("AVAILABILITY ZONES not supported for unmanaged clusters");
5302 }
5303 if replication_factor.is_some() {
5304 sql_bail!("REPLICATION FACTOR not supported for unmanaged clusters");
5305 }
5306 if introspection_debugging.is_some() {
5307 sql_bail!("INTROSPECTION DEBUGGING not supported for unmanaged clusters");
5308 }
5309 if introspection_interval.is_some() {
5310 sql_bail!("INTROSPECTION INTERVAL not supported for unmanaged clusters");
5311 }
5312 if experimental_arrangement_compression.is_some() {
5313 sql_bail!("EXPERIMENTAL ARRANGEMENT COMPRESSION not supported for unmanaged clusters");
5314 }
5315 if size.is_some() {
5316 sql_bail!("SIZE not supported for unmanaged clusters");
5317 }
5318 if disk.is_some() {
5319 sql_bail!("DISK not supported for unmanaged clusters");
5320 }
5321 if !features.is_empty() {
5322 sql_bail!("FEATURES not supported for unmanaged clusters");
5323 }
5324 if !matches!(schedule, ClusterScheduleOptionValue::Manual) {
5325 sql_bail!(
5326 "cluster schedules other than MANUAL are not supported for unmanaged clusters"
5327 );
5328 }
5329
5330 let mut replicas = vec![];
5331 for ReplicaDefinition { name, options } in replica_defs {
5332 replicas.push((normalize::ident(name), plan_replica_config(scx, options)?));
5333 }
5334
5335 Ok(CreateClusterPlan {
5336 name: normalize::ident(name),
5337 variant: CreateClusterVariant::Unmanaged(CreateClusterUnmanagedPlan { replicas }),
5338 workload_class,
5339 if_not_exists,
5340 })
5341 }
5342}
5343
5344pub fn unplan_create_cluster(
5351 scx: &StatementContext,
5352 CreateClusterPlan {
5353 name,
5354 variant,
5355 workload_class,
5356 if_not_exists,
5357 }: CreateClusterPlan,
5358) -> Result<CreateClusterStatement<Aug>, PlanError> {
5359 match variant {
5360 CreateClusterVariant::Managed(CreateClusterManagedPlan {
5361 replication_factor,
5362 size,
5363 availability_zones,
5364 compute,
5365 optimizer_feature_overrides,
5366 schedule,
5367 auto_scaling_strategy,
5368 }) => {
5369 let schedule = unplan_cluster_schedule(schedule);
5370 let auto_scaling_strategy = auto_scaling_strategy
5371 .as_ref()
5372 .map(unplan_auto_scaling_strategy);
5373 let OptimizerFeatureOverrides {
5374 enable_reduce_mfp_fusion: _,
5375 enable_cardinality_estimates: _,
5376 persist_fast_path_limit: _,
5377 reoptimize_imported_views,
5378 enable_eager_delta_joins,
5379 enable_new_outer_join_lowering,
5380 enable_variadic_left_join_lowering,
5381 enable_letrec_fixpoint_analysis,
5382 enable_join_prioritize_arranged,
5383 enable_projection_pushdown_after_relation_cse,
5384 enable_union_cancellation_after_relation_cse,
5385 enable_less_reduce_in_eqprop: _,
5386 enable_dequadratic_eqprop_map: _,
5387 enable_eq_classes_withholding_errors: _,
5388 enable_fast_path_plan_insights: _,
5389 enable_cast_elimination: _,
5390 enable_case_literal_transform: _,
5391 enable_simplify_quantified_comparisons: _,
5392 enable_simplify_from_less_existence: _,
5393 enable_coalesce_case_transform: _,
5394 enable_will_distinct_propagation: _,
5395 enable_fixed_correlated_cte_lowering: _,
5396 } = optimizer_feature_overrides;
5397 let features_extracted = ClusterFeatureExtracted {
5399 seen: Default::default(),
5401 reoptimize_imported_views,
5402 enable_eager_delta_joins,
5403 enable_new_outer_join_lowering,
5404 enable_variadic_left_join_lowering,
5405 enable_letrec_fixpoint_analysis,
5406 enable_join_prioritize_arranged,
5407 enable_projection_pushdown_after_relation_cse,
5408 enable_union_cancellation_after_relation_cse,
5409 };
5410 let features = features_extracted.into_values(scx.catalog);
5411 let availability_zones = if availability_zones.is_empty() {
5412 None
5413 } else {
5414 Some(availability_zones)
5415 };
5416 let (introspection_interval, introspection_debugging, arrangement_compression) =
5417 unplan_compute_replica_config(compute);
5418 let replication_factor = match &schedule {
5421 ClusterScheduleOptionValue::Manual => Some(replication_factor),
5422 ClusterScheduleOptionValue::Refresh { .. } => {
5423 soft_assert_or_log!(
5430 replication_factor <= 1,
5431 "replication factor, {replication_factor:?}, must be <= 1 with a refresh schedule"
5432 );
5433 None
5434 }
5435 };
5436 let workload_class = workload_class.map(|s| OptionalString(Some(s)));
5437 let options_extracted = ClusterOptionExtracted {
5438 seen: Default::default(),
5440 auto_scaling_strategy,
5441 availability_zones,
5442 disk: None,
5443 experimental_arrangement_compression: Some(arrangement_compression),
5444 introspection_debugging: Some(introspection_debugging),
5445 introspection_interval,
5446 managed: Some(true),
5447 replicas: None,
5448 replication_factor,
5449 size: Some(size),
5450 schedule: Some(schedule),
5451 workload_class,
5452 };
5453 let options = options_extracted.into_values(scx.catalog);
5454 let name = Ident::new_unchecked(name);
5455 Ok(CreateClusterStatement {
5456 name,
5457 options,
5458 features,
5459 if_not_exists,
5460 })
5461 }
5462 CreateClusterVariant::Unmanaged(_) => {
5463 bail_unsupported!("SHOW CREATE for unmanaged clusters")
5464 }
5465 }
5466}
5467
5468generate_extracted_config!(
5469 ReplicaOption,
5470 (AvailabilityZone, String),
5471 (BilledAs, String),
5472 (ComputeAddresses, Vec<String>),
5473 (ComputectlAddresses, Vec<String>),
5474 (Disk, bool),
5475 (ExperimentalArrangementCompression, bool, Default(false)),
5476 (Internal, bool, Default(false)),
5477 (IntrospectionDebugging, bool, Default(false)),
5478 (IntrospectionInterval, OptionalDuration),
5479 (Size, String),
5480 (StorageAddresses, Vec<String>),
5481 (StoragectlAddresses, Vec<String>),
5482 (Workers, u16)
5483);
5484
5485fn plan_replica_config(
5486 scx: &StatementContext,
5487 options: Vec<ReplicaOption<Aug>>,
5488) -> Result<ReplicaConfig, PlanError> {
5489 let ReplicaOptionExtracted {
5490 availability_zone,
5491 billed_as,
5492 computectl_addresses,
5493 disk,
5494 experimental_arrangement_compression,
5495 internal,
5496 introspection_debugging,
5497 introspection_interval,
5498 size,
5499 storagectl_addresses,
5500 ..
5501 }: ReplicaOptionExtracted = options.try_into()?;
5502
5503 let compute = plan_compute_replica_config(
5504 introspection_interval,
5505 introspection_debugging,
5506 experimental_arrangement_compression,
5507 )?;
5508
5509 match (
5510 size,
5511 availability_zone,
5512 billed_as,
5513 storagectl_addresses,
5514 computectl_addresses,
5515 ) {
5516 (None, _, None, None, None) => {
5518 sql_bail!("SIZE option must be specified");
5521 }
5522 (Some(size), availability_zone, billed_as, None, None) => {
5523 if disk.is_some() {
5524 if scx.catalog.is_cluster_size_cc(&size) {
5528 sql_bail!(
5529 "DISK option not supported for modern cluster sizes because disk is always enabled"
5530 );
5531 }
5532
5533 scx.catalog
5534 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated);
5535 }
5536
5537 Ok(ReplicaConfig::Orchestrated {
5538 size,
5539 availability_zone,
5540 compute,
5541 billed_as,
5542 internal,
5543 })
5544 }
5545
5546 (None, None, None, storagectl_addresses, computectl_addresses) => {
5547 scx.require_feature_flag(&vars::UNSAFE_ENABLE_UNORCHESTRATED_CLUSTER_REPLICAS)?;
5548
5549 let Some(storagectl_addrs) = storagectl_addresses else {
5553 sql_bail!("missing STORAGECTL ADDRESSES option");
5554 };
5555 let Some(computectl_addrs) = computectl_addresses else {
5556 sql_bail!("missing COMPUTECTL ADDRESSES option");
5557 };
5558
5559 if storagectl_addrs.len() != computectl_addrs.len() {
5560 sql_bail!(
5561 "COMPUTECTL ADDRESSES and STORAGECTL ADDRESSES must have the same length"
5562 );
5563 }
5564
5565 if disk.is_some() {
5566 sql_bail!("DISK can't be specified for unorchestrated clusters");
5567 }
5568
5569 Ok(ReplicaConfig::Unorchestrated {
5570 storagectl_addrs,
5571 computectl_addrs,
5572 compute,
5573 })
5574 }
5575 _ => {
5576 sql_bail!("invalid mixture of orchestrated and unorchestrated replica options");
5579 }
5580 }
5581}
5582
5583fn plan_compute_replica_config(
5587 introspection_interval: Option<OptionalDuration>,
5588 introspection_debugging: bool,
5589 arrangement_compression: bool,
5590) -> Result<ComputeReplicaConfig, PlanError> {
5591 let introspection_interval = introspection_interval
5592 .map(|OptionalDuration(i)| i)
5593 .unwrap_or(Some(DEFAULT_REPLICA_LOGGING_INTERVAL));
5594 let introspection = match introspection_interval {
5595 Some(interval) => Some(ComputeReplicaIntrospectionConfig {
5596 interval,
5597 debugging: introspection_debugging,
5598 }),
5599 None if introspection_debugging => {
5600 sql_bail!("INTROSPECTION DEBUGGING cannot be specified without INTROSPECTION INTERVAL")
5601 }
5602 None => None,
5603 };
5604 let compute = ComputeReplicaConfig {
5605 introspection,
5606 arrangement_compression,
5607 };
5608 Ok(compute)
5609}
5610
5611fn unplan_compute_replica_config(
5616 compute_replica_config: ComputeReplicaConfig,
5617) -> (Option<OptionalDuration>, bool, bool) {
5618 let ComputeReplicaConfig {
5619 introspection,
5620 arrangement_compression,
5621 } = compute_replica_config;
5622 match introspection {
5623 Some(ComputeReplicaIntrospectionConfig {
5624 debugging,
5625 interval,
5626 }) => (
5627 Some(OptionalDuration(Some(interval))),
5628 debugging,
5629 arrangement_compression,
5630 ),
5631 None => (Some(OptionalDuration(None)), false, arrangement_compression),
5632 }
5633}
5634
5635fn plan_cluster_schedule(
5639 schedule: ClusterScheduleOptionValue,
5640) -> Result<ClusterSchedule, PlanError> {
5641 Ok(match schedule {
5642 ClusterScheduleOptionValue::Manual => ClusterSchedule::Manual,
5643 ClusterScheduleOptionValue::Refresh {
5645 hydration_time_estimate: None,
5646 } => ClusterSchedule::Refresh {
5647 hydration_time_estimate: Duration::from_millis(0),
5648 },
5649 ClusterScheduleOptionValue::Refresh {
5651 hydration_time_estimate: Some(interval_value),
5652 } => {
5653 let interval = Interval::try_from_value(Value::Interval(interval_value))?;
5654 if interval.as_microseconds() < 0 {
5655 sql_bail!(
5656 "HYDRATION TIME ESTIMATE must be non-negative; got: {}",
5657 interval
5658 );
5659 }
5660 if interval.months != 0 {
5661 sql_bail!("HYDRATION TIME ESTIMATE must not involve units larger than days");
5665 }
5666 let duration = interval.duration()?;
5667 if u64::try_from(duration.as_millis()).is_err()
5668 || Interval::from_duration(&duration).is_err()
5669 {
5670 sql_bail!("HYDRATION TIME ESTIMATE too large");
5671 }
5672 ClusterSchedule::Refresh {
5673 hydration_time_estimate: duration,
5674 }
5675 }
5676 })
5677}
5678
5679fn unplan_cluster_schedule(schedule: ClusterSchedule) -> ClusterScheduleOptionValue {
5683 match schedule {
5684 ClusterSchedule::Manual => ClusterScheduleOptionValue::Manual,
5685 ClusterSchedule::Refresh {
5686 hydration_time_estimate,
5687 } => {
5688 let interval = Interval::from_duration(&hydration_time_estimate)
5689 .expect("planning ensured that this is convertible back to Interval");
5690 let interval_value = literal::unplan_interval(&interval);
5691 ClusterScheduleOptionValue::Refresh {
5692 hydration_time_estimate: Some(interval_value),
5693 }
5694 }
5695 }
5696}
5697
5698fn plan_auto_scaling_strategy(
5706 value: ClusterAutoScalingStrategyOptionValue,
5707) -> Result<Option<AutoScalingStrategy>, PlanError> {
5708 let ClusterAutoScalingStrategyOptionValue { on_hydration } = value;
5709 let Some(on_hydration) = on_hydration else {
5710 return Ok(None);
5712 };
5713
5714 let hydration_size = String::try_from_value(on_hydration.hydration_size)?;
5715
5716 let linger_duration = on_hydration
5717 .linger_duration
5718 .map(Duration::try_from_value)
5719 .transpose()?;
5720
5721 Ok(Some(AutoScalingStrategy {
5722 on_hydration: Some(OnHydration {
5723 hydration_size,
5724 linger_duration,
5725 }),
5726 }))
5727}
5728
5729fn validate_auto_scaling_strategy(
5738 strategy: &AutoScalingStrategy,
5739 cluster_size: Option<&str>,
5740 schedule_non_manual: bool,
5741) -> Result<(), PlanError> {
5742 if let (Some(on_hydration), Some(cluster_size)) = (&strategy.on_hydration, cluster_size) {
5743 if on_hydration.hydration_size == cluster_size {
5744 return Err(PlanError::HydrationSizeEqualsClusterSize {
5745 size: cluster_size.to_string(),
5746 });
5747 }
5748 }
5749 if schedule_non_manual {
5750 sql_bail!("AUTO SCALING STRATEGY cannot be combined with a SCHEDULE other than MANUAL");
5751 }
5752 Ok(())
5753}
5754
5755fn unplan_auto_scaling_strategy(
5760 strategy: &AutoScalingStrategy,
5761) -> ClusterAutoScalingStrategyOptionValue {
5762 ClusterAutoScalingStrategyOptionValue {
5763 on_hydration: strategy
5764 .on_hydration
5765 .as_ref()
5766 .map(|on_hydration| OnHydrationOptionValue {
5767 hydration_size: Value::String(on_hydration.hydration_size.clone()),
5768 linger_duration: on_hydration.linger_duration.map(|d| {
5769 let interval = Interval::from_duration(&d)
5770 .expect("planning ensured this is convertible back to Interval");
5771 Value::Interval(literal::unplan_interval(&interval))
5772 }),
5773 }),
5774 }
5775}
5776
5777pub fn describe_create_cluster_replica(
5778 _: &StatementContext,
5779 _: CreateClusterReplicaStatement<Aug>,
5780) -> Result<StatementDesc, PlanError> {
5781 Ok(StatementDesc::new(None))
5782}
5783
5784pub fn plan_create_cluster_replica(
5785 scx: &StatementContext,
5786 CreateClusterReplicaStatement {
5787 definition: ReplicaDefinition { name, options },
5788 of_cluster,
5789 if_not_exists,
5790 }: CreateClusterReplicaStatement<Aug>,
5791) -> Result<Plan, PlanError> {
5792 let cluster = scx
5793 .catalog
5794 .resolve_cluster(Some(&normalize::ident(of_cluster)))?;
5795
5796 let config = plan_replica_config(scx, options)?;
5797
5798 if let ReplicaConfig::Orchestrated { internal: true, .. } = &config {
5799 if MANAGED_REPLICA_PATTERN.is_match(name.as_str()) {
5800 return Err(PlanError::MangedReplicaName(name.into_string()));
5801 }
5802 } else {
5803 ensure_cluster_is_not_managed(scx, cluster.id())?;
5804 }
5805
5806 Ok(Plan::CreateClusterReplica(CreateClusterReplicaPlan {
5807 name: normalize::ident(name),
5808 cluster_id: cluster.id(),
5809 config,
5810 if_not_exists,
5811 }))
5812}
5813
5814pub fn describe_create_secret(
5815 _: &StatementContext,
5816 _: CreateSecretStatement<Aug>,
5817) -> Result<StatementDesc, PlanError> {
5818 Ok(StatementDesc::new(None))
5819}
5820
5821pub fn plan_create_secret(
5822 scx: &StatementContext,
5823 stmt: CreateSecretStatement<Aug>,
5824) -> Result<Plan, PlanError> {
5825 let CreateSecretStatement {
5826 name,
5827 if_not_exists,
5828 value,
5829 } = &stmt;
5830
5831 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name.to_owned())?)?;
5832 let mut create_sql_statement = stmt.clone();
5833 create_sql_statement.value = Expr::Value(Value::String("********".to_string()));
5834 let create_sql =
5835 normalize::create_statement(scx, Statement::CreateSecret(create_sql_statement))?;
5836 let secret_as = query::plan_secret_as(scx, value.clone())?;
5837
5838 let secret = Secret {
5839 create_sql,
5840 secret_as,
5841 };
5842
5843 Ok(Plan::CreateSecret(CreateSecretPlan {
5844 name,
5845 secret,
5846 if_not_exists: *if_not_exists,
5847 }))
5848}
5849
5850pub fn describe_create_connection(
5851 _: &StatementContext,
5852 _: CreateConnectionStatement<Aug>,
5853) -> Result<StatementDesc, PlanError> {
5854 Ok(StatementDesc::new(None))
5855}
5856
5857generate_extracted_config!(CreateConnectionOption, (Validate, bool));
5858
5859pub fn plan_create_connection(
5860 scx: &StatementContext,
5861 mut stmt: CreateConnectionStatement<Aug>,
5862) -> Result<Plan, PlanError> {
5863 let CreateConnectionStatement {
5864 name,
5865 connection_type,
5866 values,
5867 if_not_exists,
5868 with_options,
5869 } = stmt.clone();
5870 let connection_options_extracted = connection::ConnectionOptionExtracted::try_from(values)?;
5871 let details = connection_options_extracted.try_into_connection_details(scx, connection_type)?;
5872 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
5873
5874 let options = CreateConnectionOptionExtracted::try_from(with_options)?;
5875 if options.validate.is_some() {
5876 scx.require_feature_flag(&vars::ENABLE_CONNECTION_VALIDATION_SYNTAX)?;
5877 }
5878 let validate = match options.validate {
5879 Some(val) => val,
5880 None => {
5881 scx.catalog
5882 .system_vars()
5883 .enable_default_connection_validation()
5884 && details.to_connection().validate_by_default()
5885 }
5886 };
5887
5888 let full_name = scx.catalog.resolve_full_name(&name);
5890 let partial_name = PartialItemName::from(full_name.clone());
5891 if let (false, Ok(item)) = (if_not_exists, scx.catalog.resolve_item(&partial_name)) {
5892 return Err(PlanError::ItemAlreadyExists {
5893 name: full_name.to_string(),
5894 item_type: item.item_type(),
5895 });
5896 }
5897
5898 if let ConnectionDetails::Ssh { key_1, key_2, .. } = &details {
5901 stmt.values.retain(|v| {
5902 v.name != ConnectionOptionName::PublicKey1 && v.name != ConnectionOptionName::PublicKey2
5903 });
5904 stmt.values.push(ConnectionOption {
5905 name: ConnectionOptionName::PublicKey1,
5906 value: Some(WithOptionValue::Value(Value::String(key_1.public_key()))),
5907 });
5908 stmt.values.push(ConnectionOption {
5909 name: ConnectionOptionName::PublicKey2,
5910 value: Some(WithOptionValue::Value(Value::String(key_2.public_key()))),
5911 });
5912 }
5913 let create_sql = normalize::create_statement(scx, Statement::CreateConnection(stmt))?;
5914
5915 let plan = CreateConnectionPlan {
5916 name,
5917 if_not_exists,
5918 connection: crate::plan::Connection {
5919 create_sql,
5920 details,
5921 },
5922 validate,
5923 };
5924 Ok(Plan::CreateConnection(plan))
5925}
5926
5927fn plan_drop_database(
5928 scx: &StatementContext,
5929 if_exists: bool,
5930 name: &UnresolvedDatabaseName,
5931 cascade: bool,
5932) -> Result<Option<DatabaseId>, PlanError> {
5933 Ok(match resolve_database(scx, name, if_exists)? {
5934 Some(database) => {
5935 if !cascade && database.has_schemas() {
5936 sql_bail!(
5937 "database '{}' cannot be dropped with RESTRICT while it contains schemas",
5938 name,
5939 );
5940 }
5941 Some(database.id())
5942 }
5943 None => None,
5944 })
5945}
5946
5947pub fn describe_drop_objects(
5948 _: &StatementContext,
5949 _: DropObjectsStatement,
5950) -> Result<StatementDesc, PlanError> {
5951 Ok(StatementDesc::new(None))
5952}
5953
5954pub fn plan_drop_objects(
5955 scx: &mut StatementContext,
5956 DropObjectsStatement {
5957 object_type,
5958 if_exists,
5959 names,
5960 cascade,
5961 }: DropObjectsStatement,
5962) -> Result<Plan, PlanError> {
5963 if object_type == mz_sql_parser::ast::ObjectType::Func {
5964 bail_unsupported!("DROP FUNCTION");
5965 }
5966 let object_type = object_type.into();
5967
5968 let mut referenced_ids = Vec::new();
5969 for name in names {
5970 let id = match &name {
5971 UnresolvedObjectName::Cluster(name) => {
5972 plan_drop_cluster(scx, if_exists, name, cascade)?.map(ObjectId::Cluster)
5973 }
5974 UnresolvedObjectName::ClusterReplica(name) => {
5975 plan_drop_cluster_replica(scx, if_exists, name)?.map(ObjectId::ClusterReplica)
5976 }
5977 UnresolvedObjectName::Database(name) => {
5978 plan_drop_database(scx, if_exists, name, cascade)?.map(ObjectId::Database)
5979 }
5980 UnresolvedObjectName::Schema(name) => {
5981 plan_drop_schema(scx, if_exists, name, cascade)?.map(ObjectId::Schema)
5982 }
5983 UnresolvedObjectName::Role(name) => {
5984 plan_drop_role(scx, if_exists, name)?.map(ObjectId::Role)
5985 }
5986 UnresolvedObjectName::Item(name) => {
5987 plan_drop_item_name(scx, object_type, if_exists, name.clone())?.map(ObjectId::Item)
5991 }
5992 UnresolvedObjectName::NetworkPolicy(name) => {
5993 plan_drop_network_policy(scx, if_exists, name)?.map(ObjectId::NetworkPolicy)
5994 }
5995 };
5996 match id {
5997 Some(id) => referenced_ids.push(id),
5998 None => scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
5999 name: name.to_ast_string_simple(),
6000 object_type,
6001 }),
6002 }
6003 }
6004
6005 if !cascade {
6009 let dropped_items: BTreeSet<CatalogItemId> = referenced_ids
6010 .iter()
6011 .filter_map(|id| match id {
6012 ObjectId::Item(id) => Some(*id),
6013 _ => None,
6014 })
6015 .collect();
6016 for id in &dropped_items {
6017 let catalog_item = scx.catalog.get_item(id);
6018 ensure_no_blocking_dependents(scx, object_type, catalog_item, &dropped_items)?;
6019 }
6020 }
6021
6022 let drop_ids = scx.catalog.object_dependents(&referenced_ids);
6023
6024 Ok(Plan::DropObjects(DropObjectsPlan {
6025 referenced_ids,
6026 drop_ids,
6027 object_type,
6028 }))
6029}
6030
6031fn plan_drop_schema(
6032 scx: &StatementContext,
6033 if_exists: bool,
6034 name: &UnresolvedSchemaName,
6035 cascade: bool,
6036) -> Result<Option<(ResolvedDatabaseSpecifier, SchemaSpecifier)>, PlanError> {
6037 let normalized = normalize::unresolved_schema_name(name.clone())?;
6041 if normalized.database.is_none() && normalized.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA {
6042 sql_bail!("cannot drop schema {name} because it is a temporary schema",)
6043 }
6044
6045 Ok(match resolve_schema(scx, name.clone(), if_exists)? {
6046 Some((database_spec, schema_spec)) => {
6047 if let ResolvedDatabaseSpecifier::Ambient = database_spec {
6048 sql_bail!(
6049 "cannot drop schema {name} because it is required by the database system",
6050 );
6051 }
6052 if let SchemaSpecifier::Temporary = schema_spec {
6053 sql_bail!("cannot drop schema {name} because it is a temporary schema",)
6054 }
6055 let schema = scx.get_schema(&database_spec, &schema_spec);
6056 if !cascade && schema.has_items() {
6057 let full_schema_name = scx.catalog.resolve_full_schema_name(schema.name());
6058 sql_bail!(
6059 "schema '{}' cannot be dropped without CASCADE while it contains objects",
6060 full_schema_name
6061 );
6062 }
6063 Some((database_spec, schema_spec))
6064 }
6065 None => None,
6066 })
6067}
6068
6069fn plan_drop_role(
6070 scx: &StatementContext,
6071 if_exists: bool,
6072 name: &Ident,
6073) -> Result<Option<RoleId>, PlanError> {
6074 match scx.catalog.resolve_role(name.as_str()) {
6075 Ok(role) => {
6076 let id = role.id();
6077 if &id == scx.catalog.active_role_id() {
6078 sql_bail!("current role cannot be dropped");
6079 }
6080 for role in scx.catalog.get_roles() {
6081 for (member_id, grantor_id) in role.membership() {
6082 if &id == grantor_id {
6083 let member_role = scx.catalog.get_role(member_id);
6084 sql_bail!(
6085 "cannot drop role {}: still depended up by membership of role {} in role {}",
6086 name.as_str(),
6087 role.name(),
6088 member_role.name()
6089 );
6090 }
6091 }
6092 }
6093 Ok(Some(role.id()))
6094 }
6095 Err(_) if if_exists => Ok(None),
6096 Err(e) => Err(e.into()),
6097 }
6098}
6099
6100fn plan_drop_cluster(
6101 scx: &StatementContext,
6102 if_exists: bool,
6103 name: &Ident,
6104 cascade: bool,
6105) -> Result<Option<ClusterId>, PlanError> {
6106 Ok(match resolve_cluster(scx, name, if_exists)? {
6107 Some(cluster) => {
6108 if !cascade && !cluster.bound_objects().is_empty() {
6109 return Err(PlanError::DependentObjectsStillExist {
6110 object_type: "cluster".to_string(),
6111 object_name: cluster.name().to_string(),
6112 dependents: Vec::new(),
6113 });
6114 }
6115 Some(cluster.id())
6116 }
6117 None => None,
6118 })
6119}
6120
6121fn plan_drop_network_policy(
6122 scx: &StatementContext,
6123 if_exists: bool,
6124 name: &Ident,
6125) -> Result<Option<NetworkPolicyId>, PlanError> {
6126 match scx.catalog.resolve_network_policy(name.as_str()) {
6127 Ok(policy) => {
6128 if scx.catalog.system_vars().default_network_policy_name() == policy.name() {
6131 Err(PlanError::NetworkPolicyInUse)
6132 } else {
6133 Ok(Some(policy.id()))
6134 }
6135 }
6136 Err(_) if if_exists => Ok(None),
6137 Err(e) => Err(e.into()),
6138 }
6139}
6140
6141fn plan_drop_cluster_replica(
6142 scx: &StatementContext,
6143 if_exists: bool,
6144 name: &QualifiedReplica,
6145) -> Result<Option<(ClusterId, ReplicaId)>, PlanError> {
6146 let cluster = resolve_cluster_replica(scx, name, if_exists)?;
6147 Ok(cluster.map(|(cluster, replica_id)| (cluster.id(), replica_id)))
6148}
6149
6150fn plan_drop_item(
6152 scx: &StatementContext,
6153 object_type: ObjectType,
6154 if_exists: bool,
6155 name: UnresolvedItemName,
6156 cascade: bool,
6157) -> Result<Option<CatalogItemId>, PlanError> {
6158 let Some(id) = plan_drop_item_name(scx, object_type, if_exists, name)? else {
6159 return Ok(None);
6160 };
6161 if !cascade {
6162 let catalog_item = scx.catalog.get_item(&id);
6163 ensure_no_blocking_dependents(scx, object_type, catalog_item, &BTreeSet::new())?;
6164 }
6165 Ok(Some(id))
6166}
6167
6168fn plan_drop_item_name(
6172 scx: &StatementContext,
6173 object_type: ObjectType,
6174 if_exists: bool,
6175 name: UnresolvedItemName,
6176) -> Result<Option<CatalogItemId>, PlanError> {
6177 let resolved = match resolve_item_or_type(scx, object_type, name, if_exists) {
6178 Ok(r) => r,
6179 Err(PlanError::MismatchedObjectType {
6181 name,
6182 is_type: ObjectType::MaterializedView,
6183 expected_type: ObjectType::View,
6184 }) => {
6185 return Err(PlanError::DropViewOnMaterializedView(name.to_string()));
6186 }
6187 e => e?,
6188 };
6189
6190 Ok(match resolved {
6191 Some(catalog_item) => {
6192 if catalog_item.id().is_system() {
6193 sql_bail!(
6194 "cannot drop {} {} because it is required by the database system",
6195 catalog_item.item_type(),
6196 scx.catalog.minimal_qualification(catalog_item.name()),
6197 );
6198 }
6199 Some(catalog_item.id())
6200 }
6201 None => None,
6202 })
6203}
6204
6205fn ensure_no_blocking_dependents(
6210 scx: &StatementContext,
6211 object_type: ObjectType,
6212 catalog_item: &dyn CatalogItem,
6213 also_dropped: &BTreeSet<CatalogItemId>,
6214) -> Result<(), PlanError> {
6215 for id in catalog_item.used_by() {
6216 if also_dropped.contains(id) {
6217 continue;
6218 }
6219 let dep = scx.catalog.get_item(id);
6220 if dependency_prevents_drop(object_type, dep) {
6221 return Err(PlanError::DependentObjectsStillExist {
6222 object_type: catalog_item.item_type().to_string(),
6223 object_name: scx
6224 .catalog
6225 .minimal_qualification(catalog_item.name())
6226 .to_string(),
6227 dependents: vec![(
6228 dep.item_type().to_string(),
6229 scx.catalog.minimal_qualification(dep.name()).to_string(),
6230 )],
6231 });
6232 }
6233 }
6234 Ok(())
6237}
6238
6239fn dependency_prevents_drop(object_type: ObjectType, dep: &dyn CatalogItem) -> bool {
6241 match object_type {
6242 ObjectType::Type => true,
6243 ObjectType::Table
6244 | ObjectType::View
6245 | ObjectType::MaterializedView
6246 | ObjectType::Source
6247 | ObjectType::Sink
6248 | ObjectType::MetricSink
6249 | ObjectType::Index
6250 | ObjectType::Role
6251 | ObjectType::Cluster
6252 | ObjectType::ClusterReplica
6253 | ObjectType::Secret
6254 | ObjectType::Connection
6255 | ObjectType::Database
6256 | ObjectType::Schema
6257 | ObjectType::Func
6258 | ObjectType::NetworkPolicy => match dep.item_type() {
6259 CatalogItemType::Func
6260 | CatalogItemType::Table
6261 | CatalogItemType::Source
6262 | CatalogItemType::View
6263 | CatalogItemType::MaterializedView
6264 | CatalogItemType::Sink
6265 | CatalogItemType::MetricSink
6266 | CatalogItemType::Type
6267 | CatalogItemType::Secret
6268 | CatalogItemType::Connection => true,
6269 CatalogItemType::Index => false,
6270 },
6271 }
6272}
6273
6274pub fn describe_alter_index_options(
6275 _: &StatementContext,
6276 _: AlterIndexStatement<Aug>,
6277) -> Result<StatementDesc, PlanError> {
6278 Ok(StatementDesc::new(None))
6279}
6280
6281pub fn describe_drop_owned(
6282 _: &StatementContext,
6283 _: DropOwnedStatement<Aug>,
6284) -> Result<StatementDesc, PlanError> {
6285 Ok(StatementDesc::new(None))
6286}
6287
6288pub fn plan_drop_owned(
6289 scx: &StatementContext,
6290 drop: DropOwnedStatement<Aug>,
6291) -> Result<Plan, PlanError> {
6292 let cascade = drop.cascade();
6293 let role_ids: BTreeSet<_> = drop.role_names.into_iter().map(|role| role.id).collect();
6294 let mut drop_ids = Vec::new();
6295 let mut privilege_revokes = Vec::new();
6296 let mut default_privilege_revokes = Vec::new();
6297
6298 fn update_privilege_revokes(
6299 object_id: SystemObjectId,
6300 privileges: &PrivilegeMap,
6301 role_ids: &BTreeSet<RoleId>,
6302 privilege_revokes: &mut Vec<(SystemObjectId, MzAclItem)>,
6303 ) {
6304 privilege_revokes.extend(iter::zip(
6305 iter::repeat(object_id),
6306 privileges
6307 .all_values()
6308 .filter(|privilege| role_ids.contains(&privilege.grantee))
6309 .cloned(),
6310 ));
6311 }
6312
6313 for replica in scx.catalog.get_cluster_replicas() {
6315 if role_ids.contains(&replica.owner_id()) {
6316 drop_ids.push((replica.cluster_id(), replica.replica_id()).into());
6317 }
6318 }
6319
6320 for cluster in scx.catalog.get_clusters() {
6322 if role_ids.contains(&cluster.owner_id()) {
6323 if !cascade {
6325 let non_owned_bound_objects: Vec<_> = cluster
6326 .bound_objects()
6327 .into_iter()
6328 .map(|item_id| scx.catalog.get_item(item_id))
6329 .filter(|item| !role_ids.contains(&item.owner_id()))
6330 .collect();
6331 if !non_owned_bound_objects.is_empty() {
6332 let names: Vec<_> = non_owned_bound_objects
6333 .into_iter()
6334 .map(|item| {
6335 (
6336 item.item_type().to_string(),
6337 scx.catalog.resolve_full_name(item.name()).to_string(),
6338 )
6339 })
6340 .collect();
6341 return Err(PlanError::DependentObjectsStillExist {
6342 object_type: "cluster".to_string(),
6343 object_name: cluster.name().to_string(),
6344 dependents: names,
6345 });
6346 }
6347 }
6348 drop_ids.push(cluster.id().into());
6349 }
6350 update_privilege_revokes(
6351 SystemObjectId::Object(cluster.id().into()),
6352 cluster.privileges(),
6353 &role_ids,
6354 &mut privilege_revokes,
6355 );
6356 }
6357
6358 for item in scx.catalog.get_items() {
6360 if role_ids.contains(&item.owner_id()) {
6361 if !cascade {
6362 let check_if_dependents_exist = |used_by: &[CatalogItemId]| {
6364 let non_owned_dependencies: Vec<_> = used_by
6365 .into_iter()
6366 .map(|item_id| scx.catalog.get_item(item_id))
6367 .filter(|item| dependency_prevents_drop(item.item_type().into(), *item))
6368 .filter(|item| !role_ids.contains(&item.owner_id()))
6369 .collect();
6370 if !non_owned_dependencies.is_empty() {
6371 let names: Vec<_> = non_owned_dependencies
6372 .into_iter()
6373 .map(|item| {
6374 let item_typ = item.item_type().to_string();
6375 let item_name =
6376 scx.catalog.resolve_full_name(item.name()).to_string();
6377 (item_typ, item_name)
6378 })
6379 .collect();
6380 Err(PlanError::DependentObjectsStillExist {
6381 object_type: item.item_type().to_string(),
6382 object_name: scx
6383 .catalog
6384 .resolve_full_name(item.name())
6385 .to_string()
6386 .to_string(),
6387 dependents: names,
6388 })
6389 } else {
6390 Ok(())
6391 }
6392 };
6393
6394 if let Some(id) = item.progress_id() {
6397 let progress_item = scx.catalog.get_item(&id);
6398 check_if_dependents_exist(progress_item.used_by())?;
6399 }
6400 check_if_dependents_exist(item.used_by())?;
6401 }
6402 drop_ids.push(item.id().into());
6403 }
6404 update_privilege_revokes(
6405 SystemObjectId::Object(item.id().into()),
6406 item.privileges(),
6407 &role_ids,
6408 &mut privilege_revokes,
6409 );
6410 }
6411
6412 for schema in scx.catalog.get_schemas() {
6414 if !schema.id().is_temporary() {
6415 if role_ids.contains(&schema.owner_id()) {
6416 if !cascade {
6417 let non_owned_dependencies: Vec<_> = schema
6418 .item_ids()
6419 .map(|item_id| scx.catalog.get_item(&item_id))
6420 .filter(|item| dependency_prevents_drop(item.item_type().into(), *item))
6421 .filter(|item| !role_ids.contains(&item.owner_id()))
6422 .collect();
6423 if !non_owned_dependencies.is_empty() {
6424 let full_schema_name = scx.catalog.resolve_full_schema_name(schema.name());
6425 sql_bail!(
6426 "schema {} cannot be dropped without CASCADE while it contains non-owned objects",
6427 full_schema_name.to_string().quoted()
6428 );
6429 }
6430 }
6431 drop_ids.push((*schema.database(), *schema.id()).into())
6432 }
6433 update_privilege_revokes(
6434 SystemObjectId::Object((*schema.database(), *schema.id()).into()),
6435 schema.privileges(),
6436 &role_ids,
6437 &mut privilege_revokes,
6438 );
6439 }
6440 }
6441
6442 for database in scx.catalog.get_databases() {
6444 if role_ids.contains(&database.owner_id()) {
6445 if !cascade {
6446 let non_owned_schemas: Vec<_> = database
6447 .schemas()
6448 .into_iter()
6449 .filter(|schema| !role_ids.contains(&schema.owner_id()))
6450 .collect();
6451 if !non_owned_schemas.is_empty() {
6452 sql_bail!(
6453 "database {} cannot be dropped without CASCADE while it contains non-owned schemas",
6454 database.name().quoted(),
6455 );
6456 }
6457 }
6458 drop_ids.push(database.id().into());
6459 }
6460 update_privilege_revokes(
6461 SystemObjectId::Object(database.id().into()),
6462 database.privileges(),
6463 &role_ids,
6464 &mut privilege_revokes,
6465 );
6466 }
6467
6468 for network_policy in scx.catalog.get_network_policies() {
6470 if role_ids.contains(&network_policy.owner_id()) {
6471 drop_ids.push(ObjectId::NetworkPolicy(network_policy.id()));
6472 }
6473 update_privilege_revokes(
6474 SystemObjectId::Object(ObjectId::NetworkPolicy(network_policy.id())),
6475 network_policy.privileges(),
6476 &role_ids,
6477 &mut privilege_revokes,
6478 );
6479 }
6480
6481 update_privilege_revokes(
6483 SystemObjectId::System,
6484 scx.catalog.get_system_privileges(),
6485 &role_ids,
6486 &mut privilege_revokes,
6487 );
6488
6489 for (default_privilege_object, default_privilege_acl_items) in
6490 scx.catalog.get_default_privileges()
6491 {
6492 for default_privilege_acl_item in default_privilege_acl_items {
6493 if role_ids.contains(&default_privilege_object.role_id)
6494 || role_ids.contains(&default_privilege_acl_item.grantee)
6495 {
6496 default_privilege_revokes.push((
6497 default_privilege_object.clone(),
6498 default_privilege_acl_item.clone(),
6499 ));
6500 }
6501 }
6502 }
6503
6504 let drop_ids = scx.catalog.object_dependents(&drop_ids);
6505
6506 let system_ids: Vec<_> = drop_ids.iter().filter(|id| id.is_system()).collect();
6507 if !system_ids.is_empty() {
6508 let mut owners = system_ids
6509 .into_iter()
6510 .filter_map(|object_id| scx.catalog.get_owner_id(object_id))
6511 .collect::<BTreeSet<_>>()
6512 .into_iter()
6513 .map(|role_id| scx.catalog.get_role(&role_id).name().quoted());
6514 sql_bail!(
6515 "cannot drop objects owned by role {} because they are required by the database system",
6516 owners.join(", "),
6517 );
6518 }
6519
6520 Ok(Plan::DropOwned(DropOwnedPlan {
6521 role_ids: role_ids.into_iter().collect(),
6522 drop_ids,
6523 privilege_revokes,
6524 default_privilege_revokes,
6525 }))
6526}
6527
6528fn plan_retain_history_option(
6529 scx: &StatementContext,
6530 retain_history: Option<OptionalDuration>,
6531) -> Result<Option<CompactionWindow>, PlanError> {
6532 if let Some(OptionalDuration(lcw)) = retain_history {
6533 Ok(Some(plan_retain_history(scx, lcw)?))
6534 } else {
6535 Ok(None)
6536 }
6537}
6538
6539fn plan_retain_history(
6545 scx: &StatementContext,
6546 lcw: Option<Duration>,
6547) -> Result<CompactionWindow, PlanError> {
6548 scx.require_feature_flag(&vars::ENABLE_LOGICAL_COMPACTION_WINDOW)?;
6549 match lcw {
6550 Some(Duration::ZERO) => Err(PlanError::InvalidOptionValue {
6555 option_name: "RETAIN HISTORY".to_string(),
6556 err: Box::new(PlanError::Unstructured(
6557 "internal error: unexpectedly zero".to_string(),
6558 )),
6559 }),
6560 Some(duration) => {
6561 if duration < DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION
6564 && scx
6565 .require_feature_flag(&vars::ENABLE_UNLIMITED_RETAIN_HISTORY)
6566 .is_err()
6567 {
6568 return Err(PlanError::RetainHistoryLow {
6569 limit: DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION,
6570 });
6571 }
6572 Ok(duration.try_into()?)
6573 }
6574 None => {
6577 if scx
6578 .require_feature_flag(&vars::ENABLE_UNLIMITED_RETAIN_HISTORY)
6579 .is_err()
6580 {
6581 Err(PlanError::RetainHistoryRequired)
6582 } else {
6583 Ok(CompactionWindow::DisableCompaction)
6584 }
6585 }
6586 }
6587}
6588
6589generate_extracted_config!(IndexOption, (RetainHistory, OptionalDuration));
6590
6591fn plan_index_options(
6592 scx: &StatementContext,
6593 with_opts: Vec<IndexOption<Aug>>,
6594) -> Result<Vec<crate::plan::IndexOption>, PlanError> {
6595 if !with_opts.is_empty() {
6596 scx.require_feature_flag(&vars::ENABLE_INDEX_OPTIONS)?;
6598 }
6599
6600 let IndexOptionExtracted { retain_history, .. }: IndexOptionExtracted = with_opts.try_into()?;
6601
6602 let mut out = Vec::with_capacity(1);
6603 if let Some(cw) = plan_retain_history_option(scx, retain_history)? {
6604 out.push(crate::plan::IndexOption::RetainHistory(cw));
6605 }
6606 Ok(out)
6607}
6608
6609generate_extracted_config!(
6610 TableOption,
6611 (PartitionBy, Vec<Ident>),
6612 (RetainHistory, OptionalDuration),
6613 (RedactedTest, String)
6614);
6615
6616fn plan_table_options(
6617 scx: &StatementContext,
6618 desc: &RelationDesc,
6619 with_opts: Vec<TableOption<Aug>>,
6620) -> Result<Vec<crate::plan::TableOption>, PlanError> {
6621 let TableOptionExtracted {
6622 partition_by,
6623 retain_history,
6624 redacted_test,
6625 ..
6626 }: TableOptionExtracted = with_opts.try_into()?;
6627
6628 if let Some(partition_by) = partition_by {
6629 scx.require_feature_flag(&ENABLE_COLLECTION_PARTITION_BY)?;
6630 check_partition_by(desc, partition_by)?;
6631 }
6632
6633 if redacted_test.is_some() {
6634 scx.require_feature_flag(&vars::ENABLE_REDACTED_TEST_OPTION)?;
6635 }
6636
6637 let mut out = Vec::with_capacity(1);
6638 if let Some(cw) = plan_retain_history_option(scx, retain_history)? {
6639 out.push(crate::plan::TableOption::RetainHistory(cw));
6640 }
6641 Ok(out)
6642}
6643
6644pub fn plan_alter_index_options(
6645 scx: &mut StatementContext,
6646 AlterIndexStatement {
6647 index_name,
6648 if_exists,
6649 action,
6650 }: AlterIndexStatement<Aug>,
6651) -> Result<Plan, PlanError> {
6652 let object_type = ObjectType::Index;
6653 match action {
6654 AlterIndexAction::ResetOptions(options) => {
6655 let mut options = options.into_iter();
6656 if let Some(opt) = options.next() {
6657 match opt {
6658 IndexOptionName::RetainHistory => {
6659 if options.next().is_some() {
6660 sql_bail!("RETAIN HISTORY must be only option");
6661 }
6662 return alter_retain_history(
6663 scx,
6664 object_type,
6665 if_exists,
6666 UnresolvedObjectName::Item(index_name),
6667 None,
6668 );
6669 }
6670 }
6671 }
6672 sql_bail!("expected option");
6673 }
6674 AlterIndexAction::SetOptions(options) => {
6675 let mut options = options.into_iter();
6676 if let Some(opt) = options.next() {
6677 match opt.name {
6678 IndexOptionName::RetainHistory => {
6679 if options.next().is_some() {
6680 sql_bail!("RETAIN HISTORY must be only option");
6681 }
6682 return alter_retain_history(
6683 scx,
6684 object_type,
6685 if_exists,
6686 UnresolvedObjectName::Item(index_name),
6687 opt.value,
6688 );
6689 }
6690 }
6691 }
6692 sql_bail!("expected option");
6693 }
6694 }
6695}
6696
6697pub fn describe_alter_cluster_set_options(
6698 _: &StatementContext,
6699 _: AlterClusterStatement<Aug>,
6700) -> Result<StatementDesc, PlanError> {
6701 Ok(StatementDesc::new(None))
6702}
6703
6704pub fn plan_alter_cluster(
6705 scx: &mut StatementContext,
6706 AlterClusterStatement {
6707 name,
6708 action,
6709 if_exists,
6710 }: AlterClusterStatement<Aug>,
6711) -> Result<Plan, PlanError> {
6712 let cluster = match resolve_cluster(scx, &name, if_exists)? {
6713 Some(entry) => entry,
6714 None => {
6715 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6716 name: name.to_ast_string_simple(),
6717 object_type: ObjectType::Cluster,
6718 });
6719
6720 return Ok(Plan::AlterNoop(AlterNoopPlan {
6721 object_type: ObjectType::Cluster,
6722 }));
6723 }
6724 };
6725
6726 let mut options: PlanClusterOption = Default::default();
6727 let mut alter_strategy: AlterClusterPlanStrategy = AlterClusterPlanStrategy::None;
6728
6729 match action {
6730 AlterClusterAction::SetOptions {
6731 options: set_options,
6732 with_options,
6733 } => {
6734 let ClusterOptionExtracted {
6735 auto_scaling_strategy,
6736 availability_zones,
6737 experimental_arrangement_compression,
6738 introspection_debugging,
6739 introspection_interval,
6740 managed,
6741 replicas: replica_defs,
6742 replication_factor,
6743 seen: _,
6744 size,
6745 disk,
6746 schedule,
6747 workload_class,
6748 }: ClusterOptionExtracted = set_options.try_into()?;
6749
6750 if !scx.catalog.active_role_id().is_system() {
6751 if workload_class.is_some() {
6752 sql_bail!("WORKLOAD CLASS not supported for non-system users");
6753 }
6754 }
6755
6756 match managed.unwrap_or_else(|| cluster.is_managed()) {
6757 true => {
6758 let alter_strategy_extracted =
6759 ClusterAlterOptionExtracted::try_from(with_options)?;
6760 alter_strategy = AlterClusterPlanStrategy::try_from(alter_strategy_extracted)?;
6761
6762 if !matches!(alter_strategy, AlterClusterPlanStrategy::None)
6766 && size.is_none()
6767 && replication_factor.is_none()
6768 && availability_zones.is_none()
6769 && introspection_debugging.is_none()
6770 && introspection_interval.is_none()
6771 && experimental_arrangement_compression.is_none()
6772 {
6773 sql_bail!(
6774 "WAIT can only be used together with a SIZE, REPLICATION FACTOR, \
6775 AVAILABILITY ZONES, INTROSPECTION, or EXPERIMENTAL ARRANGEMENT COMPRESSION change"
6776 );
6777 }
6778
6779 if replica_defs.is_some() {
6780 sql_bail!("REPLICAS not supported for managed clusters");
6781 }
6782 if schedule.is_some()
6783 && !matches!(schedule, Some(ClusterScheduleOptionValue::Manual))
6784 {
6785 scx.require_feature_flag(&ENABLE_CLUSTER_SCHEDULE_REFRESH)?;
6786
6787 if replication_factor.is_none()
6796 && cluster.replication_factor().is_some_and(|rf| rf > 1)
6797 {
6798 sql_bail!(
6799 "SCHEDULE cannot be set to anything other than MANUAL while the \
6800 cluster's REPLICATION FACTOR is greater than 1; \
6801 set the REPLICATION FACTOR to 1 first"
6802 );
6803 }
6804 }
6805
6806 if replication_factor.is_some() {
6807 if schedule.is_some()
6808 && !matches!(schedule, Some(ClusterScheduleOptionValue::Manual))
6809 {
6810 sql_bail!(
6811 "REPLICATION FACTOR cannot be given together with any SCHEDULE other than MANUAL"
6812 );
6813 }
6814 if let Some(current_schedule) = cluster.schedule() {
6815 if !matches!(current_schedule, ClusterSchedule::Manual) {
6816 sql_bail!(
6817 "REPLICATION FACTOR cannot be set if the cluster SCHEDULE is anything other than MANUAL"
6818 );
6819 }
6820 }
6821 }
6822
6823 if let Some(value) = auto_scaling_strategy {
6824 scx.require_feature_flag(&ENABLE_AUTO_SCALING_STRATEGY)?;
6825 let strategy = plan_auto_scaling_strategy(value)?;
6826 options.auto_scaling_strategy = AlterOptionParameter::Set(strategy);
6827 }
6828
6829 let effective_strategy = match &options.auto_scaling_strategy {
6833 AlterOptionParameter::Set(s) => s.clone(),
6834 AlterOptionParameter::Reset => None,
6835 AlterOptionParameter::Unchanged => cluster.auto_scaling_strategy().cloned(),
6836 };
6837 if let Some(effective_strategy) = &effective_strategy {
6838 let effective_size = size.as_deref().or_else(|| cluster.managed_size());
6839 let schedule_non_manual = match &schedule {
6840 Some(s) => !matches!(s, ClusterScheduleOptionValue::Manual),
6841 None => cluster
6842 .schedule()
6843 .is_some_and(|s| !matches!(s, ClusterSchedule::Manual)),
6844 };
6845 validate_auto_scaling_strategy(
6846 effective_strategy,
6847 effective_size,
6848 schedule_non_manual,
6849 )?;
6850 }
6851 }
6852 false => {
6853 if !with_options.is_empty() {
6854 sql_bail!("ALTER... WITH not supported for unmanaged clusters");
6855 }
6856 if auto_scaling_strategy.is_some() {
6857 sql_bail!("AUTO SCALING STRATEGY not supported for unmanaged clusters");
6858 }
6859 if availability_zones.is_some() {
6860 sql_bail!("AVAILABILITY ZONES not supported for unmanaged clusters");
6861 }
6862 if replication_factor.is_some() {
6863 sql_bail!("REPLICATION FACTOR not supported for unmanaged clusters");
6864 }
6865 if introspection_debugging.is_some() {
6866 sql_bail!("INTROSPECTION DEBUGGING not supported for unmanaged clusters");
6867 }
6868 if introspection_interval.is_some() {
6869 sql_bail!("INTROSPECTION INTERVAL not supported for unmanaged clusters");
6870 }
6871 if experimental_arrangement_compression.is_some() {
6872 sql_bail!(
6873 "EXPERIMENTAL ARRANGEMENT COMPRESSION not supported for unmanaged clusters"
6874 );
6875 }
6876 if size.is_some() {
6877 sql_bail!("SIZE not supported for unmanaged clusters");
6878 }
6879 if disk.is_some() {
6880 sql_bail!("DISK not supported for unmanaged clusters");
6881 }
6882 if schedule.is_some()
6883 && !matches!(schedule, Some(ClusterScheduleOptionValue::Manual))
6884 {
6885 sql_bail!(
6886 "cluster schedules other than MANUAL are not supported for unmanaged clusters"
6887 );
6888 }
6889 if let Some(current_schedule) = cluster.schedule() {
6890 if !matches!(current_schedule, ClusterSchedule::Manual)
6891 && schedule.is_none()
6892 {
6893 sql_bail!(
6894 "when switching a cluster to unmanaged, if the managed \
6895 cluster's SCHEDULE is anything other than MANUAL, you have to \
6896 explicitly set the SCHEDULE to MANUAL"
6897 );
6898 }
6899 }
6900 }
6901 }
6902
6903 let mut replicas = vec![];
6904 for ReplicaDefinition { name, options } in
6905 replica_defs.into_iter().flat_map(Vec::into_iter)
6906 {
6907 replicas.push((normalize::ident(name), plan_replica_config(scx, options)?));
6908 }
6909
6910 if let Some(managed) = managed {
6911 options.managed = AlterOptionParameter::Set(managed);
6912 }
6913 if let Some(replication_factor) = replication_factor {
6914 options.replication_factor = AlterOptionParameter::Set(replication_factor);
6915 } else if schedule
6916 .as_ref()
6917 .is_some_and(|s| !matches!(s, ClusterScheduleOptionValue::Manual))
6918 && managed != Some(true)
6919 {
6920 options.replication_factor = AlterOptionParameter::Set(0);
6933 }
6934 if let Some(size) = &size {
6935 options.size = AlterOptionParameter::Set(size.clone());
6936 }
6937 if let Some(availability_zones) = availability_zones {
6938 options.availability_zones = AlterOptionParameter::Set(availability_zones);
6939 }
6940 if let Some(introspection_debugging) = introspection_debugging {
6941 options.introspection_debugging =
6942 AlterOptionParameter::Set(introspection_debugging);
6943 }
6944 if let Some(introspection_interval) = introspection_interval {
6945 options.introspection_interval = AlterOptionParameter::Set(introspection_interval);
6946 }
6947 if let Some(experimental_arrangement_compression) = experimental_arrangement_compression
6948 {
6949 options.arrangement_compression =
6950 AlterOptionParameter::Set(experimental_arrangement_compression);
6951 }
6952 if disk.is_some() {
6953 let size = match size.as_deref() {
6957 Some(s) => s,
6958 None => cluster
6959 .managed_size()
6960 .ok_or_else(|| sql_err!("cluster is not managed"))?,
6961 };
6962 if scx.catalog.is_cluster_size_cc(size) {
6963 sql_bail!(
6964 "DISK option not supported for modern cluster sizes because disk is always enabled"
6965 );
6966 }
6967
6968 scx.catalog
6969 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated);
6970 }
6971 if !replicas.is_empty() {
6972 options.replicas = AlterOptionParameter::Set(replicas);
6973 }
6974 if let Some(schedule) = schedule {
6975 options.schedule = AlterOptionParameter::Set(plan_cluster_schedule(schedule)?);
6976 }
6977 if let Some(workload_class) = workload_class {
6978 options.workload_class = AlterOptionParameter::Set(workload_class.0);
6979 }
6980 }
6981 AlterClusterAction::ResetOptions(reset_options) => {
6982 use AlterOptionParameter::Reset;
6983 use ClusterOptionName::*;
6984
6985 if !scx.catalog.active_role_id().is_system() {
6986 if reset_options.contains(&WorkloadClass) {
6987 sql_bail!("WORKLOAD CLASS not supported for non-system users");
6988 }
6989 }
6990
6991 for option in reset_options {
6997 match option {
6998 AutoScalingStrategy => options.auto_scaling_strategy = Reset,
6999 AvailabilityZones => options.availability_zones = Reset,
7000 Disk => scx
7001 .catalog
7002 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated),
7003 IntrospectionInterval => options.introspection_interval = Reset,
7004 IntrospectionDebugging => options.introspection_debugging = Reset,
7005 ExperimentalArrangementCompression => options.arrangement_compression = Reset,
7006 Managed => options.managed = Reset,
7007 Replicas => options.replicas = Reset,
7008 ReplicationFactor => options.replication_factor = Reset,
7009 Size => options.size = Reset,
7010 Schedule => options.schedule = Reset,
7011 WorkloadClass => options.workload_class = Reset,
7012 }
7013 }
7014 }
7015 }
7016 Ok(Plan::AlterCluster(AlterClusterPlan {
7017 id: cluster.id(),
7018 name: cluster.name().to_string(),
7019 options,
7020 strategy: alter_strategy,
7021 }))
7022}
7023
7024pub fn describe_alter_set_cluster(
7025 _: &StatementContext,
7026 _: AlterSetClusterStatement<Aug>,
7027) -> Result<StatementDesc, PlanError> {
7028 Ok(StatementDesc::new(None))
7029}
7030
7031pub fn plan_alter_item_set_cluster(
7032 scx: &StatementContext,
7033 AlterSetClusterStatement {
7034 if_exists,
7035 set_cluster: in_cluster_name,
7036 name,
7037 object_type,
7038 }: AlterSetClusterStatement<Aug>,
7039) -> Result<Plan, PlanError> {
7040 scx.require_feature_flag(&vars::ENABLE_ALTER_SET_CLUSTER)?;
7041
7042 let object_type = object_type.into();
7043
7044 match object_type {
7046 ObjectType::MaterializedView => {}
7047 ObjectType::Index | ObjectType::Sink | ObjectType::MetricSink | ObjectType::Source => {
7048 bail_unsupported!(29606, format!("ALTER {object_type} SET CLUSTER"))
7049 }
7050 ObjectType::Table
7051 | ObjectType::View
7052 | ObjectType::Type
7053 | ObjectType::Role
7054 | ObjectType::Cluster
7055 | ObjectType::ClusterReplica
7056 | ObjectType::Secret
7057 | ObjectType::Connection
7058 | ObjectType::Database
7059 | ObjectType::Schema
7060 | ObjectType::Func
7061 | ObjectType::NetworkPolicy => {
7062 bail_never_supported!(
7063 format!("ALTER {object_type} SET CLUSTER"),
7064 "sql/alter-set-cluster/",
7065 format!("{object_type} has no associated cluster")
7066 )
7067 }
7068 }
7069
7070 let in_cluster = scx.catalog.get_cluster(in_cluster_name.id);
7071
7072 match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
7073 Some(entry) => {
7074 let current_cluster = entry.cluster_id();
7075 let Some(current_cluster) = current_cluster else {
7076 sql_bail!("No cluster associated with {name}");
7077 };
7078
7079 if current_cluster == in_cluster.id() {
7080 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7081 } else {
7082 Ok(Plan::AlterSetCluster(AlterSetClusterPlan {
7083 id: entry.id(),
7084 set_cluster: in_cluster.id(),
7085 }))
7086 }
7087 }
7088 None => {
7089 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7090 name: name.to_ast_string_simple(),
7091 object_type,
7092 });
7093
7094 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7095 }
7096 }
7097}
7098
7099pub fn describe_alter_object_rename(
7100 _: &StatementContext,
7101 _: AlterObjectRenameStatement,
7102) -> Result<StatementDesc, PlanError> {
7103 Ok(StatementDesc::new(None))
7104}
7105
7106pub fn plan_alter_object_rename(
7107 scx: &mut StatementContext,
7108 AlterObjectRenameStatement {
7109 name,
7110 object_type,
7111 to_item_name,
7112 if_exists,
7113 }: AlterObjectRenameStatement,
7114) -> Result<Plan, PlanError> {
7115 let object_type = object_type.into();
7116 match (object_type, name) {
7117 (
7118 ObjectType::View
7119 | ObjectType::MaterializedView
7120 | ObjectType::Table
7121 | ObjectType::Source
7122 | ObjectType::Index
7123 | ObjectType::Sink
7124 | ObjectType::Secret
7125 | ObjectType::Connection,
7126 UnresolvedObjectName::Item(name),
7127 ) => plan_alter_item_rename(scx, object_type, name, to_item_name, if_exists),
7128 (ObjectType::Cluster, UnresolvedObjectName::Cluster(name)) => {
7129 plan_alter_cluster_rename(scx, object_type, name, to_item_name, if_exists)
7130 }
7131 (ObjectType::ClusterReplica, UnresolvedObjectName::ClusterReplica(name)) => {
7132 plan_alter_cluster_replica_rename(scx, object_type, name, to_item_name, if_exists)
7133 }
7134 (ObjectType::Schema, UnresolvedObjectName::Schema(name)) => {
7135 plan_alter_schema_rename(scx, name, to_item_name, if_exists)
7136 }
7137 (object_type, name) => {
7138 bail_internal!("invalid object type '{object_type}' for ALTER RENAME with name {name}")
7141 }
7142 }
7143}
7144
7145pub fn plan_alter_schema_rename(
7146 scx: &mut StatementContext,
7147 name: UnresolvedSchemaName,
7148 to_schema_name: Ident,
7149 if_exists: bool,
7150) -> Result<Plan, PlanError> {
7151 let normalized = normalize::unresolved_schema_name(name.clone())?;
7155 if normalized.database.is_none() && normalized.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA {
7156 sql_bail!(
7157 "cannot rename schemas in the ambient database: {:?}",
7158 mz_repr::namespaces::MZ_TEMP_SCHEMA
7159 );
7160 }
7161
7162 let Some((db_spec, schema_spec)) = resolve_schema(scx, name.clone(), if_exists)? else {
7163 let object_type = ObjectType::Schema;
7164 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7165 name: name.to_ast_string_simple(),
7166 object_type,
7167 });
7168 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7169 };
7170
7171 if scx
7173 .resolve_schema_in_database(&db_spec, &to_schema_name)
7174 .is_ok()
7175 {
7176 return Err(PlanError::Catalog(CatalogError::SchemaAlreadyExists(
7177 to_schema_name.clone().into_string(),
7178 )));
7179 }
7180
7181 let schema = scx.catalog.get_schema(&db_spec, &schema_spec);
7183 if schema.id().is_system() {
7184 bail_never_supported!(format!("renaming the {} schema", schema.name().schema))
7185 }
7186
7187 Ok(Plan::AlterSchemaRename(AlterSchemaRenamePlan {
7188 cur_schema_spec: (db_spec, schema_spec),
7189 new_schema_name: to_schema_name.into_string(),
7190 }))
7191}
7192
7193pub fn plan_alter_schema_swap<F>(
7194 scx: &mut StatementContext,
7195 name_a: UnresolvedSchemaName,
7196 name_b: Ident,
7197 if_exists: bool,
7198 gen_temp_suffix: F,
7199) -> Result<Plan, PlanError>
7200where
7201 F: Fn(&dyn Fn(&str) -> bool) -> Result<String, PlanError>,
7202{
7203 let normalized_a = normalize::unresolved_schema_name(name_a.clone())?;
7207 if normalized_a.database.is_none() && normalized_a.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA
7208 {
7209 sql_bail!("cannot swap schemas that are in the ambient database");
7210 }
7211 let name_b_str = normalize::ident_ref(&name_b);
7213 if name_b_str == mz_repr::namespaces::MZ_TEMP_SCHEMA {
7214 sql_bail!("cannot swap schemas that are in the ambient database");
7215 }
7216
7217 let schema_a = match scx.resolve_schema(name_a.clone()) {
7218 Ok(schema) => schema,
7219 Err(_) if if_exists => {
7220 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7221 name: name_a.to_ast_string_simple(),
7222 object_type: ObjectType::Schema,
7223 });
7224 return Ok(Plan::AlterNoop(AlterNoopPlan {
7225 object_type: ObjectType::Schema,
7226 }));
7227 }
7228 Err(e) => return Err(e),
7229 };
7230
7231 let db_spec = schema_a.database().clone();
7232 if matches!(db_spec, ResolvedDatabaseSpecifier::Ambient) {
7233 sql_bail!("cannot swap schemas that are in the ambient database");
7234 };
7235 let schema_b = scx.resolve_schema_in_database(&db_spec, &name_b)?;
7236
7237 if schema_a.id().is_system() || schema_b.id().is_system() {
7239 bail_never_supported!("swapping a system schema".to_string())
7240 }
7241
7242 const SCHEMA_SWAP_PREFIX: &str = "mz_schema_swap_";
7246 let check = |temp_suffix: &str| {
7247 let mut temp_name = ident!(SCHEMA_SWAP_PREFIX);
7248 temp_name.append_lossy(temp_suffix);
7249 scx.resolve_schema_in_database(&db_spec, &temp_name)
7250 .is_err()
7251 };
7252 let temp_suffix = gen_temp_suffix(&check)?;
7253 let name_temp = format!("{SCHEMA_SWAP_PREFIX}{temp_suffix}");
7254
7255 Ok(Plan::AlterSchemaSwap(AlterSchemaSwapPlan {
7256 schema_a_spec: (*schema_a.database(), *schema_a.id()),
7257 schema_a_name: schema_a.name().schema.to_string(),
7258 schema_b_spec: (*schema_b.database(), *schema_b.id()),
7259 schema_b_name: schema_b.name().schema.to_string(),
7260 name_temp,
7261 }))
7262}
7263
7264pub fn plan_alter_item_rename(
7265 scx: &mut StatementContext,
7266 object_type: ObjectType,
7267 name: UnresolvedItemName,
7268 to_item_name: Ident,
7269 if_exists: bool,
7270) -> Result<Plan, PlanError> {
7271 let resolved = match resolve_item_or_type(scx, object_type, name.clone(), if_exists) {
7272 Ok(r) => r,
7273 Err(PlanError::MismatchedObjectType {
7275 name,
7276 is_type: ObjectType::MaterializedView,
7277 expected_type: ObjectType::View,
7278 }) => {
7279 return Err(PlanError::AlterViewOnMaterializedView(name.to_string()));
7280 }
7281 e => e?,
7282 };
7283
7284 match resolved {
7285 Some(entry) => {
7286 let full_name = scx.catalog.resolve_full_name(entry.name());
7287 let item_type = entry.item_type();
7288
7289 let proposed_name = QualifiedItemName {
7290 qualifiers: entry.name().qualifiers.clone(),
7291 item: to_item_name.clone().into_string(),
7292 };
7293
7294 let conflicting_type_exists;
7298 let conflicting_item_exists;
7299 if item_type == CatalogItemType::Type {
7300 conflicting_type_exists = scx.catalog.get_type_by_name(&proposed_name).is_some();
7301 conflicting_item_exists = scx
7302 .catalog
7303 .get_item_by_name(&proposed_name)
7304 .map(|item| item.item_type().conflicts_with_type())
7305 .unwrap_or(false);
7306 } else {
7307 conflicting_type_exists = item_type.conflicts_with_type()
7308 && scx.catalog.get_type_by_name(&proposed_name).is_some();
7309 conflicting_item_exists = scx.catalog.get_item_by_name(&proposed_name).is_some();
7310 };
7311 if conflicting_type_exists || conflicting_item_exists {
7312 sql_bail!("catalog item '{}' already exists", to_item_name);
7313 }
7314
7315 Ok(Plan::AlterItemRename(AlterItemRenamePlan {
7316 id: entry.id(),
7317 current_full_name: full_name,
7318 to_name: normalize::ident(to_item_name),
7319 object_type,
7320 }))
7321 }
7322 None => {
7323 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7324 name: name.to_ast_string_simple(),
7325 object_type,
7326 });
7327
7328 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7329 }
7330 }
7331}
7332
7333pub fn plan_alter_cluster_rename(
7334 scx: &mut StatementContext,
7335 object_type: ObjectType,
7336 name: Ident,
7337 to_name: Ident,
7338 if_exists: bool,
7339) -> Result<Plan, PlanError> {
7340 match resolve_cluster(scx, &name, if_exists)? {
7341 Some(entry) => Ok(Plan::AlterClusterRename(AlterClusterRenamePlan {
7342 id: entry.id(),
7343 name: entry.name().to_string(),
7344 to_name: ident(to_name),
7345 })),
7346 None => {
7347 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7348 name: name.to_ast_string_simple(),
7349 object_type,
7350 });
7351
7352 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7353 }
7354 }
7355}
7356
7357pub fn plan_alter_cluster_swap<F>(
7358 scx: &mut StatementContext,
7359 name_a: Ident,
7360 name_b: Ident,
7361 if_exists: bool,
7362 gen_temp_suffix: F,
7363) -> Result<Plan, PlanError>
7364where
7365 F: Fn(&dyn Fn(&str) -> bool) -> Result<String, PlanError>,
7366{
7367 let cluster_a = match scx.resolve_cluster(Some(&name_a)) {
7368 Ok(cluster) => cluster,
7369 Err(_) if if_exists => {
7370 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7371 name: name_a.to_ast_string_simple(),
7372 object_type: ObjectType::Cluster,
7373 });
7374 return Ok(Plan::AlterNoop(AlterNoopPlan {
7375 object_type: ObjectType::Cluster,
7376 }));
7377 }
7378 Err(e) => return Err(e),
7379 };
7380 let cluster_b = scx.resolve_cluster(Some(&name_b))?;
7381
7382 const CLUSTER_SWAP_PREFIX: &str = "mz_cluster_swap_";
7383 let check = |temp_suffix: &str| {
7384 let mut temp_name = ident!(CLUSTER_SWAP_PREFIX);
7385 temp_name.append_lossy(temp_suffix);
7386 match scx.catalog.resolve_cluster(Some(temp_name.as_str())) {
7387 Err(CatalogError::UnknownCluster(_)) => true,
7389 Ok(_) | Err(_) => false,
7391 }
7392 };
7393 let temp_suffix = gen_temp_suffix(&check)?;
7394 let name_temp = format!("{CLUSTER_SWAP_PREFIX}{temp_suffix}");
7395
7396 Ok(Plan::AlterClusterSwap(AlterClusterSwapPlan {
7397 id_a: cluster_a.id(),
7398 id_b: cluster_b.id(),
7399 name_a: name_a.into_string(),
7400 name_b: name_b.into_string(),
7401 name_temp,
7402 }))
7403}
7404
7405pub fn plan_alter_cluster_replica_rename(
7406 scx: &mut StatementContext,
7407 object_type: ObjectType,
7408 name: QualifiedReplica,
7409 to_item_name: Ident,
7410 if_exists: bool,
7411) -> Result<Plan, PlanError> {
7412 match resolve_cluster_replica(scx, &name, if_exists)? {
7413 Some((cluster, replica)) => {
7414 ensure_cluster_is_not_managed(scx, cluster.id())?;
7415 Ok(Plan::AlterClusterReplicaRename(
7416 AlterClusterReplicaRenamePlan {
7417 cluster_id: cluster.id(),
7418 replica_id: replica,
7419 name: QualifiedReplica {
7420 cluster: Ident::new(cluster.name())?,
7421 replica: name.replica,
7422 },
7423 to_name: normalize::ident(to_item_name),
7424 },
7425 ))
7426 }
7427 None => {
7428 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7429 name: name.to_ast_string_simple(),
7430 object_type,
7431 });
7432
7433 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7434 }
7435 }
7436}
7437
7438pub fn describe_alter_object_swap(
7439 _: &StatementContext,
7440 _: AlterObjectSwapStatement,
7441) -> Result<StatementDesc, PlanError> {
7442 Ok(StatementDesc::new(None))
7443}
7444
7445pub fn plan_alter_object_swap(
7446 scx: &mut StatementContext,
7447 stmt: AlterObjectSwapStatement,
7448) -> Result<Plan, PlanError> {
7449 scx.require_feature_flag(&vars::ENABLE_ALTER_SWAP)?;
7450
7451 let AlterObjectSwapStatement {
7452 object_type,
7453 if_exists,
7454 name_a,
7455 name_b,
7456 } = stmt;
7457 let object_type = object_type.into();
7458
7459 let gen_temp_suffix = |check_fn: &dyn Fn(&str) -> bool| {
7461 let mut attempts = 0;
7462 let name_temp = loop {
7463 attempts += 1;
7464 if attempts > 10 {
7465 tracing::warn!("Unable to generate temp id for swapping");
7466 sql_bail!("unable to swap!");
7467 }
7468
7469 let short_id = mz_ore::id_gen::temp_id();
7471 if check_fn(&short_id) {
7472 break short_id;
7473 }
7474 };
7475
7476 Ok(name_temp)
7477 };
7478
7479 match (object_type, name_a, name_b) {
7480 (ObjectType::Schema, UnresolvedObjectName::Schema(name_a), name_b) => {
7481 plan_alter_schema_swap(scx, name_a, name_b, if_exists, gen_temp_suffix)
7482 }
7483 (ObjectType::Cluster, UnresolvedObjectName::Cluster(name_a), name_b) => {
7484 plan_alter_cluster_swap(scx, name_a, name_b, if_exists, gen_temp_suffix)
7485 }
7486 (ObjectType::Schema | ObjectType::Cluster, _, _) => {
7487 bail_internal!("name type does not match object type for ALTER SWAP")
7488 }
7489 (
7490 ObjectType::Table
7491 | ObjectType::View
7492 | ObjectType::MaterializedView
7493 | ObjectType::Source
7494 | ObjectType::Sink
7495 | ObjectType::MetricSink
7496 | ObjectType::Index
7497 | ObjectType::Type
7498 | ObjectType::Role
7499 | ObjectType::ClusterReplica
7500 | ObjectType::Secret
7501 | ObjectType::Connection
7502 | ObjectType::Database
7503 | ObjectType::Func
7504 | ObjectType::NetworkPolicy,
7505 _,
7506 _,
7507 ) => Err(PlanError::Unsupported {
7508 feature: format!("ALTER {object_type} .. SWAP WITH ..."),
7509 discussion_no: None,
7510 }),
7511 }
7512}
7513
7514pub fn describe_alter_retain_history(
7515 _: &StatementContext,
7516 _: AlterRetainHistoryStatement<Aug>,
7517) -> Result<StatementDesc, PlanError> {
7518 Ok(StatementDesc::new(None))
7519}
7520
7521pub fn plan_alter_retain_history(
7522 scx: &StatementContext,
7523 AlterRetainHistoryStatement {
7524 object_type,
7525 if_exists,
7526 name,
7527 history,
7528 }: AlterRetainHistoryStatement<Aug>,
7529) -> Result<Plan, PlanError> {
7530 alter_retain_history(scx, object_type.into(), if_exists, name, history)
7531}
7532
7533fn alter_retain_history(
7534 scx: &StatementContext,
7535 object_type: ObjectType,
7536 if_exists: bool,
7537 name: UnresolvedObjectName,
7538 history: Option<WithOptionValue<Aug>>,
7539) -> Result<Plan, PlanError> {
7540 let name = match (object_type, name) {
7541 (
7542 ObjectType::View
7544 | ObjectType::MaterializedView
7545 | ObjectType::Table
7546 | ObjectType::Source
7547 | ObjectType::Index,
7548 UnresolvedObjectName::Item(name),
7549 ) => name,
7550 (object_type, _) => {
7551 bail_unsupported!(format!("RETAIN HISTORY on {object_type}"))
7552 }
7553 };
7554 match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
7555 Some(entry) => {
7556 let full_name = scx.catalog.resolve_full_name(entry.name());
7557 let item_type = entry.item_type();
7558
7559 if object_type == ObjectType::View && item_type == CatalogItemType::MaterializedView {
7561 return Err(PlanError::AlterViewOnMaterializedView(
7562 full_name.to_string(),
7563 ));
7564 } else if object_type == ObjectType::View {
7565 sql_bail!("{object_type} does not support RETAIN HISTORY")
7566 } else if object_type != item_type {
7567 sql_bail!(
7568 "\"{}\" is a {} not a {}",
7569 full_name,
7570 entry.item_type(),
7571 format!("{object_type}").to_lowercase()
7572 )
7573 }
7574
7575 let (value, lcw) = match &history {
7577 Some(WithOptionValue::RetainHistoryFor(value)) => {
7578 let window = OptionalDuration::try_from_value(value.clone())?;
7579 (Some(value.clone()), window.0)
7580 }
7581 None => (None, Some(DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION)),
7583 _ => sql_bail!("unexpected value type for RETAIN HISTORY"),
7584 };
7585 let window = plan_retain_history(scx, lcw)?;
7586
7587 Ok(Plan::AlterRetainHistory(AlterRetainHistoryPlan {
7588 id: entry.id(),
7589 value,
7590 window,
7591 object_type,
7592 }))
7593 }
7594 None => {
7595 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7596 name: name.to_ast_string_simple(),
7597 object_type,
7598 });
7599
7600 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7601 }
7602 }
7603}
7604
7605fn alter_source_timestamp_interval(
7606 scx: &StatementContext,
7607 if_exists: bool,
7608 source_name: UnresolvedItemName,
7609 value: Option<WithOptionValue<Aug>>,
7610) -> Result<Plan, PlanError> {
7611 let object_type = ObjectType::Source;
7612 match resolve_item_or_type(scx, object_type, source_name.clone(), if_exists)? {
7613 Some(entry) => {
7614 let full_name = scx.catalog.resolve_full_name(entry.name());
7615 if entry.item_type() != CatalogItemType::Source {
7616 sql_bail!(
7617 "\"{}\" is a {} not a {}",
7618 full_name,
7619 entry.item_type(),
7620 format!("{object_type}").to_lowercase()
7621 )
7622 }
7623
7624 match value {
7625 Some(val) => {
7626 let val = match val {
7627 WithOptionValue::Value(v) => v,
7628 _ => sql_bail!("TIMESTAMP INTERVAL requires an interval value"),
7629 };
7630 let duration = Duration::try_from_value(val.clone())?;
7631
7632 let min = scx.catalog.system_vars().min_timestamp_interval();
7633 let max = scx.catalog.system_vars().max_timestamp_interval();
7634 if duration < min || duration > max {
7635 return Err(PlanError::InvalidTimestampInterval {
7636 min,
7637 max,
7638 requested: duration,
7639 });
7640 }
7641
7642 Ok(Plan::AlterSourceTimestampInterval(
7643 AlterSourceTimestampIntervalPlan {
7644 id: entry.id(),
7645 value: Some(val),
7646 interval: duration,
7647 },
7648 ))
7649 }
7650 None => {
7651 let interval = scx.catalog.system_vars().default_timestamp_interval();
7652 Ok(Plan::AlterSourceTimestampInterval(
7653 AlterSourceTimestampIntervalPlan {
7654 id: entry.id(),
7655 value: None,
7656 interval,
7657 },
7658 ))
7659 }
7660 }
7661 }
7662 None => {
7663 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7664 name: source_name.to_ast_string_simple(),
7665 object_type,
7666 });
7667
7668 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7669 }
7670 }
7671}
7672
7673pub fn describe_alter_secret_options(
7674 _: &StatementContext,
7675 _: AlterSecretStatement<Aug>,
7676) -> Result<StatementDesc, PlanError> {
7677 Ok(StatementDesc::new(None))
7678}
7679
7680pub fn plan_alter_secret(
7681 scx: &mut StatementContext,
7682 stmt: AlterSecretStatement<Aug>,
7683) -> Result<Plan, PlanError> {
7684 let AlterSecretStatement {
7685 name,
7686 if_exists,
7687 value,
7688 } = stmt;
7689 let object_type = ObjectType::Secret;
7690 let id = match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
7691 Some(entry) => entry.id(),
7692 None => {
7693 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7694 name: name.to_string(),
7695 object_type,
7696 });
7697
7698 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7699 }
7700 };
7701
7702 let secret_as = query::plan_secret_as(scx, value)?;
7703
7704 Ok(Plan::AlterSecret(AlterSecretPlan { id, secret_as }))
7705}
7706
7707pub fn describe_alter_connection(
7708 _: &StatementContext,
7709 _: AlterConnectionStatement<Aug>,
7710) -> Result<StatementDesc, PlanError> {
7711 Ok(StatementDesc::new(None))
7712}
7713
7714generate_extracted_config!(AlterConnectionOption, (Validate, bool));
7715
7716pub fn plan_alter_connection(
7717 scx: &StatementContext,
7718 stmt: AlterConnectionStatement<Aug>,
7719) -> Result<Plan, PlanError> {
7720 let AlterConnectionStatement {
7721 name,
7722 if_exists,
7723 actions,
7724 with_options,
7725 } = stmt;
7726 let conn_name = normalize::unresolved_item_name(name)?;
7727 let entry = match scx.catalog.resolve_item(&conn_name) {
7728 Ok(entry) => entry,
7729 Err(_) if if_exists => {
7730 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7731 name: conn_name.to_string(),
7732 object_type: ObjectType::Connection,
7733 });
7734
7735 return Ok(Plan::AlterNoop(AlterNoopPlan {
7736 object_type: ObjectType::Connection,
7737 }));
7738 }
7739 Err(e) => return Err(e.into()),
7740 };
7741
7742 let connection = entry.connection()?;
7743
7744 if actions
7745 .iter()
7746 .any(|action| matches!(action, AlterConnectionAction::RotateKeys))
7747 {
7748 if actions.len() > 1 {
7749 sql_bail!("cannot specify any other actions alongside ALTER CONNECTION...ROTATE KEYS");
7750 }
7751
7752 if !with_options.is_empty() {
7753 sql_bail!(
7754 "ALTER CONNECTION...ROTATE KEYS does not support WITH ({})",
7755 with_options
7756 .iter()
7757 .map(|o| o.to_ast_string_simple())
7758 .join(", ")
7759 );
7760 }
7761
7762 if !matches!(connection, Connection::Ssh(_)) {
7763 sql_bail!(
7764 "{} is not an SSH connection",
7765 scx.catalog.resolve_full_name(entry.name())
7766 )
7767 }
7768
7769 return Ok(Plan::AlterConnection(AlterConnectionPlan {
7770 id: entry.id(),
7771 action: crate::plan::AlterConnectionAction::RotateKeys,
7772 }));
7773 }
7774
7775 let options = AlterConnectionOptionExtracted::try_from(with_options)?;
7776 if options.validate.is_some() {
7777 scx.require_feature_flag(&vars::ENABLE_CONNECTION_VALIDATION_SYNTAX)?;
7778 }
7779
7780 let validate = match options.validate {
7781 Some(val) => val,
7782 None => {
7783 scx.catalog
7784 .system_vars()
7785 .enable_default_connection_validation()
7786 && connection.validate_by_default()
7787 }
7788 };
7789
7790 let connection_type = match connection {
7791 Connection::Aws(_) => CreateConnectionType::Aws,
7792 Connection::AwsPrivatelink(_) => CreateConnectionType::AwsPrivatelink,
7793 Connection::Gcp(_) => CreateConnectionType::Gcp,
7794 Connection::Kafka(_) => CreateConnectionType::Kafka,
7795 Connection::Csr(_) => CreateConnectionType::Csr,
7796 Connection::GlueSchemaRegistry(_) => CreateConnectionType::GlueSchemaRegistry,
7797 Connection::Postgres(_) => CreateConnectionType::Postgres,
7798 Connection::Ssh(_) => CreateConnectionType::Ssh,
7799 Connection::MySql(_) => CreateConnectionType::MySql,
7800 Connection::SqlServer(_) => CreateConnectionType::SqlServer,
7801 Connection::IcebergCatalog(_) => CreateConnectionType::IcebergCatalog,
7802 };
7803
7804 let specified_options: BTreeSet<_> = actions
7806 .iter()
7807 .map(|action: &AlterConnectionAction<Aug>| match action {
7808 AlterConnectionAction::SetOption(option) => Ok(option.name.clone()),
7809 AlterConnectionAction::DropOption(name) => Ok(name.clone()),
7810 AlterConnectionAction::RotateKeys => {
7811 Err(internal_err!("RotateKeys is handled separately above"))
7812 }
7813 })
7814 .collect::<Result<_, PlanError>>()?;
7815
7816 for invalid in INALTERABLE_OPTIONS {
7817 if specified_options.contains(invalid) {
7818 sql_bail!("cannot ALTER {} option {}", connection_type, invalid);
7819 }
7820 }
7821
7822 connection::validate_options_per_connection_type(connection_type, specified_options)?;
7823
7824 let mut set_options_vec: Vec<_> = Vec::new();
7826 let mut drop_options: BTreeSet<_> = BTreeSet::new();
7827 for action in actions {
7828 match action {
7829 AlterConnectionAction::SetOption(option) => set_options_vec.push(option),
7830 AlterConnectionAction::DropOption(name) => {
7831 drop_options.insert(name);
7832 }
7833 AlterConnectionAction::RotateKeys => {
7834 bail_internal!("RotateKeys is handled separately above")
7835 }
7836 }
7837 }
7838
7839 let set_options: BTreeMap<_, _> = set_options_vec
7840 .clone()
7841 .into_iter()
7842 .map(|option| (option.name, option.value))
7843 .collect();
7844
7845 let connection_options_extracted =
7849 connection::ConnectionOptionExtracted::try_from(set_options_vec)?;
7850
7851 let duplicates: Vec<_> = connection_options_extracted
7852 .seen
7853 .intersection(&drop_options)
7854 .collect();
7855
7856 if !duplicates.is_empty() {
7857 sql_bail!(
7858 "cannot both SET and DROP/RESET options {}",
7859 duplicates
7860 .iter()
7861 .map(|option| option.to_string())
7862 .join(", ")
7863 )
7864 }
7865
7866 for mutually_exclusive_options in MUTUALLY_EXCLUSIVE_SETS {
7867 let set_options_count = mutually_exclusive_options
7868 .iter()
7869 .filter(|o| set_options.contains_key(o))
7870 .count();
7871 let drop_options_count = mutually_exclusive_options
7872 .iter()
7873 .filter(|o| drop_options.contains(o))
7874 .count();
7875
7876 if set_options_count > 0 && drop_options_count > 0 {
7878 sql_bail!(
7879 "cannot both SET and DROP/RESET mutually exclusive {} options {}",
7880 connection_type,
7881 mutually_exclusive_options
7882 .iter()
7883 .map(|option| option.to_string())
7884 .join(", ")
7885 )
7886 }
7887
7888 if set_options_count > 0 || drop_options_count > 0 {
7893 drop_options.extend(mutually_exclusive_options.iter().cloned());
7894 }
7895
7896 }
7899
7900 Ok(Plan::AlterConnection(AlterConnectionPlan {
7901 id: entry.id(),
7902 action: crate::plan::AlterConnectionAction::AlterOptions {
7903 set_options,
7904 drop_options,
7905 validate,
7906 },
7907 }))
7908}
7909
7910pub fn describe_alter_sink(
7911 _: &StatementContext,
7912 _: AlterSinkStatement<Aug>,
7913) -> Result<StatementDesc, PlanError> {
7914 Ok(StatementDesc::new(None))
7915}
7916
7917pub fn plan_alter_sink(
7918 scx: &mut StatementContext,
7919 stmt: AlterSinkStatement<Aug>,
7920) -> Result<Plan, PlanError> {
7921 let AlterSinkStatement {
7922 sink_name,
7923 if_exists,
7924 action,
7925 } = stmt;
7926
7927 let object_type = ObjectType::Sink;
7928 let item = resolve_item_or_type(scx, object_type, sink_name.clone(), if_exists)?;
7929
7930 let Some(item) = item else {
7931 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7932 name: sink_name.to_string(),
7933 object_type,
7934 });
7935
7936 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7937 };
7938 let item = item.at_version(RelationVersionSelector::Latest);
7940
7941 let create_sql = item.create_sql();
7943 let stmts = mz_sql_parser::parser::parse_statements(create_sql)?;
7944 let [stmt]: [StatementParseResult; 1] = stmts
7945 .try_into()
7946 .map_err(|_| internal_err!("create SQL of sink was not exactly one statement"))?;
7947 let Statement::CreateSink(stmt) = stmt.ast else {
7948 bail_internal!("create SQL of sink is not a CREATE SINK statement");
7949 };
7950 let (mut stmt, _) = crate::names::resolve(scx.catalog, stmt)?;
7951
7952 let mut set_options = vec![];
7954 let mut reset_options = vec![];
7955 match action {
7956 AlterSinkAction::ChangeRelation(new_from) => {
7957 stmt.from = new_from;
7958 }
7959 AlterSinkAction::SetOptions(options) => {
7960 for option in &options {
7961 match &option.name {
7962 CreateSinkOptionName::CommitInterval => {}
7963 name => bail_unsupported!(format!(
7964 "ALTER SINK ... SET ({})",
7965 name.to_ast_string_simple()
7966 )),
7967 }
7968 }
7969 if options.iter().all(|o| stmt.with_options.contains(o)) {
7985 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7986 }
7987 set_options = options;
7988 }
7989 AlterSinkAction::ResetOptions(names) => {
7990 for name in &names {
7991 match name {
7992 CreateSinkOptionName::CommitInterval => {}
7993 name => bail_unsupported!(format!(
7994 "ALTER SINK ... RESET ({})",
7995 name.to_ast_string_simple()
7996 )),
7997 }
7998 if !stmt.with_options.iter().any(|o| o.name == *name) {
8001 sql_bail!(
8002 "cannot RESET {}: option is not set",
8003 name.to_ast_string_simple()
8004 );
8005 }
8006 }
8007 reset_options = names;
8008 }
8009 }
8010 crate::plan::apply_sink_option_edits(&mut stmt.with_options, &set_options, &reset_options);
8011
8012 let Plan::CreateSink(mut plan) = plan_sink(scx, stmt)? else {
8014 bail_internal!("plan_sink did not produce a CreateSink plan");
8015 };
8016
8017 plan.sink.version += 1;
8018
8019 Ok(Plan::AlterSink(AlterSinkPlan {
8020 item_id: item.id(),
8021 global_id: item.global_id(),
8022 sink: plan.sink,
8023 with_snapshot: plan.with_snapshot,
8024 in_cluster: plan.in_cluster,
8025 set_options,
8026 reset_options,
8027 }))
8028}
8029
8030pub fn describe_alter_source(
8031 _: &StatementContext,
8032 _: AlterSourceStatement<Aug>,
8033) -> Result<StatementDesc, PlanError> {
8034 Ok(StatementDesc::new(None))
8036}
8037
8038generate_extracted_config!(
8039 AlterSourceAddSubsourceOption,
8040 (TextColumns, Vec::<UnresolvedItemName>, Default(vec![])),
8041 (ExcludeColumns, Vec::<UnresolvedItemName>, Default(vec![])),
8042 (Details, String)
8043);
8044
8045pub fn plan_alter_source(
8046 scx: &mut StatementContext,
8047 stmt: AlterSourceStatement<Aug>,
8048) -> Result<Plan, PlanError> {
8049 let AlterSourceStatement {
8050 source_name,
8051 if_exists,
8052 action,
8053 } = stmt;
8054 let object_type = ObjectType::Source;
8055
8056 if resolve_item_or_type(scx, object_type, source_name.clone(), if_exists)?.is_none() {
8057 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
8058 name: source_name.to_string(),
8059 object_type,
8060 });
8061
8062 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
8063 }
8064
8065 match action {
8066 AlterSourceAction::SetOptions(options) => {
8067 let mut options = options.into_iter();
8068 let option = options
8069 .next()
8070 .ok_or_else(|| sql_err!("ALTER SOURCE SET requires at least one option"))?;
8071 if option.name == CreateSourceOptionName::RetainHistory {
8072 if options.next().is_some() {
8073 sql_bail!("RETAIN HISTORY must be only option");
8074 }
8075 return alter_retain_history(
8076 scx,
8077 object_type,
8078 if_exists,
8079 UnresolvedObjectName::Item(source_name),
8080 option.value,
8081 );
8082 }
8083 if option.name == CreateSourceOptionName::TimestampInterval {
8084 if options.next().is_some() {
8085 sql_bail!("TIMESTAMP INTERVAL must be only option");
8086 }
8087 return alter_source_timestamp_interval(scx, if_exists, source_name, option.value);
8088 }
8089 sql_bail!(
8092 "Cannot modify the {} of a SOURCE.",
8093 option.name.to_ast_string_simple()
8094 );
8095 }
8096 AlterSourceAction::ResetOptions(reset) => {
8097 let mut options = reset.into_iter();
8098 let option = options
8099 .next()
8100 .ok_or_else(|| sql_err!("ALTER SOURCE RESET requires at least one option"))?;
8101 if option == CreateSourceOptionName::RetainHistory {
8102 if options.next().is_some() {
8103 sql_bail!("RETAIN HISTORY must be only option");
8104 }
8105 return alter_retain_history(
8106 scx,
8107 object_type,
8108 if_exists,
8109 UnresolvedObjectName::Item(source_name),
8110 None,
8111 );
8112 }
8113 if option == CreateSourceOptionName::TimestampInterval {
8114 if options.next().is_some() {
8115 sql_bail!("TIMESTAMP INTERVAL must be only option");
8116 }
8117 return alter_source_timestamp_interval(scx, if_exists, source_name, None);
8118 }
8119 sql_bail!(
8120 "Cannot modify the {} of a SOURCE.",
8121 option.to_ast_string_simple()
8122 );
8123 }
8124 AlterSourceAction::DropSubsources { .. } => {
8125 sql_bail!("ALTER SOURCE...DROP SUBSOURCE no longer supported; use DROP SOURCE")
8126 }
8127 AlterSourceAction::AddSubsources { .. } => {
8128 sql_bail!("ALTER SOURCE...ADD SUBSOURCE must be purified before planning")
8129 }
8130 AlterSourceAction::RefreshReferences => {
8131 sql_bail!("ALTER SOURCE...REFRESH REFERENCES must be purified before planning")
8132 }
8133 };
8134}
8135
8136pub fn describe_alter_system_set(
8137 _: &StatementContext,
8138 _: AlterSystemSetStatement,
8139) -> Result<StatementDesc, PlanError> {
8140 Ok(StatementDesc::new(None))
8141}
8142
8143pub fn plan_alter_system_set(
8144 _: &StatementContext,
8145 AlterSystemSetStatement { name, to }: AlterSystemSetStatement,
8146) -> Result<Plan, PlanError> {
8147 let name = name.to_string();
8148 Ok(Plan::AlterSystemSet(AlterSystemSetPlan {
8149 name,
8150 value: scl::plan_set_variable_to(to)?,
8151 }))
8152}
8153
8154pub fn describe_alter_system_reset(
8155 _: &StatementContext,
8156 _: AlterSystemResetStatement,
8157) -> Result<StatementDesc, PlanError> {
8158 Ok(StatementDesc::new(None))
8159}
8160
8161pub fn plan_alter_system_reset(
8162 _: &StatementContext,
8163 AlterSystemResetStatement { name }: AlterSystemResetStatement,
8164) -> Result<Plan, PlanError> {
8165 let name = name.to_string();
8166 Ok(Plan::AlterSystemReset(AlterSystemResetPlan { name }))
8167}
8168
8169pub fn describe_alter_system_reset_all(
8170 _: &StatementContext,
8171 _: AlterSystemResetAllStatement,
8172) -> Result<StatementDesc, PlanError> {
8173 Ok(StatementDesc::new(None))
8174}
8175
8176pub fn plan_alter_system_reset_all(
8177 _: &StatementContext,
8178 _: AlterSystemResetAllStatement,
8179) -> Result<Plan, PlanError> {
8180 Ok(Plan::AlterSystemResetAll(AlterSystemResetAllPlan {}))
8181}
8182
8183pub fn describe_alter_role(
8184 _: &StatementContext,
8185 _: AlterRoleStatement<Aug>,
8186) -> Result<StatementDesc, PlanError> {
8187 Ok(StatementDesc::new(None))
8188}
8189
8190pub fn plan_alter_role(
8191 scx: &StatementContext,
8192 AlterRoleStatement { name, option }: AlterRoleStatement<Aug>,
8193) -> Result<Plan, PlanError> {
8194 let option = match option {
8195 AlterRoleOption::Attributes(attrs) => {
8196 let attrs = plan_role_attributes(attrs, scx)?;
8197 PlannedAlterRoleOption::Attributes(attrs)
8198 }
8199 AlterRoleOption::Variable(variable) => {
8200 let var = plan_role_variable(scx, variable)?;
8201 PlannedAlterRoleOption::Variable(var)
8202 }
8203 };
8204
8205 Ok(Plan::AlterRole(AlterRolePlan {
8206 id: name.id,
8207 name: name.name,
8208 option,
8209 }))
8210}
8211
8212pub fn describe_alter_table_add_column(
8213 _: &StatementContext,
8214 _: AlterTableAddColumnStatement<Aug>,
8215) -> Result<StatementDesc, PlanError> {
8216 Ok(StatementDesc::new(None))
8217}
8218
8219pub fn plan_alter_table_add_column(
8220 scx: &StatementContext,
8221 stmt: AlterTableAddColumnStatement<Aug>,
8222) -> Result<Plan, PlanError> {
8223 let AlterTableAddColumnStatement {
8224 if_exists,
8225 name,
8226 if_col_not_exist,
8227 column_name,
8228 data_type,
8229 } = stmt;
8230 let object_type = ObjectType::Table;
8231
8232 scx.require_feature_flag(&vars::ENABLE_ALTER_TABLE_ADD_COLUMN)?;
8233
8234 let (relation_id, item_name, desc) =
8235 match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
8236 Some(item) => {
8237 let item_name = scx.catalog.resolve_full_name(item.name());
8239 let item = item.at_version(RelationVersionSelector::Latest);
8240 let desc = item
8241 .relation_desc()
8242 .ok_or_else(|| sql_err!("item does not have a relation description"))?
8243 .into_owned();
8244 (item.id(), item_name, desc)
8245 }
8246 None => {
8247 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
8248 name: name.to_ast_string_simple(),
8249 object_type,
8250 });
8251 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
8252 }
8253 };
8254
8255 let column_name = ColumnName::from(column_name.as_str());
8256 if desc.get_by_name(&column_name).is_some() {
8257 if if_col_not_exist {
8258 scx.catalog.add_notice(PlanNotice::ColumnAlreadyExists {
8259 column_name: column_name.to_string(),
8260 object_name: item_name.item,
8261 });
8262 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
8263 } else {
8264 return Err(PlanError::ColumnAlreadyExists {
8265 column_name,
8266 object_name: item_name.item,
8267 });
8268 }
8269 }
8270
8271 let scalar_type = scalar_type_from_sql(scx, &data_type)?;
8272 let column_type = scalar_type.nullable(true);
8274 let raw_sql_type = mz_sql_parser::parser::parse_data_type(&data_type.to_ast_string_stable())?;
8276
8277 Ok(Plan::AlterTableAddColumn(AlterTablePlan {
8278 relation_id,
8279 column_name,
8280 column_type,
8281 raw_sql_type,
8282 }))
8283}
8284
8285pub fn describe_alter_materialized_view_apply_replacement(
8286 _: &StatementContext,
8287 _: AlterMaterializedViewApplyReplacementStatement,
8288) -> Result<StatementDesc, PlanError> {
8289 Ok(StatementDesc::new(None))
8290}
8291
8292pub fn plan_alter_materialized_view_apply_replacement(
8293 scx: &StatementContext,
8294 stmt: AlterMaterializedViewApplyReplacementStatement,
8295) -> Result<Plan, PlanError> {
8296 let AlterMaterializedViewApplyReplacementStatement {
8297 if_exists,
8298 name,
8299 replacement_name,
8300 } = stmt;
8301
8302 scx.require_feature_flag(&vars::ENABLE_REPLACEMENT_MATERIALIZED_VIEWS)?;
8303
8304 let object_type = ObjectType::MaterializedView;
8305 let Some(mv) = resolve_item_or_type(scx, object_type, name.clone(), if_exists)? else {
8306 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
8307 name: name.to_ast_string_simple(),
8308 object_type,
8309 });
8310 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
8311 };
8312
8313 let replacement = resolve_item_or_type(scx, object_type, replacement_name, false)?
8314 .ok_or_else(|| sql_err!("replacement materialized view does not exist"))?;
8315
8316 if replacement.replacement_target() != Some(mv.id()) {
8317 return Err(PlanError::InvalidReplacement {
8318 item_type: mv.item_type(),
8319 item_name: scx.catalog.minimal_qualification(mv.name()),
8320 replacement_type: replacement.item_type(),
8321 replacement_name: scx.catalog.minimal_qualification(replacement.name()),
8322 });
8323 }
8324
8325 Ok(Plan::AlterMaterializedViewApplyReplacement(
8326 AlterMaterializedViewApplyReplacementPlan {
8327 id: mv.id(),
8328 replacement_id: replacement.id(),
8329 },
8330 ))
8331}
8332
8333pub fn describe_comment(
8334 _: &StatementContext,
8335 _: CommentStatement<Aug>,
8336) -> Result<StatementDesc, PlanError> {
8337 Ok(StatementDesc::new(None))
8338}
8339
8340pub fn plan_comment(
8341 scx: &mut StatementContext,
8342 stmt: CommentStatement<Aug>,
8343) -> Result<Plan, PlanError> {
8344 const MAX_COMMENT_LENGTH: usize = 1024;
8345
8346 let CommentStatement { object, comment } = stmt;
8347
8348 if let Some(c) = &comment {
8350 if c.len() > 1024 {
8351 return Err(PlanError::CommentTooLong {
8352 length: c.len(),
8353 max_size: MAX_COMMENT_LENGTH,
8354 });
8355 }
8356 }
8357
8358 let (object_id, column_pos) = match &object {
8359 com_ty @ CommentObjectType::Table { name }
8360 | com_ty @ CommentObjectType::View { name }
8361 | com_ty @ CommentObjectType::MaterializedView { name }
8362 | com_ty @ CommentObjectType::Index { name }
8363 | com_ty @ CommentObjectType::Func { name }
8364 | com_ty @ CommentObjectType::Connection { name }
8365 | com_ty @ CommentObjectType::Source { name }
8366 | com_ty @ CommentObjectType::Sink { name }
8367 | com_ty @ CommentObjectType::Secret { name } => {
8368 let item = scx.get_item_by_resolved_name(name)?;
8369 match (com_ty, item.item_type()) {
8370 (CommentObjectType::Table { .. }, CatalogItemType::Table) => {
8371 (CommentObjectId::Table(item.id()), None)
8372 }
8373 (CommentObjectType::View { .. }, CatalogItemType::View) => {
8374 (CommentObjectId::View(item.id()), None)
8375 }
8376 (CommentObjectType::MaterializedView { .. }, CatalogItemType::MaterializedView) => {
8377 (CommentObjectId::MaterializedView(item.id()), None)
8378 }
8379 (CommentObjectType::Index { .. }, CatalogItemType::Index) => {
8380 (CommentObjectId::Index(item.id()), None)
8381 }
8382 (CommentObjectType::Func { .. }, CatalogItemType::Func) => {
8383 (CommentObjectId::Func(item.id()), None)
8384 }
8385 (CommentObjectType::Connection { .. }, CatalogItemType::Connection) => {
8386 (CommentObjectId::Connection(item.id()), None)
8387 }
8388 (CommentObjectType::Source { .. }, CatalogItemType::Source) => {
8389 (CommentObjectId::Source(item.id()), None)
8390 }
8391 (CommentObjectType::Sink { .. }, CatalogItemType::Sink) => {
8392 (CommentObjectId::Sink(item.id()), None)
8393 }
8394 (CommentObjectType::Secret { .. }, CatalogItemType::Secret) => {
8395 (CommentObjectId::Secret(item.id()), None)
8396 }
8397 (com_ty, cat_ty) => {
8398 let expected_type = match com_ty {
8399 CommentObjectType::Table { .. } => ObjectType::Table,
8400 CommentObjectType::View { .. } => ObjectType::View,
8401 CommentObjectType::MaterializedView { .. } => ObjectType::MaterializedView,
8402 CommentObjectType::Index { .. } => ObjectType::Index,
8403 CommentObjectType::Func { .. } => ObjectType::Func,
8404 CommentObjectType::Connection { .. } => ObjectType::Connection,
8405 CommentObjectType::Source { .. } => ObjectType::Source,
8406 CommentObjectType::Sink { .. } => ObjectType::Sink,
8407 CommentObjectType::Secret { .. } => ObjectType::Secret,
8408 _ => sql_bail!("cannot comment on this object type"),
8409 };
8410
8411 return Err(PlanError::InvalidObjectType {
8412 expected_type: SystemObjectType::Object(expected_type),
8413 actual_type: SystemObjectType::Object(cat_ty.into()),
8414 object_name: item.name().item.clone(),
8415 });
8416 }
8417 }
8418 }
8419 CommentObjectType::Type { ty } => match ty {
8420 ResolvedDataType::AnonymousList(_) | ResolvedDataType::AnonymousMap { .. } => {
8421 sql_bail!("cannot comment on anonymous list or map type");
8422 }
8423 ResolvedDataType::Named { id, modifiers, .. } => {
8424 if !modifiers.is_empty() {
8425 sql_bail!("cannot comment on type with modifiers");
8426 }
8427 (CommentObjectId::Type(*id), None)
8428 }
8429 ResolvedDataType::Error => bail_internal!("unresolved data type"),
8430 },
8431 CommentObjectType::Column { name } => {
8432 let (item, pos) = scx.get_column_by_resolved_name(name)?;
8433 match item.item_type() {
8434 CatalogItemType::Table => (CommentObjectId::Table(item.id()), Some(pos + 1)),
8435 CatalogItemType::Source => (CommentObjectId::Source(item.id()), Some(pos + 1)),
8436 CatalogItemType::View => (CommentObjectId::View(item.id()), Some(pos + 1)),
8437 CatalogItemType::MaterializedView => {
8438 (CommentObjectId::MaterializedView(item.id()), Some(pos + 1))
8439 }
8440 CatalogItemType::Type => (CommentObjectId::Type(item.id()), Some(pos + 1)),
8441 r => {
8442 return Err(PlanError::Unsupported {
8443 feature: format!("Specifying comments on a column of {r}"),
8444 discussion_no: None,
8445 });
8446 }
8447 }
8448 }
8449 CommentObjectType::Role { name } => (CommentObjectId::Role(name.id), None),
8450 CommentObjectType::Database { name } => {
8451 (CommentObjectId::Database(*name.database_id()), None)
8452 }
8453 CommentObjectType::Schema { name } => {
8454 if matches!(name.schema_spec(), SchemaSpecifier::Temporary) {
8458 sql_bail!(
8459 "cannot comment on schema {} because it is a temporary schema",
8460 mz_repr::namespaces::MZ_TEMP_SCHEMA
8461 );
8462 }
8463 (
8464 CommentObjectId::Schema((*name.database_spec(), *name.schema_spec())),
8465 None,
8466 )
8467 }
8468 CommentObjectType::Cluster { name } => (CommentObjectId::Cluster(name.id), None),
8469 CommentObjectType::ClusterReplica { name } => {
8470 let replica = scx.catalog.resolve_cluster_replica(name)?;
8471 (
8472 CommentObjectId::ClusterReplica((replica.cluster_id(), replica.replica_id())),
8473 None,
8474 )
8475 }
8476 CommentObjectType::NetworkPolicy { name } => {
8477 (CommentObjectId::NetworkPolicy(name.id), None)
8478 }
8479 };
8480
8481 if let Some(p) = column_pos {
8487 i32::try_from(p).map_err(|_| PlanError::TooManyColumns {
8488 max_num_columns: MAX_NUM_COLUMNS,
8489 req_num_columns: p,
8490 })?;
8491 }
8492
8493 Ok(Plan::Comment(CommentPlan {
8494 object_id,
8495 sub_component: column_pos,
8496 comment,
8497 }))
8498}
8499
8500pub(crate) fn resolve_cluster<'a>(
8501 scx: &'a StatementContext,
8502 name: &'a Ident,
8503 if_exists: bool,
8504) -> Result<Option<&'a dyn CatalogCluster<'a>>, PlanError> {
8505 match scx.resolve_cluster(Some(name)) {
8506 Ok(cluster) => Ok(Some(cluster)),
8507 Err(_) if if_exists => Ok(None),
8508 Err(e) => Err(e),
8509 }
8510}
8511
8512pub(crate) fn resolve_cluster_replica<'a>(
8513 scx: &'a StatementContext,
8514 name: &QualifiedReplica,
8515 if_exists: bool,
8516) -> Result<Option<(&'a dyn CatalogCluster<'a>, ReplicaId)>, PlanError> {
8517 match scx.resolve_cluster(Some(&name.cluster)) {
8518 Ok(cluster) => match cluster.replica_ids().get(name.replica.as_str()) {
8519 Some(replica_id) => Ok(Some((cluster, *replica_id))),
8520 None if if_exists => Ok(None),
8521 None => Err(sql_err!(
8522 "CLUSTER {} has no CLUSTER REPLICA named {}",
8523 cluster.name(),
8524 name.replica.as_str().quoted(),
8525 )),
8526 },
8527 Err(_) if if_exists => Ok(None),
8528 Err(e) => Err(e),
8529 }
8530}
8531
8532pub(crate) fn resolve_database<'a>(
8533 scx: &'a StatementContext,
8534 name: &'a UnresolvedDatabaseName,
8535 if_exists: bool,
8536) -> Result<Option<&'a dyn CatalogDatabase>, PlanError> {
8537 match scx.resolve_database(name) {
8538 Ok(database) => Ok(Some(database)),
8539 Err(_) if if_exists => Ok(None),
8540 Err(e) => Err(e),
8541 }
8542}
8543
8544pub(crate) fn resolve_schema<'a>(
8545 scx: &'a StatementContext,
8546 name: UnresolvedSchemaName,
8547 if_exists: bool,
8548) -> Result<Option<(ResolvedDatabaseSpecifier, SchemaSpecifier)>, PlanError> {
8549 match scx.resolve_schema(name) {
8550 Ok(schema) => Ok(Some((schema.database().clone(), schema.id().clone()))),
8551 Err(_) if if_exists => Ok(None),
8552 Err(e) => Err(e),
8553 }
8554}
8555
8556pub(crate) fn resolve_network_policy<'a>(
8557 scx: &'a StatementContext,
8558 name: Ident,
8559 if_exists: bool,
8560) -> Result<Option<ResolvedNetworkPolicyName>, PlanError> {
8561 match scx
8562 .catalog
8563 .resolve_network_policy(normalize::ident_ref(&name))
8564 {
8565 Ok(policy) => Ok(Some(ResolvedNetworkPolicyName {
8566 id: policy.id(),
8567 name: policy.name().to_string(),
8568 })),
8569 Err(_) if if_exists => Ok(None),
8570 Err(e) => Err(e.into()),
8571 }
8572}
8573
8574pub(crate) fn resolve_item_or_type<'a>(
8575 scx: &'a StatementContext,
8576 object_type: ObjectType,
8577 name: UnresolvedItemName,
8578 if_exists: bool,
8579) -> Result<Option<&'a dyn CatalogItem>, PlanError> {
8580 let name = normalize::unresolved_item_name(name)?;
8581 let catalog_item = match object_type {
8582 ObjectType::Type => scx.catalog.resolve_type(&name),
8583 ObjectType::Table
8584 | ObjectType::View
8585 | ObjectType::MaterializedView
8586 | ObjectType::Source
8587 | ObjectType::Sink
8588 | ObjectType::MetricSink
8589 | ObjectType::Index
8590 | ObjectType::Role
8591 | ObjectType::Cluster
8592 | ObjectType::ClusterReplica
8593 | ObjectType::Secret
8594 | ObjectType::Connection
8595 | ObjectType::Database
8596 | ObjectType::Schema
8597 | ObjectType::Func
8598 | ObjectType::NetworkPolicy => scx.catalog.resolve_item(&name),
8599 };
8600
8601 match catalog_item {
8602 Ok(item) => {
8603 let is_type = ObjectType::from(item.item_type());
8604 if object_type == is_type {
8605 Ok(Some(item))
8606 } else {
8607 Err(PlanError::MismatchedObjectType {
8608 name: scx.catalog.minimal_qualification(item.name()),
8609 is_type,
8610 expected_type: object_type,
8611 })
8612 }
8613 }
8614 Err(_) if if_exists => Ok(None),
8615 Err(e) => Err(e.into()),
8616 }
8617}
8618
8619fn ensure_cluster_is_not_managed(
8621 scx: &StatementContext,
8622 cluster_id: ClusterId,
8623) -> Result<(), PlanError> {
8624 let cluster = scx.catalog.get_cluster(cluster_id);
8625 if cluster.is_managed() {
8626 Err(PlanError::ManagedCluster {
8627 cluster_name: cluster.name().to_string(),
8628 })
8629 } else {
8630 Ok(())
8631 }
8632}
8633
8634#[cfg(test)]
8635mod tests {
8636 use super::{
8637 METRIC_SINK_CURATED_PREFIX_MARKER, METRIC_SINK_PREFIX_MARKER,
8638 validate_user_metric_sink_prefix,
8639 };
8640
8641 #[mz_ore::test]
8642 fn user_metric_sink_prefix_cannot_reach_the_curated_prefix() {
8643 assert!(validate_user_metric_sink_prefix(METRIC_SINK_CURATED_PREFIX_MARKER).is_err());
8645 assert!(validate_user_metric_sink_prefix("mz_metric_sink_curated_x").is_err());
8646 assert!(validate_user_metric_sink_prefix(METRIC_SINK_PREFIX_MARKER).is_err());
8648 assert!(validate_user_metric_sink_prefix("mz_metric_sink_cur").is_err());
8649 assert!(validate_user_metric_sink_prefix("myapp_").is_err());
8651 assert!(validate_user_metric_sink_prefix("mz_metric_sink_myapp_").is_ok());
8654 assert!(validate_user_metric_sink_prefix("mz_metric_sink_curatex_").is_ok());
8655 assert!(validate_user_metric_sink_prefix("mz_metric_sink_my_curated_").is_ok());
8656 }
8657}