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, plan_expr, scalar_type_from_catalog, 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 if commit_interval.is_none() {
3762 sql_bail!("Iceberg sink must specify COMMIT INTERVAL");
3763 }
3764
3765 Ok(StorageSinkConnection::Iceberg(IcebergSinkConnection {
3766 catalog_connection_id,
3767 catalog_connection: catalog_connection_id,
3768 storage_connection_id,
3769 storage_connection: storage_connection_id,
3770 table,
3771 namespace,
3772 relation_key_indices,
3773 key_desc_and_indices,
3774 }))
3775}
3776
3777fn kafka_sink_builder(
3778 scx: &StatementContext,
3779 connection: ResolvedItemName,
3780 options: Vec<KafkaSinkConfigOption<Aug>>,
3781 format: Option<FormatSpecifier<Aug>>,
3782 relation_key_indices: Option<Vec<usize>>,
3783 key_desc_and_indices: Option<(RelationDesc, Vec<usize>)>,
3784 headers_index: Option<usize>,
3785 value_desc: RelationDesc,
3786 envelope: SinkEnvelope,
3787 sink_from: CatalogItemId,
3788 commit_interval: Option<Duration>,
3789) -> Result<StorageSinkConnection<ReferencedConnection>, PlanError> {
3790 let connection_item = scx.get_item_by_resolved_name(&connection)?;
3792 let connection_id = connection_item.id();
3793 match connection_item.connection()? {
3794 Connection::Kafka(_) => (),
3795 _ => sql_bail!(
3796 "{} is not a kafka connection",
3797 scx.catalog.resolve_full_name(connection_item.name())
3798 ),
3799 };
3800
3801 if commit_interval.is_some() {
3802 sql_bail!("COMMIT INTERVAL option is not supported with KAFKA sinks");
3803 }
3804
3805 let KafkaSinkConfigOptionExtracted {
3806 topic,
3807 compression_type,
3808 partition_by,
3809 progress_group_id_prefix,
3810 transactional_id_prefix,
3811 legacy_ids,
3812 topic_config,
3813 topic_metadata_refresh_interval,
3814 topic_partition_count,
3815 topic_replication_factor,
3816 seen: _,
3817 }: KafkaSinkConfigOptionExtracted = options.try_into()?;
3818
3819 let transactional_id = match (transactional_id_prefix, legacy_ids) {
3820 (Some(_), Some(true)) => {
3821 sql_bail!("LEGACY IDS cannot be used at the same time as TRANSACTIONAL ID PREFIX")
3822 }
3823 (None, Some(true)) => KafkaIdStyle::Legacy,
3824 (prefix, _) => KafkaIdStyle::Prefix(prefix),
3825 };
3826
3827 let progress_group_id = match (progress_group_id_prefix, legacy_ids) {
3828 (Some(_), Some(true)) => {
3829 sql_bail!("LEGACY IDS cannot be used at the same time as PROGRESS GROUP ID PREFIX")
3830 }
3831 (None, Some(true)) => KafkaIdStyle::Legacy,
3832 (prefix, _) => KafkaIdStyle::Prefix(prefix),
3833 };
3834
3835 let topic_name = topic.ok_or_else(|| sql_err!("KAFKA CONNECTION must specify TOPIC"))?;
3836
3837 if topic_metadata_refresh_interval > MAX_KAFKA_TOPIC_METADATA_REFRESH_INTERVAL {
3838 sql_bail!("TOPIC METADATA REFRESH INTERVAL cannot be greater than 1 hour");
3841 } else if topic_metadata_refresh_interval < MIN_KAFKA_TOPIC_METADATA_REFRESH_INTERVAL {
3842 sql_bail!("TOPIC METADATA REFRESH INTERVAL must be at least 1 second");
3845 }
3846
3847 let assert_positive = |val: Option<i32>, name: &str| {
3848 if let Some(val) = val {
3849 if val <= 0 {
3850 sql_bail!("{} must be a positive integer", name);
3851 }
3852 }
3853 val.map(NonNeg::try_from)
3854 .transpose()
3855 .map_err(|_| PlanError::Unstructured(format!("{} must be a positive integer", name)))
3856 };
3857 let topic_partition_count = assert_positive(topic_partition_count, "TOPIC PARTITION COUNT")?;
3858 let topic_replication_factor =
3859 assert_positive(topic_replication_factor, "TOPIC REPLICATION FACTOR")?;
3860
3861 let gen_avro_schema_options = |conn| {
3864 let CsrConnectionAvro {
3865 connection:
3866 CsrConnection {
3867 connection,
3868 options,
3869 },
3870 seed,
3871 key_strategy,
3872 value_strategy,
3873 } = conn;
3874 if seed.is_some() {
3875 sql_bail!("SEED option does not make sense with sinks");
3876 }
3877 if key_strategy.is_some() {
3878 sql_bail!("KEY STRATEGY option does not make sense with sinks");
3879 }
3880 if value_strategy.is_some() {
3881 sql_bail!("VALUE STRATEGY option does not make sense with sinks");
3882 }
3883
3884 let item = scx.get_item_by_resolved_name(&connection)?;
3885 let csr_connection = match item.connection()? {
3886 Connection::Csr(_) => item.id(),
3887 _ => {
3888 sql_bail!(
3889 "{} is not a schema registry connection",
3890 scx.catalog
3891 .resolve_full_name(item.name())
3892 .to_string()
3893 .quoted()
3894 )
3895 }
3896 };
3897 let extracted_options: CsrConfigOptionExtracted = options.try_into()?;
3898
3899 if key_desc_and_indices.is_none() && extracted_options.avro_key_fullname.is_some() {
3900 sql_bail!("Cannot specify AVRO KEY FULLNAME without a corresponding KEY field");
3901 }
3902
3903 if key_desc_and_indices.is_some()
3904 && (extracted_options.avro_key_fullname.is_some()
3905 ^ extracted_options.avro_value_fullname.is_some())
3906 {
3907 sql_bail!(
3908 "Must specify both AVRO KEY FULLNAME and AVRO VALUE FULLNAME when specifying generated schema names"
3909 );
3910 }
3911
3912 Ok((csr_connection, extracted_options))
3913 };
3914
3915 let map_format = |format: Format<Aug>, desc: &RelationDesc, is_key: bool| match format {
3916 Format::Json { array: false } => Ok::<_, PlanError>(KafkaSinkFormatType::Json),
3917 Format::Bytes if desc.arity() == 1 => {
3918 let col_type = &desc.typ().column_types[0].scalar_type;
3919 if !mz_pgrepr::Value::can_encode_binary(col_type) {
3920 bail_unsupported!(format!(
3921 "BYTES format with non-encodable type: {:?}",
3922 col_type
3923 ));
3924 }
3925
3926 Ok(KafkaSinkFormatType::Bytes)
3927 }
3928 Format::Text if desc.arity() == 1 => Ok(KafkaSinkFormatType::Text),
3929 Format::Bytes | Format::Text => {
3930 bail_unsupported!("BYTES or TEXT format with multiple columns")
3931 }
3932 Format::Json { array: true } => bail_unsupported!("JSON ARRAY format in sinks"),
3933 Format::Avro(AvroSchema::Csr { csr_connection }) => {
3934 let (csr_connection, options) = gen_avro_schema_options(csr_connection)?;
3935 let schema = if is_key {
3936 AvroSchemaGenerator::new(
3937 desc.clone(),
3938 false,
3939 options.key_doc_options,
3940 options.avro_key_fullname.as_deref().unwrap_or("row"),
3941 options.null_defaults,
3942 Some(sink_from),
3943 false,
3944 )?
3945 .schema()
3946 .to_string()
3947 } else {
3948 AvroSchemaGenerator::new(
3949 desc.clone(),
3950 matches!(envelope, SinkEnvelope::Debezium),
3951 options.value_doc_options,
3952 options.avro_value_fullname.as_deref().unwrap_or("envelope"),
3953 options.null_defaults,
3954 Some(sink_from),
3955 true,
3956 )?
3957 .schema()
3958 .to_string()
3959 };
3960 Ok(KafkaSinkFormatType::Avro {
3961 schema,
3962 compatibility_level: if is_key {
3963 options.key_compatibility_level
3964 } else {
3965 options.value_compatibility_level
3966 },
3967 wire_format: WireFormat::Confluent {
3968 registry: Some(csr_connection),
3969 },
3970 })
3971 }
3972 format => bail_unsupported!(format!("sink format {:?}", format)),
3973 };
3974
3975 let partition_by = match &partition_by {
3976 Some(partition_by) => {
3977 let mut scope = Scope::from_source(None, value_desc.iter_names());
3978
3979 match envelope {
3980 SinkEnvelope::Upsert | SinkEnvelope::Append => (),
3981 SinkEnvelope::Debezium => {
3982 let key_indices: HashSet<_> = key_desc_and_indices
3983 .as_ref()
3984 .map(|(_desc, indices)| indices.as_slice())
3985 .unwrap_or_default()
3986 .into_iter()
3987 .collect();
3988 for (i, item) in scope.items.iter_mut().enumerate() {
3989 if !key_indices.contains(&i) {
3990 item.error_if_referenced = Some(|_table, column| {
3991 PlanError::InvalidPartitionByEnvelopeDebezium {
3992 column_name: column.to_string(),
3993 }
3994 });
3995 }
3996 }
3997 }
3998 };
3999
4000 let ecx = &ExprContext {
4001 qcx: &QueryContext::root(scx, QueryLifetime::OneShot),
4002 name: "PARTITION BY",
4003 scope: &scope,
4004 relation_type: value_desc.typ(),
4005 allow_aggregates: false,
4006 allow_subqueries: false,
4007 allow_parameters: false,
4008 allow_windows: false,
4009 };
4010 let expr = plan_expr(ecx, partition_by)?.cast_to(
4011 ecx,
4012 CastContext::Assignment,
4013 &SqlScalarType::UInt64,
4014 )?;
4015 let expr = expr.lower_uncorrelated(scx.catalog.system_vars())?;
4016
4017 Some(expr)
4018 }
4019 _ => None,
4020 };
4021
4022 let format = match format {
4024 Some(FormatSpecifier::KeyValue { key, value }) => {
4025 let key_format = match key_desc_and_indices.as_ref() {
4026 Some((desc, _indices)) => Some(map_format(key, desc, true)?),
4027 None => None,
4028 };
4029 KafkaSinkFormat {
4030 value_format: map_format(value, &value_desc, false)?,
4031 key_format,
4032 }
4033 }
4034 Some(FormatSpecifier::Bare(format)) => {
4035 let key_format = match key_desc_and_indices.as_ref() {
4036 Some((desc, _indices)) => Some(map_format(format.clone(), desc, true)?),
4037 None => None,
4038 };
4039 KafkaSinkFormat {
4040 value_format: map_format(format, &value_desc, false)?,
4041 key_format,
4042 }
4043 }
4044 None => bail_unsupported!("sink without format"),
4045 };
4046
4047 Ok(StorageSinkConnection::Kafka(KafkaSinkConnection {
4048 connection_id,
4049 connection: connection_id,
4050 format,
4051 topic: topic_name,
4052 relation_key_indices,
4053 key_desc_and_indices,
4054 headers_index,
4055 value_desc,
4056 partition_by,
4057 compression_type,
4058 progress_group_id,
4059 transactional_id,
4060 topic_options: KafkaTopicOptions {
4061 partition_count: topic_partition_count,
4062 replication_factor: topic_replication_factor,
4063 topic_config: topic_config.unwrap_or_default(),
4064 },
4065 topic_metadata_refresh_interval,
4066 }))
4067}
4068
4069pub fn describe_create_index(
4070 _: &StatementContext,
4071 _: CreateIndexStatement<Aug>,
4072) -> Result<StatementDesc, PlanError> {
4073 Ok(StatementDesc::new(None))
4074}
4075
4076pub fn plan_create_index(
4077 scx: &StatementContext,
4078 mut stmt: CreateIndexStatement<Aug>,
4079) -> Result<Plan, PlanError> {
4080 let CreateIndexStatement {
4081 name,
4082 on_name,
4083 in_cluster,
4084 key_parts,
4085 with_options,
4086 if_not_exists,
4087 } = &mut stmt;
4088 let on = scx.get_item_by_resolved_name(on_name)?;
4089
4090 {
4091 use CatalogItemType::*;
4092 match on.item_type() {
4093 Table | Source | View | MaterializedView => {
4094 if on.replacement_target().is_some() {
4095 sql_bail!(
4096 "index cannot be created on {} because it is a replacement {}",
4097 on_name.full_name_str(),
4098 on.item_type(),
4099 );
4100 }
4101 }
4102 Sink | Index | Type | Func | Secret | Connection => {
4103 sql_bail!(
4104 "index cannot be created on {} because it is a {}",
4105 on_name.full_name_str(),
4106 on.item_type(),
4107 );
4108 }
4109 }
4110 }
4111
4112 let on_desc = on
4113 .relation_desc()
4114 .ok_or_else(|| sql_err!("item does not have a relation description"))?;
4115
4116 let filled_key_parts = match key_parts {
4117 Some(kp) => kp.to_vec(),
4118 None => {
4119 let mut name_counts = BTreeMap::new();
4124 for name in on_desc.iter_names() {
4125 *name_counts.entry(name).or_insert(0usize) += 1;
4126 }
4127 let key = on_desc.typ().default_key();
4128 key.iter()
4129 .map(|i| {
4130 let name = on_desc.get_name(*i);
4131 if name_counts.get(name).copied() == Some(1) {
4132 Expr::Identifier(vec![name.clone().into()])
4133 } else {
4134 Expr::Value(Value::Number((i + 1).to_string()))
4135 }
4136 })
4137 .collect()
4138 }
4139 };
4140 let keys = query::plan_index_exprs(scx, &on_desc, filled_key_parts.clone())?;
4141
4142 let index_name = if let Some(name) = name {
4143 QualifiedItemName {
4144 qualifiers: on.name().qualifiers.clone(),
4145 item: normalize::ident(name.clone()),
4146 }
4147 } else {
4148 let mut idx_name = QualifiedItemName {
4149 qualifiers: on.name().qualifiers.clone(),
4150 item: on.name().item.clone(),
4151 };
4152 if key_parts.is_none() {
4153 idx_name.item += "_primary_idx";
4155 } else {
4156 let index_name_col_suffix = keys
4159 .iter()
4160 .map(|k| match k {
4161 mz_expr::MirScalarExpr::Column(i, name) => {
4162 match (on_desc.get_unambiguous_name(*i), &name.0) {
4163 (Some(col_name), _) => col_name.to_string(),
4164 (None, Some(name)) => name.to_string(),
4165 (None, None) => format!("{}", i + 1),
4166 }
4167 }
4168 _ => "expr".to_string(),
4169 })
4170 .join("_");
4171 write!(idx_name.item, "_{index_name_col_suffix}_idx")
4172 .expect("write on strings cannot fail");
4173 idx_name.item = normalize::ident(Ident::new(&idx_name.item)?)
4174 }
4175
4176 if !*if_not_exists {
4177 scx.catalog.find_available_name(idx_name)
4178 } else {
4179 idx_name
4180 }
4181 };
4182
4183 let full_name = scx.catalog.resolve_full_name(&index_name);
4185 let partial_name = PartialItemName::from(full_name.clone());
4186 if let (Ok(item), false, false) = (
4194 scx.catalog.resolve_item_or_type(&partial_name),
4195 *if_not_exists,
4196 scx.pcx().map_or(false, |pcx| pcx.ignore_if_exists_errors),
4197 ) {
4198 return Err(PlanError::ItemAlreadyExists {
4199 name: full_name.to_string(),
4200 item_type: item.item_type(),
4201 });
4202 }
4203
4204 let options = plan_index_options(scx, with_options.clone())?;
4205 let cluster_id = match in_cluster {
4206 None => scx.resolve_cluster(None)?.id(),
4207 Some(in_cluster) => in_cluster.id,
4208 };
4209
4210 *in_cluster = Some(ResolvedClusterName {
4211 id: cluster_id,
4212 print_name: None,
4213 });
4214
4215 *name = Some(Ident::new(index_name.item.clone())?);
4217 *key_parts = Some(filled_key_parts);
4218 let if_not_exists = *if_not_exists;
4219
4220 let create_sql = normalize::create_statement(scx, Statement::CreateIndex(stmt))?;
4221 let compaction_window = options.iter().find_map(|o| {
4222 #[allow(irrefutable_let_patterns)]
4223 if let crate::plan::IndexOption::RetainHistory(lcw) = o {
4224 Some(lcw.clone())
4225 } else {
4226 None
4227 }
4228 });
4229
4230 Ok(Plan::CreateIndex(CreateIndexPlan {
4231 name: index_name,
4232 index: Index {
4233 create_sql,
4234 on: on.global_id(),
4235 keys,
4236 cluster_id,
4237 compaction_window,
4238 },
4239 if_not_exists,
4240 }))
4241}
4242
4243pub fn describe_create_type(
4244 _: &StatementContext,
4245 _: CreateTypeStatement<Aug>,
4246) -> Result<StatementDesc, PlanError> {
4247 Ok(StatementDesc::new(None))
4248}
4249
4250pub fn plan_create_type(
4251 scx: &StatementContext,
4252 stmt: CreateTypeStatement<Aug>,
4253) -> Result<Plan, PlanError> {
4254 let create_sql = normalize::create_statement(scx, Statement::CreateType(stmt.clone()))?;
4255 let CreateTypeStatement { name, as_type, .. } = stmt;
4256
4257 fn validate_data_type(
4258 scx: &StatementContext,
4259 data_type: ResolvedDataType,
4260 as_type: &str,
4261 key: &str,
4262 ) -> Result<(CatalogItemId, Vec<i64>), PlanError> {
4263 let (id, modifiers) = match data_type {
4264 ResolvedDataType::Named { id, modifiers, .. } => (id, modifiers),
4265 _ => sql_bail!(
4266 "CREATE TYPE ... AS {}option {} can only use named data types, but \
4267 found unnamed data type {}. Use CREATE TYPE to create a named type first",
4268 as_type,
4269 key,
4270 data_type.human_readable_name(),
4271 ),
4272 };
4273
4274 let item = scx.catalog.get_item(&id);
4275 match item.type_details() {
4276 None => sql_bail!(
4277 "{} must be of class type, but received {} which is of class {}",
4278 key,
4279 scx.catalog.resolve_full_name(item.name()),
4280 item.item_type()
4281 ),
4282 Some(CatalogTypeDetails {
4283 typ: CatalogType::Char,
4284 ..
4285 }) => {
4286 bail_unsupported!("embedding char type in a list or map")
4287 }
4288 _ => {
4289 scalar_type_from_catalog(scx.catalog, id, &modifiers)?;
4291
4292 Ok((id, modifiers))
4293 }
4294 }
4295 }
4296
4297 let inner = match as_type {
4298 CreateTypeAs::List { options } => {
4299 let CreateTypeListOptionExtracted {
4300 element_type,
4301 seen: _,
4302 } = CreateTypeListOptionExtracted::try_from(options)?;
4303 let element_type =
4304 element_type.ok_or_else(|| sql_err!("ELEMENT TYPE option is required"))?;
4305 let (id, modifiers) = validate_data_type(scx, element_type, "LIST ", "ELEMENT TYPE")?;
4306 CatalogType::List {
4307 element_reference: id,
4308 element_modifiers: modifiers,
4309 }
4310 }
4311 CreateTypeAs::Map { options } => {
4312 let CreateTypeMapOptionExtracted {
4313 key_type,
4314 value_type,
4315 seen: _,
4316 } = CreateTypeMapOptionExtracted::try_from(options)?;
4317 let key_type = key_type.ok_or_else(|| sql_err!("KEY TYPE option is required"))?;
4318 let value_type = value_type.ok_or_else(|| sql_err!("VALUE TYPE option is required"))?;
4319 let (key_id, key_modifiers) = validate_data_type(scx, key_type, "MAP ", "KEY TYPE")?;
4320 let (value_id, value_modifiers) =
4321 validate_data_type(scx, value_type, "MAP ", "VALUE TYPE")?;
4322 CatalogType::Map {
4323 key_reference: key_id,
4324 key_modifiers,
4325 value_reference: value_id,
4326 value_modifiers,
4327 }
4328 }
4329 CreateTypeAs::Record { column_defs } => {
4330 let mut fields = vec![];
4331 for column_def in column_defs {
4332 let data_type = column_def.data_type;
4333 let key = ident(column_def.name.clone());
4334 let (id, modifiers) = validate_data_type(scx, data_type, "", &key)?;
4335 fields.push(CatalogRecordField {
4336 name: ColumnName::from(key.clone()),
4337 type_reference: id,
4338 type_modifiers: modifiers,
4339 });
4340 }
4341 CatalogType::Record { fields }
4342 }
4343 };
4344
4345 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
4346
4347 let full_name = scx.catalog.resolve_full_name(&name);
4349 let partial_name = PartialItemName::from(full_name.clone());
4350 if let Ok(item) = scx.catalog.resolve_item_or_type(&partial_name) {
4353 if item.item_type().conflicts_with_type() {
4354 return Err(PlanError::ItemAlreadyExists {
4355 name: full_name.to_string(),
4356 item_type: item.item_type(),
4357 });
4358 }
4359 }
4360
4361 Ok(Plan::CreateType(CreateTypePlan {
4362 name,
4363 typ: Type { create_sql, inner },
4364 }))
4365}
4366
4367generate_extracted_config!(CreateTypeListOption, (ElementType, ResolvedDataType));
4368
4369generate_extracted_config!(
4370 CreateTypeMapOption,
4371 (KeyType, ResolvedDataType),
4372 (ValueType, ResolvedDataType)
4373);
4374
4375#[derive(Debug)]
4376pub enum PlannedAlterRoleOption {
4377 Attributes(PlannedRoleAttributes),
4378 Variable(PlannedRoleVariable),
4379}
4380
4381#[derive(Debug, Clone)]
4382pub struct PlannedRoleAttributes {
4383 pub inherit: Option<bool>,
4384 pub password: Option<Password>,
4385 pub scram_iterations: Option<NonZeroU32>,
4386 pub nopassword: Option<bool>,
4390 pub superuser: Option<bool>,
4391 pub login: Option<bool>,
4392}
4393
4394fn plan_role_attributes(
4395 options: Vec<RoleAttribute>,
4396 scx: &StatementContext,
4397) -> Result<PlannedRoleAttributes, PlanError> {
4398 let mut planned_attributes = PlannedRoleAttributes {
4399 inherit: None,
4400 password: None,
4401 scram_iterations: None,
4402 superuser: None,
4403 login: None,
4404 nopassword: None,
4405 };
4406
4407 for option in options {
4408 match option {
4409 RoleAttribute::Inherit | RoleAttribute::NoInherit
4410 if planned_attributes.inherit.is_some() =>
4411 {
4412 sql_bail!("conflicting or redundant options");
4413 }
4414 RoleAttribute::CreateCluster | RoleAttribute::NoCreateCluster => {
4415 bail_never_supported!(
4416 "CREATECLUSTER attribute",
4417 "sql/create-role/#details",
4418 "Use system privileges instead."
4419 );
4420 }
4421 RoleAttribute::CreateDB | RoleAttribute::NoCreateDB => {
4422 bail_never_supported!(
4423 "CREATEDB attribute",
4424 "sql/create-role/#details",
4425 "Use system privileges instead."
4426 );
4427 }
4428 RoleAttribute::CreateRole | RoleAttribute::NoCreateRole => {
4429 bail_never_supported!(
4430 "CREATEROLE attribute",
4431 "sql/create-role/#details",
4432 "Use system privileges instead."
4433 );
4434 }
4435 RoleAttribute::Password(_) if planned_attributes.password.is_some() => {
4436 sql_bail!("conflicting or redundant options");
4437 }
4438
4439 RoleAttribute::Inherit => planned_attributes.inherit = Some(true),
4440 RoleAttribute::NoInherit => planned_attributes.inherit = Some(false),
4441 RoleAttribute::Password(password) => {
4442 if let Some(password) = password {
4443 planned_attributes.password = Some(password.into());
4444 planned_attributes.scram_iterations =
4445 Some(scx.catalog.system_vars().scram_iterations())
4446 } else {
4447 planned_attributes.nopassword = Some(true);
4448 }
4449 }
4450 RoleAttribute::SuperUser => {
4451 if planned_attributes.superuser == Some(false) {
4452 sql_bail!("conflicting or redundant options");
4453 }
4454 planned_attributes.superuser = Some(true);
4455 }
4456 RoleAttribute::NoSuperUser => {
4457 if planned_attributes.superuser == Some(true) {
4458 sql_bail!("conflicting or redundant options");
4459 }
4460 planned_attributes.superuser = Some(false);
4461 }
4462 RoleAttribute::Login => {
4463 if planned_attributes.login == Some(false) {
4464 sql_bail!("conflicting or redundant options");
4465 }
4466 planned_attributes.login = Some(true);
4467 }
4468 RoleAttribute::NoLogin => {
4469 if planned_attributes.login == Some(true) {
4470 sql_bail!("conflicting or redundant options");
4471 }
4472 planned_attributes.login = Some(false);
4473 }
4474 }
4475 }
4476 if planned_attributes.inherit == Some(false) {
4477 bail_unsupported!("non inherit roles");
4478 }
4479
4480 Ok(planned_attributes)
4481}
4482
4483#[derive(Debug)]
4484pub enum PlannedRoleVariable {
4485 Set { name: String, value: VariableValue },
4486 Reset { name: String },
4487}
4488
4489impl PlannedRoleVariable {
4490 pub fn name(&self) -> &str {
4491 match self {
4492 PlannedRoleVariable::Set { name, .. } => name,
4493 PlannedRoleVariable::Reset { name } => name,
4494 }
4495 }
4496}
4497
4498fn plan_role_variable(
4499 scx: &StatementContext,
4500 variable: SetRoleVar,
4501) -> Result<PlannedRoleVariable, PlanError> {
4502 let plan = match variable {
4503 SetRoleVar::Set { name, value } => {
4504 let name = name.to_string();
4505 let value = scl::plan_set_variable_to(value)?;
4506 if let VariableValue::Values(values) = &value {
4509 vars::check_transaction_isolation_feature_flag(
4510 &name,
4511 VarInput::SqlSet(values),
4512 scx.catalog.system_vars(),
4513 )?;
4514 }
4515 PlannedRoleVariable::Set { name, value }
4516 }
4517 SetRoleVar::Reset { name } => PlannedRoleVariable::Reset {
4518 name: name.to_string(),
4519 },
4520 };
4521 Ok(plan)
4522}
4523
4524pub fn describe_create_role(
4525 _: &StatementContext,
4526 _: CreateRoleStatement,
4527) -> Result<StatementDesc, PlanError> {
4528 Ok(StatementDesc::new(None))
4529}
4530
4531pub fn plan_create_role(
4532 scx: &StatementContext,
4533 CreateRoleStatement { name, options }: CreateRoleStatement,
4534) -> Result<Plan, PlanError> {
4535 let attributes = plan_role_attributes(options, scx)?;
4536 Ok(Plan::CreateRole(CreateRolePlan {
4537 name: normalize::ident(name),
4538 attributes: attributes.into(),
4539 }))
4540}
4541
4542pub fn plan_create_network_policy(
4543 ctx: &StatementContext,
4544 CreateNetworkPolicyStatement { name, options }: CreateNetworkPolicyStatement<Aug>,
4545) -> Result<Plan, PlanError> {
4546 ctx.require_feature_flag(&vars::ENABLE_NETWORK_POLICIES)?;
4547 let policy_options: NetworkPolicyOptionExtracted = options.try_into()?;
4548
4549 let Some(rule_defs) = policy_options.rules else {
4550 sql_bail!("RULES must be specified when creating network policies.");
4551 };
4552
4553 let mut rules = vec![];
4554 for NetworkPolicyRuleDefinition { name, options } in rule_defs {
4555 let NetworkPolicyRuleOptionExtracted {
4556 seen: _,
4557 direction,
4558 action,
4559 address,
4560 } = options.try_into()?;
4561 let (direction, action, address) = match (direction, action, address) {
4562 (Some(direction), Some(action), Some(address)) => (
4563 NetworkPolicyRuleDirection::try_from(direction.as_str())?,
4564 NetworkPolicyRuleAction::try_from(action.as_str())?,
4565 PolicyAddress::try_from(address.as_str())?,
4566 ),
4567 (_, _, _) => {
4568 sql_bail!("Direction, Address, and Action must specified when creating a rule")
4569 }
4570 };
4571 rules.push(NetworkPolicyRule {
4572 name: normalize::ident(name),
4573 direction,
4574 action,
4575 address,
4576 });
4577 }
4578
4579 if rules.len()
4580 > ctx
4581 .catalog
4582 .system_vars()
4583 .max_rules_per_network_policy()
4584 .try_into()?
4585 {
4586 sql_bail!("RULES count exceeds max_rules_per_network_policy.")
4587 }
4588
4589 Ok(Plan::CreateNetworkPolicy(CreateNetworkPolicyPlan {
4590 name: normalize::ident(name),
4591 rules,
4592 }))
4593}
4594
4595pub fn plan_alter_network_policy(
4596 ctx: &StatementContext,
4597 AlterNetworkPolicyStatement { name, options }: AlterNetworkPolicyStatement<Aug>,
4598) -> Result<Plan, PlanError> {
4599 ctx.require_feature_flag(&vars::ENABLE_NETWORK_POLICIES)?;
4600
4601 let policy_options: NetworkPolicyOptionExtracted = options.try_into()?;
4602 let policy = ctx.catalog.resolve_network_policy(&name.to_string())?;
4603
4604 let Some(rule_defs) = policy_options.rules else {
4605 sql_bail!("RULES must be specified when creating network policies.");
4606 };
4607
4608 let mut rules = vec![];
4609 for NetworkPolicyRuleDefinition { name, options } in rule_defs {
4610 let NetworkPolicyRuleOptionExtracted {
4611 seen: _,
4612 direction,
4613 action,
4614 address,
4615 } = options.try_into()?;
4616
4617 let (direction, action, address) = match (direction, action, address) {
4618 (Some(direction), Some(action), Some(address)) => (
4619 NetworkPolicyRuleDirection::try_from(direction.as_str())?,
4620 NetworkPolicyRuleAction::try_from(action.as_str())?,
4621 PolicyAddress::try_from(address.as_str())?,
4622 ),
4623 (_, _, _) => {
4624 sql_bail!("Direction, Address, and Action must specified when creating a rule")
4625 }
4626 };
4627 rules.push(NetworkPolicyRule {
4628 name: normalize::ident(name),
4629 direction,
4630 action,
4631 address,
4632 });
4633 }
4634 if rules.len()
4635 > ctx
4636 .catalog
4637 .system_vars()
4638 .max_rules_per_network_policy()
4639 .try_into()?
4640 {
4641 sql_bail!("RULES count exceeds max_rules_per_network_policy.")
4642 }
4643
4644 Ok(Plan::AlterNetworkPolicy(AlterNetworkPolicyPlan {
4645 id: policy.id(),
4646 name: normalize::ident(name),
4647 rules,
4648 }))
4649}
4650
4651pub fn describe_create_cluster(
4652 _: &StatementContext,
4653 _: CreateClusterStatement<Aug>,
4654) -> Result<StatementDesc, PlanError> {
4655 Ok(StatementDesc::new(None))
4656}
4657
4658generate_extracted_config!(
4664 ClusterOption,
4665 (AvailabilityZones, Vec<String>),
4666 (Disk, bool),
4667 (IntrospectionDebugging, bool),
4668 (IntrospectionInterval, OptionalDuration),
4669 (Managed, bool),
4670 (Replicas, Vec<ReplicaDefinition<Aug>>),
4671 (ReplicationFactor, u32),
4672 (Size, String),
4673 (Schedule, ClusterScheduleOptionValue),
4674 (WorkloadClass, OptionalString)
4675);
4676
4677generate_extracted_config!(
4678 NetworkPolicyOption,
4679 (Rules, Vec<NetworkPolicyRuleDefinition<Aug>>)
4680);
4681
4682generate_extracted_config!(
4683 NetworkPolicyRuleOption,
4684 (Direction, String),
4685 (Action, String),
4686 (Address, String)
4687);
4688
4689generate_extracted_config!(ClusterAlterOption, (Wait, ClusterAlterOptionValue<Aug>));
4690
4691generate_extracted_config!(
4692 ClusterAlterUntilReadyOption,
4693 (Timeout, Duration),
4694 (OnTimeout, String)
4695);
4696
4697generate_extracted_config!(
4698 ClusterFeature,
4699 (ReoptimizeImportedViews, Option<bool>, Default(None)),
4700 (EnableEagerDeltaJoins, Option<bool>, Default(None)),
4701 (EnableNewOuterJoinLowering, Option<bool>, Default(None)),
4702 (EnableVariadicLeftJoinLowering, Option<bool>, Default(None)),
4703 (EnableLetrecFixpointAnalysis, Option<bool>, Default(None)),
4704 (EnableJoinPrioritizeArranged, Option<bool>, Default(None)),
4705 (
4706 EnableProjectionPushdownAfterRelationCse,
4707 Option<bool>,
4708 Default(None)
4709 )
4710);
4711
4712pub fn plan_create_cluster(
4716 scx: &StatementContext,
4717 stmt: CreateClusterStatement<Aug>,
4718) -> Result<Plan, PlanError> {
4719 let plan = plan_create_cluster_inner(scx, stmt)?;
4720
4721 if let CreateClusterVariant::Managed(_) = &plan.variant {
4723 let stmt = unplan_create_cluster(scx, plan.clone())
4724 .map_err(|e| PlanError::Replan(e.to_string()))?;
4725 let create_sql = stmt.to_ast_string_stable();
4726 let stmt = parse::parse(&create_sql)
4727 .map_err(|e| PlanError::Replan(e.to_string()))?
4728 .into_element()
4729 .ast;
4730 let (stmt, _resolved_ids) =
4731 names::resolve(scx.catalog, stmt).map_err(|e| PlanError::Replan(e.to_string()))?;
4732 let stmt = match stmt {
4733 Statement::CreateCluster(stmt) => stmt,
4734 stmt => {
4735 return Err(PlanError::Replan(format!(
4736 "replan does not match: plan={plan:?}, create_sql={create_sql:?}, stmt={stmt:?}"
4737 )));
4738 }
4739 };
4740 let replan =
4741 plan_create_cluster_inner(scx, stmt).map_err(|e| PlanError::Replan(e.to_string()))?;
4742 if plan != replan {
4743 return Err(PlanError::Replan(format!(
4744 "replan does not match: plan={plan:?}, replan={replan:?}"
4745 )));
4746 }
4747 }
4748
4749 Ok(Plan::CreateCluster(plan))
4750}
4751
4752pub fn plan_create_cluster_inner(
4753 scx: &StatementContext,
4754 CreateClusterStatement {
4755 name,
4756 options,
4757 features,
4758 }: CreateClusterStatement<Aug>,
4759) -> Result<CreateClusterPlan, PlanError> {
4760 let ClusterOptionExtracted {
4761 availability_zones,
4762 introspection_debugging,
4763 introspection_interval,
4764 managed,
4765 replicas,
4766 replication_factor,
4767 seen: _,
4768 size,
4769 disk,
4770 schedule,
4771 workload_class,
4772 }: ClusterOptionExtracted = options.try_into()?;
4773
4774 let managed = managed.unwrap_or_else(|| replicas.is_none());
4775
4776 if !scx.catalog.active_role_id().is_system() {
4777 if !features.is_empty() {
4778 sql_bail!("FEATURES not supported for non-system users");
4779 }
4780 if workload_class.is_some() {
4781 sql_bail!("WORKLOAD CLASS not supported for non-system users");
4782 }
4783 }
4784
4785 let schedule = schedule.unwrap_or(ClusterScheduleOptionValue::Manual);
4786 let workload_class = workload_class.and_then(|v| v.0);
4787
4788 if managed {
4789 if replicas.is_some() {
4790 sql_bail!("REPLICAS not supported for managed clusters");
4791 }
4792 let Some(size) = size else {
4793 sql_bail!("SIZE must be specified for managed clusters");
4794 };
4795
4796 if disk.is_some() {
4797 if scx.catalog.is_cluster_size_cc(&size) {
4801 sql_bail!(
4802 "DISK option not supported for modern cluster sizes because disk is always enabled"
4803 );
4804 }
4805
4806 scx.catalog
4807 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated);
4808 }
4809
4810 let compute = plan_compute_replica_config(
4811 introspection_interval,
4812 introspection_debugging.unwrap_or(false),
4813 )?;
4814
4815 let replication_factor = if matches!(schedule, ClusterScheduleOptionValue::Manual) {
4816 replication_factor.unwrap_or_else(|| {
4817 scx.catalog
4818 .system_vars()
4819 .default_cluster_replication_factor()
4820 })
4821 } else {
4822 scx.require_feature_flag(&ENABLE_CLUSTER_SCHEDULE_REFRESH)?;
4823 if replication_factor.is_some() {
4824 sql_bail!(
4825 "REPLICATION FACTOR cannot be given together with any SCHEDULE other than MANUAL"
4826 );
4827 }
4828 0
4832 };
4833 let availability_zones = availability_zones.unwrap_or_default();
4834
4835 if !availability_zones.is_empty() {
4836 scx.require_feature_flag(&vars::ENABLE_MANAGED_CLUSTER_AVAILABILITY_ZONES)?;
4837 }
4838
4839 let ClusterFeatureExtracted {
4841 reoptimize_imported_views,
4842 enable_eager_delta_joins,
4843 enable_new_outer_join_lowering,
4844 enable_variadic_left_join_lowering,
4845 enable_letrec_fixpoint_analysis,
4846 enable_join_prioritize_arranged,
4847 enable_projection_pushdown_after_relation_cse,
4848 seen: _,
4849 } = ClusterFeatureExtracted::try_from(features)?;
4850 let optimizer_feature_overrides = OptimizerFeatureOverrides {
4851 reoptimize_imported_views,
4852 enable_eager_delta_joins,
4853 enable_new_outer_join_lowering,
4854 enable_variadic_left_join_lowering,
4855 enable_letrec_fixpoint_analysis,
4856 enable_join_prioritize_arranged,
4857 enable_projection_pushdown_after_relation_cse,
4858 ..Default::default()
4859 };
4860
4861 let schedule = plan_cluster_schedule(schedule)?;
4862
4863 Ok(CreateClusterPlan {
4864 name: normalize::ident(name),
4865 variant: CreateClusterVariant::Managed(CreateClusterManagedPlan {
4866 replication_factor,
4867 size,
4868 availability_zones,
4869 compute,
4870 optimizer_feature_overrides,
4871 schedule,
4872 }),
4873 workload_class,
4874 })
4875 } else {
4876 let Some(replica_defs) = replicas else {
4877 sql_bail!("REPLICAS must be specified for unmanaged clusters");
4878 };
4879 if availability_zones.is_some() {
4880 sql_bail!("AVAILABILITY ZONES not supported for unmanaged clusters");
4881 }
4882 if replication_factor.is_some() {
4883 sql_bail!("REPLICATION FACTOR not supported for unmanaged clusters");
4884 }
4885 if introspection_debugging.is_some() {
4886 sql_bail!("INTROSPECTION DEBUGGING not supported for unmanaged clusters");
4887 }
4888 if introspection_interval.is_some() {
4889 sql_bail!("INTROSPECTION INTERVAL not supported for unmanaged clusters");
4890 }
4891 if size.is_some() {
4892 sql_bail!("SIZE not supported for unmanaged clusters");
4893 }
4894 if disk.is_some() {
4895 sql_bail!("DISK not supported for unmanaged clusters");
4896 }
4897 if !features.is_empty() {
4898 sql_bail!("FEATURES not supported for unmanaged clusters");
4899 }
4900 if !matches!(schedule, ClusterScheduleOptionValue::Manual) {
4901 sql_bail!(
4902 "cluster schedules other than MANUAL are not supported for unmanaged clusters"
4903 );
4904 }
4905
4906 let mut replicas = vec![];
4907 for ReplicaDefinition { name, options } in replica_defs {
4908 replicas.push((normalize::ident(name), plan_replica_config(scx, options)?));
4909 }
4910
4911 Ok(CreateClusterPlan {
4912 name: normalize::ident(name),
4913 variant: CreateClusterVariant::Unmanaged(CreateClusterUnmanagedPlan { replicas }),
4914 workload_class,
4915 })
4916 }
4917}
4918
4919pub fn unplan_create_cluster(
4923 scx: &StatementContext,
4924 CreateClusterPlan {
4925 name,
4926 variant,
4927 workload_class,
4928 }: CreateClusterPlan,
4929) -> Result<CreateClusterStatement<Aug>, PlanError> {
4930 match variant {
4931 CreateClusterVariant::Managed(CreateClusterManagedPlan {
4932 replication_factor,
4933 size,
4934 availability_zones,
4935 compute,
4936 optimizer_feature_overrides,
4937 schedule,
4938 }) => {
4939 let schedule = unplan_cluster_schedule(schedule);
4940 let OptimizerFeatureOverrides {
4941 enable_reduce_mfp_fusion: _,
4942 enable_cardinality_estimates: _,
4943 persist_fast_path_limit: _,
4944 reoptimize_imported_views,
4945 enable_eager_delta_joins,
4946 enable_new_outer_join_lowering,
4947 enable_variadic_left_join_lowering,
4948 enable_letrec_fixpoint_analysis,
4949 enable_join_prioritize_arranged,
4950 enable_projection_pushdown_after_relation_cse,
4951 enable_less_reduce_in_eqprop: _,
4952 enable_dequadratic_eqprop_map: _,
4953 enable_eq_classes_withholding_errors: _,
4954 enable_fast_path_plan_insights: _,
4955 enable_cast_elimination: _,
4956 enable_case_literal_transform: _,
4957 enable_simplify_quantified_comparisons: _,
4958 enable_coalesce_case_transform: _,
4959 enable_will_distinct_propagation: _,
4960 enable_fixed_correlated_cte_lowering: _,
4961 } = optimizer_feature_overrides;
4962 let features_extracted = ClusterFeatureExtracted {
4964 seen: Default::default(),
4966 reoptimize_imported_views,
4967 enable_eager_delta_joins,
4968 enable_new_outer_join_lowering,
4969 enable_variadic_left_join_lowering,
4970 enable_letrec_fixpoint_analysis,
4971 enable_join_prioritize_arranged,
4972 enable_projection_pushdown_after_relation_cse,
4973 };
4974 let features = features_extracted.into_values(scx.catalog);
4975 let availability_zones = if availability_zones.is_empty() {
4976 None
4977 } else {
4978 Some(availability_zones)
4979 };
4980 let (introspection_interval, introspection_debugging) =
4981 unplan_compute_replica_config(compute);
4982 let replication_factor = match &schedule {
4985 ClusterScheduleOptionValue::Manual => Some(replication_factor),
4986 ClusterScheduleOptionValue::Refresh { .. } => {
4987 soft_assert_or_log!(
4994 replication_factor <= 1,
4995 "replication factor, {replication_factor:?}, must be <= 1 with a refresh schedule"
4996 );
4997 None
4998 }
4999 };
5000 let workload_class = workload_class.map(|s| OptionalString(Some(s)));
5001 let options_extracted = ClusterOptionExtracted {
5002 seen: Default::default(),
5004 availability_zones,
5005 disk: None,
5006 introspection_debugging: Some(introspection_debugging),
5007 introspection_interval,
5008 managed: Some(true),
5009 replicas: None,
5010 replication_factor,
5011 size: Some(size),
5012 schedule: Some(schedule),
5013 workload_class,
5014 };
5015 let options = options_extracted.into_values(scx.catalog);
5016 let name = Ident::new_unchecked(name);
5017 Ok(CreateClusterStatement {
5018 name,
5019 options,
5020 features,
5021 })
5022 }
5023 CreateClusterVariant::Unmanaged(_) => {
5024 bail_unsupported!("SHOW CREATE for unmanaged clusters")
5025 }
5026 }
5027}
5028
5029generate_extracted_config!(
5030 ReplicaOption,
5031 (AvailabilityZone, String),
5032 (BilledAs, String),
5033 (ComputeAddresses, Vec<String>),
5034 (ComputectlAddresses, Vec<String>),
5035 (Disk, bool),
5036 (Internal, bool, Default(false)),
5037 (IntrospectionDebugging, bool, Default(false)),
5038 (IntrospectionInterval, OptionalDuration),
5039 (Size, String),
5040 (StorageAddresses, Vec<String>),
5041 (StoragectlAddresses, Vec<String>),
5042 (Workers, u16)
5043);
5044
5045fn plan_replica_config(
5046 scx: &StatementContext,
5047 options: Vec<ReplicaOption<Aug>>,
5048) -> Result<ReplicaConfig, PlanError> {
5049 let ReplicaOptionExtracted {
5050 availability_zone,
5051 billed_as,
5052 computectl_addresses,
5053 disk,
5054 internal,
5055 introspection_debugging,
5056 introspection_interval,
5057 size,
5058 storagectl_addresses,
5059 ..
5060 }: ReplicaOptionExtracted = options.try_into()?;
5061
5062 let compute = plan_compute_replica_config(introspection_interval, introspection_debugging)?;
5063
5064 match (
5065 size,
5066 availability_zone,
5067 billed_as,
5068 storagectl_addresses,
5069 computectl_addresses,
5070 ) {
5071 (None, _, None, None, None) => {
5073 sql_bail!("SIZE option must be specified");
5076 }
5077 (Some(size), availability_zone, billed_as, None, None) => {
5078 if disk.is_some() {
5079 if scx.catalog.is_cluster_size_cc(&size) {
5083 sql_bail!(
5084 "DISK option not supported for modern cluster sizes because disk is always enabled"
5085 );
5086 }
5087
5088 scx.catalog
5089 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated);
5090 }
5091
5092 Ok(ReplicaConfig::Orchestrated {
5093 size,
5094 availability_zone,
5095 compute,
5096 billed_as,
5097 internal,
5098 })
5099 }
5100
5101 (None, None, None, storagectl_addresses, computectl_addresses) => {
5102 scx.require_feature_flag(&vars::UNSAFE_ENABLE_UNORCHESTRATED_CLUSTER_REPLICAS)?;
5103
5104 let Some(storagectl_addrs) = storagectl_addresses else {
5108 sql_bail!("missing STORAGECTL ADDRESSES option");
5109 };
5110 let Some(computectl_addrs) = computectl_addresses else {
5111 sql_bail!("missing COMPUTECTL ADDRESSES option");
5112 };
5113
5114 if storagectl_addrs.len() != computectl_addrs.len() {
5115 sql_bail!(
5116 "COMPUTECTL ADDRESSES and STORAGECTL ADDRESSES must have the same length"
5117 );
5118 }
5119
5120 if disk.is_some() {
5121 sql_bail!("DISK can't be specified for unorchestrated clusters");
5122 }
5123
5124 Ok(ReplicaConfig::Unorchestrated {
5125 storagectl_addrs,
5126 computectl_addrs,
5127 compute,
5128 })
5129 }
5130 _ => {
5131 sql_bail!("invalid mixture of orchestrated and unorchestrated replica options");
5134 }
5135 }
5136}
5137
5138fn plan_compute_replica_config(
5142 introspection_interval: Option<OptionalDuration>,
5143 introspection_debugging: bool,
5144) -> Result<ComputeReplicaConfig, PlanError> {
5145 let introspection_interval = introspection_interval
5146 .map(|OptionalDuration(i)| i)
5147 .unwrap_or(Some(DEFAULT_REPLICA_LOGGING_INTERVAL));
5148 let introspection = match introspection_interval {
5149 Some(interval) => Some(ComputeReplicaIntrospectionConfig {
5150 interval,
5151 debugging: introspection_debugging,
5152 }),
5153 None if introspection_debugging => {
5154 sql_bail!("INTROSPECTION DEBUGGING cannot be specified without INTROSPECTION INTERVAL")
5155 }
5156 None => None,
5157 };
5158 let compute = ComputeReplicaConfig { introspection };
5159 Ok(compute)
5160}
5161
5162fn unplan_compute_replica_config(
5166 compute_replica_config: ComputeReplicaConfig,
5167) -> (Option<OptionalDuration>, bool) {
5168 match compute_replica_config.introspection {
5169 Some(ComputeReplicaIntrospectionConfig {
5170 debugging,
5171 interval,
5172 }) => (Some(OptionalDuration(Some(interval))), debugging),
5173 None => (Some(OptionalDuration(None)), false),
5174 }
5175}
5176
5177fn plan_cluster_schedule(
5181 schedule: ClusterScheduleOptionValue,
5182) -> Result<ClusterSchedule, PlanError> {
5183 Ok(match schedule {
5184 ClusterScheduleOptionValue::Manual => ClusterSchedule::Manual,
5185 ClusterScheduleOptionValue::Refresh {
5187 hydration_time_estimate: None,
5188 } => ClusterSchedule::Refresh {
5189 hydration_time_estimate: Duration::from_millis(0),
5190 },
5191 ClusterScheduleOptionValue::Refresh {
5193 hydration_time_estimate: Some(interval_value),
5194 } => {
5195 let interval = Interval::try_from_value(Value::Interval(interval_value))?;
5196 if interval.as_microseconds() < 0 {
5197 sql_bail!(
5198 "HYDRATION TIME ESTIMATE must be non-negative; got: {}",
5199 interval
5200 );
5201 }
5202 if interval.months != 0 {
5203 sql_bail!("HYDRATION TIME ESTIMATE must not involve units larger than days");
5207 }
5208 let duration = interval.duration()?;
5209 if u64::try_from(duration.as_millis()).is_err()
5210 || Interval::from_duration(&duration).is_err()
5211 {
5212 sql_bail!("HYDRATION TIME ESTIMATE too large");
5213 }
5214 ClusterSchedule::Refresh {
5215 hydration_time_estimate: duration,
5216 }
5217 }
5218 })
5219}
5220
5221fn unplan_cluster_schedule(schedule: ClusterSchedule) -> ClusterScheduleOptionValue {
5225 match schedule {
5226 ClusterSchedule::Manual => ClusterScheduleOptionValue::Manual,
5227 ClusterSchedule::Refresh {
5228 hydration_time_estimate,
5229 } => {
5230 let interval = Interval::from_duration(&hydration_time_estimate)
5231 .expect("planning ensured that this is convertible back to Interval");
5232 let interval_value = literal::unplan_interval(&interval);
5233 ClusterScheduleOptionValue::Refresh {
5234 hydration_time_estimate: Some(interval_value),
5235 }
5236 }
5237 }
5238}
5239
5240pub fn describe_create_cluster_replica(
5241 _: &StatementContext,
5242 _: CreateClusterReplicaStatement<Aug>,
5243) -> Result<StatementDesc, PlanError> {
5244 Ok(StatementDesc::new(None))
5245}
5246
5247pub fn plan_create_cluster_replica(
5248 scx: &StatementContext,
5249 CreateClusterReplicaStatement {
5250 definition: ReplicaDefinition { name, options },
5251 of_cluster,
5252 }: CreateClusterReplicaStatement<Aug>,
5253) -> Result<Plan, PlanError> {
5254 let cluster = scx
5255 .catalog
5256 .resolve_cluster(Some(&normalize::ident(of_cluster)))?;
5257
5258 let config = plan_replica_config(scx, options)?;
5259
5260 if let ReplicaConfig::Orchestrated { internal: true, .. } = &config {
5261 if MANAGED_REPLICA_PATTERN.is_match(name.as_str()) {
5262 return Err(PlanError::MangedReplicaName(name.into_string()));
5263 }
5264 } else {
5265 ensure_cluster_is_not_managed(scx, cluster.id())?;
5266 }
5267
5268 Ok(Plan::CreateClusterReplica(CreateClusterReplicaPlan {
5269 name: normalize::ident(name),
5270 cluster_id: cluster.id(),
5271 config,
5272 }))
5273}
5274
5275pub fn describe_create_secret(
5276 _: &StatementContext,
5277 _: CreateSecretStatement<Aug>,
5278) -> Result<StatementDesc, PlanError> {
5279 Ok(StatementDesc::new(None))
5280}
5281
5282pub fn plan_create_secret(
5283 scx: &StatementContext,
5284 stmt: CreateSecretStatement<Aug>,
5285) -> Result<Plan, PlanError> {
5286 let CreateSecretStatement {
5287 name,
5288 if_not_exists,
5289 value,
5290 } = &stmt;
5291
5292 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name.to_owned())?)?;
5293 let mut create_sql_statement = stmt.clone();
5294 create_sql_statement.value = Expr::Value(Value::String("********".to_string()));
5295 let create_sql =
5296 normalize::create_statement(scx, Statement::CreateSecret(create_sql_statement))?;
5297 let secret_as = query::plan_secret_as(scx, value.clone())?;
5298
5299 let secret = Secret {
5300 create_sql,
5301 secret_as,
5302 };
5303
5304 Ok(Plan::CreateSecret(CreateSecretPlan {
5305 name,
5306 secret,
5307 if_not_exists: *if_not_exists,
5308 }))
5309}
5310
5311pub fn describe_create_connection(
5312 _: &StatementContext,
5313 _: CreateConnectionStatement<Aug>,
5314) -> Result<StatementDesc, PlanError> {
5315 Ok(StatementDesc::new(None))
5316}
5317
5318generate_extracted_config!(CreateConnectionOption, (Validate, bool));
5319
5320pub fn plan_create_connection(
5321 scx: &StatementContext,
5322 mut stmt: CreateConnectionStatement<Aug>,
5323) -> Result<Plan, PlanError> {
5324 let CreateConnectionStatement {
5325 name,
5326 connection_type,
5327 values,
5328 if_not_exists,
5329 with_options,
5330 } = stmt.clone();
5331 let connection_options_extracted = connection::ConnectionOptionExtracted::try_from(values)?;
5332 let details = connection_options_extracted.try_into_connection_details(scx, connection_type)?;
5333 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
5334
5335 let options = CreateConnectionOptionExtracted::try_from(with_options)?;
5336 if options.validate.is_some() {
5337 scx.require_feature_flag(&vars::ENABLE_CONNECTION_VALIDATION_SYNTAX)?;
5338 }
5339 let validate = match options.validate {
5340 Some(val) => val,
5341 None => {
5342 scx.catalog
5343 .system_vars()
5344 .enable_default_connection_validation()
5345 && details.to_connection().validate_by_default()
5346 }
5347 };
5348
5349 let full_name = scx.catalog.resolve_full_name(&name);
5351 let partial_name = PartialItemName::from(full_name.clone());
5352 if let (false, Ok(item)) = (if_not_exists, scx.catalog.resolve_item(&partial_name)) {
5353 return Err(PlanError::ItemAlreadyExists {
5354 name: full_name.to_string(),
5355 item_type: item.item_type(),
5356 });
5357 }
5358
5359 if let ConnectionDetails::Ssh { key_1, key_2, .. } = &details {
5362 stmt.values.retain(|v| {
5363 v.name != ConnectionOptionName::PublicKey1 && v.name != ConnectionOptionName::PublicKey2
5364 });
5365 stmt.values.push(ConnectionOption {
5366 name: ConnectionOptionName::PublicKey1,
5367 value: Some(WithOptionValue::Value(Value::String(key_1.public_key()))),
5368 });
5369 stmt.values.push(ConnectionOption {
5370 name: ConnectionOptionName::PublicKey2,
5371 value: Some(WithOptionValue::Value(Value::String(key_2.public_key()))),
5372 });
5373 }
5374 let create_sql = normalize::create_statement(scx, Statement::CreateConnection(stmt))?;
5375
5376 let plan = CreateConnectionPlan {
5377 name,
5378 if_not_exists,
5379 connection: crate::plan::Connection {
5380 create_sql,
5381 details,
5382 },
5383 validate,
5384 };
5385 Ok(Plan::CreateConnection(plan))
5386}
5387
5388fn plan_drop_database(
5389 scx: &StatementContext,
5390 if_exists: bool,
5391 name: &UnresolvedDatabaseName,
5392 cascade: bool,
5393) -> Result<Option<DatabaseId>, PlanError> {
5394 Ok(match resolve_database(scx, name, if_exists)? {
5395 Some(database) => {
5396 if !cascade && database.has_schemas() {
5397 sql_bail!(
5398 "database '{}' cannot be dropped with RESTRICT while it contains schemas",
5399 name,
5400 );
5401 }
5402 Some(database.id())
5403 }
5404 None => None,
5405 })
5406}
5407
5408pub fn describe_drop_objects(
5409 _: &StatementContext,
5410 _: DropObjectsStatement,
5411) -> Result<StatementDesc, PlanError> {
5412 Ok(StatementDesc::new(None))
5413}
5414
5415pub fn plan_drop_objects(
5416 scx: &mut StatementContext,
5417 DropObjectsStatement {
5418 object_type,
5419 if_exists,
5420 names,
5421 cascade,
5422 }: DropObjectsStatement,
5423) -> Result<Plan, PlanError> {
5424 if object_type == mz_sql_parser::ast::ObjectType::Func {
5425 bail_unsupported!("DROP FUNCTION");
5426 }
5427 let object_type = object_type.into();
5428
5429 let mut referenced_ids = Vec::new();
5430 for name in names {
5431 let id = match &name {
5432 UnresolvedObjectName::Cluster(name) => {
5433 plan_drop_cluster(scx, if_exists, name, cascade)?.map(ObjectId::Cluster)
5434 }
5435 UnresolvedObjectName::ClusterReplica(name) => {
5436 plan_drop_cluster_replica(scx, if_exists, name)?.map(ObjectId::ClusterReplica)
5437 }
5438 UnresolvedObjectName::Database(name) => {
5439 plan_drop_database(scx, if_exists, name, cascade)?.map(ObjectId::Database)
5440 }
5441 UnresolvedObjectName::Schema(name) => {
5442 plan_drop_schema(scx, if_exists, name, cascade)?.map(ObjectId::Schema)
5443 }
5444 UnresolvedObjectName::Role(name) => {
5445 plan_drop_role(scx, if_exists, name)?.map(ObjectId::Role)
5446 }
5447 UnresolvedObjectName::Item(name) => {
5448 plan_drop_item_name(scx, object_type, if_exists, name.clone())?.map(ObjectId::Item)
5452 }
5453 UnresolvedObjectName::NetworkPolicy(name) => {
5454 plan_drop_network_policy(scx, if_exists, name)?.map(ObjectId::NetworkPolicy)
5455 }
5456 };
5457 match id {
5458 Some(id) => referenced_ids.push(id),
5459 None => scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
5460 name: name.to_ast_string_simple(),
5461 object_type,
5462 }),
5463 }
5464 }
5465
5466 if !cascade {
5470 let dropped_items: BTreeSet<CatalogItemId> = referenced_ids
5471 .iter()
5472 .filter_map(|id| match id {
5473 ObjectId::Item(id) => Some(*id),
5474 _ => None,
5475 })
5476 .collect();
5477 for id in &dropped_items {
5478 let catalog_item = scx.catalog.get_item(id);
5479 ensure_no_blocking_dependents(scx, object_type, catalog_item, &dropped_items)?;
5480 }
5481 }
5482
5483 let drop_ids = scx.catalog.object_dependents(&referenced_ids);
5484
5485 Ok(Plan::DropObjects(DropObjectsPlan {
5486 referenced_ids,
5487 drop_ids,
5488 object_type,
5489 }))
5490}
5491
5492fn plan_drop_schema(
5493 scx: &StatementContext,
5494 if_exists: bool,
5495 name: &UnresolvedSchemaName,
5496 cascade: bool,
5497) -> Result<Option<(ResolvedDatabaseSpecifier, SchemaSpecifier)>, PlanError> {
5498 let normalized = normalize::unresolved_schema_name(name.clone())?;
5502 if normalized.database.is_none() && normalized.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA {
5503 sql_bail!("cannot drop schema {name} because it is a temporary schema",)
5504 }
5505
5506 Ok(match resolve_schema(scx, name.clone(), if_exists)? {
5507 Some((database_spec, schema_spec)) => {
5508 if let ResolvedDatabaseSpecifier::Ambient = database_spec {
5509 sql_bail!(
5510 "cannot drop schema {name} because it is required by the database system",
5511 );
5512 }
5513 if let SchemaSpecifier::Temporary = schema_spec {
5514 sql_bail!("cannot drop schema {name} because it is a temporary schema",)
5515 }
5516 let schema = scx.get_schema(&database_spec, &schema_spec);
5517 if !cascade && schema.has_items() {
5518 let full_schema_name = scx.catalog.resolve_full_schema_name(schema.name());
5519 sql_bail!(
5520 "schema '{}' cannot be dropped without CASCADE while it contains objects",
5521 full_schema_name
5522 );
5523 }
5524 Some((database_spec, schema_spec))
5525 }
5526 None => None,
5527 })
5528}
5529
5530fn plan_drop_role(
5531 scx: &StatementContext,
5532 if_exists: bool,
5533 name: &Ident,
5534) -> Result<Option<RoleId>, PlanError> {
5535 match scx.catalog.resolve_role(name.as_str()) {
5536 Ok(role) => {
5537 let id = role.id();
5538 if &id == scx.catalog.active_role_id() {
5539 sql_bail!("current role cannot be dropped");
5540 }
5541 for role in scx.catalog.get_roles() {
5542 for (member_id, grantor_id) in role.membership() {
5543 if &id == grantor_id {
5544 let member_role = scx.catalog.get_role(member_id);
5545 sql_bail!(
5546 "cannot drop role {}: still depended up by membership of role {} in role {}",
5547 name.as_str(),
5548 role.name(),
5549 member_role.name()
5550 );
5551 }
5552 }
5553 }
5554 Ok(Some(role.id()))
5555 }
5556 Err(_) if if_exists => Ok(None),
5557 Err(e) => Err(e.into()),
5558 }
5559}
5560
5561fn plan_drop_cluster(
5562 scx: &StatementContext,
5563 if_exists: bool,
5564 name: &Ident,
5565 cascade: bool,
5566) -> Result<Option<ClusterId>, PlanError> {
5567 Ok(match resolve_cluster(scx, name, if_exists)? {
5568 Some(cluster) => {
5569 if !cascade && !cluster.bound_objects().is_empty() {
5570 return Err(PlanError::DependentObjectsStillExist {
5571 object_type: "cluster".to_string(),
5572 object_name: cluster.name().to_string(),
5573 dependents: Vec::new(),
5574 });
5575 }
5576 Some(cluster.id())
5577 }
5578 None => None,
5579 })
5580}
5581
5582fn plan_drop_network_policy(
5583 scx: &StatementContext,
5584 if_exists: bool,
5585 name: &Ident,
5586) -> Result<Option<NetworkPolicyId>, PlanError> {
5587 match scx.catalog.resolve_network_policy(name.as_str()) {
5588 Ok(policy) => {
5589 if scx.catalog.system_vars().default_network_policy_name() == policy.name() {
5592 Err(PlanError::NetworkPolicyInUse)
5593 } else {
5594 Ok(Some(policy.id()))
5595 }
5596 }
5597 Err(_) if if_exists => Ok(None),
5598 Err(e) => Err(e.into()),
5599 }
5600}
5601
5602fn plan_drop_cluster_replica(
5603 scx: &StatementContext,
5604 if_exists: bool,
5605 name: &QualifiedReplica,
5606) -> Result<Option<(ClusterId, ReplicaId)>, PlanError> {
5607 let cluster = resolve_cluster_replica(scx, name, if_exists)?;
5608 Ok(cluster.map(|(cluster, replica_id)| (cluster.id(), replica_id)))
5609}
5610
5611fn plan_drop_item(
5613 scx: &StatementContext,
5614 object_type: ObjectType,
5615 if_exists: bool,
5616 name: UnresolvedItemName,
5617 cascade: bool,
5618) -> Result<Option<CatalogItemId>, PlanError> {
5619 let Some(id) = plan_drop_item_name(scx, object_type, if_exists, name)? else {
5620 return Ok(None);
5621 };
5622 if !cascade {
5623 let catalog_item = scx.catalog.get_item(&id);
5624 ensure_no_blocking_dependents(scx, object_type, catalog_item, &BTreeSet::new())?;
5625 }
5626 Ok(Some(id))
5627}
5628
5629fn plan_drop_item_name(
5633 scx: &StatementContext,
5634 object_type: ObjectType,
5635 if_exists: bool,
5636 name: UnresolvedItemName,
5637) -> Result<Option<CatalogItemId>, PlanError> {
5638 let resolved = match resolve_item_or_type(scx, object_type, name, if_exists) {
5639 Ok(r) => r,
5640 Err(PlanError::MismatchedObjectType {
5642 name,
5643 is_type: ObjectType::MaterializedView,
5644 expected_type: ObjectType::View,
5645 }) => {
5646 return Err(PlanError::DropViewOnMaterializedView(name.to_string()));
5647 }
5648 e => e?,
5649 };
5650
5651 Ok(match resolved {
5652 Some(catalog_item) => {
5653 if catalog_item.id().is_system() {
5654 sql_bail!(
5655 "cannot drop {} {} because it is required by the database system",
5656 catalog_item.item_type(),
5657 scx.catalog.minimal_qualification(catalog_item.name()),
5658 );
5659 }
5660 Some(catalog_item.id())
5661 }
5662 None => None,
5663 })
5664}
5665
5666fn ensure_no_blocking_dependents(
5671 scx: &StatementContext,
5672 object_type: ObjectType,
5673 catalog_item: &dyn CatalogItem,
5674 also_dropped: &BTreeSet<CatalogItemId>,
5675) -> Result<(), PlanError> {
5676 for id in catalog_item.used_by() {
5677 if also_dropped.contains(id) {
5678 continue;
5679 }
5680 let dep = scx.catalog.get_item(id);
5681 if dependency_prevents_drop(object_type, dep) {
5682 return Err(PlanError::DependentObjectsStillExist {
5683 object_type: catalog_item.item_type().to_string(),
5684 object_name: scx
5685 .catalog
5686 .minimal_qualification(catalog_item.name())
5687 .to_string(),
5688 dependents: vec![(
5689 dep.item_type().to_string(),
5690 scx.catalog.minimal_qualification(dep.name()).to_string(),
5691 )],
5692 });
5693 }
5694 }
5695 Ok(())
5698}
5699
5700fn dependency_prevents_drop(object_type: ObjectType, dep: &dyn CatalogItem) -> bool {
5702 match object_type {
5703 ObjectType::Type => true,
5704 ObjectType::Table
5705 | ObjectType::View
5706 | ObjectType::MaterializedView
5707 | ObjectType::Source
5708 | ObjectType::Sink
5709 | ObjectType::Index
5710 | ObjectType::Role
5711 | ObjectType::Cluster
5712 | ObjectType::ClusterReplica
5713 | ObjectType::Secret
5714 | ObjectType::Connection
5715 | ObjectType::Database
5716 | ObjectType::Schema
5717 | ObjectType::Func
5718 | ObjectType::NetworkPolicy => match dep.item_type() {
5719 CatalogItemType::Func
5720 | CatalogItemType::Table
5721 | CatalogItemType::Source
5722 | CatalogItemType::View
5723 | CatalogItemType::MaterializedView
5724 | CatalogItemType::Sink
5725 | CatalogItemType::Type
5726 | CatalogItemType::Secret
5727 | CatalogItemType::Connection => true,
5728 CatalogItemType::Index => false,
5729 },
5730 }
5731}
5732
5733pub fn describe_alter_index_options(
5734 _: &StatementContext,
5735 _: AlterIndexStatement<Aug>,
5736) -> Result<StatementDesc, PlanError> {
5737 Ok(StatementDesc::new(None))
5738}
5739
5740pub fn describe_drop_owned(
5741 _: &StatementContext,
5742 _: DropOwnedStatement<Aug>,
5743) -> Result<StatementDesc, PlanError> {
5744 Ok(StatementDesc::new(None))
5745}
5746
5747pub fn plan_drop_owned(
5748 scx: &StatementContext,
5749 drop: DropOwnedStatement<Aug>,
5750) -> Result<Plan, PlanError> {
5751 let cascade = drop.cascade();
5752 let role_ids: BTreeSet<_> = drop.role_names.into_iter().map(|role| role.id).collect();
5753 let mut drop_ids = Vec::new();
5754 let mut privilege_revokes = Vec::new();
5755 let mut default_privilege_revokes = Vec::new();
5756
5757 fn update_privilege_revokes(
5758 object_id: SystemObjectId,
5759 privileges: &PrivilegeMap,
5760 role_ids: &BTreeSet<RoleId>,
5761 privilege_revokes: &mut Vec<(SystemObjectId, MzAclItem)>,
5762 ) {
5763 privilege_revokes.extend(iter::zip(
5764 iter::repeat(object_id),
5765 privileges
5766 .all_values()
5767 .filter(|privilege| role_ids.contains(&privilege.grantee))
5768 .cloned(),
5769 ));
5770 }
5771
5772 for replica in scx.catalog.get_cluster_replicas() {
5774 if role_ids.contains(&replica.owner_id()) {
5775 drop_ids.push((replica.cluster_id(), replica.replica_id()).into());
5776 }
5777 }
5778
5779 for cluster in scx.catalog.get_clusters() {
5781 if role_ids.contains(&cluster.owner_id()) {
5782 if !cascade {
5784 let non_owned_bound_objects: Vec<_> = cluster
5785 .bound_objects()
5786 .into_iter()
5787 .map(|item_id| scx.catalog.get_item(item_id))
5788 .filter(|item| !role_ids.contains(&item.owner_id()))
5789 .collect();
5790 if !non_owned_bound_objects.is_empty() {
5791 let names: Vec<_> = non_owned_bound_objects
5792 .into_iter()
5793 .map(|item| {
5794 (
5795 item.item_type().to_string(),
5796 scx.catalog.resolve_full_name(item.name()).to_string(),
5797 )
5798 })
5799 .collect();
5800 return Err(PlanError::DependentObjectsStillExist {
5801 object_type: "cluster".to_string(),
5802 object_name: cluster.name().to_string(),
5803 dependents: names,
5804 });
5805 }
5806 }
5807 drop_ids.push(cluster.id().into());
5808 }
5809 update_privilege_revokes(
5810 SystemObjectId::Object(cluster.id().into()),
5811 cluster.privileges(),
5812 &role_ids,
5813 &mut privilege_revokes,
5814 );
5815 }
5816
5817 for item in scx.catalog.get_items() {
5819 if role_ids.contains(&item.owner_id()) {
5820 if !cascade {
5821 let check_if_dependents_exist = |used_by: &[CatalogItemId]| {
5823 let non_owned_dependencies: Vec<_> = used_by
5824 .into_iter()
5825 .map(|item_id| scx.catalog.get_item(item_id))
5826 .filter(|item| dependency_prevents_drop(item.item_type().into(), *item))
5827 .filter(|item| !role_ids.contains(&item.owner_id()))
5828 .collect();
5829 if !non_owned_dependencies.is_empty() {
5830 let names: Vec<_> = non_owned_dependencies
5831 .into_iter()
5832 .map(|item| {
5833 let item_typ = item.item_type().to_string();
5834 let item_name =
5835 scx.catalog.resolve_full_name(item.name()).to_string();
5836 (item_typ, item_name)
5837 })
5838 .collect();
5839 Err(PlanError::DependentObjectsStillExist {
5840 object_type: item.item_type().to_string(),
5841 object_name: scx
5842 .catalog
5843 .resolve_full_name(item.name())
5844 .to_string()
5845 .to_string(),
5846 dependents: names,
5847 })
5848 } else {
5849 Ok(())
5850 }
5851 };
5852
5853 if let Some(id) = item.progress_id() {
5856 let progress_item = scx.catalog.get_item(&id);
5857 check_if_dependents_exist(progress_item.used_by())?;
5858 }
5859 check_if_dependents_exist(item.used_by())?;
5860 }
5861 drop_ids.push(item.id().into());
5862 }
5863 update_privilege_revokes(
5864 SystemObjectId::Object(item.id().into()),
5865 item.privileges(),
5866 &role_ids,
5867 &mut privilege_revokes,
5868 );
5869 }
5870
5871 for schema in scx.catalog.get_schemas() {
5873 if !schema.id().is_temporary() {
5874 if role_ids.contains(&schema.owner_id()) {
5875 if !cascade {
5876 let non_owned_dependencies: Vec<_> = schema
5877 .item_ids()
5878 .map(|item_id| scx.catalog.get_item(&item_id))
5879 .filter(|item| dependency_prevents_drop(item.item_type().into(), *item))
5880 .filter(|item| !role_ids.contains(&item.owner_id()))
5881 .collect();
5882 if !non_owned_dependencies.is_empty() {
5883 let full_schema_name = scx.catalog.resolve_full_schema_name(schema.name());
5884 sql_bail!(
5885 "schema {} cannot be dropped without CASCADE while it contains non-owned objects",
5886 full_schema_name.to_string().quoted()
5887 );
5888 }
5889 }
5890 drop_ids.push((*schema.database(), *schema.id()).into())
5891 }
5892 update_privilege_revokes(
5893 SystemObjectId::Object((*schema.database(), *schema.id()).into()),
5894 schema.privileges(),
5895 &role_ids,
5896 &mut privilege_revokes,
5897 );
5898 }
5899 }
5900
5901 for database in scx.catalog.get_databases() {
5903 if role_ids.contains(&database.owner_id()) {
5904 if !cascade {
5905 let non_owned_schemas: Vec<_> = database
5906 .schemas()
5907 .into_iter()
5908 .filter(|schema| !role_ids.contains(&schema.owner_id()))
5909 .collect();
5910 if !non_owned_schemas.is_empty() {
5911 sql_bail!(
5912 "database {} cannot be dropped without CASCADE while it contains non-owned schemas",
5913 database.name().quoted(),
5914 );
5915 }
5916 }
5917 drop_ids.push(database.id().into());
5918 }
5919 update_privilege_revokes(
5920 SystemObjectId::Object(database.id().into()),
5921 database.privileges(),
5922 &role_ids,
5923 &mut privilege_revokes,
5924 );
5925 }
5926
5927 for network_policy in scx.catalog.get_network_policies() {
5929 if role_ids.contains(&network_policy.owner_id()) {
5930 drop_ids.push(ObjectId::NetworkPolicy(network_policy.id()));
5931 }
5932 update_privilege_revokes(
5933 SystemObjectId::Object(ObjectId::NetworkPolicy(network_policy.id())),
5934 network_policy.privileges(),
5935 &role_ids,
5936 &mut privilege_revokes,
5937 );
5938 }
5939
5940 update_privilege_revokes(
5942 SystemObjectId::System,
5943 scx.catalog.get_system_privileges(),
5944 &role_ids,
5945 &mut privilege_revokes,
5946 );
5947
5948 for (default_privilege_object, default_privilege_acl_items) in
5949 scx.catalog.get_default_privileges()
5950 {
5951 for default_privilege_acl_item in default_privilege_acl_items {
5952 if role_ids.contains(&default_privilege_object.role_id)
5953 || role_ids.contains(&default_privilege_acl_item.grantee)
5954 {
5955 default_privilege_revokes.push((
5956 default_privilege_object.clone(),
5957 default_privilege_acl_item.clone(),
5958 ));
5959 }
5960 }
5961 }
5962
5963 let drop_ids = scx.catalog.object_dependents(&drop_ids);
5964
5965 let system_ids: Vec<_> = drop_ids.iter().filter(|id| id.is_system()).collect();
5966 if !system_ids.is_empty() {
5967 let mut owners = system_ids
5968 .into_iter()
5969 .filter_map(|object_id| scx.catalog.get_owner_id(object_id))
5970 .collect::<BTreeSet<_>>()
5971 .into_iter()
5972 .map(|role_id| scx.catalog.get_role(&role_id).name().quoted());
5973 sql_bail!(
5974 "cannot drop objects owned by role {} because they are required by the database system",
5975 owners.join(", "),
5976 );
5977 }
5978
5979 Ok(Plan::DropOwned(DropOwnedPlan {
5980 role_ids: role_ids.into_iter().collect(),
5981 drop_ids,
5982 privilege_revokes,
5983 default_privilege_revokes,
5984 }))
5985}
5986
5987fn plan_retain_history_option(
5988 scx: &StatementContext,
5989 retain_history: Option<OptionalDuration>,
5990) -> Result<Option<CompactionWindow>, PlanError> {
5991 if let Some(OptionalDuration(lcw)) = retain_history {
5992 Ok(Some(plan_retain_history(scx, lcw)?))
5993 } else {
5994 Ok(None)
5995 }
5996}
5997
5998fn plan_retain_history(
6004 scx: &StatementContext,
6005 lcw: Option<Duration>,
6006) -> Result<CompactionWindow, PlanError> {
6007 scx.require_feature_flag(&vars::ENABLE_LOGICAL_COMPACTION_WINDOW)?;
6008 match lcw {
6009 Some(Duration::ZERO) => Err(PlanError::InvalidOptionValue {
6014 option_name: "RETAIN HISTORY".to_string(),
6015 err: Box::new(PlanError::Unstructured(
6016 "internal error: unexpectedly zero".to_string(),
6017 )),
6018 }),
6019 Some(duration) => {
6020 if duration < DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION
6023 && scx
6024 .require_feature_flag(&vars::ENABLE_UNLIMITED_RETAIN_HISTORY)
6025 .is_err()
6026 {
6027 return Err(PlanError::RetainHistoryLow {
6028 limit: DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION,
6029 });
6030 }
6031 Ok(duration.try_into()?)
6032 }
6033 None => {
6036 if scx
6037 .require_feature_flag(&vars::ENABLE_UNLIMITED_RETAIN_HISTORY)
6038 .is_err()
6039 {
6040 Err(PlanError::RetainHistoryRequired)
6041 } else {
6042 Ok(CompactionWindow::DisableCompaction)
6043 }
6044 }
6045 }
6046}
6047
6048generate_extracted_config!(IndexOption, (RetainHistory, OptionalDuration));
6049
6050fn plan_index_options(
6051 scx: &StatementContext,
6052 with_opts: Vec<IndexOption<Aug>>,
6053) -> Result<Vec<crate::plan::IndexOption>, PlanError> {
6054 if !with_opts.is_empty() {
6055 scx.require_feature_flag(&vars::ENABLE_INDEX_OPTIONS)?;
6057 }
6058
6059 let IndexOptionExtracted { retain_history, .. }: IndexOptionExtracted = with_opts.try_into()?;
6060
6061 let mut out = Vec::with_capacity(1);
6062 if let Some(cw) = plan_retain_history_option(scx, retain_history)? {
6063 out.push(crate::plan::IndexOption::RetainHistory(cw));
6064 }
6065 Ok(out)
6066}
6067
6068generate_extracted_config!(
6069 TableOption,
6070 (PartitionBy, Vec<Ident>),
6071 (RetainHistory, OptionalDuration),
6072 (RedactedTest, String)
6073);
6074
6075fn plan_table_options(
6076 scx: &StatementContext,
6077 desc: &RelationDesc,
6078 with_opts: Vec<TableOption<Aug>>,
6079) -> Result<Vec<crate::plan::TableOption>, PlanError> {
6080 let TableOptionExtracted {
6081 partition_by,
6082 retain_history,
6083 redacted_test,
6084 ..
6085 }: TableOptionExtracted = with_opts.try_into()?;
6086
6087 if let Some(partition_by) = partition_by {
6088 scx.require_feature_flag(&ENABLE_COLLECTION_PARTITION_BY)?;
6089 check_partition_by(desc, partition_by)?;
6090 }
6091
6092 if redacted_test.is_some() {
6093 scx.require_feature_flag(&vars::ENABLE_REDACTED_TEST_OPTION)?;
6094 }
6095
6096 let mut out = Vec::with_capacity(1);
6097 if let Some(cw) = plan_retain_history_option(scx, retain_history)? {
6098 out.push(crate::plan::TableOption::RetainHistory(cw));
6099 }
6100 Ok(out)
6101}
6102
6103pub fn plan_alter_index_options(
6104 scx: &mut StatementContext,
6105 AlterIndexStatement {
6106 index_name,
6107 if_exists,
6108 action,
6109 }: AlterIndexStatement<Aug>,
6110) -> Result<Plan, PlanError> {
6111 let object_type = ObjectType::Index;
6112 match action {
6113 AlterIndexAction::ResetOptions(options) => {
6114 let mut options = options.into_iter();
6115 if let Some(opt) = options.next() {
6116 match opt {
6117 IndexOptionName::RetainHistory => {
6118 if options.next().is_some() {
6119 sql_bail!("RETAIN HISTORY must be only option");
6120 }
6121 return alter_retain_history(
6122 scx,
6123 object_type,
6124 if_exists,
6125 UnresolvedObjectName::Item(index_name),
6126 None,
6127 );
6128 }
6129 }
6130 }
6131 sql_bail!("expected option");
6132 }
6133 AlterIndexAction::SetOptions(options) => {
6134 let mut options = options.into_iter();
6135 if let Some(opt) = options.next() {
6136 match opt.name {
6137 IndexOptionName::RetainHistory => {
6138 if options.next().is_some() {
6139 sql_bail!("RETAIN HISTORY must be only option");
6140 }
6141 return alter_retain_history(
6142 scx,
6143 object_type,
6144 if_exists,
6145 UnresolvedObjectName::Item(index_name),
6146 opt.value,
6147 );
6148 }
6149 }
6150 }
6151 sql_bail!("expected option");
6152 }
6153 }
6154}
6155
6156pub fn describe_alter_cluster_set_options(
6157 _: &StatementContext,
6158 _: AlterClusterStatement<Aug>,
6159) -> Result<StatementDesc, PlanError> {
6160 Ok(StatementDesc::new(None))
6161}
6162
6163pub fn plan_alter_cluster(
6164 scx: &mut StatementContext,
6165 AlterClusterStatement {
6166 name,
6167 action,
6168 if_exists,
6169 }: AlterClusterStatement<Aug>,
6170) -> Result<Plan, PlanError> {
6171 let cluster = match resolve_cluster(scx, &name, if_exists)? {
6172 Some(entry) => entry,
6173 None => {
6174 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6175 name: name.to_ast_string_simple(),
6176 object_type: ObjectType::Cluster,
6177 });
6178
6179 return Ok(Plan::AlterNoop(AlterNoopPlan {
6180 object_type: ObjectType::Cluster,
6181 }));
6182 }
6183 };
6184
6185 let mut options: PlanClusterOption = Default::default();
6186 let mut alter_strategy: AlterClusterPlanStrategy = AlterClusterPlanStrategy::None;
6187
6188 match action {
6189 AlterClusterAction::SetOptions {
6190 options: set_options,
6191 with_options,
6192 } => {
6193 let ClusterOptionExtracted {
6194 availability_zones,
6195 introspection_debugging,
6196 introspection_interval,
6197 managed,
6198 replicas: replica_defs,
6199 replication_factor,
6200 seen: _,
6201 size,
6202 disk,
6203 schedule,
6204 workload_class,
6205 }: ClusterOptionExtracted = set_options.try_into()?;
6206
6207 if !scx.catalog.active_role_id().is_system() {
6208 if workload_class.is_some() {
6209 sql_bail!("WORKLOAD CLASS not supported for non-system users");
6210 }
6211 }
6212
6213 match managed.unwrap_or_else(|| cluster.is_managed()) {
6214 true => {
6215 let alter_strategy_extracted =
6216 ClusterAlterOptionExtracted::try_from(with_options)?;
6217 alter_strategy = AlterClusterPlanStrategy::try_from(alter_strategy_extracted)?;
6218
6219 match alter_strategy {
6220 AlterClusterPlanStrategy::None => {}
6221 _ => {
6222 scx.require_feature_flag(
6223 &crate::session::vars::ENABLE_ZERO_DOWNTIME_CLUSTER_RECONFIGURATION,
6224 )?;
6225 }
6226 }
6227
6228 if replica_defs.is_some() {
6229 sql_bail!("REPLICAS not supported for managed clusters");
6230 }
6231 if schedule.is_some()
6232 && !matches!(schedule, Some(ClusterScheduleOptionValue::Manual))
6233 {
6234 scx.require_feature_flag(&ENABLE_CLUSTER_SCHEDULE_REFRESH)?;
6235
6236 if replication_factor.is_none()
6245 && cluster.replication_factor().is_some_and(|rf| rf > 1)
6246 {
6247 sql_bail!(
6248 "SCHEDULE cannot be set to anything other than MANUAL while the \
6249 cluster's REPLICATION FACTOR is greater than 1; \
6250 set the REPLICATION FACTOR to 1 first"
6251 );
6252 }
6253 }
6254
6255 if replication_factor.is_some() {
6256 if schedule.is_some()
6257 && !matches!(schedule, Some(ClusterScheduleOptionValue::Manual))
6258 {
6259 sql_bail!(
6260 "REPLICATION FACTOR cannot be given together with any SCHEDULE other than MANUAL"
6261 );
6262 }
6263 if let Some(current_schedule) = cluster.schedule() {
6264 if !matches!(current_schedule, ClusterSchedule::Manual) {
6265 sql_bail!(
6266 "REPLICATION FACTOR cannot be set if the cluster SCHEDULE is anything other than MANUAL"
6267 );
6268 }
6269 }
6270 }
6271 }
6272 false => {
6273 if !with_options.is_empty() {
6274 sql_bail!("ALTER... WITH not supported for unmanaged clusters");
6275 }
6276 if availability_zones.is_some() {
6277 sql_bail!("AVAILABILITY ZONES not supported for unmanaged clusters");
6278 }
6279 if replication_factor.is_some() {
6280 sql_bail!("REPLICATION FACTOR not supported for unmanaged clusters");
6281 }
6282 if introspection_debugging.is_some() {
6283 sql_bail!("INTROSPECTION DEBUGGING not supported for unmanaged clusters");
6284 }
6285 if introspection_interval.is_some() {
6286 sql_bail!("INTROSPECTION INTERVAL not supported for unmanaged clusters");
6287 }
6288 if size.is_some() {
6289 sql_bail!("SIZE not supported for unmanaged clusters");
6290 }
6291 if disk.is_some() {
6292 sql_bail!("DISK not supported for unmanaged clusters");
6293 }
6294 if schedule.is_some()
6295 && !matches!(schedule, Some(ClusterScheduleOptionValue::Manual))
6296 {
6297 sql_bail!(
6298 "cluster schedules other than MANUAL are not supported for unmanaged clusters"
6299 );
6300 }
6301 if let Some(current_schedule) = cluster.schedule() {
6302 if !matches!(current_schedule, ClusterSchedule::Manual)
6303 && schedule.is_none()
6304 {
6305 sql_bail!(
6306 "when switching a cluster to unmanaged, if the managed \
6307 cluster's SCHEDULE is anything other than MANUAL, you have to \
6308 explicitly set the SCHEDULE to MANUAL"
6309 );
6310 }
6311 }
6312 }
6313 }
6314
6315 let mut replicas = vec![];
6316 for ReplicaDefinition { name, options } in
6317 replica_defs.into_iter().flat_map(Vec::into_iter)
6318 {
6319 replicas.push((normalize::ident(name), plan_replica_config(scx, options)?));
6320 }
6321
6322 if let Some(managed) = managed {
6323 options.managed = AlterOptionParameter::Set(managed);
6324 }
6325 if let Some(replication_factor) = replication_factor {
6326 options.replication_factor = AlterOptionParameter::Set(replication_factor);
6327 }
6328 if let Some(size) = &size {
6329 options.size = AlterOptionParameter::Set(size.clone());
6330 }
6331 if let Some(availability_zones) = availability_zones {
6332 options.availability_zones = AlterOptionParameter::Set(availability_zones);
6333 }
6334 if let Some(introspection_debugging) = introspection_debugging {
6335 options.introspection_debugging =
6336 AlterOptionParameter::Set(introspection_debugging);
6337 }
6338 if let Some(introspection_interval) = introspection_interval {
6339 options.introspection_interval = AlterOptionParameter::Set(introspection_interval);
6340 }
6341 if disk.is_some() {
6342 let size = match size.as_deref() {
6346 Some(s) => s,
6347 None => cluster
6348 .managed_size()
6349 .ok_or_else(|| sql_err!("cluster is not managed"))?,
6350 };
6351 if scx.catalog.is_cluster_size_cc(size) {
6352 sql_bail!(
6353 "DISK option not supported for modern cluster sizes because disk is always enabled"
6354 );
6355 }
6356
6357 scx.catalog
6358 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated);
6359 }
6360 if !replicas.is_empty() {
6361 options.replicas = AlterOptionParameter::Set(replicas);
6362 }
6363 if let Some(schedule) = schedule {
6364 options.schedule = AlterOptionParameter::Set(plan_cluster_schedule(schedule)?);
6365 }
6366 if let Some(workload_class) = workload_class {
6367 options.workload_class = AlterOptionParameter::Set(workload_class.0);
6368 }
6369 }
6370 AlterClusterAction::ResetOptions(reset_options) => {
6371 use AlterOptionParameter::Reset;
6372 use ClusterOptionName::*;
6373
6374 if !scx.catalog.active_role_id().is_system() {
6375 if reset_options.contains(&WorkloadClass) {
6376 sql_bail!("WORKLOAD CLASS not supported for non-system users");
6377 }
6378 }
6379
6380 for option in reset_options {
6381 match option {
6382 AvailabilityZones => options.availability_zones = Reset,
6383 Disk => scx
6384 .catalog
6385 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated),
6386 IntrospectionInterval => options.introspection_interval = Reset,
6387 IntrospectionDebugging => options.introspection_debugging = Reset,
6388 Managed => options.managed = Reset,
6389 Replicas => options.replicas = Reset,
6390 ReplicationFactor => options.replication_factor = Reset,
6391 Size => options.size = Reset,
6392 Schedule => options.schedule = Reset,
6393 WorkloadClass => options.workload_class = Reset,
6394 }
6395 }
6396 }
6397 }
6398 Ok(Plan::AlterCluster(AlterClusterPlan {
6399 id: cluster.id(),
6400 name: cluster.name().to_string(),
6401 options,
6402 strategy: alter_strategy,
6403 }))
6404}
6405
6406pub fn describe_alter_set_cluster(
6407 _: &StatementContext,
6408 _: AlterSetClusterStatement<Aug>,
6409) -> Result<StatementDesc, PlanError> {
6410 Ok(StatementDesc::new(None))
6411}
6412
6413pub fn plan_alter_item_set_cluster(
6414 scx: &StatementContext,
6415 AlterSetClusterStatement {
6416 if_exists,
6417 set_cluster: in_cluster_name,
6418 name,
6419 object_type,
6420 }: AlterSetClusterStatement<Aug>,
6421) -> Result<Plan, PlanError> {
6422 scx.require_feature_flag(&vars::ENABLE_ALTER_SET_CLUSTER)?;
6423
6424 let object_type = object_type.into();
6425
6426 match object_type {
6428 ObjectType::MaterializedView => {}
6429 ObjectType::Index | ObjectType::Sink | ObjectType::Source => {
6430 bail_unsupported!(29606, format!("ALTER {object_type} SET CLUSTER"))
6431 }
6432 ObjectType::Table
6433 | ObjectType::View
6434 | ObjectType::Type
6435 | ObjectType::Role
6436 | ObjectType::Cluster
6437 | ObjectType::ClusterReplica
6438 | ObjectType::Secret
6439 | ObjectType::Connection
6440 | ObjectType::Database
6441 | ObjectType::Schema
6442 | ObjectType::Func
6443 | ObjectType::NetworkPolicy => {
6444 bail_never_supported!(
6445 format!("ALTER {object_type} SET CLUSTER"),
6446 "sql/alter-set-cluster/",
6447 format!("{object_type} has no associated cluster")
6448 )
6449 }
6450 }
6451
6452 let in_cluster = scx.catalog.get_cluster(in_cluster_name.id);
6453
6454 match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
6455 Some(entry) => {
6456 let current_cluster = entry.cluster_id();
6457 let Some(current_cluster) = current_cluster else {
6458 sql_bail!("No cluster associated with {name}");
6459 };
6460
6461 if current_cluster == in_cluster.id() {
6462 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
6463 } else {
6464 Ok(Plan::AlterSetCluster(AlterSetClusterPlan {
6465 id: entry.id(),
6466 set_cluster: in_cluster.id(),
6467 }))
6468 }
6469 }
6470 None => {
6471 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6472 name: name.to_ast_string_simple(),
6473 object_type,
6474 });
6475
6476 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
6477 }
6478 }
6479}
6480
6481pub fn describe_alter_object_rename(
6482 _: &StatementContext,
6483 _: AlterObjectRenameStatement,
6484) -> Result<StatementDesc, PlanError> {
6485 Ok(StatementDesc::new(None))
6486}
6487
6488pub fn plan_alter_object_rename(
6489 scx: &mut StatementContext,
6490 AlterObjectRenameStatement {
6491 name,
6492 object_type,
6493 to_item_name,
6494 if_exists,
6495 }: AlterObjectRenameStatement,
6496) -> Result<Plan, PlanError> {
6497 let object_type = object_type.into();
6498 match (object_type, name) {
6499 (
6500 ObjectType::View
6501 | ObjectType::MaterializedView
6502 | ObjectType::Table
6503 | ObjectType::Source
6504 | ObjectType::Index
6505 | ObjectType::Sink
6506 | ObjectType::Secret
6507 | ObjectType::Connection,
6508 UnresolvedObjectName::Item(name),
6509 ) => plan_alter_item_rename(scx, object_type, name, to_item_name, if_exists),
6510 (ObjectType::Cluster, UnresolvedObjectName::Cluster(name)) => {
6511 plan_alter_cluster_rename(scx, object_type, name, to_item_name, if_exists)
6512 }
6513 (ObjectType::ClusterReplica, UnresolvedObjectName::ClusterReplica(name)) => {
6514 plan_alter_cluster_replica_rename(scx, object_type, name, to_item_name, if_exists)
6515 }
6516 (ObjectType::Schema, UnresolvedObjectName::Schema(name)) => {
6517 plan_alter_schema_rename(scx, name, to_item_name, if_exists)
6518 }
6519 (object_type, name) => {
6520 bail_internal!("invalid object type '{object_type}' for ALTER RENAME with name {name}")
6523 }
6524 }
6525}
6526
6527pub fn plan_alter_schema_rename(
6528 scx: &mut StatementContext,
6529 name: UnresolvedSchemaName,
6530 to_schema_name: Ident,
6531 if_exists: bool,
6532) -> Result<Plan, PlanError> {
6533 let normalized = normalize::unresolved_schema_name(name.clone())?;
6537 if normalized.database.is_none() && normalized.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA {
6538 sql_bail!(
6539 "cannot rename schemas in the ambient database: {:?}",
6540 mz_repr::namespaces::MZ_TEMP_SCHEMA
6541 );
6542 }
6543
6544 let Some((db_spec, schema_spec)) = resolve_schema(scx, name.clone(), if_exists)? else {
6545 let object_type = ObjectType::Schema;
6546 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6547 name: name.to_ast_string_simple(),
6548 object_type,
6549 });
6550 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
6551 };
6552
6553 if scx
6555 .resolve_schema_in_database(&db_spec, &to_schema_name)
6556 .is_ok()
6557 {
6558 return Err(PlanError::Catalog(CatalogError::SchemaAlreadyExists(
6559 to_schema_name.clone().into_string(),
6560 )));
6561 }
6562
6563 let schema = scx.catalog.get_schema(&db_spec, &schema_spec);
6565 if schema.id().is_system() {
6566 bail_never_supported!(format!("renaming the {} schema", schema.name().schema))
6567 }
6568
6569 Ok(Plan::AlterSchemaRename(AlterSchemaRenamePlan {
6570 cur_schema_spec: (db_spec, schema_spec),
6571 new_schema_name: to_schema_name.into_string(),
6572 }))
6573}
6574
6575pub fn plan_alter_schema_swap<F>(
6576 scx: &mut StatementContext,
6577 name_a: UnresolvedSchemaName,
6578 name_b: Ident,
6579 if_exists: bool,
6580 gen_temp_suffix: F,
6581) -> Result<Plan, PlanError>
6582where
6583 F: Fn(&dyn Fn(&str) -> bool) -> Result<String, PlanError>,
6584{
6585 let normalized_a = normalize::unresolved_schema_name(name_a.clone())?;
6589 if normalized_a.database.is_none() && normalized_a.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA
6590 {
6591 sql_bail!("cannot swap schemas that are in the ambient database");
6592 }
6593 let name_b_str = normalize::ident_ref(&name_b);
6595 if name_b_str == mz_repr::namespaces::MZ_TEMP_SCHEMA {
6596 sql_bail!("cannot swap schemas that are in the ambient database");
6597 }
6598
6599 let schema_a = match scx.resolve_schema(name_a.clone()) {
6600 Ok(schema) => schema,
6601 Err(_) if if_exists => {
6602 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6603 name: name_a.to_ast_string_simple(),
6604 object_type: ObjectType::Schema,
6605 });
6606 return Ok(Plan::AlterNoop(AlterNoopPlan {
6607 object_type: ObjectType::Schema,
6608 }));
6609 }
6610 Err(e) => return Err(e),
6611 };
6612
6613 let db_spec = schema_a.database().clone();
6614 if matches!(db_spec, ResolvedDatabaseSpecifier::Ambient) {
6615 sql_bail!("cannot swap schemas that are in the ambient database");
6616 };
6617 let schema_b = scx.resolve_schema_in_database(&db_spec, &name_b)?;
6618
6619 if schema_a.id().is_system() || schema_b.id().is_system() {
6621 bail_never_supported!("swapping a system schema".to_string())
6622 }
6623
6624 const SCHEMA_SWAP_PREFIX: &str = "mz_schema_swap_";
6628 let check = |temp_suffix: &str| {
6629 let mut temp_name = ident!(SCHEMA_SWAP_PREFIX);
6630 temp_name.append_lossy(temp_suffix);
6631 scx.resolve_schema_in_database(&db_spec, &temp_name)
6632 .is_err()
6633 };
6634 let temp_suffix = gen_temp_suffix(&check)?;
6635 let name_temp = format!("{SCHEMA_SWAP_PREFIX}{temp_suffix}");
6636
6637 Ok(Plan::AlterSchemaSwap(AlterSchemaSwapPlan {
6638 schema_a_spec: (*schema_a.database(), *schema_a.id()),
6639 schema_a_name: schema_a.name().schema.to_string(),
6640 schema_b_spec: (*schema_b.database(), *schema_b.id()),
6641 schema_b_name: schema_b.name().schema.to_string(),
6642 name_temp,
6643 }))
6644}
6645
6646pub fn plan_alter_item_rename(
6647 scx: &mut StatementContext,
6648 object_type: ObjectType,
6649 name: UnresolvedItemName,
6650 to_item_name: Ident,
6651 if_exists: bool,
6652) -> Result<Plan, PlanError> {
6653 let resolved = match resolve_item_or_type(scx, object_type, name.clone(), if_exists) {
6654 Ok(r) => r,
6655 Err(PlanError::MismatchedObjectType {
6657 name,
6658 is_type: ObjectType::MaterializedView,
6659 expected_type: ObjectType::View,
6660 }) => {
6661 return Err(PlanError::AlterViewOnMaterializedView(name.to_string()));
6662 }
6663 e => e?,
6664 };
6665
6666 match resolved {
6667 Some(entry) => {
6668 let full_name = scx.catalog.resolve_full_name(entry.name());
6669 let item_type = entry.item_type();
6670
6671 let proposed_name = QualifiedItemName {
6672 qualifiers: entry.name().qualifiers.clone(),
6673 item: to_item_name.clone().into_string(),
6674 };
6675
6676 let conflicting_type_exists;
6680 let conflicting_item_exists;
6681 if item_type == CatalogItemType::Type {
6682 conflicting_type_exists = scx.catalog.get_type_by_name(&proposed_name).is_some();
6683 conflicting_item_exists = scx
6684 .catalog
6685 .get_item_by_name(&proposed_name)
6686 .map(|item| item.item_type().conflicts_with_type())
6687 .unwrap_or(false);
6688 } else {
6689 conflicting_type_exists = item_type.conflicts_with_type()
6690 && scx.catalog.get_type_by_name(&proposed_name).is_some();
6691 conflicting_item_exists = scx.catalog.get_item_by_name(&proposed_name).is_some();
6692 };
6693 if conflicting_type_exists || conflicting_item_exists {
6694 sql_bail!("catalog item '{}' already exists", to_item_name);
6695 }
6696
6697 Ok(Plan::AlterItemRename(AlterItemRenamePlan {
6698 id: entry.id(),
6699 current_full_name: full_name,
6700 to_name: normalize::ident(to_item_name),
6701 object_type,
6702 }))
6703 }
6704 None => {
6705 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6706 name: name.to_ast_string_simple(),
6707 object_type,
6708 });
6709
6710 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
6711 }
6712 }
6713}
6714
6715pub fn plan_alter_cluster_rename(
6716 scx: &mut StatementContext,
6717 object_type: ObjectType,
6718 name: Ident,
6719 to_name: Ident,
6720 if_exists: bool,
6721) -> Result<Plan, PlanError> {
6722 match resolve_cluster(scx, &name, if_exists)? {
6723 Some(entry) => Ok(Plan::AlterClusterRename(AlterClusterRenamePlan {
6724 id: entry.id(),
6725 name: entry.name().to_string(),
6726 to_name: ident(to_name),
6727 })),
6728 None => {
6729 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6730 name: name.to_ast_string_simple(),
6731 object_type,
6732 });
6733
6734 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
6735 }
6736 }
6737}
6738
6739pub fn plan_alter_cluster_swap<F>(
6740 scx: &mut StatementContext,
6741 name_a: Ident,
6742 name_b: Ident,
6743 if_exists: bool,
6744 gen_temp_suffix: F,
6745) -> Result<Plan, PlanError>
6746where
6747 F: Fn(&dyn Fn(&str) -> bool) -> Result<String, PlanError>,
6748{
6749 let cluster_a = match scx.resolve_cluster(Some(&name_a)) {
6750 Ok(cluster) => cluster,
6751 Err(_) if if_exists => {
6752 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6753 name: name_a.to_ast_string_simple(),
6754 object_type: ObjectType::Cluster,
6755 });
6756 return Ok(Plan::AlterNoop(AlterNoopPlan {
6757 object_type: ObjectType::Cluster,
6758 }));
6759 }
6760 Err(e) => return Err(e),
6761 };
6762 let cluster_b = scx.resolve_cluster(Some(&name_b))?;
6763
6764 const CLUSTER_SWAP_PREFIX: &str = "mz_cluster_swap_";
6765 let check = |temp_suffix: &str| {
6766 let mut temp_name = ident!(CLUSTER_SWAP_PREFIX);
6767 temp_name.append_lossy(temp_suffix);
6768 match scx.catalog.resolve_cluster(Some(temp_name.as_str())) {
6769 Err(CatalogError::UnknownCluster(_)) => true,
6771 Ok(_) | Err(_) => false,
6773 }
6774 };
6775 let temp_suffix = gen_temp_suffix(&check)?;
6776 let name_temp = format!("{CLUSTER_SWAP_PREFIX}{temp_suffix}");
6777
6778 Ok(Plan::AlterClusterSwap(AlterClusterSwapPlan {
6779 id_a: cluster_a.id(),
6780 id_b: cluster_b.id(),
6781 name_a: name_a.into_string(),
6782 name_b: name_b.into_string(),
6783 name_temp,
6784 }))
6785}
6786
6787pub fn plan_alter_cluster_replica_rename(
6788 scx: &mut StatementContext,
6789 object_type: ObjectType,
6790 name: QualifiedReplica,
6791 to_item_name: Ident,
6792 if_exists: bool,
6793) -> Result<Plan, PlanError> {
6794 match resolve_cluster_replica(scx, &name, if_exists)? {
6795 Some((cluster, replica)) => {
6796 ensure_cluster_is_not_managed(scx, cluster.id())?;
6797 Ok(Plan::AlterClusterReplicaRename(
6798 AlterClusterReplicaRenamePlan {
6799 cluster_id: cluster.id(),
6800 replica_id: replica,
6801 name: QualifiedReplica {
6802 cluster: Ident::new(cluster.name())?,
6803 replica: name.replica,
6804 },
6805 to_name: normalize::ident(to_item_name),
6806 },
6807 ))
6808 }
6809 None => {
6810 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6811 name: name.to_ast_string_simple(),
6812 object_type,
6813 });
6814
6815 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
6816 }
6817 }
6818}
6819
6820pub fn describe_alter_object_swap(
6821 _: &StatementContext,
6822 _: AlterObjectSwapStatement,
6823) -> Result<StatementDesc, PlanError> {
6824 Ok(StatementDesc::new(None))
6825}
6826
6827pub fn plan_alter_object_swap(
6828 scx: &mut StatementContext,
6829 stmt: AlterObjectSwapStatement,
6830) -> Result<Plan, PlanError> {
6831 scx.require_feature_flag(&vars::ENABLE_ALTER_SWAP)?;
6832
6833 let AlterObjectSwapStatement {
6834 object_type,
6835 if_exists,
6836 name_a,
6837 name_b,
6838 } = stmt;
6839 let object_type = object_type.into();
6840
6841 let gen_temp_suffix = |check_fn: &dyn Fn(&str) -> bool| {
6843 let mut attempts = 0;
6844 let name_temp = loop {
6845 attempts += 1;
6846 if attempts > 10 {
6847 tracing::warn!("Unable to generate temp id for swapping");
6848 sql_bail!("unable to swap!");
6849 }
6850
6851 let short_id = mz_ore::id_gen::temp_id();
6853 if check_fn(&short_id) {
6854 break short_id;
6855 }
6856 };
6857
6858 Ok(name_temp)
6859 };
6860
6861 match (object_type, name_a, name_b) {
6862 (ObjectType::Schema, UnresolvedObjectName::Schema(name_a), name_b) => {
6863 plan_alter_schema_swap(scx, name_a, name_b, if_exists, gen_temp_suffix)
6864 }
6865 (ObjectType::Cluster, UnresolvedObjectName::Cluster(name_a), name_b) => {
6866 plan_alter_cluster_swap(scx, name_a, name_b, if_exists, gen_temp_suffix)
6867 }
6868 (ObjectType::Schema | ObjectType::Cluster, _, _) => {
6869 bail_internal!("name type does not match object type for ALTER SWAP")
6870 }
6871 (
6872 ObjectType::Table
6873 | ObjectType::View
6874 | ObjectType::MaterializedView
6875 | ObjectType::Source
6876 | ObjectType::Sink
6877 | ObjectType::Index
6878 | ObjectType::Type
6879 | ObjectType::Role
6880 | ObjectType::ClusterReplica
6881 | ObjectType::Secret
6882 | ObjectType::Connection
6883 | ObjectType::Database
6884 | ObjectType::Func
6885 | ObjectType::NetworkPolicy,
6886 _,
6887 _,
6888 ) => Err(PlanError::Unsupported {
6889 feature: format!("ALTER {object_type} .. SWAP WITH ..."),
6890 discussion_no: None,
6891 }),
6892 }
6893}
6894
6895pub fn describe_alter_retain_history(
6896 _: &StatementContext,
6897 _: AlterRetainHistoryStatement<Aug>,
6898) -> Result<StatementDesc, PlanError> {
6899 Ok(StatementDesc::new(None))
6900}
6901
6902pub fn plan_alter_retain_history(
6903 scx: &StatementContext,
6904 AlterRetainHistoryStatement {
6905 object_type,
6906 if_exists,
6907 name,
6908 history,
6909 }: AlterRetainHistoryStatement<Aug>,
6910) -> Result<Plan, PlanError> {
6911 alter_retain_history(scx, object_type.into(), if_exists, name, history)
6912}
6913
6914fn alter_retain_history(
6915 scx: &StatementContext,
6916 object_type: ObjectType,
6917 if_exists: bool,
6918 name: UnresolvedObjectName,
6919 history: Option<WithOptionValue<Aug>>,
6920) -> Result<Plan, PlanError> {
6921 let name = match (object_type, name) {
6922 (
6923 ObjectType::View
6925 | ObjectType::MaterializedView
6926 | ObjectType::Table
6927 | ObjectType::Source
6928 | ObjectType::Index,
6929 UnresolvedObjectName::Item(name),
6930 ) => name,
6931 (object_type, _) => {
6932 bail_unsupported!(format!("RETAIN HISTORY on {object_type}"))
6933 }
6934 };
6935 match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
6936 Some(entry) => {
6937 let full_name = scx.catalog.resolve_full_name(entry.name());
6938 let item_type = entry.item_type();
6939
6940 if object_type == ObjectType::View && item_type == CatalogItemType::MaterializedView {
6942 return Err(PlanError::AlterViewOnMaterializedView(
6943 full_name.to_string(),
6944 ));
6945 } else if object_type == ObjectType::View {
6946 sql_bail!("{object_type} does not support RETAIN HISTORY")
6947 } else if object_type != item_type {
6948 sql_bail!(
6949 "\"{}\" is a {} not a {}",
6950 full_name,
6951 entry.item_type(),
6952 format!("{object_type}").to_lowercase()
6953 )
6954 }
6955
6956 let (value, lcw) = match &history {
6958 Some(WithOptionValue::RetainHistoryFor(value)) => {
6959 let window = OptionalDuration::try_from_value(value.clone())?;
6960 (Some(value.clone()), window.0)
6961 }
6962 None => (None, Some(DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION)),
6964 _ => sql_bail!("unexpected value type for RETAIN HISTORY"),
6965 };
6966 let window = plan_retain_history(scx, lcw)?;
6967
6968 Ok(Plan::AlterRetainHistory(AlterRetainHistoryPlan {
6969 id: entry.id(),
6970 value,
6971 window,
6972 object_type,
6973 }))
6974 }
6975 None => {
6976 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6977 name: name.to_ast_string_simple(),
6978 object_type,
6979 });
6980
6981 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
6982 }
6983 }
6984}
6985
6986fn alter_source_timestamp_interval(
6987 scx: &StatementContext,
6988 if_exists: bool,
6989 source_name: UnresolvedItemName,
6990 value: Option<WithOptionValue<Aug>>,
6991) -> Result<Plan, PlanError> {
6992 let object_type = ObjectType::Source;
6993 match resolve_item_or_type(scx, object_type, source_name.clone(), if_exists)? {
6994 Some(entry) => {
6995 let full_name = scx.catalog.resolve_full_name(entry.name());
6996 if entry.item_type() != CatalogItemType::Source {
6997 sql_bail!(
6998 "\"{}\" is a {} not a {}",
6999 full_name,
7000 entry.item_type(),
7001 format!("{object_type}").to_lowercase()
7002 )
7003 }
7004
7005 match value {
7006 Some(val) => {
7007 let val = match val {
7008 WithOptionValue::Value(v) => v,
7009 _ => sql_bail!("TIMESTAMP INTERVAL requires an interval value"),
7010 };
7011 let duration = Duration::try_from_value(val.clone())?;
7012
7013 let min = scx.catalog.system_vars().min_timestamp_interval();
7014 let max = scx.catalog.system_vars().max_timestamp_interval();
7015 if duration < min || duration > max {
7016 return Err(PlanError::InvalidTimestampInterval {
7017 min,
7018 max,
7019 requested: duration,
7020 });
7021 }
7022
7023 Ok(Plan::AlterSourceTimestampInterval(
7024 AlterSourceTimestampIntervalPlan {
7025 id: entry.id(),
7026 value: Some(val),
7027 interval: duration,
7028 },
7029 ))
7030 }
7031 None => {
7032 let interval = scx.catalog.system_vars().default_timestamp_interval();
7033 Ok(Plan::AlterSourceTimestampInterval(
7034 AlterSourceTimestampIntervalPlan {
7035 id: entry.id(),
7036 value: None,
7037 interval,
7038 },
7039 ))
7040 }
7041 }
7042 }
7043 None => {
7044 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7045 name: source_name.to_ast_string_simple(),
7046 object_type,
7047 });
7048
7049 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7050 }
7051 }
7052}
7053
7054pub fn describe_alter_secret_options(
7055 _: &StatementContext,
7056 _: AlterSecretStatement<Aug>,
7057) -> Result<StatementDesc, PlanError> {
7058 Ok(StatementDesc::new(None))
7059}
7060
7061pub fn plan_alter_secret(
7062 scx: &mut StatementContext,
7063 stmt: AlterSecretStatement<Aug>,
7064) -> Result<Plan, PlanError> {
7065 let AlterSecretStatement {
7066 name,
7067 if_exists,
7068 value,
7069 } = stmt;
7070 let object_type = ObjectType::Secret;
7071 let id = match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
7072 Some(entry) => entry.id(),
7073 None => {
7074 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7075 name: name.to_string(),
7076 object_type,
7077 });
7078
7079 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7080 }
7081 };
7082
7083 let secret_as = query::plan_secret_as(scx, value)?;
7084
7085 Ok(Plan::AlterSecret(AlterSecretPlan { id, secret_as }))
7086}
7087
7088pub fn describe_alter_connection(
7089 _: &StatementContext,
7090 _: AlterConnectionStatement<Aug>,
7091) -> Result<StatementDesc, PlanError> {
7092 Ok(StatementDesc::new(None))
7093}
7094
7095generate_extracted_config!(AlterConnectionOption, (Validate, bool));
7096
7097pub fn plan_alter_connection(
7098 scx: &StatementContext,
7099 stmt: AlterConnectionStatement<Aug>,
7100) -> Result<Plan, PlanError> {
7101 let AlterConnectionStatement {
7102 name,
7103 if_exists,
7104 actions,
7105 with_options,
7106 } = stmt;
7107 let conn_name = normalize::unresolved_item_name(name)?;
7108 let entry = match scx.catalog.resolve_item(&conn_name) {
7109 Ok(entry) => entry,
7110 Err(_) if if_exists => {
7111 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7112 name: conn_name.to_string(),
7113 object_type: ObjectType::Connection,
7114 });
7115
7116 return Ok(Plan::AlterNoop(AlterNoopPlan {
7117 object_type: ObjectType::Connection,
7118 }));
7119 }
7120 Err(e) => return Err(e.into()),
7121 };
7122
7123 let connection = entry.connection()?;
7124
7125 if actions
7126 .iter()
7127 .any(|action| matches!(action, AlterConnectionAction::RotateKeys))
7128 {
7129 if actions.len() > 1 {
7130 sql_bail!("cannot specify any other actions alongside ALTER CONNECTION...ROTATE KEYS");
7131 }
7132
7133 if !with_options.is_empty() {
7134 sql_bail!(
7135 "ALTER CONNECTION...ROTATE KEYS does not support WITH ({})",
7136 with_options
7137 .iter()
7138 .map(|o| o.to_ast_string_simple())
7139 .join(", ")
7140 );
7141 }
7142
7143 if !matches!(connection, Connection::Ssh(_)) {
7144 sql_bail!(
7145 "{} is not an SSH connection",
7146 scx.catalog.resolve_full_name(entry.name())
7147 )
7148 }
7149
7150 return Ok(Plan::AlterConnection(AlterConnectionPlan {
7151 id: entry.id(),
7152 action: crate::plan::AlterConnectionAction::RotateKeys,
7153 }));
7154 }
7155
7156 let options = AlterConnectionOptionExtracted::try_from(with_options)?;
7157 if options.validate.is_some() {
7158 scx.require_feature_flag(&vars::ENABLE_CONNECTION_VALIDATION_SYNTAX)?;
7159 }
7160
7161 let validate = match options.validate {
7162 Some(val) => val,
7163 None => {
7164 scx.catalog
7165 .system_vars()
7166 .enable_default_connection_validation()
7167 && connection.validate_by_default()
7168 }
7169 };
7170
7171 let connection_type = match connection {
7172 Connection::Aws(_) => CreateConnectionType::Aws,
7173 Connection::AwsPrivatelink(_) => CreateConnectionType::AwsPrivatelink,
7174 Connection::Gcp(_) => CreateConnectionType::Gcp,
7175 Connection::Kafka(_) => CreateConnectionType::Kafka,
7176 Connection::Csr(_) => CreateConnectionType::Csr,
7177 Connection::GlueSchemaRegistry(_) => CreateConnectionType::GlueSchemaRegistry,
7178 Connection::Postgres(_) => CreateConnectionType::Postgres,
7179 Connection::Ssh(_) => CreateConnectionType::Ssh,
7180 Connection::MySql(_) => CreateConnectionType::MySql,
7181 Connection::SqlServer(_) => CreateConnectionType::SqlServer,
7182 Connection::IcebergCatalog(_) => CreateConnectionType::IcebergCatalog,
7183 };
7184
7185 let specified_options: BTreeSet<_> = actions
7187 .iter()
7188 .map(|action: &AlterConnectionAction<Aug>| match action {
7189 AlterConnectionAction::SetOption(option) => Ok(option.name.clone()),
7190 AlterConnectionAction::DropOption(name) => Ok(name.clone()),
7191 AlterConnectionAction::RotateKeys => {
7192 Err(internal_err!("RotateKeys is handled separately above"))
7193 }
7194 })
7195 .collect::<Result<_, PlanError>>()?;
7196
7197 for invalid in INALTERABLE_OPTIONS {
7198 if specified_options.contains(invalid) {
7199 sql_bail!("cannot ALTER {} option {}", connection_type, invalid);
7200 }
7201 }
7202
7203 connection::validate_options_per_connection_type(connection_type, specified_options)?;
7204
7205 let mut set_options_vec: Vec<_> = Vec::new();
7207 let mut drop_options: BTreeSet<_> = BTreeSet::new();
7208 for action in actions {
7209 match action {
7210 AlterConnectionAction::SetOption(option) => set_options_vec.push(option),
7211 AlterConnectionAction::DropOption(name) => {
7212 drop_options.insert(name);
7213 }
7214 AlterConnectionAction::RotateKeys => {
7215 bail_internal!("RotateKeys is handled separately above")
7216 }
7217 }
7218 }
7219
7220 let set_options: BTreeMap<_, _> = set_options_vec
7221 .clone()
7222 .into_iter()
7223 .map(|option| (option.name, option.value))
7224 .collect();
7225
7226 let connection_options_extracted =
7230 connection::ConnectionOptionExtracted::try_from(set_options_vec)?;
7231
7232 let duplicates: Vec<_> = connection_options_extracted
7233 .seen
7234 .intersection(&drop_options)
7235 .collect();
7236
7237 if !duplicates.is_empty() {
7238 sql_bail!(
7239 "cannot both SET and DROP/RESET options {}",
7240 duplicates
7241 .iter()
7242 .map(|option| option.to_string())
7243 .join(", ")
7244 )
7245 }
7246
7247 for mutually_exclusive_options in MUTUALLY_EXCLUSIVE_SETS {
7248 let set_options_count = mutually_exclusive_options
7249 .iter()
7250 .filter(|o| set_options.contains_key(o))
7251 .count();
7252 let drop_options_count = mutually_exclusive_options
7253 .iter()
7254 .filter(|o| drop_options.contains(o))
7255 .count();
7256
7257 if set_options_count > 0 && drop_options_count > 0 {
7259 sql_bail!(
7260 "cannot both SET and DROP/RESET mutually exclusive {} options {}",
7261 connection_type,
7262 mutually_exclusive_options
7263 .iter()
7264 .map(|option| option.to_string())
7265 .join(", ")
7266 )
7267 }
7268
7269 if set_options_count > 0 || drop_options_count > 0 {
7274 drop_options.extend(mutually_exclusive_options.iter().cloned());
7275 }
7276
7277 }
7280
7281 Ok(Plan::AlterConnection(AlterConnectionPlan {
7282 id: entry.id(),
7283 action: crate::plan::AlterConnectionAction::AlterOptions {
7284 set_options,
7285 drop_options,
7286 validate,
7287 },
7288 }))
7289}
7290
7291pub fn describe_alter_sink(
7292 _: &StatementContext,
7293 _: AlterSinkStatement<Aug>,
7294) -> Result<StatementDesc, PlanError> {
7295 Ok(StatementDesc::new(None))
7296}
7297
7298pub fn plan_alter_sink(
7299 scx: &mut StatementContext,
7300 stmt: AlterSinkStatement<Aug>,
7301) -> Result<Plan, PlanError> {
7302 let AlterSinkStatement {
7303 sink_name,
7304 if_exists,
7305 action,
7306 } = stmt;
7307
7308 let object_type = ObjectType::Sink;
7309 let item = resolve_item_or_type(scx, object_type, sink_name.clone(), if_exists)?;
7310
7311 let Some(item) = item else {
7312 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7313 name: sink_name.to_string(),
7314 object_type,
7315 });
7316
7317 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7318 };
7319 let item = item.at_version(RelationVersionSelector::Latest);
7321
7322 match action {
7323 AlterSinkAction::ChangeRelation(new_from) => {
7324 let create_sql = item.create_sql();
7326 let stmts = mz_sql_parser::parser::parse_statements(create_sql)?;
7327 let [stmt]: [StatementParseResult; 1] = stmts
7328 .try_into()
7329 .map_err(|_| internal_err!("create SQL of sink was not exactly one statement"))?;
7330 let Statement::CreateSink(stmt) = stmt.ast else {
7331 bail_internal!("create SQL of sink is not a CREATE SINK statement");
7332 };
7333
7334 let (mut stmt, _) = crate::names::resolve(scx.catalog, stmt)?;
7336 stmt.from = new_from;
7337
7338 let Plan::CreateSink(mut plan) = plan_sink(scx, stmt)? else {
7340 bail_internal!("plan_sink did not produce a CreateSink plan");
7341 };
7342
7343 plan.sink.version += 1;
7344
7345 Ok(Plan::AlterSink(AlterSinkPlan {
7346 item_id: item.id(),
7347 global_id: item.global_id(),
7348 sink: plan.sink,
7349 with_snapshot: plan.with_snapshot,
7350 in_cluster: plan.in_cluster,
7351 }))
7352 }
7353 AlterSinkAction::SetOptions(_) => bail_unsupported!("ALTER SINK SET options"),
7354 AlterSinkAction::ResetOptions(_) => bail_unsupported!("ALTER SINK RESET option"),
7355 }
7356}
7357
7358pub fn describe_alter_source(
7359 _: &StatementContext,
7360 _: AlterSourceStatement<Aug>,
7361) -> Result<StatementDesc, PlanError> {
7362 Ok(StatementDesc::new(None))
7364}
7365
7366generate_extracted_config!(
7367 AlterSourceAddSubsourceOption,
7368 (TextColumns, Vec::<UnresolvedItemName>, Default(vec![])),
7369 (ExcludeColumns, Vec::<UnresolvedItemName>, Default(vec![])),
7370 (Details, String)
7371);
7372
7373pub fn plan_alter_source(
7374 scx: &mut StatementContext,
7375 stmt: AlterSourceStatement<Aug>,
7376) -> Result<Plan, PlanError> {
7377 let AlterSourceStatement {
7378 source_name,
7379 if_exists,
7380 action,
7381 } = stmt;
7382 let object_type = ObjectType::Source;
7383
7384 if resolve_item_or_type(scx, object_type, source_name.clone(), if_exists)?.is_none() {
7385 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7386 name: source_name.to_string(),
7387 object_type,
7388 });
7389
7390 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7391 }
7392
7393 match action {
7394 AlterSourceAction::SetOptions(options) => {
7395 let mut options = options.into_iter();
7396 let option = options
7397 .next()
7398 .ok_or_else(|| sql_err!("ALTER SOURCE SET requires at least one option"))?;
7399 if option.name == CreateSourceOptionName::RetainHistory {
7400 if options.next().is_some() {
7401 sql_bail!("RETAIN HISTORY must be only option");
7402 }
7403 return alter_retain_history(
7404 scx,
7405 object_type,
7406 if_exists,
7407 UnresolvedObjectName::Item(source_name),
7408 option.value,
7409 );
7410 }
7411 if option.name == CreateSourceOptionName::TimestampInterval {
7412 if options.next().is_some() {
7413 sql_bail!("TIMESTAMP INTERVAL must be only option");
7414 }
7415 return alter_source_timestamp_interval(scx, if_exists, source_name, option.value);
7416 }
7417 sql_bail!(
7420 "Cannot modify the {} of a SOURCE.",
7421 option.name.to_ast_string_simple()
7422 );
7423 }
7424 AlterSourceAction::ResetOptions(reset) => {
7425 let mut options = reset.into_iter();
7426 let option = options
7427 .next()
7428 .ok_or_else(|| sql_err!("ALTER SOURCE RESET requires at least one option"))?;
7429 if option == CreateSourceOptionName::RetainHistory {
7430 if options.next().is_some() {
7431 sql_bail!("RETAIN HISTORY must be only option");
7432 }
7433 return alter_retain_history(
7434 scx,
7435 object_type,
7436 if_exists,
7437 UnresolvedObjectName::Item(source_name),
7438 None,
7439 );
7440 }
7441 if option == CreateSourceOptionName::TimestampInterval {
7442 if options.next().is_some() {
7443 sql_bail!("TIMESTAMP INTERVAL must be only option");
7444 }
7445 return alter_source_timestamp_interval(scx, if_exists, source_name, None);
7446 }
7447 sql_bail!(
7448 "Cannot modify the {} of a SOURCE.",
7449 option.to_ast_string_simple()
7450 );
7451 }
7452 AlterSourceAction::DropSubsources { .. } => {
7453 sql_bail!("ALTER SOURCE...DROP SUBSOURCE no longer supported; use DROP SOURCE")
7454 }
7455 AlterSourceAction::AddSubsources { .. } => {
7456 sql_bail!("ALTER SOURCE...ADD SUBSOURCE must be purified before planning")
7457 }
7458 AlterSourceAction::RefreshReferences => {
7459 sql_bail!("ALTER SOURCE...REFRESH REFERENCES must be purified before planning")
7460 }
7461 };
7462}
7463
7464pub fn describe_alter_system_set(
7465 _: &StatementContext,
7466 _: AlterSystemSetStatement,
7467) -> Result<StatementDesc, PlanError> {
7468 Ok(StatementDesc::new(None))
7469}
7470
7471pub fn plan_alter_system_set(
7472 _: &StatementContext,
7473 AlterSystemSetStatement { name, to }: AlterSystemSetStatement,
7474) -> Result<Plan, PlanError> {
7475 let name = name.to_string();
7476 Ok(Plan::AlterSystemSet(AlterSystemSetPlan {
7477 name,
7478 value: scl::plan_set_variable_to(to)?,
7479 }))
7480}
7481
7482pub fn describe_alter_system_reset(
7483 _: &StatementContext,
7484 _: AlterSystemResetStatement,
7485) -> Result<StatementDesc, PlanError> {
7486 Ok(StatementDesc::new(None))
7487}
7488
7489pub fn plan_alter_system_reset(
7490 _: &StatementContext,
7491 AlterSystemResetStatement { name }: AlterSystemResetStatement,
7492) -> Result<Plan, PlanError> {
7493 let name = name.to_string();
7494 Ok(Plan::AlterSystemReset(AlterSystemResetPlan { name }))
7495}
7496
7497pub fn describe_alter_system_reset_all(
7498 _: &StatementContext,
7499 _: AlterSystemResetAllStatement,
7500) -> Result<StatementDesc, PlanError> {
7501 Ok(StatementDesc::new(None))
7502}
7503
7504pub fn plan_alter_system_reset_all(
7505 _: &StatementContext,
7506 _: AlterSystemResetAllStatement,
7507) -> Result<Plan, PlanError> {
7508 Ok(Plan::AlterSystemResetAll(AlterSystemResetAllPlan {}))
7509}
7510
7511pub fn describe_alter_role(
7512 _: &StatementContext,
7513 _: AlterRoleStatement<Aug>,
7514) -> Result<StatementDesc, PlanError> {
7515 Ok(StatementDesc::new(None))
7516}
7517
7518pub fn plan_alter_role(
7519 scx: &StatementContext,
7520 AlterRoleStatement { name, option }: AlterRoleStatement<Aug>,
7521) -> Result<Plan, PlanError> {
7522 let option = match option {
7523 AlterRoleOption::Attributes(attrs) => {
7524 let attrs = plan_role_attributes(attrs, scx)?;
7525 PlannedAlterRoleOption::Attributes(attrs)
7526 }
7527 AlterRoleOption::Variable(variable) => {
7528 let var = plan_role_variable(scx, variable)?;
7529 PlannedAlterRoleOption::Variable(var)
7530 }
7531 };
7532
7533 Ok(Plan::AlterRole(AlterRolePlan {
7534 id: name.id,
7535 name: name.name,
7536 option,
7537 }))
7538}
7539
7540pub fn describe_alter_table_add_column(
7541 _: &StatementContext,
7542 _: AlterTableAddColumnStatement<Aug>,
7543) -> Result<StatementDesc, PlanError> {
7544 Ok(StatementDesc::new(None))
7545}
7546
7547pub fn plan_alter_table_add_column(
7548 scx: &StatementContext,
7549 stmt: AlterTableAddColumnStatement<Aug>,
7550) -> Result<Plan, PlanError> {
7551 let AlterTableAddColumnStatement {
7552 if_exists,
7553 name,
7554 if_col_not_exist,
7555 column_name,
7556 data_type,
7557 } = stmt;
7558 let object_type = ObjectType::Table;
7559
7560 scx.require_feature_flag(&vars::ENABLE_ALTER_TABLE_ADD_COLUMN)?;
7561
7562 let (relation_id, item_name, desc) =
7563 match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
7564 Some(item) => {
7565 let item_name = scx.catalog.resolve_full_name(item.name());
7567 let item = item.at_version(RelationVersionSelector::Latest);
7568 let desc = item
7569 .relation_desc()
7570 .ok_or_else(|| sql_err!("item does not have a relation description"))?
7571 .into_owned();
7572 (item.id(), item_name, desc)
7573 }
7574 None => {
7575 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7576 name: name.to_ast_string_simple(),
7577 object_type,
7578 });
7579 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7580 }
7581 };
7582
7583 let column_name = ColumnName::from(column_name.as_str());
7584 if desc.get_by_name(&column_name).is_some() {
7585 if if_col_not_exist {
7586 scx.catalog.add_notice(PlanNotice::ColumnAlreadyExists {
7587 column_name: column_name.to_string(),
7588 object_name: item_name.item,
7589 });
7590 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7591 } else {
7592 return Err(PlanError::ColumnAlreadyExists {
7593 column_name,
7594 object_name: item_name.item,
7595 });
7596 }
7597 }
7598
7599 let scalar_type = scalar_type_from_sql(scx, &data_type)?;
7600 let column_type = scalar_type.nullable(true);
7602 let raw_sql_type = mz_sql_parser::parser::parse_data_type(&data_type.to_ast_string_stable())?;
7604
7605 Ok(Plan::AlterTableAddColumn(AlterTablePlan {
7606 relation_id,
7607 column_name,
7608 column_type,
7609 raw_sql_type,
7610 }))
7611}
7612
7613pub fn describe_alter_materialized_view_apply_replacement(
7614 _: &StatementContext,
7615 _: AlterMaterializedViewApplyReplacementStatement,
7616) -> Result<StatementDesc, PlanError> {
7617 Ok(StatementDesc::new(None))
7618}
7619
7620pub fn plan_alter_materialized_view_apply_replacement(
7621 scx: &StatementContext,
7622 stmt: AlterMaterializedViewApplyReplacementStatement,
7623) -> Result<Plan, PlanError> {
7624 let AlterMaterializedViewApplyReplacementStatement {
7625 if_exists,
7626 name,
7627 replacement_name,
7628 } = stmt;
7629
7630 scx.require_feature_flag(&vars::ENABLE_REPLACEMENT_MATERIALIZED_VIEWS)?;
7631
7632 let object_type = ObjectType::MaterializedView;
7633 let Some(mv) = resolve_item_or_type(scx, object_type, name.clone(), if_exists)? else {
7634 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7635 name: name.to_ast_string_simple(),
7636 object_type,
7637 });
7638 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7639 };
7640
7641 let replacement = resolve_item_or_type(scx, object_type, replacement_name, false)?
7642 .ok_or_else(|| sql_err!("replacement materialized view does not exist"))?;
7643
7644 if replacement.replacement_target() != Some(mv.id()) {
7645 return Err(PlanError::InvalidReplacement {
7646 item_type: mv.item_type(),
7647 item_name: scx.catalog.minimal_qualification(mv.name()),
7648 replacement_type: replacement.item_type(),
7649 replacement_name: scx.catalog.minimal_qualification(replacement.name()),
7650 });
7651 }
7652
7653 Ok(Plan::AlterMaterializedViewApplyReplacement(
7654 AlterMaterializedViewApplyReplacementPlan {
7655 id: mv.id(),
7656 replacement_id: replacement.id(),
7657 },
7658 ))
7659}
7660
7661pub fn describe_comment(
7662 _: &StatementContext,
7663 _: CommentStatement<Aug>,
7664) -> Result<StatementDesc, PlanError> {
7665 Ok(StatementDesc::new(None))
7666}
7667
7668pub fn plan_comment(
7669 scx: &mut StatementContext,
7670 stmt: CommentStatement<Aug>,
7671) -> Result<Plan, PlanError> {
7672 const MAX_COMMENT_LENGTH: usize = 1024;
7673
7674 let CommentStatement { object, comment } = stmt;
7675
7676 if let Some(c) = &comment {
7678 if c.len() > 1024 {
7679 return Err(PlanError::CommentTooLong {
7680 length: c.len(),
7681 max_size: MAX_COMMENT_LENGTH,
7682 });
7683 }
7684 }
7685
7686 let (object_id, column_pos) = match &object {
7687 com_ty @ CommentObjectType::Table { name }
7688 | com_ty @ CommentObjectType::View { name }
7689 | com_ty @ CommentObjectType::MaterializedView { name }
7690 | com_ty @ CommentObjectType::Index { name }
7691 | com_ty @ CommentObjectType::Func { name }
7692 | com_ty @ CommentObjectType::Connection { name }
7693 | com_ty @ CommentObjectType::Source { name }
7694 | com_ty @ CommentObjectType::Sink { name }
7695 | com_ty @ CommentObjectType::Secret { name } => {
7696 let item = scx.get_item_by_resolved_name(name)?;
7697 match (com_ty, item.item_type()) {
7698 (CommentObjectType::Table { .. }, CatalogItemType::Table) => {
7699 (CommentObjectId::Table(item.id()), None)
7700 }
7701 (CommentObjectType::View { .. }, CatalogItemType::View) => {
7702 (CommentObjectId::View(item.id()), None)
7703 }
7704 (CommentObjectType::MaterializedView { .. }, CatalogItemType::MaterializedView) => {
7705 (CommentObjectId::MaterializedView(item.id()), None)
7706 }
7707 (CommentObjectType::Index { .. }, CatalogItemType::Index) => {
7708 (CommentObjectId::Index(item.id()), None)
7709 }
7710 (CommentObjectType::Func { .. }, CatalogItemType::Func) => {
7711 (CommentObjectId::Func(item.id()), None)
7712 }
7713 (CommentObjectType::Connection { .. }, CatalogItemType::Connection) => {
7714 (CommentObjectId::Connection(item.id()), None)
7715 }
7716 (CommentObjectType::Source { .. }, CatalogItemType::Source) => {
7717 (CommentObjectId::Source(item.id()), None)
7718 }
7719 (CommentObjectType::Sink { .. }, CatalogItemType::Sink) => {
7720 (CommentObjectId::Sink(item.id()), None)
7721 }
7722 (CommentObjectType::Secret { .. }, CatalogItemType::Secret) => {
7723 (CommentObjectId::Secret(item.id()), None)
7724 }
7725 (com_ty, cat_ty) => {
7726 let expected_type = match com_ty {
7727 CommentObjectType::Table { .. } => ObjectType::Table,
7728 CommentObjectType::View { .. } => ObjectType::View,
7729 CommentObjectType::MaterializedView { .. } => ObjectType::MaterializedView,
7730 CommentObjectType::Index { .. } => ObjectType::Index,
7731 CommentObjectType::Func { .. } => ObjectType::Func,
7732 CommentObjectType::Connection { .. } => ObjectType::Connection,
7733 CommentObjectType::Source { .. } => ObjectType::Source,
7734 CommentObjectType::Sink { .. } => ObjectType::Sink,
7735 CommentObjectType::Secret { .. } => ObjectType::Secret,
7736 _ => sql_bail!("cannot comment on this object type"),
7737 };
7738
7739 return Err(PlanError::InvalidObjectType {
7740 expected_type: SystemObjectType::Object(expected_type),
7741 actual_type: SystemObjectType::Object(cat_ty.into()),
7742 object_name: item.name().item.clone(),
7743 });
7744 }
7745 }
7746 }
7747 CommentObjectType::Type { ty } => match ty {
7748 ResolvedDataType::AnonymousList(_) | ResolvedDataType::AnonymousMap { .. } => {
7749 sql_bail!("cannot comment on anonymous list or map type");
7750 }
7751 ResolvedDataType::Named { id, modifiers, .. } => {
7752 if !modifiers.is_empty() {
7753 sql_bail!("cannot comment on type with modifiers");
7754 }
7755 (CommentObjectId::Type(*id), None)
7756 }
7757 ResolvedDataType::Error => bail_internal!("unresolved data type"),
7758 },
7759 CommentObjectType::Column { name } => {
7760 let (item, pos) = scx.get_column_by_resolved_name(name)?;
7761 match item.item_type() {
7762 CatalogItemType::Table => (CommentObjectId::Table(item.id()), Some(pos + 1)),
7763 CatalogItemType::Source => (CommentObjectId::Source(item.id()), Some(pos + 1)),
7764 CatalogItemType::View => (CommentObjectId::View(item.id()), Some(pos + 1)),
7765 CatalogItemType::MaterializedView => {
7766 (CommentObjectId::MaterializedView(item.id()), Some(pos + 1))
7767 }
7768 CatalogItemType::Type => (CommentObjectId::Type(item.id()), Some(pos + 1)),
7769 r => {
7770 return Err(PlanError::Unsupported {
7771 feature: format!("Specifying comments on a column of {r}"),
7772 discussion_no: None,
7773 });
7774 }
7775 }
7776 }
7777 CommentObjectType::Role { name } => (CommentObjectId::Role(name.id), None),
7778 CommentObjectType::Database { name } => {
7779 (CommentObjectId::Database(*name.database_id()), None)
7780 }
7781 CommentObjectType::Schema { name } => {
7782 if matches!(name.schema_spec(), SchemaSpecifier::Temporary) {
7786 sql_bail!(
7787 "cannot comment on schema {} because it is a temporary schema",
7788 mz_repr::namespaces::MZ_TEMP_SCHEMA
7789 );
7790 }
7791 (
7792 CommentObjectId::Schema((*name.database_spec(), *name.schema_spec())),
7793 None,
7794 )
7795 }
7796 CommentObjectType::Cluster { name } => (CommentObjectId::Cluster(name.id), None),
7797 CommentObjectType::ClusterReplica { name } => {
7798 let replica = scx.catalog.resolve_cluster_replica(name)?;
7799 (
7800 CommentObjectId::ClusterReplica((replica.cluster_id(), replica.replica_id())),
7801 None,
7802 )
7803 }
7804 CommentObjectType::NetworkPolicy { name } => {
7805 (CommentObjectId::NetworkPolicy(name.id), None)
7806 }
7807 };
7808
7809 if let Some(p) = column_pos {
7815 i32::try_from(p).map_err(|_| PlanError::TooManyColumns {
7816 max_num_columns: MAX_NUM_COLUMNS,
7817 req_num_columns: p,
7818 })?;
7819 }
7820
7821 Ok(Plan::Comment(CommentPlan {
7822 object_id,
7823 sub_component: column_pos,
7824 comment,
7825 }))
7826}
7827
7828pub(crate) fn resolve_cluster<'a>(
7829 scx: &'a StatementContext,
7830 name: &'a Ident,
7831 if_exists: bool,
7832) -> Result<Option<&'a dyn CatalogCluster<'a>>, PlanError> {
7833 match scx.resolve_cluster(Some(name)) {
7834 Ok(cluster) => Ok(Some(cluster)),
7835 Err(_) if if_exists => Ok(None),
7836 Err(e) => Err(e),
7837 }
7838}
7839
7840pub(crate) fn resolve_cluster_replica<'a>(
7841 scx: &'a StatementContext,
7842 name: &QualifiedReplica,
7843 if_exists: bool,
7844) -> Result<Option<(&'a dyn CatalogCluster<'a>, ReplicaId)>, PlanError> {
7845 match scx.resolve_cluster(Some(&name.cluster)) {
7846 Ok(cluster) => match cluster.replica_ids().get(name.replica.as_str()) {
7847 Some(replica_id) => Ok(Some((cluster, *replica_id))),
7848 None if if_exists => Ok(None),
7849 None => Err(sql_err!(
7850 "CLUSTER {} has no CLUSTER REPLICA named {}",
7851 cluster.name(),
7852 name.replica.as_str().quoted(),
7853 )),
7854 },
7855 Err(_) if if_exists => Ok(None),
7856 Err(e) => Err(e),
7857 }
7858}
7859
7860pub(crate) fn resolve_database<'a>(
7861 scx: &'a StatementContext,
7862 name: &'a UnresolvedDatabaseName,
7863 if_exists: bool,
7864) -> Result<Option<&'a dyn CatalogDatabase>, PlanError> {
7865 match scx.resolve_database(name) {
7866 Ok(database) => Ok(Some(database)),
7867 Err(_) if if_exists => Ok(None),
7868 Err(e) => Err(e),
7869 }
7870}
7871
7872pub(crate) fn resolve_schema<'a>(
7873 scx: &'a StatementContext,
7874 name: UnresolvedSchemaName,
7875 if_exists: bool,
7876) -> Result<Option<(ResolvedDatabaseSpecifier, SchemaSpecifier)>, PlanError> {
7877 match scx.resolve_schema(name) {
7878 Ok(schema) => Ok(Some((schema.database().clone(), schema.id().clone()))),
7879 Err(_) if if_exists => Ok(None),
7880 Err(e) => Err(e),
7881 }
7882}
7883
7884pub(crate) fn resolve_network_policy<'a>(
7885 scx: &'a StatementContext,
7886 name: Ident,
7887 if_exists: bool,
7888) -> Result<Option<ResolvedNetworkPolicyName>, PlanError> {
7889 match scx.catalog.resolve_network_policy(&name.to_string()) {
7890 Ok(policy) => Ok(Some(ResolvedNetworkPolicyName {
7891 id: policy.id(),
7892 name: policy.name().to_string(),
7893 })),
7894 Err(_) if if_exists => Ok(None),
7895 Err(e) => Err(e.into()),
7896 }
7897}
7898
7899pub(crate) fn resolve_item_or_type<'a>(
7900 scx: &'a StatementContext,
7901 object_type: ObjectType,
7902 name: UnresolvedItemName,
7903 if_exists: bool,
7904) -> Result<Option<&'a dyn CatalogItem>, PlanError> {
7905 let name = normalize::unresolved_item_name(name)?;
7906 let catalog_item = match object_type {
7907 ObjectType::Type => scx.catalog.resolve_type(&name),
7908 ObjectType::Table
7909 | ObjectType::View
7910 | ObjectType::MaterializedView
7911 | ObjectType::Source
7912 | ObjectType::Sink
7913 | ObjectType::Index
7914 | ObjectType::Role
7915 | ObjectType::Cluster
7916 | ObjectType::ClusterReplica
7917 | ObjectType::Secret
7918 | ObjectType::Connection
7919 | ObjectType::Database
7920 | ObjectType::Schema
7921 | ObjectType::Func
7922 | ObjectType::NetworkPolicy => scx.catalog.resolve_item(&name),
7923 };
7924
7925 match catalog_item {
7926 Ok(item) => {
7927 let is_type = ObjectType::from(item.item_type());
7928 if object_type == is_type {
7929 Ok(Some(item))
7930 } else {
7931 Err(PlanError::MismatchedObjectType {
7932 name: scx.catalog.minimal_qualification(item.name()),
7933 is_type,
7934 expected_type: object_type,
7935 })
7936 }
7937 }
7938 Err(_) if if_exists => Ok(None),
7939 Err(e) => Err(e.into()),
7940 }
7941}
7942
7943fn ensure_cluster_is_not_managed(
7945 scx: &StatementContext,
7946 cluster_id: ClusterId,
7947) -> Result<(), PlanError> {
7948 let cluster = scx.catalog.get_cluster(cluster_id);
7949 if cluster.is_managed() {
7950 Err(PlanError::ManagedCluster {
7951 cluster_name: cluster.name().to_string(),
7952 })
7953 } else {
7954 Ok(())
7955 }
7956}