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