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