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