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