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 (ExcludeConstraints, Vec::<String>, Default(vec![])),
1752 (ExcludeAllConstraints, bool, Default(false)),
1753 (PartitionBy, Vec<Ident>),
1754 (RetainHistory, OptionalDuration),
1755 (Details, String)
1756);
1757
1758pub fn plan_create_table_from_source(
1759 scx: &StatementContext,
1760 stmt: CreateTableFromSourceStatement<Aug>,
1761) -> Result<Plan, PlanError> {
1762 if !scx.catalog.system_vars().enable_create_table_from_source() {
1763 sql_bail!("CREATE TABLE ... FROM SOURCE is not supported");
1764 }
1765
1766 let CreateTableFromSourceStatement {
1767 name,
1768 columns,
1769 constraints,
1770 if_not_exists,
1771 source,
1772 external_reference,
1773 envelope,
1774 format,
1775 include_metadata,
1776 with_options,
1777 } = &stmt;
1778
1779 let envelope = envelope.clone().unwrap_or(ast::SourceEnvelope::None);
1780
1781 let TableFromSourceOptionExtracted {
1782 text_columns,
1783 exclude_columns,
1784 exclude_constraints: _,
1785 exclude_all_constraints: _,
1786 retain_history,
1787 partition_by,
1788 details,
1789 seen: _,
1790 } = with_options.clone().try_into()?;
1791
1792 let source_item = scx.get_item_by_resolved_name(source)?;
1793 let ingestion_id = source_item.id();
1794
1795 let details = details
1798 .as_ref()
1799 .ok_or_else(|| internal_err!("source-export missing details"))?;
1800 let details = hex::decode(details).map_err(|e| sql_err!("{}", e))?;
1801 let details =
1802 ProtoSourceExportStatementDetails::decode(&*details).map_err(|e| sql_err!("{}", e))?;
1803 let details =
1804 SourceExportStatementDetails::from_proto(details).map_err(|e| sql_err!("{}", e))?;
1805
1806 if !matches!(details, SourceExportStatementDetails::Kafka { .. })
1807 && include_metadata
1808 .iter()
1809 .any(|sic| matches!(sic, SourceIncludeMetadata::Headers { .. }))
1810 {
1811 sql_bail!("INCLUDE HEADERS with non-Kafka source table not supported");
1813 }
1814 if !matches!(
1815 details,
1816 SourceExportStatementDetails::Kafka { .. }
1817 | SourceExportStatementDetails::LoadGenerator { .. }
1818 ) && !include_metadata.is_empty()
1819 {
1820 bail_unsupported!("INCLUDE metadata with non-Kafka source table");
1821 }
1822
1823 let details = match details {
1824 SourceExportStatementDetails::Postgres {
1825 table,
1826 cast_oid_full_range,
1827 } => SourceExportDetails::Postgres(PostgresSourceExportDetails {
1828 column_casts: crate::pure::postgres::generate_column_casts(
1829 scx,
1830 &table,
1831 &text_columns,
1832 cast_oid_full_range,
1833 )?,
1834 table,
1835 }),
1836 SourceExportStatementDetails::MySql {
1837 table,
1838 initial_gtid_set,
1839 binlog_full_metadata,
1840 } => SourceExportDetails::MySql(MySqlSourceExportDetails {
1841 table,
1842 initial_gtid_set,
1843 text_columns: text_columns.into_iter().map(|c| c.into_string()).collect(),
1844 exclude_columns: exclude_columns
1845 .into_iter()
1846 .map(|c| c.into_string())
1847 .collect(),
1848 binlog_full_metadata,
1849 }),
1850 SourceExportStatementDetails::SqlServer {
1851 table,
1852 capture_instance,
1853 initial_lsn,
1854 } => SourceExportDetails::SqlServer(SqlServerSourceExportDetails {
1855 table,
1856 capture_instance,
1857 initial_lsn,
1858 text_columns: text_columns.into_iter().map(|c| c.into_string()).collect(),
1859 exclude_columns: exclude_columns
1860 .into_iter()
1861 .map(|c| c.into_string())
1862 .collect(),
1863 }),
1864 SourceExportStatementDetails::LoadGenerator { output } => {
1865 SourceExportDetails::LoadGenerator(LoadGeneratorSourceExportDetails { output })
1866 }
1867 SourceExportStatementDetails::Kafka {} => {
1868 if !include_metadata.is_empty()
1869 && !matches!(
1870 envelope,
1871 ast::SourceEnvelope::Upsert { .. }
1872 | ast::SourceEnvelope::None
1873 | ast::SourceEnvelope::Debezium
1874 )
1875 {
1876 sql_bail!("INCLUDE <metadata> requires ENVELOPE (NONE|UPSERT|DEBEZIUM)");
1878 }
1879
1880 let metadata_columns = include_metadata
1881 .into_iter()
1882 .flat_map(|item| match item {
1883 SourceIncludeMetadata::Timestamp { alias } => {
1884 let name = match alias {
1885 Some(name) => name.to_string(),
1886 None => "timestamp".to_owned(),
1887 };
1888 Some((name, KafkaMetadataKind::Timestamp))
1889 }
1890 SourceIncludeMetadata::Partition { alias } => {
1891 let name = match alias {
1892 Some(name) => name.to_string(),
1893 None => "partition".to_owned(),
1894 };
1895 Some((name, KafkaMetadataKind::Partition))
1896 }
1897 SourceIncludeMetadata::Offset { alias } => {
1898 let name = match alias {
1899 Some(name) => name.to_string(),
1900 None => "offset".to_owned(),
1901 };
1902 Some((name, KafkaMetadataKind::Offset))
1903 }
1904 SourceIncludeMetadata::Headers { alias } => {
1905 let name = match alias {
1906 Some(name) => name.to_string(),
1907 None => "headers".to_owned(),
1908 };
1909 Some((name, KafkaMetadataKind::Headers))
1910 }
1911 SourceIncludeMetadata::Header {
1912 alias,
1913 key,
1914 use_bytes,
1915 } => Some((
1916 alias.to_string(),
1917 KafkaMetadataKind::Header {
1918 key: key.clone(),
1919 use_bytes: *use_bytes,
1920 },
1921 )),
1922 SourceIncludeMetadata::Key { .. } => {
1923 None
1925 }
1926 })
1927 .collect();
1928
1929 SourceExportDetails::Kafka(KafkaSourceExportDetails { metadata_columns })
1930 }
1931 };
1932
1933 let source_connection = &source_item
1934 .source_desc()?
1935 .ok_or_else(|| sql_err!("item is not a source"))?
1936 .connection;
1937
1938 let (key_desc, value_desc) =
1943 if matches!(columns, TableFromSourceColumns::Defined(_)) || !constraints.is_empty() {
1944 let columns = match columns {
1945 TableFromSourceColumns::Defined(columns) => columns,
1946 _ => bail_internal!("expected column definitions to be present"),
1947 };
1948 let desc = plan_source_export_desc(scx, name, columns, constraints)?;
1949 (None, desc)
1950 } else {
1951 let key_desc = source_connection.default_key_desc();
1952 let value_desc = source_connection.default_value_desc();
1953 (Some(key_desc), value_desc)
1954 };
1955
1956 let metadata_columns_desc = match &details {
1957 SourceExportDetails::Kafka(KafkaSourceExportDetails {
1958 metadata_columns, ..
1959 }) => kafka_metadata_columns_desc(metadata_columns),
1960 _ => vec![],
1961 };
1962
1963 let (mut desc, envelope, encoding) = apply_source_envelope_encoding(
1964 scx,
1965 &envelope,
1966 format,
1967 key_desc,
1968 value_desc,
1969 include_metadata,
1970 metadata_columns_desc,
1971 source_connection,
1972 )?;
1973 if let TableFromSourceColumns::Named(col_names) = columns {
1974 plan_utils::maybe_rename_columns(format!("source table {}", name), &mut desc, col_names)?;
1975 }
1976
1977 let names: Vec<_> = desc.iter_names().cloned().collect();
1978 if let Some(dup) = names.iter().duplicates().next() {
1979 sql_bail!("column {} specified more than once", dup.quoted());
1980 }
1981
1982 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name.clone())?)?;
1983
1984 let timeline = match envelope {
1987 SourceEnvelope::CdcV2 => {
1988 Timeline::External(scx.catalog.resolve_full_name(&name).to_string())
1989 }
1990 _ => Timeline::EpochMilliseconds,
1991 };
1992
1993 if let Some(partition_by) = partition_by {
1994 scx.require_feature_flag(&ENABLE_COLLECTION_PARTITION_BY)?;
1995 check_partition_by(&desc, partition_by)?;
1996 }
1997
1998 let data_source = DataSourceDesc::IngestionExport {
1999 ingestion_id,
2000 external_reference: external_reference
2002 .as_ref()
2003 .ok_or_else(|| sql_err!("EXTERNAL REFERENCE is required"))?
2004 .clone(),
2005 details,
2006 data_config: SourceExportDataConfig { envelope, encoding },
2007 };
2008
2009 let if_not_exists = *if_not_exists;
2010
2011 let create_sql = normalize::create_statement(scx, Statement::CreateTableFromSource(stmt))?;
2012
2013 let compaction_window = plan_retain_history_option(scx, retain_history)?;
2014 let table = Table {
2015 create_sql,
2016 desc: VersionedRelationDesc::new(desc),
2017 temporary: false,
2018 compaction_window,
2019 data_source: TableDataSource::DataSource {
2020 desc: data_source,
2021 timeline,
2022 },
2023 };
2024
2025 Ok(Plan::CreateTable(CreateTablePlan {
2026 name,
2027 table,
2028 if_not_exists,
2029 }))
2030}
2031
2032generate_extracted_config!(
2033 LoadGeneratorOption,
2034 (TickInterval, Duration),
2035 (AsOf, u64, Default(0_u64)),
2036 (UpTo, u64, Default(u64::MAX)),
2037 (ScaleFactor, f64),
2038 (MaxCardinality, u64),
2039 (Keys, u64),
2040 (SnapshotRounds, u64),
2041 (TransactionalSnapshot, bool),
2042 (ValueSize, u64),
2043 (Seed, u64),
2044 (Partitions, u64),
2045 (BatchSize, u64)
2046);
2047
2048impl LoadGeneratorOptionExtracted {
2049 pub(super) fn ensure_only_valid_options(
2050 &self,
2051 loadgen: &ast::LoadGenerator,
2052 ) -> Result<(), PlanError> {
2053 use mz_sql_parser::ast::LoadGeneratorOptionName::*;
2054
2055 let mut options = self.seen.clone();
2056
2057 let permitted_options: &[_] = match loadgen {
2058 ast::LoadGenerator::Auction => &[TickInterval, AsOf, UpTo],
2059 ast::LoadGenerator::Clock => &[TickInterval, AsOf, UpTo],
2060 ast::LoadGenerator::Counter => &[TickInterval, AsOf, UpTo, MaxCardinality],
2061 ast::LoadGenerator::Marketing => &[TickInterval, AsOf, UpTo],
2062 ast::LoadGenerator::Datums => &[TickInterval, AsOf, UpTo],
2063 ast::LoadGenerator::Tpch => &[TickInterval, AsOf, UpTo, ScaleFactor],
2064 ast::LoadGenerator::KeyValue => &[
2065 TickInterval,
2066 Keys,
2067 SnapshotRounds,
2068 TransactionalSnapshot,
2069 ValueSize,
2070 Seed,
2071 Partitions,
2072 BatchSize,
2073 ],
2074 };
2075
2076 for o in permitted_options {
2077 options.remove(o);
2078 }
2079
2080 if !options.is_empty() {
2081 sql_bail!(
2082 "{} load generators do not support {} values",
2083 loadgen,
2084 options.iter().join(", ")
2085 )
2086 }
2087
2088 Ok(())
2089 }
2090}
2091
2092pub(crate) fn load_generator_ast_to_generator(
2093 scx: &StatementContext,
2094 loadgen: &ast::LoadGenerator,
2095 options: &[LoadGeneratorOption<Aug>],
2096 include_metadata: &[SourceIncludeMetadata],
2097) -> Result<LoadGenerator, PlanError> {
2098 let extracted: LoadGeneratorOptionExtracted = options.to_vec().try_into()?;
2099 extracted.ensure_only_valid_options(loadgen)?;
2100
2101 if loadgen != &ast::LoadGenerator::KeyValue && !include_metadata.is_empty() {
2102 sql_bail!("INCLUDE metadata only supported with `KEY VALUE` load generators");
2103 }
2104
2105 let load_generator = match loadgen {
2106 ast::LoadGenerator::Auction => LoadGenerator::Auction,
2107 ast::LoadGenerator::Clock => {
2108 scx.require_feature_flag(&vars::ENABLE_LOAD_GENERATOR_CLOCK)?;
2109 LoadGenerator::Clock
2110 }
2111 ast::LoadGenerator::Counter => {
2112 scx.require_feature_flag(&vars::ENABLE_LOAD_GENERATOR_COUNTER)?;
2113 let LoadGeneratorOptionExtracted {
2114 max_cardinality, ..
2115 } = extracted;
2116 LoadGenerator::Counter { max_cardinality }
2117 }
2118 ast::LoadGenerator::Marketing => LoadGenerator::Marketing,
2119 ast::LoadGenerator::Datums => {
2120 scx.require_feature_flag(&vars::ENABLE_LOAD_GENERATOR_DATUMS)?;
2121 LoadGenerator::Datums
2122 }
2123 ast::LoadGenerator::Tpch => {
2124 let LoadGeneratorOptionExtracted { scale_factor, .. } = extracted;
2125
2126 let sf: f64 = scale_factor.unwrap_or(0.01);
2128 if !sf.is_finite() || sf < 0.0 {
2129 sql_bail!("unsupported scale factor {sf}");
2130 }
2131
2132 let f_to_i = |multiplier: f64| -> Result<i64, PlanError> {
2133 let total = (sf * multiplier).floor();
2134 let mut i = i64::try_cast_from(total)
2135 .ok_or_else(|| sql_err!("unsupported scale factor {sf}"))?;
2136 if i < 1 {
2137 i = 1;
2138 }
2139 Ok(i)
2140 };
2141
2142 let count_supplier = f_to_i(10_000f64)?;
2145 let count_part = f_to_i(200_000f64)?;
2146 let count_customer = f_to_i(150_000f64)?;
2147 let count_orders = f_to_i(150_000f64 * 10f64)?;
2148 let count_clerk = f_to_i(1_000f64)?;
2149
2150 LoadGenerator::Tpch {
2151 count_supplier,
2152 count_part,
2153 count_customer,
2154 count_orders,
2155 count_clerk,
2156 }
2157 }
2158 mz_sql_parser::ast::LoadGenerator::KeyValue => {
2159 scx.require_feature_flag(&vars::ENABLE_LOAD_GENERATOR_KEY_VALUE)?;
2160 let LoadGeneratorOptionExtracted {
2161 keys,
2162 snapshot_rounds,
2163 transactional_snapshot,
2164 value_size,
2165 tick_interval,
2166 seed,
2167 partitions,
2168 batch_size,
2169 ..
2170 } = extracted;
2171
2172 let mut include_offset = None;
2173 for im in include_metadata {
2174 match im {
2175 SourceIncludeMetadata::Offset { alias } => {
2176 include_offset = match alias {
2177 Some(alias) => Some(alias.to_string()),
2178 None => Some(LOAD_GENERATOR_KEY_VALUE_OFFSET_DEFAULT.to_string()),
2179 }
2180 }
2181 SourceIncludeMetadata::Key { .. } => continue,
2182
2183 _ => {
2184 sql_bail!("only `INCLUDE OFFSET` and `INCLUDE KEY` is supported");
2185 }
2186 };
2187 }
2188
2189 let lgkv = KeyValueLoadGenerator {
2190 keys: keys.ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires KEYS"))?,
2191 snapshot_rounds: snapshot_rounds
2192 .ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires SNAPSHOT ROUNDS"))?,
2193 transactional_snapshot: transactional_snapshot.unwrap_or(true),
2195 value_size: value_size
2196 .ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires VALUE SIZE"))?,
2197 partitions: partitions
2198 .ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires PARTITIONS"))?,
2199 tick_interval,
2200 batch_size: batch_size
2201 .ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires BATCH SIZE"))?,
2202 seed: seed.ok_or_else(|| sql_err!("LOAD GENERATOR KEY VALUE requires SEED"))?,
2203 include_offset,
2204 };
2205
2206 if lgkv.keys == 0
2207 || lgkv.partitions == 0
2208 || lgkv.value_size == 0
2209 || lgkv.batch_size == 0
2210 {
2211 sql_bail!("LOAD GENERATOR KEY VALUE options must be non-zero")
2212 }
2213
2214 if lgkv.keys % lgkv.partitions != 0 {
2215 sql_bail!("KEYS must be a multiple of PARTITIONS")
2216 }
2217
2218 if lgkv.batch_size > lgkv.keys {
2219 sql_bail!("KEYS must be larger than BATCH SIZE")
2220 }
2221
2222 if (lgkv.keys / lgkv.partitions) % lgkv.batch_size != 0 {
2225 sql_bail!("PARTITIONS * BATCH SIZE must be a divisor of KEYS")
2226 }
2227
2228 if lgkv.snapshot_rounds == 0 {
2229 sql_bail!("SNAPSHOT ROUNDS must be larger than 0")
2230 }
2231
2232 LoadGenerator::KeyValue(lgkv)
2233 }
2234 };
2235
2236 Ok(load_generator)
2237}
2238
2239fn typecheck_debezium(value_desc: &RelationDesc) -> Result<(Option<usize>, usize), PlanError> {
2240 let before = value_desc.get_by_name(&"before".into());
2241 let (after_idx, after_ty) = value_desc
2242 .get_by_name(&"after".into())
2243 .ok_or_else(|| sql_err!("'after' column missing from debezium input"))?;
2244 let before_idx = if let Some((before_idx, before_ty)) = before {
2245 if !matches!(before_ty.scalar_type, SqlScalarType::Record { .. }) {
2246 sql_bail!("'before' column must be of type record");
2247 }
2248 if before_ty != after_ty {
2249 sql_bail!("'before' type differs from 'after' column");
2250 }
2251 Some(before_idx)
2252 } else {
2253 None
2254 };
2255 Ok((before_idx, after_idx))
2256}
2257
2258fn get_encoding(
2259 scx: &StatementContext,
2260 format: &FormatSpecifier<Aug>,
2261 envelope: &ast::SourceEnvelope,
2262) -> Result<SourceDataEncoding<ReferencedConnection>, PlanError> {
2263 let encoding = match format {
2264 FormatSpecifier::Bare(format) => get_encoding_inner(scx, format)?,
2265 FormatSpecifier::KeyValue { key, value } => {
2266 let key = {
2267 let encoding = get_encoding_inner(scx, key)?;
2268 Some(encoding.key.unwrap_or(encoding.value))
2269 };
2270 let value = get_encoding_inner(scx, value)?.value;
2271 SourceDataEncoding { key, value }
2272 }
2273 };
2274
2275 let requires_keyvalue = matches!(
2276 envelope,
2277 ast::SourceEnvelope::Debezium | ast::SourceEnvelope::Upsert { .. }
2278 );
2279 let is_keyvalue = encoding.key.is_some();
2280 if requires_keyvalue && !is_keyvalue {
2281 sql_bail!("ENVELOPE [DEBEZIUM] UPSERT requires that KEY FORMAT be specified");
2282 };
2283
2284 Ok(encoding)
2285}
2286
2287fn source_sink_cluster_config<'a, 'ctx>(
2293 scx: &'a StatementContext<'ctx>,
2294 in_cluster: &mut Option<ResolvedClusterName>,
2295) -> Result<&'a dyn CatalogCluster<'ctx>, PlanError> {
2296 let cluster = match in_cluster {
2297 None => {
2298 let cluster = scx.catalog.resolve_cluster(None)?;
2299 *in_cluster = Some(ResolvedClusterName {
2300 id: cluster.id(),
2301 print_name: None,
2302 });
2303 cluster
2304 }
2305 Some(in_cluster) => scx.catalog.get_cluster(in_cluster.id),
2306 };
2307
2308 Ok(cluster)
2309}
2310
2311generate_extracted_config!(AvroSchemaOption, (ConfluentWireFormat, bool, Default(true)));
2312
2313generate_extracted_config!(
2317 GlueAvroOption,
2318 (SchemaName, String),
2319 (KeySchemaName, String),
2320 (ValueSchemaName, String),
2321 (KeyCompatibilityLevel, String),
2322 (ValueCompatibilityLevel, String)
2323);
2324
2325#[derive(Debug)]
2326pub struct Schema {
2327 pub key_schema: Option<String>,
2328 pub value_schema: String,
2329 pub key_reference_schemas: Vec<String>,
2331 pub value_reference_schemas: Vec<String>,
2333 pub wire_format: WireFormat<ReferencedConnection>,
2337}
2338
2339fn get_encoding_inner(
2340 scx: &StatementContext,
2341 format: &Format<Aug>,
2342) -> Result<SourceDataEncoding<ReferencedConnection>, PlanError> {
2343 let value = match format {
2344 Format::Bytes => DataEncoding::Bytes,
2345 Format::Avro(schema) => {
2346 let Schema {
2347 key_schema,
2348 value_schema,
2349 key_reference_schemas,
2350 value_reference_schemas,
2351 wire_format,
2352 } = match schema {
2353 AvroSchema::InlineSchema {
2356 schema: ast::Schema { schema },
2357 with_options,
2358 } => {
2359 let AvroSchemaOptionExtracted {
2360 confluent_wire_format,
2361 ..
2362 } = with_options.clone().try_into()?;
2363 let wire_format = if confluent_wire_format {
2364 WireFormat::Confluent { registry: None }
2365 } else {
2366 WireFormat::None
2367 };
2368 Schema {
2369 key_schema: None,
2370 value_schema: schema.clone(),
2371 key_reference_schemas: vec![],
2372 value_reference_schemas: vec![],
2373 wire_format,
2374 }
2375 }
2376 AvroSchema::Csr {
2377 csr_connection:
2378 CsrConnectionAvro {
2379 connection,
2380 seed,
2381 key_strategy: _,
2382 value_strategy: _,
2383 },
2384 } => {
2385 let item = scx.get_item_by_resolved_name(&connection.connection)?;
2386 let csr_connection = match item.connection()? {
2387 Connection::Csr(_) => item.id(),
2388 _ => {
2389 sql_bail!(
2390 "{} is not a Confluent Schema Registry connection",
2391 scx.catalog
2392 .resolve_full_name(item.name())
2393 .to_string()
2394 .quoted()
2395 )
2396 }
2397 };
2398
2399 if let Some(seed) = seed {
2400 Schema {
2401 key_schema: seed.key_schema.clone(),
2402 value_schema: seed.value_schema.clone(),
2403 key_reference_schemas: seed.key_reference_schemas.clone(),
2404 value_reference_schemas: seed.value_reference_schemas.clone(),
2405 wire_format: WireFormat::Confluent {
2406 registry: Some(csr_connection),
2407 },
2408 }
2409 } else {
2410 sql_bail!("Avro CSR seed resolution has not been performed")
2411 }
2412 }
2413 AvroSchema::Glue {
2414 connection,
2415 with_options: _,
2416 seed,
2417 } => {
2418 let item = scx.get_item_by_resolved_name(connection)?;
2419 let glue_connection = match item.connection()? {
2420 Connection::GlueSchemaRegistry(_) => item.id(),
2421 _ => {
2422 sql_bail!(
2423 "{} is not an AWS Glue Schema Registry connection",
2424 scx.catalog
2425 .resolve_full_name(item.name())
2426 .to_string()
2427 .quoted()
2428 )
2429 }
2430 };
2431
2432 let Some(seed) = seed else {
2436 sql_bail!("Avro Glue seed resolution has not been performed");
2437 };
2438
2439 Schema {
2440 key_schema: None,
2441 value_schema: seed.value_schema.clone(),
2442 key_reference_schemas: vec![],
2443 value_reference_schemas: vec![],
2444 wire_format: WireFormat::Glue {
2445 registry: Some(glue_connection),
2446 },
2447 }
2448 }
2449 };
2450
2451 if let Some(key_schema) = key_schema {
2452 return Ok(SourceDataEncoding {
2453 key: Some(DataEncoding::Avro(AvroEncoding {
2454 schema: key_schema,
2455 reference_schemas: key_reference_schemas,
2456 wire_format: wire_format.clone(),
2457 })),
2458 value: DataEncoding::Avro(AvroEncoding {
2459 schema: value_schema,
2460 reference_schemas: value_reference_schemas,
2461 wire_format,
2462 }),
2463 });
2464 } else {
2465 DataEncoding::Avro(AvroEncoding {
2466 schema: value_schema,
2467 reference_schemas: value_reference_schemas,
2468 wire_format,
2469 })
2470 }
2471 }
2472 Format::Protobuf(schema) => match schema {
2473 ProtobufSchema::Csr {
2474 csr_connection:
2475 CsrConnectionProtobuf {
2476 connection:
2477 CsrConnection {
2478 connection,
2479 options,
2480 },
2481 seed,
2482 },
2483 } => {
2484 if let Some(CsrSeedProtobuf { key, value }) = seed {
2485 let item = scx.get_item_by_resolved_name(connection)?;
2486 let _ = match item.connection()? {
2487 Connection::Csr(connection) => connection,
2488 _ => {
2489 sql_bail!(
2490 "{} is not a schema registry connection",
2491 scx.catalog
2492 .resolve_full_name(item.name())
2493 .to_string()
2494 .quoted()
2495 )
2496 }
2497 };
2498
2499 if !options.is_empty() {
2500 sql_bail!("Protobuf CSR connections do not support any options");
2501 }
2502
2503 let value = DataEncoding::Protobuf(ProtobufEncoding {
2504 descriptors: strconv::parse_bytes(&value.schema)?,
2505 message_name: value.message_name.clone(),
2506 confluent_wire_format: true,
2507 });
2508 if let Some(key) = key {
2509 return Ok(SourceDataEncoding {
2510 key: Some(DataEncoding::Protobuf(ProtobufEncoding {
2511 descriptors: strconv::parse_bytes(&key.schema)?,
2512 message_name: key.message_name.clone(),
2513 confluent_wire_format: true,
2514 })),
2515 value,
2516 });
2517 }
2518 value
2519 } else {
2520 sql_bail!("Protobuf CSR seed resolution has not been performed")
2521 }
2522 }
2523 ProtobufSchema::InlineSchema {
2524 message_name,
2525 schema: ast::Schema { schema },
2526 } => {
2527 let descriptors = strconv::parse_bytes(schema)?;
2528
2529 DataEncoding::Protobuf(ProtobufEncoding {
2530 descriptors,
2531 message_name: message_name.to_owned(),
2532 confluent_wire_format: false,
2533 })
2534 }
2535 },
2536 Format::Regex(regex) => DataEncoding::Regex(RegexEncoding {
2537 regex: mz_repr::adt::regex::Regex::new(regex, false)
2538 .map_err(|e| sql_err!("parsing regex: {e}"))?,
2539 }),
2540 Format::Csv { columns, delimiter } => {
2541 let columns = match columns {
2542 CsvColumns::Header { names } => {
2543 if names.is_empty() {
2544 sql_bail!("[internal error] column spec should get names in purify")
2545 }
2546 ColumnSpec::Header {
2547 names: names.iter().cloned().map(|n| n.into_string()).collect(),
2548 }
2549 }
2550 CsvColumns::Count(n) => ColumnSpec::Count(usize::cast_from(*n)),
2551 };
2552 DataEncoding::Csv(CsvEncoding {
2553 columns,
2554 delimiter: u8::try_from(*delimiter)
2555 .map_err(|_| sql_err!("CSV delimiter must be an ASCII character"))?,
2556 })
2557 }
2558 Format::Json { array: false } => DataEncoding::Json,
2559 Format::Json { array: true } => bail_unsupported!("JSON ARRAY format in sources"),
2560 Format::Text => DataEncoding::Text,
2561 };
2562 Ok(SourceDataEncoding { key: None, value })
2563}
2564
2565fn get_key_envelope(
2567 included_items: &[SourceIncludeMetadata],
2568 encoding: Option<&SourceDataEncoding<ReferencedConnection>>,
2569 key_envelope_no_encoding: bool,
2570) -> Result<KeyEnvelope, PlanError> {
2571 let key_definition = included_items
2572 .iter()
2573 .find(|i| matches!(i, SourceIncludeMetadata::Key { .. }));
2574 if let Some(SourceIncludeMetadata::Key { alias }) = key_definition {
2575 match (alias, encoding.and_then(|e| e.key.as_ref())) {
2576 (Some(name), Some(_)) => Ok(KeyEnvelope::Named(name.as_str().to_string())),
2577 (None, Some(key)) => get_unnamed_key_envelope(Some(key)),
2578 (Some(name), _) if key_envelope_no_encoding => {
2579 Ok(KeyEnvelope::Named(name.as_str().to_string()))
2580 }
2581 (None, _) if key_envelope_no_encoding => get_unnamed_key_envelope(None),
2582 (_, None) => {
2583 sql_bail!(
2587 "INCLUDE KEY requires specifying KEY FORMAT .. VALUE FORMAT, \
2588 got bare FORMAT"
2589 );
2590 }
2591 }
2592 } else {
2593 Ok(KeyEnvelope::None)
2594 }
2595}
2596
2597fn get_unnamed_key_envelope(
2600 key: Option<&DataEncoding<ReferencedConnection>>,
2601) -> Result<KeyEnvelope, PlanError> {
2602 let is_composite = match key {
2606 Some(DataEncoding::Bytes | DataEncoding::Json | DataEncoding::Text) => false,
2607 Some(
2608 DataEncoding::Avro(_)
2609 | DataEncoding::Csv(_)
2610 | DataEncoding::Protobuf(_)
2611 | DataEncoding::Regex { .. },
2612 ) => true,
2613 None => false,
2614 };
2615
2616 if is_composite {
2617 Ok(KeyEnvelope::Flattened)
2618 } else {
2619 Ok(KeyEnvelope::Named("key".to_string()))
2620 }
2621}
2622
2623pub fn describe_create_view(
2624 _: &StatementContext,
2625 _: CreateViewStatement<Aug>,
2626) -> Result<StatementDesc, PlanError> {
2627 Ok(StatementDesc::new(None))
2628}
2629
2630pub fn plan_view(
2631 scx: &StatementContext,
2632 def: &mut ViewDefinition<Aug>,
2633 temporary: bool,
2634) -> Result<(QualifiedItemName, View), PlanError> {
2635 let create_sql = normalize::create_statement(
2636 scx,
2637 Statement::CreateView(CreateViewStatement {
2638 if_exists: IfExistsBehavior::Error,
2639 temporary,
2640 definition: def.clone(),
2641 }),
2642 )?;
2643
2644 let ViewDefinition {
2645 name,
2646 columns,
2647 query,
2648 } = def;
2649
2650 let query::PlannedRootQuery {
2651 expr,
2652 mut desc,
2653 finishing,
2654 scope: _,
2655 } = query::plan_root_query(scx, query.clone(), QueryLifetime::View)?;
2656 assert!(HirRelationExpr::is_trivial_row_set_finishing_hir(
2662 &finishing,
2663 expr.arity()
2664 ));
2665 if expr.contains_parameters()? {
2666 return Err(PlanError::ParameterNotAllowed("views".to_string()));
2667 }
2668
2669 let dependencies = expr
2670 .depends_on()
2671 .into_iter()
2672 .map(|gid| scx.catalog.resolve_item_id(&gid))
2673 .collect();
2674
2675 let name = if temporary {
2676 scx.allocate_temporary_qualified_name(normalize::unresolved_item_name(name.to_owned())?)?
2677 } else {
2678 scx.allocate_qualified_name(normalize::unresolved_item_name(name.to_owned())?)?
2679 };
2680
2681 plan_utils::maybe_rename_columns_exact(
2682 scx.catalog,
2683 format!("view {}", scx.catalog.resolve_full_name(&name)),
2684 &mut desc,
2685 columns,
2686 )?;
2687 let names: Vec<ColumnName> = desc.iter_names().cloned().collect();
2688
2689 if let Some(dup) = names.iter().duplicates().next() {
2690 sql_bail!("column {} specified more than once", dup.quoted());
2691 }
2692
2693 let view = View {
2694 create_sql,
2695 expr,
2696 dependencies,
2697 column_names: names,
2698 temporary,
2699 };
2700
2701 Ok((name, view))
2702}
2703
2704pub fn plan_create_view(
2705 scx: &StatementContext,
2706 mut stmt: CreateViewStatement<Aug>,
2707) -> Result<Plan, PlanError> {
2708 let CreateViewStatement {
2709 temporary,
2710 if_exists,
2711 definition,
2712 } = &mut stmt;
2713 let (name, view) = plan_view(scx, definition, *temporary)?;
2714
2715 let ignore_if_exists_errors = scx.pcx().map_or(false, |pcx| pcx.ignore_if_exists_errors);
2718
2719 let replace = if *if_exists == IfExistsBehavior::Replace && !ignore_if_exists_errors {
2720 let if_exists = true;
2721 let cascade = false;
2722 let maybe_item_to_drop = plan_drop_item(
2723 scx,
2724 ObjectType::View,
2725 if_exists,
2726 definition.name.clone(),
2727 cascade,
2728 )?;
2729
2730 if let Some(id) = maybe_item_to_drop {
2732 let dependencies = view.expr.depends_on();
2733 let invalid_drop = scx
2734 .get_item(&id)
2735 .global_ids()
2736 .any(|gid| dependencies.contains(&gid));
2737 if invalid_drop {
2738 let item = scx.catalog.get_item(&id);
2739 sql_bail!(
2740 "cannot replace view {0}: depended upon by new {0} definition",
2741 scx.catalog.resolve_full_name(item.name())
2742 );
2743 }
2744
2745 Some(id)
2746 } else {
2747 None
2748 }
2749 } else {
2750 None
2751 };
2752 let drop_ids = replace
2753 .map(|id| {
2754 scx.catalog
2755 .item_dependents(id)
2756 .into_iter()
2757 .map(|id| id.unwrap_item_id())
2758 .collect()
2759 })
2760 .unwrap_or_default();
2761
2762 validate_view_dependencies(scx, &view.dependencies.0)?;
2763
2764 let full_name = scx.catalog.resolve_full_name(&name);
2766 let partial_name = PartialItemName::from(full_name.clone());
2767 if let (Ok(item), IfExistsBehavior::Error, false) = (
2770 scx.catalog.resolve_item_or_type(&partial_name),
2771 *if_exists,
2772 ignore_if_exists_errors,
2773 ) {
2774 return Err(PlanError::ItemAlreadyExists {
2775 name: full_name.to_string(),
2776 item_type: item.item_type(),
2777 });
2778 }
2779
2780 Ok(Plan::CreateView(CreateViewPlan {
2781 name,
2782 view,
2783 replace,
2784 drop_ids,
2785 if_not_exists: *if_exists == IfExistsBehavior::Skip,
2786 ambiguous_columns: *scx.ambiguous_columns.borrow(),
2787 }))
2788}
2789
2790fn validate_view_dependencies(
2792 scx: &StatementContext,
2793 dependencies: &BTreeSet<CatalogItemId>,
2794) -> Result<(), PlanError> {
2795 for id in dependencies {
2796 let item = scx.catalog.get_item(id);
2797 if item.replacement_target().is_some() {
2798 let name = scx.catalog.minimal_qualification(item.name());
2799 return Err(PlanError::InvalidDependency {
2800 name: name.to_string(),
2801 item_type: format!("replacement {}", item.item_type()),
2802 });
2803 }
2804 }
2805
2806 Ok(())
2807}
2808
2809pub fn describe_create_materialized_view(
2810 _: &StatementContext,
2811 _: CreateMaterializedViewStatement<Aug>,
2812) -> Result<StatementDesc, PlanError> {
2813 Ok(StatementDesc::new(None))
2814}
2815
2816pub fn describe_create_network_policy(
2817 _: &StatementContext,
2818 _: CreateNetworkPolicyStatement<Aug>,
2819) -> Result<StatementDesc, PlanError> {
2820 Ok(StatementDesc::new(None))
2821}
2822
2823pub fn describe_alter_network_policy(
2824 _: &StatementContext,
2825 _: AlterNetworkPolicyStatement<Aug>,
2826) -> Result<StatementDesc, PlanError> {
2827 Ok(StatementDesc::new(None))
2828}
2829
2830fn check_refresh_time(option: &str, ts: Timestamp) -> Result<(), PlanError> {
2833 let renderable = i64::try_from(ts)
2834 .ok()
2835 .and_then(DateTime::from_timestamp_millis)
2836 .is_some_and(|dt| CheckedTimestamp::try_from(dt).is_ok());
2837 if !renderable {
2838 sql_bail!("{option} time too large: {ts}");
2839 }
2840 Ok(())
2841}
2842
2843pub fn plan_create_materialized_view(
2844 scx: &StatementContext,
2845 mut stmt: CreateMaterializedViewStatement<Aug>,
2846) -> Result<Plan, PlanError> {
2847 let cluster_id =
2848 crate::plan::statement::resolve_cluster_for_materialized_view(scx.catalog, &stmt)?;
2849 stmt.in_cluster = Some(ResolvedClusterName {
2850 id: cluster_id,
2851 print_name: None,
2852 });
2853
2854 let target_replica = match &stmt.in_cluster_replica {
2855 Some(replica_name) => {
2856 scx.require_feature_flag(&ENABLE_REPLICA_TARGETED_MATERIALIZED_VIEWS)?;
2857
2858 let cluster = scx.catalog.get_cluster(cluster_id);
2859 let replica_id = cluster
2860 .replica_ids()
2861 .get(replica_name.as_str())
2862 .copied()
2863 .ok_or_else(|| {
2864 CatalogError::UnknownClusterReplica(replica_name.as_str().to_string())
2865 })?;
2866 Some(replica_id)
2867 }
2868 None => None,
2869 };
2870
2871 let create_sql =
2872 normalize::create_statement(scx, Statement::CreateMaterializedView(stmt.clone()))?;
2873
2874 let partial_name = normalize::unresolved_item_name(stmt.name)?;
2875 let name = scx.allocate_qualified_name(partial_name.clone())?;
2876
2877 let query::PlannedRootQuery {
2878 expr,
2879 mut desc,
2880 finishing,
2881 scope: _,
2882 } = query::plan_root_query(scx, stmt.query, QueryLifetime::MaterializedView)?;
2883 assert!(HirRelationExpr::is_trivial_row_set_finishing_hir(
2885 &finishing,
2886 expr.arity()
2887 ));
2888 if expr.contains_parameters()? {
2889 return Err(PlanError::ParameterNotAllowed(
2890 "materialized views".to_string(),
2891 ));
2892 }
2893
2894 plan_utils::maybe_rename_columns_exact(
2895 scx.catalog,
2896 format!("materialized view {}", scx.catalog.resolve_full_name(&name)),
2897 &mut desc,
2898 &stmt.columns,
2899 )?;
2900 let column_names: Vec<ColumnName> = desc.iter_names().cloned().collect();
2901
2902 let MaterializedViewOptionExtracted {
2903 assert_not_null,
2904 partition_by,
2905 retain_history,
2906 refresh,
2907 seen: _,
2908 }: MaterializedViewOptionExtracted = stmt.with_options.try_into()?;
2909
2910 if let Some(partition_by) = partition_by {
2911 scx.require_feature_flag(&ENABLE_COLLECTION_PARTITION_BY)?;
2912 check_partition_by(&desc, partition_by)?;
2913 }
2914
2915 let refresh_schedule = {
2916 let mut refresh_schedule = RefreshSchedule::default();
2917 let mut on_commits_seen = 0;
2918 for refresh_option_value in refresh {
2919 if !matches!(refresh_option_value, RefreshOptionValue::OnCommit) {
2920 scx.require_feature_flag(&ENABLE_REFRESH_EVERY_MVS)?;
2921 }
2922 match refresh_option_value {
2923 RefreshOptionValue::OnCommit => {
2924 on_commits_seen += 1;
2925 }
2926 RefreshOptionValue::AtCreation => {
2927 soft_panic_or_log!("REFRESH AT CREATION should have been purified away");
2928 bail_internal!("REFRESH AT CREATION should have been purified away")
2929 }
2930 RefreshOptionValue::At(RefreshAtOptionValue { mut time }) => {
2931 transform_ast::transform(scx, &mut time)?; let ecx = &ExprContext {
2933 qcx: &QueryContext::root(scx, QueryLifetime::OneShot),
2934 name: "REFRESH AT",
2935 scope: &Scope::empty(),
2936 relation_type: &SqlRelationType::empty(),
2937 allow_aggregates: false,
2938 allow_subqueries: false,
2939 allow_parameters: false,
2940 allow_windows: false,
2941 };
2942 let hir = plan_expr(ecx, &time)?.cast_to(
2943 ecx,
2944 CastContext::Assignment,
2945 &SqlScalarType::MzTimestamp,
2946 )?;
2947 let timestamp = hir
2949 .into_literal_mz_timestamp()
2950 .ok_or_else(|| PlanError::InvalidRefreshAt)?;
2951 check_refresh_time("REFRESH AT", timestamp)?;
2952 refresh_schedule.ats.push(timestamp);
2953 }
2954 RefreshOptionValue::Every(RefreshEveryOptionValue {
2955 interval,
2956 aligned_to,
2957 }) => {
2958 let interval = Interval::try_from_value(Value::Interval(interval))?;
2959 if interval.as_microseconds() <= 0 {
2960 sql_bail!("REFRESH interval must be positive; got: {}", interval);
2961 }
2962 if interval.months != 0 {
2963 sql_bail!("REFRESH interval must not involve units larger than days");
2968 }
2969 let interval = interval.duration()?;
2970 if u64::try_from(interval.as_millis()).is_err()
2975 || Interval::from_duration(&interval).is_err()
2976 {
2977 sql_bail!("REFRESH interval too large");
2978 }
2979 if interval.as_micros() < 1000 {
2980 sql_bail!("REFRESH interval must be at least 1 ms")
2981 }
2982
2983 let mut aligned_to = match aligned_to {
2984 Some(aligned_to) => aligned_to,
2985 None => {
2986 soft_panic_or_log!(
2987 "ALIGNED TO should have been filled in by purification"
2988 );
2989 sql_bail!(
2990 "INTERNAL ERROR: ALIGNED TO should have been filled in by purification"
2991 )
2992 }
2993 };
2994
2995 transform_ast::transform(scx, &mut aligned_to)?;
2997
2998 let ecx = &ExprContext {
2999 qcx: &QueryContext::root(scx, QueryLifetime::OneShot),
3000 name: "REFRESH EVERY ... ALIGNED TO",
3001 scope: &Scope::empty(),
3002 relation_type: &SqlRelationType::empty(),
3003 allow_aggregates: false,
3004 allow_subqueries: false,
3005 allow_parameters: false,
3006 allow_windows: false,
3007 };
3008 let aligned_to_hir = plan_expr(ecx, &aligned_to)?.cast_to(
3009 ecx,
3010 CastContext::Assignment,
3011 &SqlScalarType::MzTimestamp,
3012 )?;
3013 let aligned_to_const = aligned_to_hir
3015 .into_literal_mz_timestamp()
3016 .ok_or_else(|| PlanError::InvalidRefreshEveryAlignedTo)?;
3017 check_refresh_time("REFRESH EVERY ... ALIGNED TO", aligned_to_const)?;
3018
3019 refresh_schedule.everies.push(RefreshEvery {
3020 interval,
3021 aligned_to: aligned_to_const,
3022 });
3023 }
3024 }
3025 }
3026
3027 if on_commits_seen > 1 {
3028 sql_bail!("REFRESH ON COMMIT cannot be specified multiple times");
3029 }
3030 if on_commits_seen > 0 && refresh_schedule != RefreshSchedule::default() {
3031 sql_bail!("REFRESH ON COMMIT is not compatible with any of the other REFRESH options");
3032 }
3033
3034 if refresh_schedule == RefreshSchedule::default() {
3035 None
3036 } else {
3037 Some(refresh_schedule)
3038 }
3039 };
3040
3041 let as_of = stmt.as_of.map(Timestamp::from);
3042 let compaction_window = plan_retain_history_option(scx, retain_history)?;
3043 let mut non_null_assertions = assert_not_null
3044 .into_iter()
3045 .map(normalize::column_name)
3046 .map(|assertion_name| {
3047 column_names
3048 .iter()
3049 .position(|col| col == &assertion_name)
3050 .ok_or_else(|| {
3051 sql_err!(
3052 "column {} in ASSERT NOT NULL option not found",
3053 assertion_name.quoted()
3054 )
3055 })
3056 })
3057 .collect::<Result<Vec<_>, _>>()?;
3058 non_null_assertions.sort();
3059 if let Some(dup) = non_null_assertions.iter().duplicates().next() {
3060 let dup = &column_names[*dup];
3061 sql_bail!("duplicate column {} in non-null assertions", dup.quoted());
3062 }
3063
3064 if let Some(dup) = column_names.iter().duplicates().next() {
3065 sql_bail!("column {} specified more than once", dup.quoted());
3066 }
3067
3068 let if_exists = match scx.pcx().map(|pcx| pcx.ignore_if_exists_errors) {
3071 Ok(true) => IfExistsBehavior::Skip,
3072 _ => stmt.if_exists,
3073 };
3074
3075 let mut replace = None;
3076 let mut if_not_exists = false;
3077 match if_exists {
3078 IfExistsBehavior::Replace => {
3079 let if_exists = true;
3080 let cascade = false;
3081 let replace_id = plan_drop_item(
3082 scx,
3083 ObjectType::MaterializedView,
3084 if_exists,
3085 partial_name.clone().into(),
3086 cascade,
3087 )?;
3088
3089 if let Some(id) = replace_id {
3091 let dependencies = expr.depends_on();
3092 let invalid_drop = scx
3093 .get_item(&id)
3094 .global_ids()
3095 .any(|gid| dependencies.contains(&gid));
3096 if invalid_drop {
3097 let item = scx.catalog.get_item(&id);
3098 sql_bail!(
3099 "cannot replace materialized view {0}: depended upon by new {0} definition",
3100 scx.catalog.resolve_full_name(item.name())
3101 );
3102 }
3103 replace = Some(id);
3104 }
3105 }
3106 IfExistsBehavior::Skip => if_not_exists = true,
3107 IfExistsBehavior::Error => (),
3108 }
3109 let drop_ids = replace
3110 .map(|id| {
3111 scx.catalog
3112 .item_dependents(id)
3113 .into_iter()
3114 .map(|id| id.unwrap_item_id())
3115 .collect()
3116 })
3117 .unwrap_or_default();
3118 let mut dependencies: BTreeSet<_> = expr
3119 .depends_on()
3120 .into_iter()
3121 .map(|gid| scx.catalog.resolve_item_id(&gid))
3122 .collect();
3123
3124 let mut replacement_target = None;
3126 if let Some(target_name) = &stmt.replacement_for {
3127 scx.require_feature_flag(&vars::ENABLE_REPLACEMENT_MATERIALIZED_VIEWS)?;
3128
3129 let target = scx.get_item_by_resolved_name(target_name)?;
3130 if target.item_type() != CatalogItemType::MaterializedView {
3131 return Err(PlanError::InvalidReplacement {
3132 item_type: target.item_type(),
3133 item_name: scx.catalog.minimal_qualification(target.name()),
3134 replacement_type: CatalogItemType::MaterializedView,
3135 replacement_name: partial_name,
3136 });
3137 }
3138 if target.id().is_system() {
3139 sql_bail!(
3140 "cannot replace {} because it is required by the database system",
3141 scx.catalog.minimal_qualification(target.name()),
3142 );
3143 }
3144
3145 for dependent in scx.catalog.item_dependents(target.id()) {
3147 if let ObjectId::Item(id) = dependent
3148 && dependencies.contains(&id)
3149 {
3150 sql_bail!(
3151 "replacement would cause {} to depend on itself",
3152 scx.catalog.minimal_qualification(target.name()),
3153 );
3154 }
3155 }
3156
3157 dependencies.insert(target.id());
3158
3159 for use_id in target.used_by() {
3160 let use_item = scx.get_item(use_id);
3161 if use_item.replacement_target() == Some(target.id()) {
3162 sql_bail!(
3163 "cannot replace {} because it already has a replacement: {}",
3164 scx.catalog.minimal_qualification(target.name()),
3165 scx.catalog.minimal_qualification(use_item.name()),
3166 );
3167 }
3168 }
3169
3170 replacement_target = Some(target.id());
3171 }
3172
3173 validate_view_dependencies(scx, &dependencies)?;
3174
3175 let full_name = scx.catalog.resolve_full_name(&name);
3177 let partial_name = PartialItemName::from(full_name.clone());
3178 if let (IfExistsBehavior::Error, Ok(item)) =
3181 (if_exists, scx.catalog.resolve_item_or_type(&partial_name))
3182 {
3183 return Err(PlanError::ItemAlreadyExists {
3184 name: full_name.to_string(),
3185 item_type: item.item_type(),
3186 });
3187 }
3188
3189 Ok(Plan::CreateMaterializedView(CreateMaterializedViewPlan {
3190 name,
3191 materialized_view: MaterializedView {
3192 create_sql,
3193 expr,
3194 dependencies: DependencyIds(dependencies),
3195 column_names,
3196 replacement_target,
3197 cluster_id,
3198 target_replica,
3199 non_null_assertions,
3200 compaction_window,
3201 refresh_schedule,
3202 as_of,
3203 },
3204 replace,
3205 drop_ids,
3206 if_not_exists,
3207 ambiguous_columns: *scx.ambiguous_columns.borrow(),
3208 }))
3209}
3210
3211generate_extracted_config!(
3212 MaterializedViewOption,
3213 (AssertNotNull, Ident, AllowMultiple),
3214 (PartitionBy, Vec<Ident>),
3215 (RetainHistory, OptionalDuration),
3216 (Refresh, RefreshOptionValue<Aug>, AllowMultiple)
3217);
3218
3219pub fn describe_create_sink(
3220 _: &StatementContext,
3221 _: CreateSinkStatement<Aug>,
3222) -> Result<StatementDesc, PlanError> {
3223 Ok(StatementDesc::new(None))
3224}
3225
3226generate_extracted_config!(
3227 CreateSinkOption,
3228 (Snapshot, bool),
3229 (PartitionStrategy, String),
3230 (Version, u64),
3231 (CommitInterval, Duration)
3232);
3233
3234pub fn plan_create_sink(
3235 scx: &StatementContext,
3236 stmt: CreateSinkStatement<Aug>,
3237) -> Result<Plan, PlanError> {
3238 let Some(name) = stmt.name.clone() else {
3240 return Err(PlanError::MissingName(CatalogItemType::Sink));
3241 };
3242 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
3243 let full_name = scx.catalog.resolve_full_name(&name);
3244 let partial_name = PartialItemName::from(full_name.clone());
3245 if let (false, Ok(item)) = (stmt.if_not_exists, scx.catalog.resolve_item(&partial_name)) {
3246 return Err(PlanError::ItemAlreadyExists {
3247 name: full_name.to_string(),
3248 item_type: item.item_type(),
3249 });
3250 }
3251
3252 plan_sink(scx, stmt)
3253}
3254
3255fn plan_sink(
3260 scx: &StatementContext,
3261 mut stmt: CreateSinkStatement<Aug>,
3262) -> Result<Plan, PlanError> {
3263 let CreateSinkStatement {
3264 name,
3265 in_cluster: _,
3266 from,
3267 connection,
3268 format,
3269 envelope,
3270 mode,
3271 if_not_exists,
3272 with_options,
3273 } = stmt.clone();
3274
3275 let Some(name) = name else {
3276 return Err(PlanError::MissingName(CatalogItemType::Sink));
3277 };
3278 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
3279
3280 let envelope = match (&connection, envelope, mode) {
3281 (CreateSinkConnection::Kafka { .. }, Some(ast::SinkEnvelope::Upsert), None) => {
3283 SinkEnvelope::Upsert
3284 }
3285 (CreateSinkConnection::Kafka { .. }, Some(ast::SinkEnvelope::Debezium), None) => {
3286 SinkEnvelope::Debezium
3287 }
3288 (CreateSinkConnection::Kafka { .. }, None, None) => {
3289 sql_bail!("ENVELOPE clause is required")
3290 }
3291 (CreateSinkConnection::Kafka { .. }, _, Some(_)) => {
3292 sql_bail!("MODE is not supported for Kafka sinks, use ENVELOPE instead")
3293 }
3294 (CreateSinkConnection::Iceberg { .. }, None, Some(ast::IcebergSinkMode::Upsert)) => {
3296 SinkEnvelope::Upsert
3297 }
3298 (CreateSinkConnection::Iceberg { .. }, None, Some(ast::IcebergSinkMode::Append)) => {
3299 SinkEnvelope::Append
3300 }
3301 (CreateSinkConnection::Iceberg { .. }, None, None) => {
3302 sql_bail!("MODE clause is required")
3303 }
3304 (CreateSinkConnection::Iceberg { .. }, Some(_), _) => {
3305 sql_bail!("ENVELOPE is not supported for Iceberg sinks, use MODE instead")
3306 }
3307 };
3308
3309 let from_name = &from;
3310 let from = scx.get_item_by_resolved_name(&from)?;
3311
3312 {
3313 use CatalogItemType::*;
3314 match from.item_type() {
3315 Table | Source | MaterializedView => {
3316 if from.replacement_target().is_some() {
3317 let name = scx.catalog.minimal_qualification(from.name());
3318 return Err(PlanError::InvalidSinkFrom {
3319 name: name.to_string(),
3320 item_type: format!("replacement {}", from.item_type()),
3321 });
3322 }
3323 }
3324 Sink | MetricSink | View | Index | Type | Func | Secret | Connection => {
3325 let name = scx.catalog.minimal_qualification(from.name());
3326 return Err(PlanError::InvalidSinkFrom {
3327 name: name.to_string(),
3328 item_type: from.item_type().to_string(),
3329 });
3330 }
3331 }
3332 }
3333
3334 if from.id().is_system() {
3335 bail_unsupported!("creating a sink directly on a catalog object");
3336 }
3337
3338 let desc = from
3339 .relation_desc()
3340 .ok_or_else(|| sql_err!("item does not have a relation description"))?;
3341 let key_indices = match &connection {
3342 CreateSinkConnection::Kafka { key: Some(key), .. }
3343 | CreateSinkConnection::Iceberg { key: Some(key), .. } => {
3344 let key_columns = key
3345 .key_columns
3346 .clone()
3347 .into_iter()
3348 .map(normalize::column_name)
3349 .collect::<Vec<_>>();
3350 let mut uniq = BTreeSet::new();
3351 for col in key_columns.iter() {
3352 if !uniq.insert(col) {
3353 sql_bail!("duplicate column referenced in KEY: {}", col);
3354 }
3355 }
3356 let indices = key_columns
3357 .iter()
3358 .map(|col| {
3359 let name_idx =
3360 desc.get_by_name(col)
3361 .map(|(idx, _type)| idx)
3362 .ok_or_else(|| {
3363 sql_err!("column referenced in KEY does not exist: {}", col)
3364 })?;
3365 if desc.get_unambiguous_name(name_idx).is_none() {
3366 sql_bail!("column referenced in KEY is ambiguous: {}", col);
3367 }
3368 Ok(name_idx)
3369 })
3370 .collect::<Result<Vec<_>, _>>()?;
3371
3372 if matches!(&connection, CreateSinkConnection::Iceberg { .. }) {
3375 let cols: Vec<_> = desc.iter().collect();
3376 for &idx in &indices {
3377 let (col_name, col_type) = cols[idx];
3378 let scalar = &col_type.scalar_type;
3379 let is_valid = matches!(
3380 scalar,
3381 SqlScalarType::Bool
3383 | SqlScalarType::Int16
3384 | SqlScalarType::Int32
3385 | SqlScalarType::Int64
3386 | SqlScalarType::UInt16
3387 | SqlScalarType::UInt32
3388 | SqlScalarType::UInt64
3389 | SqlScalarType::Numeric { .. }
3391 | SqlScalarType::Date
3393 | SqlScalarType::Time
3394 | SqlScalarType::Timestamp { .. }
3395 | SqlScalarType::TimestampTz { .. }
3396 | SqlScalarType::Interval
3397 | SqlScalarType::MzTimestamp
3398 | SqlScalarType::String
3400 | SqlScalarType::Char { .. }
3401 | SqlScalarType::VarChar { .. }
3402 | SqlScalarType::PgLegacyChar
3403 | SqlScalarType::PgLegacyName
3404 | SqlScalarType::Bytes
3405 | SqlScalarType::Jsonb
3406 | SqlScalarType::Uuid
3408 | SqlScalarType::Oid
3409 | SqlScalarType::RegProc
3410 | SqlScalarType::RegType
3411 | SqlScalarType::RegClass
3412 | SqlScalarType::MzAclItem
3413 | SqlScalarType::AclItem
3414 | SqlScalarType::Int2Vector
3415 );
3416 if !is_valid {
3417 return Err(PlanError::IcebergSinkUnsupportedKeyType {
3418 column: col_name.to_string(),
3419 column_type: format!("{:?}", scalar),
3420 });
3421 }
3422 }
3423 }
3424
3425 let is_valid_key = desc
3426 .typ()
3427 .keys
3428 .iter()
3429 .any(|key_columns| key_columns.iter().all(|column| indices.contains(column)));
3430
3431 if !is_valid_key && envelope == SinkEnvelope::Upsert {
3432 if key.not_enforced {
3433 scx.catalog
3434 .add_notice(PlanNotice::UpsertSinkKeyNotEnforced {
3435 key: key_columns.clone(),
3436 name: name.item.clone(),
3437 })
3438 } else {
3439 return Err(PlanError::UpsertSinkWithInvalidKey {
3440 name: from_name.full_name_str(),
3441 desired_key: key_columns.iter().map(|c| c.to_string()).collect(),
3442 valid_keys: desc
3443 .typ()
3444 .keys
3445 .iter()
3446 .map(|key| {
3447 key.iter()
3448 .map(|col| desc.get_name(*col).as_str().into())
3449 .collect()
3450 })
3451 .collect(),
3452 });
3453 }
3454 }
3455 Some(indices)
3456 }
3457 CreateSinkConnection::Kafka { key: None, .. }
3458 | CreateSinkConnection::Iceberg { key: None, .. } => None,
3459 };
3460
3461 if key_indices.is_some() && envelope == SinkEnvelope::Append {
3462 sql_bail!("KEY is not supported for MODE APPEND Iceberg sinks");
3463 }
3464
3465 if envelope == SinkEnvelope::Append {
3467 if let CreateSinkConnection::Iceberg { .. } = &connection {
3468 use mz_storage_types::sinks::{
3469 ICEBERG_APPEND_DIFF_COLUMN, ICEBERG_APPEND_TIMESTAMP_COLUMN,
3470 };
3471 for (col_name, _) in desc.iter() {
3472 if col_name.as_str() == ICEBERG_APPEND_DIFF_COLUMN
3473 || col_name.as_str() == ICEBERG_APPEND_TIMESTAMP_COLUMN
3474 {
3475 sql_bail!(
3476 "column {} conflicts with the system column that MODE APPEND \
3477 adds to the Iceberg table",
3478 col_name.quoted()
3479 );
3480 }
3481 }
3482 }
3483 }
3484
3485 let headers_index = match &connection {
3486 CreateSinkConnection::Kafka {
3487 headers: Some(headers),
3488 ..
3489 } => {
3490 scx.require_feature_flag(&ENABLE_KAFKA_SINK_HEADERS)?;
3491
3492 match envelope {
3493 SinkEnvelope::Upsert | SinkEnvelope::Append => (),
3494 SinkEnvelope::Debezium => {
3495 sql_bail!("HEADERS option is not supported with ENVELOPE DEBEZIUM")
3496 }
3497 };
3498
3499 let headers = normalize::column_name(headers.clone());
3500 let (idx, ty) = desc
3501 .get_by_name(&headers)
3502 .ok_or_else(|| sql_err!("HEADERS column ({}) is unknown", headers))?;
3503
3504 if desc.get_unambiguous_name(idx).is_none() {
3505 sql_bail!("HEADERS column ({}) is ambiguous", headers);
3506 }
3507
3508 match &ty.scalar_type {
3509 SqlScalarType::Map { value_type, .. }
3510 if matches!(&**value_type, SqlScalarType::String | SqlScalarType::Bytes) => {}
3511 _ => sql_bail!(
3512 "HEADERS column must have type map[text => text] or map[text => bytea]"
3513 ),
3514 }
3515
3516 Some(idx)
3517 }
3518 _ => None,
3519 };
3520
3521 let relation_key_indices = desc.typ().keys.get(0).cloned();
3523
3524 let key_desc_and_indices = key_indices.map(|key_indices| {
3525 let cols = desc
3526 .iter()
3527 .map(|(name, ty)| (name.clone(), ty.clone()))
3528 .collect::<Vec<_>>();
3529 let (names, types): (Vec<_>, Vec<_>) =
3530 key_indices.iter().map(|&idx| cols[idx].clone()).unzip();
3531 let typ = SqlRelationType::new(types);
3532 (RelationDesc::new(typ, names), key_indices)
3533 });
3534
3535 if key_desc_and_indices.is_none() && envelope == SinkEnvelope::Upsert {
3536 return Err(PlanError::UpsertSinkWithoutKey);
3537 }
3538
3539 let CreateSinkOptionExtracted {
3540 snapshot,
3541 version,
3542 partition_strategy: _,
3543 seen: _,
3544 commit_interval,
3545 } = with_options.try_into()?;
3546
3547 let connection_builder = match connection {
3548 CreateSinkConnection::Kafka {
3549 connection,
3550 options,
3551 ..
3552 } => kafka_sink_builder(
3553 scx,
3554 connection,
3555 options,
3556 format,
3557 relation_key_indices,
3558 key_desc_and_indices,
3559 headers_index,
3560 desc.into_owned(),
3561 envelope,
3562 from.id(),
3563 commit_interval,
3564 )?,
3565 CreateSinkConnection::Iceberg {
3566 catalog_connection,
3567 aws_connection,
3568 options,
3569 ..
3570 } => iceberg_sink_builder(
3571 scx,
3572 catalog_connection,
3573 aws_connection,
3574 options,
3575 relation_key_indices,
3576 key_desc_and_indices,
3577 commit_interval,
3578 &desc,
3579 )?,
3580 };
3581
3582 let with_snapshot = snapshot.unwrap_or(true);
3584 let version = version.unwrap_or(0);
3586
3587 let in_cluster = source_sink_cluster_config(scx, &mut stmt.in_cluster)?;
3591 let create_sql = normalize::create_statement(scx, Statement::CreateSink(stmt))?;
3592
3593 Ok(Plan::CreateSink(CreateSinkPlan {
3594 name,
3595 sink: Sink {
3596 create_sql,
3597 from: from.global_id(),
3598 connection: connection_builder,
3599 envelope,
3600 version,
3601 commit_interval,
3602 },
3603 with_snapshot,
3604 if_not_exists,
3605 in_cluster: in_cluster.id(),
3606 }))
3607}
3608
3609fn key_constraint_err(desc: &RelationDesc, user_keys: &[ColumnName]) -> PlanError {
3610 let user_keys = user_keys.iter().map(|column| column.as_str()).join(", ");
3611
3612 let existing_keys = desc
3613 .typ()
3614 .keys
3615 .iter()
3616 .map(|key_columns| {
3617 key_columns
3618 .iter()
3619 .map(|col| desc.get_name(*col).as_str())
3620 .join(", ")
3621 })
3622 .join(", ");
3623
3624 sql_err!(
3625 "Key constraint ({}) conflicts with existing key ({})",
3626 user_keys,
3627 existing_keys
3628 )
3629}
3630
3631#[derive(Debug, Default, PartialEq, Clone)]
3634pub struct CsrConfigOptionExtracted {
3635 seen: ::std::collections::BTreeSet<CsrConfigOptionName<Aug>>,
3636 pub(crate) avro_key_fullname: Option<String>,
3637 pub(crate) avro_value_fullname: Option<String>,
3638 pub(crate) null_defaults: bool,
3639 pub(crate) value_doc_options: BTreeMap<DocTarget, String>,
3640 pub(crate) key_doc_options: BTreeMap<DocTarget, String>,
3641 pub(crate) key_compatibility_level: Option<mz_ccsr::CompatibilityLevel>,
3642 pub(crate) value_compatibility_level: Option<mz_ccsr::CompatibilityLevel>,
3643}
3644
3645impl std::convert::TryFrom<Vec<CsrConfigOption<Aug>>> for CsrConfigOptionExtracted {
3646 type Error = crate::plan::PlanError;
3647 fn try_from(v: Vec<CsrConfigOption<Aug>>) -> Result<CsrConfigOptionExtracted, Self::Error> {
3648 let mut extracted = CsrConfigOptionExtracted::default();
3649 let mut common_doc_comments = BTreeMap::new();
3650 for option in v {
3651 if !extracted.seen.insert(option.name.clone()) {
3652 return Err(PlanError::Unstructured({
3653 format!("{} specified more than once", option.name)
3654 }));
3655 }
3656 let option_name = option.name.clone();
3657 let option_name_str = option_name.to_ast_string_simple();
3658 let better_error = |e: PlanError| PlanError::InvalidOptionValue {
3659 option_name: option_name.to_ast_string_simple(),
3660 err: e.into(),
3661 };
3662 let to_compatibility_level = |val: Option<WithOptionValue<Aug>>| {
3663 val.map(|s| match s {
3664 WithOptionValue::Value(Value::String(s)) => {
3665 mz_ccsr::CompatibilityLevel::try_from(s.to_uppercase().as_str())
3666 }
3667 _ => Err("must be a string".to_string()),
3668 })
3669 .transpose()
3670 .map_err(PlanError::Unstructured)
3671 .map_err(better_error)
3672 };
3673 match option.name {
3674 CsrConfigOptionName::AvroKeyFullname => {
3675 extracted.avro_key_fullname =
3676 <Option<String>>::try_from_value(option.value).map_err(better_error)?;
3677 }
3678 CsrConfigOptionName::AvroValueFullname => {
3679 extracted.avro_value_fullname =
3680 <Option<String>>::try_from_value(option.value).map_err(better_error)?;
3681 }
3682 CsrConfigOptionName::NullDefaults => {
3683 extracted.null_defaults =
3684 <bool>::try_from_value(option.value).map_err(better_error)?;
3685 }
3686 CsrConfigOptionName::AvroDocOn(doc_on) => {
3687 let value = String::try_from_value(option.value.ok_or_else(|| {
3688 PlanError::InvalidOptionValue {
3689 option_name: option_name_str,
3690 err: Box::new(PlanError::Unstructured("cannot be empty".to_string())),
3691 }
3692 })?)
3693 .map_err(better_error)?;
3694 let key = match doc_on.identifier {
3695 DocOnIdentifier::Column(ast::ColumnName {
3696 relation: ResolvedItemName::Item { id, .. },
3697 column: ResolvedColumnReference::Column { name, index: _ },
3698 }) => DocTarget::Field {
3699 object_id: id,
3700 column_name: name,
3701 },
3702 DocOnIdentifier::Type(ResolvedItemName::Item { id, .. }) => {
3703 DocTarget::Type(id)
3704 }
3705 _ => sql_bail!("invalid DOC ON identifier"),
3706 };
3707
3708 match doc_on.for_schema {
3709 DocOnSchema::KeyOnly => {
3710 extracted.key_doc_options.insert(key, value);
3711 }
3712 DocOnSchema::ValueOnly => {
3713 extracted.value_doc_options.insert(key, value);
3714 }
3715 DocOnSchema::All => {
3716 common_doc_comments.insert(key, value);
3717 }
3718 }
3719 }
3720 CsrConfigOptionName::KeyCompatibilityLevel => {
3721 extracted.key_compatibility_level = to_compatibility_level(option.value)?;
3722 }
3723 CsrConfigOptionName::ValueCompatibilityLevel => {
3724 extracted.value_compatibility_level = to_compatibility_level(option.value)?;
3725 }
3726 }
3727 }
3728
3729 for (key, value) in common_doc_comments {
3730 if !extracted.key_doc_options.contains_key(&key) {
3731 extracted.key_doc_options.insert(key.clone(), value.clone());
3732 }
3733 if !extracted.value_doc_options.contains_key(&key) {
3734 extracted.value_doc_options.insert(key, value);
3735 }
3736 }
3737 Ok(extracted)
3738 }
3739}
3740
3741fn iceberg_sink_builder(
3742 scx: &StatementContext,
3743 catalog_connection: ResolvedItemName,
3744 storage_connection: Option<ResolvedItemName>,
3745 options: Vec<IcebergSinkConfigOption<Aug>>,
3746 relation_key_indices: Option<Vec<usize>>,
3747 key_desc_and_indices: Option<(RelationDesc, Vec<usize>)>,
3748 commit_interval: Option<Duration>,
3749 desc: &RelationDesc,
3750) -> Result<StorageSinkConnection<ReferencedConnection>, PlanError> {
3751 ArrowBuilder::validate_desc_for_parquet(desc, iceberg_type_overrides)
3755 .map_err(|e| sql_err!("{}", e))?;
3756
3757 let catalog_connection_item = scx.get_item_by_resolved_name(&catalog_connection)?;
3758 let catalog_connection_id = catalog_connection_item.id();
3759 if !matches!(
3760 catalog_connection_item.connection()?,
3761 Connection::IcebergCatalog(_)
3762 ) {
3763 sql_bail!(
3764 "{} is not an iceberg catalog connection",
3765 scx.catalog
3766 .resolve_full_name(catalog_connection_item.name())
3767 .to_string()
3768 .quoted()
3769 );
3770 };
3771
3772 let storage_connection_item = storage_connection
3773 .map(|c| scx.get_item_by_resolved_name(&c))
3774 .transpose()?;
3775 let storage_connection_id = storage_connection_item.as_ref().map(|c| c.id());
3776 if let Some(c) = &storage_connection_item
3777 && !matches!(c.connection()?, Connection::Aws(_))
3778 {
3779 sql_bail!(
3780 "{} is not an AWS connection",
3781 scx.catalog.resolve_full_name(c.name()).to_string().quoted()
3782 );
3783 }
3784
3785 let IcebergSinkConfigOptionExtracted {
3786 table,
3787 namespace,
3788 seen: _,
3789 }: IcebergSinkConfigOptionExtracted = options.try_into()?;
3790
3791 let Some(table) = table else {
3792 sql_bail!("Iceberg sink must specify TABLE");
3793 };
3794 let Some(namespace) = namespace else {
3795 sql_bail!("Iceberg sink must specify NAMESPACE");
3796 };
3797 match commit_interval {
3798 None => sql_bail!("Iceberg sink must specify COMMIT INTERVAL"),
3799 Some(interval) if interval < Duration::from_secs(1) => {
3805 sql_bail!("COMMIT INTERVAL must be at least 1 second")
3806 }
3807 Some(_) => {}
3808 }
3809
3810 Ok(StorageSinkConnection::Iceberg(IcebergSinkConnection {
3811 catalog_connection_id,
3812 catalog_connection: catalog_connection_id,
3813 storage_connection_id,
3814 storage_connection: storage_connection_id,
3815 table,
3816 namespace,
3817 relation_key_indices,
3818 key_desc_and_indices,
3819 }))
3820}
3821
3822fn kafka_sink_builder(
3823 scx: &StatementContext,
3824 connection: ResolvedItemName,
3825 options: Vec<KafkaSinkConfigOption<Aug>>,
3826 format: Option<FormatSpecifier<Aug>>,
3827 relation_key_indices: Option<Vec<usize>>,
3828 key_desc_and_indices: Option<(RelationDesc, Vec<usize>)>,
3829 headers_index: Option<usize>,
3830 value_desc: RelationDesc,
3831 envelope: SinkEnvelope,
3832 sink_from: CatalogItemId,
3833 commit_interval: Option<Duration>,
3834) -> Result<StorageSinkConnection<ReferencedConnection>, PlanError> {
3835 let connection_item = scx.get_item_by_resolved_name(&connection)?;
3837 let connection_id = connection_item.id();
3838 match connection_item.connection()? {
3839 Connection::Kafka(_) => (),
3840 _ => sql_bail!(
3841 "{} is not a kafka connection",
3842 scx.catalog.resolve_full_name(connection_item.name())
3843 ),
3844 };
3845
3846 if commit_interval.is_some() {
3847 sql_bail!("COMMIT INTERVAL option is not supported with KAFKA sinks");
3848 }
3849
3850 let KafkaSinkConfigOptionExtracted {
3851 topic,
3852 compression_type,
3853 partition_by,
3854 progress_group_id_prefix,
3855 transactional_id_prefix,
3856 legacy_ids,
3857 topic_config,
3858 topic_metadata_refresh_interval,
3859 topic_partition_count,
3860 topic_replication_factor,
3861 seen: _,
3862 }: KafkaSinkConfigOptionExtracted = options.try_into()?;
3863
3864 let transactional_id = match (transactional_id_prefix, legacy_ids) {
3865 (Some(_), Some(true)) => {
3866 sql_bail!("LEGACY IDS cannot be used at the same time as TRANSACTIONAL ID PREFIX")
3867 }
3868 (None, Some(true)) => KafkaIdStyle::Legacy,
3869 (prefix, _) => KafkaIdStyle::Prefix(prefix),
3870 };
3871
3872 let progress_group_id = match (progress_group_id_prefix, legacy_ids) {
3873 (Some(_), Some(true)) => {
3874 sql_bail!("LEGACY IDS cannot be used at the same time as PROGRESS GROUP ID PREFIX")
3875 }
3876 (None, Some(true)) => KafkaIdStyle::Legacy,
3877 (prefix, _) => KafkaIdStyle::Prefix(prefix),
3878 };
3879
3880 let topic_name = topic.ok_or_else(|| sql_err!("KAFKA CONNECTION must specify TOPIC"))?;
3881
3882 if topic_metadata_refresh_interval > MAX_KAFKA_TOPIC_METADATA_REFRESH_INTERVAL {
3883 sql_bail!("TOPIC METADATA REFRESH INTERVAL cannot be greater than 1 hour");
3886 } else if topic_metadata_refresh_interval < MIN_KAFKA_TOPIC_METADATA_REFRESH_INTERVAL {
3887 sql_bail!("TOPIC METADATA REFRESH INTERVAL must be at least 1 second");
3890 }
3891
3892 let assert_positive = |val: Option<i32>, name: &str| {
3893 if let Some(val) = val {
3894 if val <= 0 {
3895 sql_bail!("{} must be a positive integer", name);
3896 }
3897 }
3898 val.map(NonNeg::try_from)
3899 .transpose()
3900 .map_err(|_| PlanError::Unstructured(format!("{} must be a positive integer", name)))
3901 };
3902 let topic_partition_count = assert_positive(topic_partition_count, "TOPIC PARTITION COUNT")?;
3903 let topic_replication_factor =
3904 assert_positive(topic_replication_factor, "TOPIC REPLICATION FACTOR")?;
3905
3906 let gen_avro_schema_options = |conn| {
3909 let CsrConnectionAvro {
3910 connection:
3911 CsrConnection {
3912 connection,
3913 options,
3914 },
3915 seed,
3916 key_strategy,
3917 value_strategy,
3918 } = conn;
3919 if seed.is_some() {
3920 sql_bail!("SEED option does not make sense with sinks");
3921 }
3922 if key_strategy.is_some() {
3923 sql_bail!("KEY STRATEGY option does not make sense with sinks");
3924 }
3925 if value_strategy.is_some() {
3926 sql_bail!("VALUE STRATEGY option does not make sense with sinks");
3927 }
3928
3929 let item = scx.get_item_by_resolved_name(&connection)?;
3930 let csr_connection = match item.connection()? {
3931 Connection::Csr(_) => item.id(),
3932 _ => {
3933 sql_bail!(
3934 "{} is not a schema registry connection",
3935 scx.catalog
3936 .resolve_full_name(item.name())
3937 .to_string()
3938 .quoted()
3939 )
3940 }
3941 };
3942 let extracted_options: CsrConfigOptionExtracted = options.try_into()?;
3943
3944 if key_desc_and_indices.is_none() && extracted_options.avro_key_fullname.is_some() {
3945 sql_bail!("Cannot specify AVRO KEY FULLNAME without a corresponding KEY field");
3946 }
3947
3948 if key_desc_and_indices.is_some()
3949 && (extracted_options.avro_key_fullname.is_some()
3950 ^ extracted_options.avro_value_fullname.is_some())
3951 {
3952 sql_bail!(
3953 "Must specify both AVRO KEY FULLNAME and AVRO VALUE FULLNAME when specifying generated schema names"
3954 );
3955 }
3956
3957 Ok((csr_connection, extracted_options))
3958 };
3959
3960 let map_format = |format: Format<Aug>, desc: &RelationDesc, is_key: bool| match format {
3961 Format::Json { array: false } => Ok::<_, PlanError>(KafkaSinkFormatType::Json),
3962 Format::Bytes if desc.arity() == 1 => {
3963 let col_type = &desc.typ().column_types[0].scalar_type;
3964 if !mz_pgrepr::Value::can_encode_binary(col_type) {
3965 bail_unsupported!(format!(
3966 "BYTES format with non-encodable type: {:?}",
3967 col_type
3968 ));
3969 }
3970
3971 Ok(KafkaSinkFormatType::Bytes)
3972 }
3973 Format::Text if desc.arity() == 1 => Ok(KafkaSinkFormatType::Text),
3974 Format::Bytes | Format::Text => {
3975 bail_unsupported!("BYTES or TEXT format with multiple columns")
3976 }
3977 Format::Json { array: true } => bail_unsupported!("JSON ARRAY format in sinks"),
3978 Format::Avro(AvroSchema::Csr { csr_connection }) => {
3979 let (csr_connection, options) = gen_avro_schema_options(csr_connection)?;
3980 let schema = if is_key {
3981 AvroSchemaGenerator::new(
3982 desc.clone(),
3983 false,
3984 options.key_doc_options,
3985 options.avro_key_fullname.as_deref().unwrap_or("row"),
3986 options.null_defaults,
3987 Some(sink_from),
3988 false,
3989 )?
3990 .schema()
3991 .to_string()
3992 } else {
3993 AvroSchemaGenerator::new(
3994 desc.clone(),
3995 matches!(envelope, SinkEnvelope::Debezium),
3996 options.value_doc_options,
3997 options.avro_value_fullname.as_deref().unwrap_or("envelope"),
3998 options.null_defaults,
3999 Some(sink_from),
4000 true,
4001 )?
4002 .schema()
4003 .to_string()
4004 };
4005 Ok(KafkaSinkFormatType::Avro {
4006 schema,
4007 compatibility_level: if is_key {
4008 options.key_compatibility_level
4009 } else {
4010 options.value_compatibility_level
4011 },
4012 schema_name: None,
4014 wire_format: WireFormat::Confluent {
4015 registry: Some(csr_connection),
4016 },
4017 })
4018 }
4019 Format::Avro(AvroSchema::Glue {
4020 connection,
4021 with_options,
4022 seed,
4023 }) => {
4024 if seed.is_some() {
4025 sql_bail!("SEED option does not make sense with sinks");
4026 }
4027
4028 let extracted: GlueAvroOptionExtracted = with_options.try_into()?;
4029 let GlueAvroOptionExtracted {
4030 schema_name,
4031 key_schema_name,
4032 value_schema_name,
4033 key_compatibility_level,
4034 value_compatibility_level,
4035 seen: _,
4036 } = extracted;
4037
4038 if schema_name.is_some() {
4043 sql_bail!(
4044 "SCHEMA NAME is not supported for AWS Glue Schema Registry sinks, \
4045 use KEY SCHEMA NAME and VALUE SCHEMA NAME instead"
4046 );
4047 }
4048
4049 if key_desc_and_indices.is_none()
4055 && (key_schema_name.is_some() || key_compatibility_level.is_some())
4056 {
4057 sql_bail!(
4058 "KEY SCHEMA NAME and KEY COMPATIBILITY LEVEL require a corresponding KEY field"
4059 );
4060 }
4061
4062 let item = scx.get_item_by_resolved_name(&connection)?;
4063 let glue_connection = match item.connection()? {
4064 Connection::GlueSchemaRegistry(_) => item.id(),
4065 _ => {
4066 sql_bail!(
4067 "{} is not an AWS Glue Schema Registry connection",
4068 scx.catalog
4069 .resolve_full_name(item.name())
4070 .to_string()
4071 .quoted()
4072 )
4073 }
4074 };
4075
4076 let compatibility_level = {
4080 let raw = if is_key {
4081 key_compatibility_level
4082 } else {
4083 value_compatibility_level
4084 };
4085 raw.map(|s| {
4086 mz_ccsr::CompatibilityLevel::try_from(s.to_uppercase().as_str())
4087 .map_err(PlanError::Unstructured)
4088 })
4089 .transpose()?
4090 };
4091
4092 let schema_name = if is_key {
4096 key_schema_name
4097 } else {
4098 value_schema_name
4099 };
4100
4101 let schema = if is_key {
4104 AvroSchemaGenerator::new(
4105 desc.clone(),
4106 false,
4107 Default::default(),
4108 "row",
4109 false,
4110 Some(sink_from),
4111 false,
4112 )?
4113 .schema()
4114 .to_string()
4115 } else {
4116 AvroSchemaGenerator::new(
4117 desc.clone(),
4118 matches!(envelope, SinkEnvelope::Debezium),
4119 Default::default(),
4120 "envelope",
4121 false,
4122 Some(sink_from),
4123 true,
4124 )?
4125 .schema()
4126 .to_string()
4127 };
4128 Ok(KafkaSinkFormatType::Avro {
4129 schema,
4130 compatibility_level,
4131 schema_name,
4132 wire_format: WireFormat::Glue {
4133 registry: Some(glue_connection),
4134 },
4135 })
4136 }
4137 format => bail_unsupported!(format!("sink format {:?}", format)),
4138 };
4139
4140 let partition_by = match &partition_by {
4141 Some(partition_by) => {
4142 let mut scope = Scope::from_source(None, value_desc.iter_names());
4143
4144 match envelope {
4145 SinkEnvelope::Upsert | SinkEnvelope::Append => (),
4146 SinkEnvelope::Debezium => {
4147 let key_indices: HashSet<_> = key_desc_and_indices
4148 .as_ref()
4149 .map(|(_desc, indices)| indices.as_slice())
4150 .unwrap_or_default()
4151 .into_iter()
4152 .collect();
4153 for (i, item) in scope.items.iter_mut().enumerate() {
4154 if !key_indices.contains(&i) {
4155 item.error_if_referenced = Some(|_table, column| {
4156 PlanError::InvalidPartitionByEnvelopeDebezium {
4157 column_name: column.to_string(),
4158 }
4159 });
4160 }
4161 }
4162 }
4163 };
4164
4165 let ecx = &ExprContext {
4166 qcx: &QueryContext::root(scx, QueryLifetime::OneShot),
4167 name: "PARTITION BY",
4168 scope: &scope,
4169 relation_type: value_desc.typ(),
4170 allow_aggregates: false,
4171 allow_subqueries: false,
4172 allow_parameters: false,
4173 allow_windows: false,
4174 };
4175 let expr = plan_expr(ecx, partition_by)?.cast_to(
4176 ecx,
4177 CastContext::Assignment,
4178 &SqlScalarType::UInt64,
4179 )?;
4180 let expr = expr.lower_uncorrelated(scx.catalog.system_vars())?;
4181
4182 Some(expr)
4183 }
4184 _ => None,
4185 };
4186
4187 let format = match format {
4189 Some(FormatSpecifier::KeyValue { key, value }) => {
4190 let key_format = match key_desc_and_indices.as_ref() {
4191 Some((desc, _indices)) => Some(map_format(key, desc, true)?),
4192 None => None,
4193 };
4194 KafkaSinkFormat {
4195 value_format: map_format(value, &value_desc, false)?,
4196 key_format,
4197 }
4198 }
4199 Some(FormatSpecifier::Bare(format)) => {
4200 let key_format = match key_desc_and_indices.as_ref() {
4201 Some((desc, _indices)) => Some(map_format(format.clone(), desc, true)?),
4202 None => None,
4203 };
4204 KafkaSinkFormat {
4205 value_format: map_format(format, &value_desc, false)?,
4206 key_format,
4207 }
4208 }
4209 None => bail_unsupported!("sink without format"),
4210 };
4211
4212 Ok(StorageSinkConnection::Kafka(KafkaSinkConnection {
4213 connection_id,
4214 connection: connection_id,
4215 format,
4216 topic: topic_name,
4217 relation_key_indices,
4218 key_desc_and_indices,
4219 headers_index,
4220 value_desc,
4221 partition_by,
4222 compression_type,
4223 progress_group_id,
4224 transactional_id,
4225 topic_options: KafkaTopicOptions {
4226 partition_count: topic_partition_count,
4227 replication_factor: topic_replication_factor,
4228 topic_config: topic_config.unwrap_or_default(),
4229 },
4230 topic_metadata_refresh_interval,
4231 }))
4232}
4233
4234pub fn describe_create_metric_sink(
4235 _: &StatementContext,
4236 _: CreateMetricSinkStatement<Aug>,
4237) -> Result<StatementDesc, PlanError> {
4238 Ok(StatementDesc::new(None))
4239}
4240
4241const METRIC_SINK_SOURCE_COLUMNS: &[(&str, fn(&SqlScalarType) -> bool)] = &[
4245 ("metric_name", |t| matches!(t, SqlScalarType::String)),
4246 ("metric_type", |t| matches!(t, SqlScalarType::String)),
4247 (
4248 "labels",
4249 |t| matches!(t, SqlScalarType::Map { value_type, .. } if matches!(**value_type, SqlScalarType::String)),
4250 ),
4251 ("value", |t| matches!(t, SqlScalarType::Float64)),
4252 ("help", |t| matches!(t, SqlScalarType::String)),
4253];
4254
4255generate_extracted_config!(CreateMetricSinkOption, (Prefix, String));
4256
4257const METRIC_SINK_PREFIX_MARKER: &str = "mz_metric_sink_";
4264
4265pub const METRIC_SINK_CURATED_PREFIX_MARKER: &str = "mz_metric_sink_curated_";
4268
4269pub fn validate_metric_sink_prefix(prefix: &str) -> Result<(), PlanError> {
4274 if prefix.is_empty() {
4275 return Err(sql_err!("metric sink prefix must not be empty"));
4276 }
4277 let mut chars = prefix.chars();
4278 let valid = match chars.next() {
4279 Some(c) if c.is_ascii_alphabetic() || c == '_' || c == ':' => {
4280 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':')
4281 }
4282 _ => false,
4283 };
4284 if !valid {
4285 return Err(sql_err!(
4286 "metric sink prefix {:?} is not a valid start of a Prometheus metric name",
4287 prefix
4288 ));
4289 }
4290 if !prefix.starts_with(METRIC_SINK_PREFIX_MARKER) {
4291 return Err(sql_err!(
4292 "metric sink prefix {:?} must start with {:?}",
4293 prefix,
4294 METRIC_SINK_PREFIX_MARKER
4295 ));
4296 }
4297 Ok(())
4298}
4299
4300pub fn validate_user_metric_sink_prefix(prefix: &str) -> Result<(), PlanError> {
4309 validate_metric_sink_prefix(prefix)?;
4310 if prefix.starts_with(METRIC_SINK_CURATED_PREFIX_MARKER)
4311 || METRIC_SINK_CURATED_PREFIX_MARKER.starts_with(prefix)
4312 {
4313 return Err(sql_err!(
4314 "metric sink prefix {:?} overlaps {:?}, which is reserved for the built-in metric sinks",
4315 prefix,
4316 METRIC_SINK_CURATED_PREFIX_MARKER
4317 ));
4318 }
4319 Ok(())
4320}
4321
4322pub fn validate_metric_sink_desc(desc: &RelationDesc) -> Result<(), PlanError> {
4328 for (name, type_ok) in METRIC_SINK_SOURCE_COLUMNS {
4329 let col = ColumnName::from(*name);
4330 let (_, column_type) = desc
4331 .get_by_name(&col)
4332 .ok_or_else(|| sql_err!("metric sink source must expose column {:?}", name))?;
4333 if !type_ok(&column_type.scalar_type) {
4334 return Err(sql_err!(
4335 "metric sink source column {:?} is not of the required type",
4336 name
4337 ));
4338 }
4339 }
4340 Ok(())
4341}
4342
4343pub fn plan_create_metric_sink(
4351 scx: &StatementContext,
4352 mut stmt: CreateMetricSinkStatement<Aug>,
4353) -> Result<Plan, PlanError> {
4354 scx.require_feature_flag(&ENABLE_METRIC_SINK)?;
4355
4356 let CreateMetricSinkStatement {
4357 name,
4358 in_cluster,
4359 if_not_exists,
4360 from,
4361 with_options,
4362 } = &mut stmt;
4363
4364 let if_not_exists = *if_not_exists;
4365 let CreateMetricSinkOptionExtracted { prefix, seen: _ } = with_options.clone().try_into()?;
4366 let Some(prefix) = prefix else {
4369 sql_bail!("CREATE METRIC SINK requires a PREFIX option");
4370 };
4371 validate_user_metric_sink_prefix(&prefix)?;
4372 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name.clone())?)?;
4373 let full_name = scx.catalog.resolve_full_name(&name);
4374 let partial_name = PartialItemName::from(full_name.clone());
4375 if let (false, Ok(item)) = (if_not_exists, scx.catalog.resolve_item(&partial_name)) {
4376 return Err(PlanError::ItemAlreadyExists {
4377 name: full_name.to_string(),
4378 item_type: item.item_type(),
4379 });
4380 }
4381
4382 let from_item = scx.get_item_by_resolved_name(from)?;
4383 let desc = from_item.relation_desc().ok_or_else(|| {
4387 sql_err!(
4388 "cannot create metric sink from {} because it is a {}",
4389 scx.catalog.minimal_qualification(from_item.name()),
4390 from_item.item_type(),
4391 )
4392 })?;
4393 validate_metric_sink_desc(&desc)?;
4394
4395 let cluster_id = match in_cluster {
4396 None => scx.resolve_cluster(None)?.id(),
4397 Some(in_cluster) => in_cluster.id,
4398 };
4399 *in_cluster = Some(ResolvedClusterName {
4400 id: cluster_id,
4401 print_name: None,
4402 });
4403 let from_global_id = from_item.global_id();
4404
4405 let create_sql = normalize::create_statement(scx, Statement::CreateMetricSink(stmt))?;
4406
4407 Ok(Plan::CreateMetricSink(CreateMetricSinkPlan {
4408 name,
4409 metric_sink: MetricSink {
4410 create_sql,
4411 from: from_global_id,
4412 cluster_id,
4413 prefix,
4414 },
4415 if_not_exists,
4416 }))
4417}
4418
4419pub fn describe_create_index(
4420 _: &StatementContext,
4421 _: CreateIndexStatement<Aug>,
4422) -> Result<StatementDesc, PlanError> {
4423 Ok(StatementDesc::new(None))
4424}
4425
4426pub fn plan_create_index(
4427 scx: &StatementContext,
4428 mut stmt: CreateIndexStatement<Aug>,
4429) -> Result<Plan, PlanError> {
4430 let CreateIndexStatement {
4431 name,
4432 on_name,
4433 in_cluster,
4434 key_parts,
4435 with_options,
4436 if_not_exists,
4437 } = &mut stmt;
4438 let on = scx.get_item_by_resolved_name(on_name)?;
4439
4440 {
4441 use CatalogItemType::*;
4442 match on.item_type() {
4443 Table | Source | View | MaterializedView => {
4444 if on.replacement_target().is_some() {
4445 sql_bail!(
4446 "index cannot be created on {} because it is a replacement {}",
4447 on_name.full_name_str(),
4448 on.item_type(),
4449 );
4450 }
4451 }
4452 Sink | MetricSink | Index | Type | Func | Secret | Connection => {
4453 sql_bail!(
4454 "index cannot be created on {} because it is a {}",
4455 on_name.full_name_str(),
4456 on.item_type(),
4457 );
4458 }
4459 }
4460 }
4461
4462 let on_desc = on
4463 .relation_desc()
4464 .ok_or_else(|| sql_err!("item does not have a relation description"))?;
4465
4466 let filled_key_parts = match key_parts {
4467 Some(kp) => kp.to_vec(),
4468 None => {
4469 let mut name_counts = BTreeMap::new();
4474 for name in on_desc.iter_names() {
4475 *name_counts.entry(name).or_insert(0usize) += 1;
4476 }
4477 let key = on_desc.typ().default_key();
4478 key.iter()
4479 .map(|i| {
4480 let name = on_desc.get_name(*i);
4481 if name_counts.get(name).copied() == Some(1) {
4482 Expr::Identifier(vec![name.clone().into()])
4483 } else {
4484 Expr::Value(Value::Number((i + 1).to_string()))
4485 }
4486 })
4487 .collect()
4488 }
4489 };
4490 let keys = query::plan_index_exprs(scx, &on_desc, filled_key_parts.clone())?;
4491
4492 let index_name = if let Some(name) = name {
4493 QualifiedItemName {
4494 qualifiers: on.name().qualifiers.clone(),
4495 item: normalize::ident(name.clone()),
4496 }
4497 } else {
4498 let mut idx_name = QualifiedItemName {
4499 qualifiers: on.name().qualifiers.clone(),
4500 item: on.name().item.clone(),
4501 };
4502 if key_parts.is_none() {
4503 idx_name.item += "_primary_idx";
4505 } else {
4506 let index_name_col_suffix = keys
4509 .iter()
4510 .map(|k| match k {
4511 mz_expr::MirScalarExpr::Column(i, name) => {
4512 match (on_desc.get_unambiguous_name(*i), &name.0) {
4513 (Some(col_name), _) => col_name.to_string(),
4514 (None, Some(name)) => name.to_string(),
4515 (None, None) => format!("{}", i + 1),
4516 }
4517 }
4518 _ => "expr".to_string(),
4519 })
4520 .join("_");
4521 write!(idx_name.item, "_{index_name_col_suffix}_idx")
4522 .expect("write on strings cannot fail");
4523 idx_name.item = normalize::ident(Ident::new(&idx_name.item)?)
4524 }
4525
4526 if !*if_not_exists {
4527 scx.catalog.find_available_name(idx_name)
4528 } else {
4529 idx_name
4530 }
4531 };
4532
4533 let full_name = scx.catalog.resolve_full_name(&index_name);
4535 let partial_name = PartialItemName::from(full_name.clone());
4536 if let (Ok(item), false, false) = (
4544 scx.catalog.resolve_item_or_type(&partial_name),
4545 *if_not_exists,
4546 scx.pcx().map_or(false, |pcx| pcx.ignore_if_exists_errors),
4547 ) {
4548 return Err(PlanError::ItemAlreadyExists {
4549 name: full_name.to_string(),
4550 item_type: item.item_type(),
4551 });
4552 }
4553
4554 let options = plan_index_options(scx, with_options.clone())?;
4555 let cluster_id = match in_cluster {
4556 None => scx.resolve_cluster(None)?.id(),
4557 Some(in_cluster) => in_cluster.id,
4558 };
4559
4560 *in_cluster = Some(ResolvedClusterName {
4561 id: cluster_id,
4562 print_name: None,
4563 });
4564
4565 *name = Some(Ident::new(index_name.item.clone())?);
4567 *key_parts = Some(filled_key_parts);
4568 let if_not_exists = *if_not_exists;
4569
4570 let create_sql = normalize::create_statement(scx, Statement::CreateIndex(stmt))?;
4571 let compaction_window = options.iter().find_map(|o| {
4572 #[allow(irrefutable_let_patterns)]
4573 if let crate::plan::IndexOption::RetainHistory(lcw) = o {
4574 Some(lcw.clone())
4575 } else {
4576 None
4577 }
4578 });
4579
4580 Ok(Plan::CreateIndex(CreateIndexPlan {
4581 name: index_name,
4582 index: Index {
4583 create_sql,
4584 on: on.global_id(),
4585 keys,
4586 cluster_id,
4587 compaction_window,
4588 },
4589 if_not_exists,
4590 }))
4591}
4592
4593pub fn describe_create_type(
4594 _: &StatementContext,
4595 _: CreateTypeStatement<Aug>,
4596) -> Result<StatementDesc, PlanError> {
4597 Ok(StatementDesc::new(None))
4598}
4599
4600pub fn plan_create_type(
4601 scx: &StatementContext,
4602 stmt: CreateTypeStatement<Aug>,
4603) -> Result<Plan, PlanError> {
4604 let create_sql = normalize::create_statement(scx, Statement::CreateType(stmt.clone()))?;
4605 let CreateTypeStatement { name, as_type, .. } = stmt;
4606
4607 fn validate_data_type(
4615 scx: &StatementContext,
4616 data_type: ResolvedDataType,
4617 as_type: &str,
4618 key: &str,
4619 budget: &mut TypeResolutionBudget,
4620 ) -> Result<(CatalogItemId, Vec<i64>), PlanError> {
4621 let (id, modifiers) = match data_type {
4622 ResolvedDataType::Named { id, modifiers, .. } => (id, modifiers),
4623 _ => sql_bail!(
4624 "CREATE TYPE ... AS {}option {} can only use named data types, but \
4625 found unnamed data type {}. Use CREATE TYPE to create a named type first",
4626 as_type,
4627 key,
4628 data_type.human_readable_name(),
4629 ),
4630 };
4631
4632 let item = scx.catalog.get_item(&id);
4633 match item.type_details() {
4634 None => sql_bail!(
4635 "{} must be of class type, but received {} which is of class {}",
4636 key,
4637 scx.catalog.resolve_full_name(item.name()),
4638 item.item_type()
4639 ),
4640 Some(CatalogTypeDetails {
4641 typ: CatalogType::Char,
4642 ..
4643 }) => {
4644 bail_unsupported!("embedding char type in a list or map")
4645 }
4646 _ => {
4647 budget.resolve_child(scx.catalog, id, &modifiers)?;
4650
4651 Ok((id, modifiers))
4652 }
4653 }
4654 }
4655
4656 let mut budget = TypeResolutionBudget::for_root(scx.catalog);
4657 let inner = match as_type {
4658 CreateTypeAs::List { options } => {
4659 let CreateTypeListOptionExtracted {
4660 element_type,
4661 seen: _,
4662 } = CreateTypeListOptionExtracted::try_from(options)?;
4663 let element_type =
4664 element_type.ok_or_else(|| sql_err!("ELEMENT TYPE option is required"))?;
4665 let (id, modifiers) =
4666 validate_data_type(scx, element_type, "LIST ", "ELEMENT TYPE", &mut budget)?;
4667 CatalogType::List {
4668 element_reference: id,
4669 element_modifiers: modifiers,
4670 }
4671 }
4672 CreateTypeAs::Map { options } => {
4673 let CreateTypeMapOptionExtracted {
4674 key_type,
4675 value_type,
4676 seen: _,
4677 } = CreateTypeMapOptionExtracted::try_from(options)?;
4678 let key_type = key_type.ok_or_else(|| sql_err!("KEY TYPE option is required"))?;
4679 let value_type = value_type.ok_or_else(|| sql_err!("VALUE TYPE option is required"))?;
4680 let (key_id, key_modifiers) = validate_data_type(
4684 scx,
4685 key_type,
4686 "MAP ",
4687 "KEY TYPE",
4688 &mut TypeResolutionBudget::for_root(scx.catalog),
4689 )?;
4690 let (value_id, value_modifiers) =
4691 validate_data_type(scx, value_type, "MAP ", "VALUE TYPE", &mut budget)?;
4692 CatalogType::Map {
4693 key_reference: key_id,
4694 key_modifiers,
4695 value_reference: value_id,
4696 value_modifiers,
4697 }
4698 }
4699 CreateTypeAs::Record { column_defs } => {
4700 let mut fields = vec![];
4701 for column_def in column_defs {
4702 let data_type = column_def.data_type;
4703 let key = ident(column_def.name.clone());
4704 let (id, modifiers) = validate_data_type(scx, data_type, "", &key, &mut budget)?;
4705 fields.push(CatalogRecordField {
4706 name: ColumnName::from(key.clone()),
4707 type_reference: id,
4708 type_modifiers: modifiers,
4709 });
4710 }
4711 CatalogType::Record { fields }
4712 }
4713 };
4714
4715 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
4716
4717 let full_name = scx.catalog.resolve_full_name(&name);
4719 let partial_name = PartialItemName::from(full_name.clone());
4720 if let Ok(item) = scx.catalog.resolve_item_or_type(&partial_name) {
4723 if item.item_type().conflicts_with_type() {
4724 return Err(PlanError::ItemAlreadyExists {
4725 name: full_name.to_string(),
4726 item_type: item.item_type(),
4727 });
4728 }
4729 }
4730
4731 Ok(Plan::CreateType(CreateTypePlan {
4732 name,
4733 typ: Type { create_sql, inner },
4734 }))
4735}
4736
4737generate_extracted_config!(CreateTypeListOption, (ElementType, ResolvedDataType));
4738
4739generate_extracted_config!(
4740 CreateTypeMapOption,
4741 (KeyType, ResolvedDataType),
4742 (ValueType, ResolvedDataType)
4743);
4744
4745#[derive(Debug)]
4746pub enum PlannedAlterRoleOption {
4747 Attributes(PlannedRoleAttributes),
4748 Variable(PlannedRoleVariable),
4749}
4750
4751#[derive(Debug, Clone)]
4752pub struct PlannedRoleAttributes {
4753 pub inherit: Option<bool>,
4754 pub password: Option<Password>,
4755 pub scram_iterations: Option<NonZeroU32>,
4756 pub nopassword: Option<bool>,
4760 pub superuser: Option<bool>,
4761 pub login: Option<bool>,
4762}
4763
4764fn plan_role_attributes(
4765 options: Vec<RoleAttribute>,
4766 scx: &StatementContext,
4767) -> Result<PlannedRoleAttributes, PlanError> {
4768 let mut planned_attributes = PlannedRoleAttributes {
4769 inherit: None,
4770 password: None,
4771 scram_iterations: None,
4772 superuser: None,
4773 login: None,
4774 nopassword: None,
4775 };
4776
4777 for option in options {
4778 match option {
4779 RoleAttribute::Inherit | RoleAttribute::NoInherit
4780 if planned_attributes.inherit.is_some() =>
4781 {
4782 sql_bail!("conflicting or redundant options");
4783 }
4784 RoleAttribute::CreateCluster | RoleAttribute::NoCreateCluster => {
4785 bail_never_supported!(
4786 "CREATECLUSTER attribute",
4787 "sql/create-role/#details",
4788 "Use system privileges instead."
4789 );
4790 }
4791 RoleAttribute::CreateDB | RoleAttribute::NoCreateDB => {
4792 bail_never_supported!(
4793 "CREATEDB attribute",
4794 "sql/create-role/#details",
4795 "Use system privileges instead."
4796 );
4797 }
4798 RoleAttribute::CreateRole | RoleAttribute::NoCreateRole => {
4799 bail_never_supported!(
4800 "CREATEROLE attribute",
4801 "sql/create-role/#details",
4802 "Use system privileges instead."
4803 );
4804 }
4805 RoleAttribute::Password(_) if planned_attributes.password.is_some() => {
4806 sql_bail!("conflicting or redundant options");
4807 }
4808
4809 RoleAttribute::Inherit => planned_attributes.inherit = Some(true),
4810 RoleAttribute::NoInherit => planned_attributes.inherit = Some(false),
4811 RoleAttribute::Password(password) => {
4812 if let Some(password) = password {
4813 planned_attributes.password = Some(password.into());
4814 planned_attributes.scram_iterations =
4815 Some(scx.catalog.system_vars().scram_iterations())
4816 } else {
4817 planned_attributes.nopassword = Some(true);
4818 }
4819 }
4820 RoleAttribute::SuperUser => {
4821 if planned_attributes.superuser == Some(false) {
4822 sql_bail!("conflicting or redundant options");
4823 }
4824 planned_attributes.superuser = Some(true);
4825 }
4826 RoleAttribute::NoSuperUser => {
4827 if planned_attributes.superuser == Some(true) {
4828 sql_bail!("conflicting or redundant options");
4829 }
4830 planned_attributes.superuser = Some(false);
4831 }
4832 RoleAttribute::Login => {
4833 if planned_attributes.login == Some(false) {
4834 sql_bail!("conflicting or redundant options");
4835 }
4836 planned_attributes.login = Some(true);
4837 }
4838 RoleAttribute::NoLogin => {
4839 if planned_attributes.login == Some(true) {
4840 sql_bail!("conflicting or redundant options");
4841 }
4842 planned_attributes.login = Some(false);
4843 }
4844 }
4845 }
4846 if planned_attributes.inherit == Some(false) {
4847 bail_unsupported!("non inherit roles");
4848 }
4849
4850 Ok(planned_attributes)
4851}
4852
4853#[derive(Debug)]
4854pub enum PlannedRoleVariable {
4855 Set { name: String, value: VariableValue },
4856 Reset { name: String },
4857}
4858
4859impl PlannedRoleVariable {
4860 pub fn name(&self) -> &str {
4861 match self {
4862 PlannedRoleVariable::Set { name, .. } => name,
4863 PlannedRoleVariable::Reset { name } => name,
4864 }
4865 }
4866}
4867
4868fn plan_role_variable(
4869 scx: &StatementContext,
4870 variable: SetRoleVar,
4871) -> Result<PlannedRoleVariable, PlanError> {
4872 let plan = match variable {
4873 SetRoleVar::Set { name, value } => {
4874 let name = name.to_string();
4875 let value = scl::plan_set_variable_to(value)?;
4876 if let VariableValue::Values(values) = &value {
4879 vars::check_transaction_isolation_feature_flag(
4880 &name,
4881 VarInput::SqlSet(values),
4882 scx.catalog.system_vars(),
4883 )?;
4884 }
4885 PlannedRoleVariable::Set { name, value }
4886 }
4887 SetRoleVar::Reset { name } => PlannedRoleVariable::Reset {
4888 name: name.to_string(),
4889 },
4890 };
4891 Ok(plan)
4892}
4893
4894pub fn describe_create_role(
4895 _: &StatementContext,
4896 _: CreateRoleStatement,
4897) -> Result<StatementDesc, PlanError> {
4898 Ok(StatementDesc::new(None))
4899}
4900
4901pub fn plan_create_role(
4902 scx: &StatementContext,
4903 CreateRoleStatement { name, options }: CreateRoleStatement,
4904) -> Result<Plan, PlanError> {
4905 let attributes = plan_role_attributes(options, scx)?;
4906 Ok(Plan::CreateRole(CreateRolePlan {
4907 name: normalize::ident(name),
4908 attributes: attributes.into(),
4909 }))
4910}
4911
4912pub fn plan_create_network_policy(
4913 ctx: &StatementContext,
4914 CreateNetworkPolicyStatement { name, options }: CreateNetworkPolicyStatement<Aug>,
4915) -> Result<Plan, PlanError> {
4916 ctx.require_feature_flag(&vars::ENABLE_NETWORK_POLICIES)?;
4917 let policy_options: NetworkPolicyOptionExtracted = options.try_into()?;
4918
4919 let Some(rule_defs) = policy_options.rules else {
4920 sql_bail!("RULES must be specified when creating network policies.");
4921 };
4922
4923 let mut rules = vec![];
4924 for NetworkPolicyRuleDefinition { name, options } in rule_defs {
4925 let NetworkPolicyRuleOptionExtracted {
4926 seen: _,
4927 direction,
4928 action,
4929 address,
4930 } = options.try_into()?;
4931 let (direction, action, address) = match (direction, action, address) {
4932 (Some(direction), Some(action), Some(address)) => (
4933 NetworkPolicyRuleDirection::try_from(direction.as_str())?,
4934 NetworkPolicyRuleAction::try_from(action.as_str())?,
4935 PolicyAddress::try_from(address.as_str())?,
4936 ),
4937 (_, _, _) => {
4938 sql_bail!("Direction, Address, and Action must specified when creating a rule")
4939 }
4940 };
4941 rules.push(NetworkPolicyRule {
4942 name: normalize::ident(name),
4943 direction,
4944 action,
4945 address,
4946 });
4947 }
4948
4949 if rules.len()
4950 > ctx
4951 .catalog
4952 .system_vars()
4953 .max_rules_per_network_policy()
4954 .try_into()?
4955 {
4956 sql_bail!("RULES count exceeds max_rules_per_network_policy.")
4957 }
4958
4959 Ok(Plan::CreateNetworkPolicy(CreateNetworkPolicyPlan {
4960 name: normalize::ident(name),
4961 rules,
4962 }))
4963}
4964
4965pub fn plan_alter_network_policy(
4966 ctx: &StatementContext,
4967 AlterNetworkPolicyStatement { name, options }: AlterNetworkPolicyStatement<Aug>,
4968) -> Result<Plan, PlanError> {
4969 ctx.require_feature_flag(&vars::ENABLE_NETWORK_POLICIES)?;
4970
4971 let policy_options: NetworkPolicyOptionExtracted = options.try_into()?;
4972 let policy = ctx
4973 .catalog
4974 .resolve_network_policy(normalize::ident_ref(&name))?;
4975
4976 let Some(rule_defs) = policy_options.rules else {
4977 sql_bail!("RULES must be specified when creating network policies.");
4978 };
4979
4980 let mut rules = vec![];
4981 for NetworkPolicyRuleDefinition { name, options } in rule_defs {
4982 let NetworkPolicyRuleOptionExtracted {
4983 seen: _,
4984 direction,
4985 action,
4986 address,
4987 } = options.try_into()?;
4988
4989 let (direction, action, address) = match (direction, action, address) {
4990 (Some(direction), Some(action), Some(address)) => (
4991 NetworkPolicyRuleDirection::try_from(direction.as_str())?,
4992 NetworkPolicyRuleAction::try_from(action.as_str())?,
4993 PolicyAddress::try_from(address.as_str())?,
4994 ),
4995 (_, _, _) => {
4996 sql_bail!("Direction, Address, and Action must specified when creating a rule")
4997 }
4998 };
4999 rules.push(NetworkPolicyRule {
5000 name: normalize::ident(name),
5001 direction,
5002 action,
5003 address,
5004 });
5005 }
5006 if rules.len()
5007 > ctx
5008 .catalog
5009 .system_vars()
5010 .max_rules_per_network_policy()
5011 .try_into()?
5012 {
5013 sql_bail!("RULES count exceeds max_rules_per_network_policy.")
5014 }
5015
5016 Ok(Plan::AlterNetworkPolicy(AlterNetworkPolicyPlan {
5017 id: policy.id(),
5018 name: normalize::ident(name),
5019 rules,
5020 }))
5021}
5022
5023pub fn describe_create_cluster(
5024 _: &StatementContext,
5025 _: CreateClusterStatement<Aug>,
5026) -> Result<StatementDesc, PlanError> {
5027 Ok(StatementDesc::new(None))
5028}
5029
5030generate_extracted_config!(
5036 ClusterOption,
5037 (AutoScalingStrategy, ClusterAutoScalingStrategyOptionValue),
5038 (AvailabilityZones, Vec<String>),
5039 (Disk, bool),
5040 (ExperimentalArrangementCompression, bool),
5041 (IntrospectionDebugging, bool),
5042 (IntrospectionInterval, OptionalDuration),
5043 (Managed, bool),
5044 (Replicas, Vec<ReplicaDefinition<Aug>>),
5045 (ReplicationFactor, u32),
5046 (Size, String),
5047 (Schedule, ClusterScheduleOptionValue),
5048 (WorkloadClass, OptionalString)
5049);
5050
5051generate_extracted_config!(
5052 NetworkPolicyOption,
5053 (Rules, Vec<NetworkPolicyRuleDefinition<Aug>>)
5054);
5055
5056generate_extracted_config!(
5057 NetworkPolicyRuleOption,
5058 (Direction, String),
5059 (Action, String),
5060 (Address, String)
5061);
5062
5063generate_extracted_config!(ClusterAlterOption, (Wait, ClusterAlterOptionValue<Aug>));
5064
5065generate_extracted_config!(
5066 ClusterAlterUntilReadyOption,
5067 (Timeout, Duration),
5068 (OnTimeout, String)
5069);
5070
5071generate_extracted_config!(
5072 ClusterFeature,
5073 (ReoptimizeImportedViews, Option<bool>, Default(None)),
5074 (EnableEagerDeltaJoins, Option<bool>, Default(None)),
5075 (EnableNewOuterJoinLowering, Option<bool>, Default(None)),
5076 (EnableVariadicLeftJoinLowering, Option<bool>, Default(None)),
5077 (EnableLetrecFixpointAnalysis, Option<bool>, Default(None)),
5078 (EnableJoinPrioritizeArranged, Option<bool>, Default(None)),
5079 (
5080 EnableProjectionPushdownAfterRelationCse,
5081 Option<bool>,
5082 Default(None)
5083 ),
5084 (
5085 EnableUnionCancellationAfterRelationCse,
5086 Option<bool>,
5087 Default(None)
5088 )
5089);
5090
5091pub fn plan_create_cluster(
5095 scx: &StatementContext,
5096 stmt: CreateClusterStatement<Aug>,
5097) -> Result<Plan, PlanError> {
5098 let plan = plan_create_cluster_inner(scx, stmt)?;
5099
5100 if let CreateClusterVariant::Managed(_) = &plan.variant {
5102 let stmt = unplan_create_cluster(scx, plan.clone())
5103 .map_err(|e| PlanError::Replan(e.to_string()))?;
5104 let create_sql = stmt.to_ast_string_stable();
5105 let stmt = parse::parse(&create_sql)
5106 .map_err(|e| PlanError::Replan(e.to_string()))?
5107 .into_element()
5108 .ast;
5109 let (stmt, _resolved_ids) =
5110 names::resolve(scx.catalog, stmt).map_err(|e| PlanError::Replan(e.to_string()))?;
5111 let stmt = match stmt {
5112 Statement::CreateCluster(stmt) => stmt,
5113 stmt => {
5114 return Err(PlanError::Replan(format!(
5115 "replan does not match: plan={plan:?}, create_sql={create_sql:?}, stmt={stmt:?}"
5116 )));
5117 }
5118 };
5119 let replan =
5120 plan_create_cluster_inner(scx, stmt).map_err(|e| PlanError::Replan(e.to_string()))?;
5121 if plan != replan {
5122 return Err(PlanError::Replan(format!(
5123 "replan does not match: plan={plan:?}, replan={replan:?}"
5124 )));
5125 }
5126 }
5127
5128 Ok(Plan::CreateCluster(plan))
5129}
5130
5131pub fn plan_create_cluster_inner(
5132 scx: &StatementContext,
5133 CreateClusterStatement {
5134 name,
5135 options,
5136 features,
5137 if_not_exists,
5138 }: CreateClusterStatement<Aug>,
5139) -> Result<CreateClusterPlan, PlanError> {
5140 let ClusterOptionExtracted {
5141 auto_scaling_strategy,
5142 availability_zones,
5143 experimental_arrangement_compression,
5144 introspection_debugging,
5145 introspection_interval,
5146 managed,
5147 replicas,
5148 replication_factor,
5149 seen: _,
5150 size,
5151 disk,
5152 schedule,
5153 workload_class,
5154 }: ClusterOptionExtracted = options.try_into()?;
5155
5156 let managed = managed.unwrap_or_else(|| replicas.is_none());
5157
5158 if !scx.catalog.active_role_id().is_system() {
5159 if !features.is_empty() {
5160 sql_bail!("FEATURES not supported for non-system users");
5161 }
5162 if workload_class.is_some() {
5163 sql_bail!("WORKLOAD CLASS not supported for non-system users");
5164 }
5165 }
5166
5167 let schedule = schedule.unwrap_or(ClusterScheduleOptionValue::Manual);
5168 let workload_class = workload_class.and_then(|v| v.0);
5169
5170 if managed {
5171 if replicas.is_some() {
5172 sql_bail!("REPLICAS not supported for managed clusters");
5173 }
5174 let Some(size) = size else {
5175 sql_bail!("SIZE must be specified for managed clusters");
5176 };
5177
5178 if disk.is_some() {
5179 if scx.catalog.is_cluster_size_cc(&size) {
5183 sql_bail!(
5184 "DISK option not supported for modern cluster sizes because disk is always enabled"
5185 );
5186 }
5187
5188 scx.catalog
5189 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated);
5190 }
5191
5192 let compute = plan_compute_replica_config(
5193 introspection_interval,
5194 introspection_debugging.unwrap_or(false),
5195 experimental_arrangement_compression.unwrap_or(false),
5196 )?;
5197
5198 let replication_factor = if matches!(schedule, ClusterScheduleOptionValue::Manual) {
5199 replication_factor.unwrap_or_else(|| {
5200 scx.catalog
5201 .system_vars()
5202 .default_cluster_replication_factor()
5203 })
5204 } else {
5205 scx.require_feature_flag(&ENABLE_CLUSTER_SCHEDULE_REFRESH)?;
5206 if replication_factor.is_some() {
5207 sql_bail!(
5208 "REPLICATION FACTOR cannot be given together with any SCHEDULE other than MANUAL"
5209 );
5210 }
5211 0
5215 };
5216 let availability_zones = availability_zones.unwrap_or_default();
5217
5218 if !availability_zones.is_empty() {
5219 scx.require_feature_flag(&vars::ENABLE_MANAGED_CLUSTER_AVAILABILITY_ZONES)?;
5220 }
5221
5222 let ClusterFeatureExtracted {
5224 reoptimize_imported_views,
5225 enable_eager_delta_joins,
5226 enable_new_outer_join_lowering,
5227 enable_variadic_left_join_lowering,
5228 enable_letrec_fixpoint_analysis,
5229 enable_join_prioritize_arranged,
5230 enable_projection_pushdown_after_relation_cse,
5231 enable_union_cancellation_after_relation_cse,
5232 seen: _,
5233 } = ClusterFeatureExtracted::try_from(features)?;
5234 let optimizer_feature_overrides = OptimizerFeatureOverrides {
5235 reoptimize_imported_views,
5236 enable_eager_delta_joins,
5237 enable_new_outer_join_lowering,
5238 enable_variadic_left_join_lowering,
5239 enable_letrec_fixpoint_analysis,
5240 enable_join_prioritize_arranged,
5241 enable_projection_pushdown_after_relation_cse,
5242 enable_union_cancellation_after_relation_cse,
5243 ..Default::default()
5244 };
5245
5246 let auto_scaling_strategy = match auto_scaling_strategy {
5249 Some(value) => {
5250 scx.require_feature_flag(&ENABLE_AUTO_SCALING_STRATEGY)?;
5251 let strategy = plan_auto_scaling_strategy(value)?;
5252 if let Some(strategy) = &strategy {
5253 let schedule_non_manual =
5254 !matches!(schedule, ClusterScheduleOptionValue::Manual);
5255 validate_auto_scaling_strategy(strategy, Some(&size), schedule_non_manual)?;
5256 }
5257 strategy
5258 }
5259 None => None,
5260 };
5261
5262 let schedule = plan_cluster_schedule(schedule)?;
5263
5264 Ok(CreateClusterPlan {
5265 name: normalize::ident(name),
5266 variant: CreateClusterVariant::Managed(CreateClusterManagedPlan {
5267 replication_factor,
5268 size,
5269 availability_zones,
5270 compute,
5271 optimizer_feature_overrides,
5272 schedule,
5273 auto_scaling_strategy,
5274 }),
5275 workload_class,
5276 if_not_exists,
5277 })
5278 } else {
5279 let Some(replica_defs) = replicas else {
5280 sql_bail!("REPLICAS must be specified for unmanaged clusters");
5281 };
5282 if auto_scaling_strategy.is_some() {
5283 sql_bail!("AUTO SCALING STRATEGY not supported for unmanaged clusters");
5284 }
5285 if availability_zones.is_some() {
5286 sql_bail!("AVAILABILITY ZONES not supported for unmanaged clusters");
5287 }
5288 if replication_factor.is_some() {
5289 sql_bail!("REPLICATION FACTOR not supported for unmanaged clusters");
5290 }
5291 if introspection_debugging.is_some() {
5292 sql_bail!("INTROSPECTION DEBUGGING not supported for unmanaged clusters");
5293 }
5294 if introspection_interval.is_some() {
5295 sql_bail!("INTROSPECTION INTERVAL not supported for unmanaged clusters");
5296 }
5297 if experimental_arrangement_compression.is_some() {
5298 sql_bail!("EXPERIMENTAL ARRANGEMENT COMPRESSION not supported for unmanaged clusters");
5299 }
5300 if size.is_some() {
5301 sql_bail!("SIZE not supported for unmanaged clusters");
5302 }
5303 if disk.is_some() {
5304 sql_bail!("DISK not supported for unmanaged clusters");
5305 }
5306 if !features.is_empty() {
5307 sql_bail!("FEATURES not supported for unmanaged clusters");
5308 }
5309 if !matches!(schedule, ClusterScheduleOptionValue::Manual) {
5310 sql_bail!(
5311 "cluster schedules other than MANUAL are not supported for unmanaged clusters"
5312 );
5313 }
5314
5315 let mut replicas = vec![];
5316 for ReplicaDefinition { name, options } in replica_defs {
5317 replicas.push((normalize::ident(name), plan_replica_config(scx, options)?));
5318 }
5319
5320 Ok(CreateClusterPlan {
5321 name: normalize::ident(name),
5322 variant: CreateClusterVariant::Unmanaged(CreateClusterUnmanagedPlan { replicas }),
5323 workload_class,
5324 if_not_exists,
5325 })
5326 }
5327}
5328
5329pub fn unplan_create_cluster(
5336 scx: &StatementContext,
5337 CreateClusterPlan {
5338 name,
5339 variant,
5340 workload_class,
5341 if_not_exists,
5342 }: CreateClusterPlan,
5343) -> Result<CreateClusterStatement<Aug>, PlanError> {
5344 match variant {
5345 CreateClusterVariant::Managed(CreateClusterManagedPlan {
5346 replication_factor,
5347 size,
5348 availability_zones,
5349 compute,
5350 optimizer_feature_overrides,
5351 schedule,
5352 auto_scaling_strategy,
5353 }) => {
5354 let schedule = unplan_cluster_schedule(schedule);
5355 let auto_scaling_strategy = auto_scaling_strategy
5356 .as_ref()
5357 .map(unplan_auto_scaling_strategy);
5358 let OptimizerFeatureOverrides {
5359 enable_reduce_mfp_fusion: _,
5360 enable_cardinality_estimates: _,
5361 persist_fast_path_limit: _,
5362 reoptimize_imported_views,
5363 enable_eager_delta_joins,
5364 enable_new_outer_join_lowering,
5365 enable_variadic_left_join_lowering,
5366 enable_letrec_fixpoint_analysis,
5367 enable_join_prioritize_arranged,
5368 enable_projection_pushdown_after_relation_cse,
5369 enable_union_cancellation_after_relation_cse,
5370 enable_less_reduce_in_eqprop: _,
5371 enable_dequadratic_eqprop_map: _,
5372 enable_eq_classes_withholding_errors: _,
5373 enable_fast_path_plan_insights: _,
5374 enable_cast_elimination: _,
5375 enable_case_literal_transform: _,
5376 enable_simplify_quantified_comparisons: _,
5377 enable_simplify_from_less_existence: _,
5378 enable_coalesce_case_transform: _,
5379 enable_will_distinct_propagation: _,
5380 enable_fixed_correlated_cte_lowering: _,
5381 } = optimizer_feature_overrides;
5382 let features_extracted = ClusterFeatureExtracted {
5384 seen: Default::default(),
5386 reoptimize_imported_views,
5387 enable_eager_delta_joins,
5388 enable_new_outer_join_lowering,
5389 enable_variadic_left_join_lowering,
5390 enable_letrec_fixpoint_analysis,
5391 enable_join_prioritize_arranged,
5392 enable_projection_pushdown_after_relation_cse,
5393 enable_union_cancellation_after_relation_cse,
5394 };
5395 let features = features_extracted.into_values(scx.catalog);
5396 let availability_zones = if availability_zones.is_empty() {
5397 None
5398 } else {
5399 Some(availability_zones)
5400 };
5401 let (introspection_interval, introspection_debugging, arrangement_compression) =
5402 unplan_compute_replica_config(compute);
5403 let replication_factor = match &schedule {
5406 ClusterScheduleOptionValue::Manual => Some(replication_factor),
5407 ClusterScheduleOptionValue::Refresh { .. } => {
5408 soft_assert_or_log!(
5415 replication_factor <= 1,
5416 "replication factor, {replication_factor:?}, must be <= 1 with a refresh schedule"
5417 );
5418 None
5419 }
5420 };
5421 let workload_class = workload_class.map(|s| OptionalString(Some(s)));
5422 let options_extracted = ClusterOptionExtracted {
5423 seen: Default::default(),
5425 auto_scaling_strategy,
5426 availability_zones,
5427 disk: None,
5428 experimental_arrangement_compression: Some(arrangement_compression),
5429 introspection_debugging: Some(introspection_debugging),
5430 introspection_interval,
5431 managed: Some(true),
5432 replicas: None,
5433 replication_factor,
5434 size: Some(size),
5435 schedule: Some(schedule),
5436 workload_class,
5437 };
5438 let options = options_extracted.into_values(scx.catalog);
5439 let name = Ident::new_unchecked(name);
5440 Ok(CreateClusterStatement {
5441 name,
5442 options,
5443 features,
5444 if_not_exists,
5445 })
5446 }
5447 CreateClusterVariant::Unmanaged(_) => {
5448 bail_unsupported!("SHOW CREATE for unmanaged clusters")
5449 }
5450 }
5451}
5452
5453generate_extracted_config!(
5454 ReplicaOption,
5455 (AvailabilityZone, String),
5456 (BilledAs, String),
5457 (ComputeAddresses, Vec<String>),
5458 (ComputectlAddresses, Vec<String>),
5459 (Disk, bool),
5460 (ExperimentalArrangementCompression, bool, Default(false)),
5461 (Internal, bool, Default(false)),
5462 (IntrospectionDebugging, bool, Default(false)),
5463 (IntrospectionInterval, OptionalDuration),
5464 (Size, String),
5465 (StorageAddresses, Vec<String>),
5466 (StoragectlAddresses, Vec<String>),
5467 (Workers, u16)
5468);
5469
5470fn plan_replica_config(
5471 scx: &StatementContext,
5472 options: Vec<ReplicaOption<Aug>>,
5473) -> Result<ReplicaConfig, PlanError> {
5474 let ReplicaOptionExtracted {
5475 availability_zone,
5476 billed_as,
5477 computectl_addresses,
5478 disk,
5479 experimental_arrangement_compression,
5480 internal,
5481 introspection_debugging,
5482 introspection_interval,
5483 size,
5484 storagectl_addresses,
5485 ..
5486 }: ReplicaOptionExtracted = options.try_into()?;
5487
5488 let compute = plan_compute_replica_config(
5489 introspection_interval,
5490 introspection_debugging,
5491 experimental_arrangement_compression,
5492 )?;
5493
5494 match (
5495 size,
5496 availability_zone,
5497 billed_as,
5498 storagectl_addresses,
5499 computectl_addresses,
5500 ) {
5501 (None, _, None, None, None) => {
5503 sql_bail!("SIZE option must be specified");
5506 }
5507 (Some(size), availability_zone, billed_as, None, None) => {
5508 if disk.is_some() {
5509 if scx.catalog.is_cluster_size_cc(&size) {
5513 sql_bail!(
5514 "DISK option not supported for modern cluster sizes because disk is always enabled"
5515 );
5516 }
5517
5518 scx.catalog
5519 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated);
5520 }
5521
5522 Ok(ReplicaConfig::Orchestrated {
5523 size,
5524 availability_zone,
5525 compute,
5526 billed_as,
5527 internal,
5528 })
5529 }
5530
5531 (None, None, None, storagectl_addresses, computectl_addresses) => {
5532 scx.require_feature_flag(&vars::UNSAFE_ENABLE_UNORCHESTRATED_CLUSTER_REPLICAS)?;
5533
5534 let Some(storagectl_addrs) = storagectl_addresses else {
5538 sql_bail!("missing STORAGECTL ADDRESSES option");
5539 };
5540 let Some(computectl_addrs) = computectl_addresses else {
5541 sql_bail!("missing COMPUTECTL ADDRESSES option");
5542 };
5543
5544 if storagectl_addrs.len() != computectl_addrs.len() {
5545 sql_bail!(
5546 "COMPUTECTL ADDRESSES and STORAGECTL ADDRESSES must have the same length"
5547 );
5548 }
5549
5550 if disk.is_some() {
5551 sql_bail!("DISK can't be specified for unorchestrated clusters");
5552 }
5553
5554 Ok(ReplicaConfig::Unorchestrated {
5555 storagectl_addrs,
5556 computectl_addrs,
5557 compute,
5558 })
5559 }
5560 _ => {
5561 sql_bail!("invalid mixture of orchestrated and unorchestrated replica options");
5564 }
5565 }
5566}
5567
5568fn plan_compute_replica_config(
5572 introspection_interval: Option<OptionalDuration>,
5573 introspection_debugging: bool,
5574 arrangement_compression: bool,
5575) -> Result<ComputeReplicaConfig, PlanError> {
5576 let introspection_interval = introspection_interval
5577 .map(|OptionalDuration(i)| i)
5578 .unwrap_or(Some(DEFAULT_REPLICA_LOGGING_INTERVAL));
5579 let introspection = match introspection_interval {
5580 Some(interval) => Some(ComputeReplicaIntrospectionConfig {
5581 interval,
5582 debugging: introspection_debugging,
5583 }),
5584 None if introspection_debugging => {
5585 sql_bail!("INTROSPECTION DEBUGGING cannot be specified without INTROSPECTION INTERVAL")
5586 }
5587 None => None,
5588 };
5589 let compute = ComputeReplicaConfig {
5590 introspection,
5591 arrangement_compression,
5592 };
5593 Ok(compute)
5594}
5595
5596fn unplan_compute_replica_config(
5601 compute_replica_config: ComputeReplicaConfig,
5602) -> (Option<OptionalDuration>, bool, bool) {
5603 let ComputeReplicaConfig {
5604 introspection,
5605 arrangement_compression,
5606 } = compute_replica_config;
5607 match introspection {
5608 Some(ComputeReplicaIntrospectionConfig {
5609 debugging,
5610 interval,
5611 }) => (
5612 Some(OptionalDuration(Some(interval))),
5613 debugging,
5614 arrangement_compression,
5615 ),
5616 None => (Some(OptionalDuration(None)), false, arrangement_compression),
5617 }
5618}
5619
5620fn plan_cluster_schedule(
5624 schedule: ClusterScheduleOptionValue,
5625) -> Result<ClusterSchedule, PlanError> {
5626 Ok(match schedule {
5627 ClusterScheduleOptionValue::Manual => ClusterSchedule::Manual,
5628 ClusterScheduleOptionValue::Refresh {
5630 hydration_time_estimate: None,
5631 } => ClusterSchedule::Refresh {
5632 hydration_time_estimate: Duration::from_millis(0),
5633 },
5634 ClusterScheduleOptionValue::Refresh {
5636 hydration_time_estimate: Some(interval_value),
5637 } => {
5638 let interval = Interval::try_from_value(Value::Interval(interval_value))?;
5639 if interval.as_microseconds() < 0 {
5640 sql_bail!(
5641 "HYDRATION TIME ESTIMATE must be non-negative; got: {}",
5642 interval
5643 );
5644 }
5645 if interval.months != 0 {
5646 sql_bail!("HYDRATION TIME ESTIMATE must not involve units larger than days");
5650 }
5651 let duration = interval.duration()?;
5652 if u64::try_from(duration.as_millis()).is_err()
5653 || Interval::from_duration(&duration).is_err()
5654 {
5655 sql_bail!("HYDRATION TIME ESTIMATE too large");
5656 }
5657 ClusterSchedule::Refresh {
5658 hydration_time_estimate: duration,
5659 }
5660 }
5661 })
5662}
5663
5664fn unplan_cluster_schedule(schedule: ClusterSchedule) -> ClusterScheduleOptionValue {
5668 match schedule {
5669 ClusterSchedule::Manual => ClusterScheduleOptionValue::Manual,
5670 ClusterSchedule::Refresh {
5671 hydration_time_estimate,
5672 } => {
5673 let interval = Interval::from_duration(&hydration_time_estimate)
5674 .expect("planning ensured that this is convertible back to Interval");
5675 let interval_value = literal::unplan_interval(&interval);
5676 ClusterScheduleOptionValue::Refresh {
5677 hydration_time_estimate: Some(interval_value),
5678 }
5679 }
5680 }
5681}
5682
5683fn plan_auto_scaling_strategy(
5691 value: ClusterAutoScalingStrategyOptionValue,
5692) -> Result<Option<AutoScalingStrategy>, PlanError> {
5693 let ClusterAutoScalingStrategyOptionValue { on_hydration } = value;
5694 let Some(on_hydration) = on_hydration else {
5695 return Ok(None);
5697 };
5698
5699 let hydration_size = String::try_from_value(on_hydration.hydration_size)?;
5700
5701 let linger_duration = on_hydration
5702 .linger_duration
5703 .map(Duration::try_from_value)
5704 .transpose()?;
5705
5706 Ok(Some(AutoScalingStrategy {
5707 on_hydration: Some(OnHydration {
5708 hydration_size,
5709 linger_duration,
5710 }),
5711 }))
5712}
5713
5714fn validate_auto_scaling_strategy(
5723 strategy: &AutoScalingStrategy,
5724 cluster_size: Option<&str>,
5725 schedule_non_manual: bool,
5726) -> Result<(), PlanError> {
5727 if let (Some(on_hydration), Some(cluster_size)) = (&strategy.on_hydration, cluster_size) {
5728 if on_hydration.hydration_size == cluster_size {
5729 return Err(PlanError::HydrationSizeEqualsClusterSize {
5730 size: cluster_size.to_string(),
5731 });
5732 }
5733 }
5734 if schedule_non_manual {
5735 sql_bail!("AUTO SCALING STRATEGY cannot be combined with a SCHEDULE other than MANUAL");
5736 }
5737 Ok(())
5738}
5739
5740fn unplan_auto_scaling_strategy(
5745 strategy: &AutoScalingStrategy,
5746) -> ClusterAutoScalingStrategyOptionValue {
5747 ClusterAutoScalingStrategyOptionValue {
5748 on_hydration: strategy
5749 .on_hydration
5750 .as_ref()
5751 .map(|on_hydration| OnHydrationOptionValue {
5752 hydration_size: Value::String(on_hydration.hydration_size.clone()),
5753 linger_duration: on_hydration.linger_duration.map(|d| {
5754 let interval = Interval::from_duration(&d)
5755 .expect("planning ensured this is convertible back to Interval");
5756 Value::Interval(literal::unplan_interval(&interval))
5757 }),
5758 }),
5759 }
5760}
5761
5762pub fn describe_create_cluster_replica(
5763 _: &StatementContext,
5764 _: CreateClusterReplicaStatement<Aug>,
5765) -> Result<StatementDesc, PlanError> {
5766 Ok(StatementDesc::new(None))
5767}
5768
5769pub fn plan_create_cluster_replica(
5770 scx: &StatementContext,
5771 CreateClusterReplicaStatement {
5772 definition: ReplicaDefinition { name, options },
5773 of_cluster,
5774 if_not_exists,
5775 }: CreateClusterReplicaStatement<Aug>,
5776) -> Result<Plan, PlanError> {
5777 let cluster = scx
5778 .catalog
5779 .resolve_cluster(Some(&normalize::ident(of_cluster)))?;
5780
5781 let config = plan_replica_config(scx, options)?;
5782
5783 if let ReplicaConfig::Orchestrated { internal: true, .. } = &config {
5784 if MANAGED_REPLICA_PATTERN.is_match(name.as_str()) {
5785 return Err(PlanError::MangedReplicaName(name.into_string()));
5786 }
5787 } else {
5788 ensure_cluster_is_not_managed(scx, cluster.id())?;
5789 }
5790
5791 Ok(Plan::CreateClusterReplica(CreateClusterReplicaPlan {
5792 name: normalize::ident(name),
5793 cluster_id: cluster.id(),
5794 config,
5795 if_not_exists,
5796 }))
5797}
5798
5799pub fn describe_create_secret(
5800 _: &StatementContext,
5801 _: CreateSecretStatement<Aug>,
5802) -> Result<StatementDesc, PlanError> {
5803 Ok(StatementDesc::new(None))
5804}
5805
5806pub fn plan_create_secret(
5807 scx: &StatementContext,
5808 stmt: CreateSecretStatement<Aug>,
5809) -> Result<Plan, PlanError> {
5810 let CreateSecretStatement {
5811 name,
5812 if_not_exists,
5813 value,
5814 } = &stmt;
5815
5816 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name.to_owned())?)?;
5817 let mut create_sql_statement = stmt.clone();
5818 create_sql_statement.value = Expr::Value(Value::String("********".to_string()));
5819 let create_sql =
5820 normalize::create_statement(scx, Statement::CreateSecret(create_sql_statement))?;
5821 let secret_as = query::plan_secret_as(scx, value.clone())?;
5822
5823 let secret = Secret {
5824 create_sql,
5825 secret_as,
5826 };
5827
5828 Ok(Plan::CreateSecret(CreateSecretPlan {
5829 name,
5830 secret,
5831 if_not_exists: *if_not_exists,
5832 }))
5833}
5834
5835pub fn describe_create_connection(
5836 _: &StatementContext,
5837 _: CreateConnectionStatement<Aug>,
5838) -> Result<StatementDesc, PlanError> {
5839 Ok(StatementDesc::new(None))
5840}
5841
5842generate_extracted_config!(CreateConnectionOption, (Validate, bool));
5843
5844pub fn plan_create_connection(
5845 scx: &StatementContext,
5846 mut stmt: CreateConnectionStatement<Aug>,
5847) -> Result<Plan, PlanError> {
5848 let CreateConnectionStatement {
5849 name,
5850 connection_type,
5851 values,
5852 if_not_exists,
5853 with_options,
5854 } = stmt.clone();
5855 let connection_options_extracted = connection::ConnectionOptionExtracted::try_from(values)?;
5856 let details = connection_options_extracted.try_into_connection_details(scx, connection_type)?;
5857 let name = scx.allocate_qualified_name(normalize::unresolved_item_name(name)?)?;
5858
5859 let options = CreateConnectionOptionExtracted::try_from(with_options)?;
5860 if options.validate.is_some() {
5861 scx.require_feature_flag(&vars::ENABLE_CONNECTION_VALIDATION_SYNTAX)?;
5862 }
5863 let validate = match options.validate {
5864 Some(val) => val,
5865 None => {
5866 scx.catalog
5867 .system_vars()
5868 .enable_default_connection_validation()
5869 && details.to_connection().validate_by_default()
5870 }
5871 };
5872
5873 let full_name = scx.catalog.resolve_full_name(&name);
5875 let partial_name = PartialItemName::from(full_name.clone());
5876 if let (false, Ok(item)) = (if_not_exists, scx.catalog.resolve_item(&partial_name)) {
5877 return Err(PlanError::ItemAlreadyExists {
5878 name: full_name.to_string(),
5879 item_type: item.item_type(),
5880 });
5881 }
5882
5883 if let ConnectionDetails::Ssh { key_1, key_2, .. } = &details {
5886 stmt.values.retain(|v| {
5887 v.name != ConnectionOptionName::PublicKey1 && v.name != ConnectionOptionName::PublicKey2
5888 });
5889 stmt.values.push(ConnectionOption {
5890 name: ConnectionOptionName::PublicKey1,
5891 value: Some(WithOptionValue::Value(Value::String(key_1.public_key()))),
5892 });
5893 stmt.values.push(ConnectionOption {
5894 name: ConnectionOptionName::PublicKey2,
5895 value: Some(WithOptionValue::Value(Value::String(key_2.public_key()))),
5896 });
5897 }
5898 let create_sql = normalize::create_statement(scx, Statement::CreateConnection(stmt))?;
5899
5900 let plan = CreateConnectionPlan {
5901 name,
5902 if_not_exists,
5903 connection: crate::plan::Connection {
5904 create_sql,
5905 details,
5906 },
5907 validate,
5908 };
5909 Ok(Plan::CreateConnection(plan))
5910}
5911
5912fn plan_drop_database(
5913 scx: &StatementContext,
5914 if_exists: bool,
5915 name: &UnresolvedDatabaseName,
5916 cascade: bool,
5917) -> Result<Option<DatabaseId>, PlanError> {
5918 Ok(match resolve_database(scx, name, if_exists)? {
5919 Some(database) => {
5920 if !cascade && database.has_schemas() {
5921 sql_bail!(
5922 "database '{}' cannot be dropped with RESTRICT while it contains schemas",
5923 name,
5924 );
5925 }
5926 Some(database.id())
5927 }
5928 None => None,
5929 })
5930}
5931
5932pub fn describe_drop_objects(
5933 _: &StatementContext,
5934 _: DropObjectsStatement,
5935) -> Result<StatementDesc, PlanError> {
5936 Ok(StatementDesc::new(None))
5937}
5938
5939pub fn plan_drop_objects(
5940 scx: &mut StatementContext,
5941 DropObjectsStatement {
5942 object_type,
5943 if_exists,
5944 names,
5945 cascade,
5946 }: DropObjectsStatement,
5947) -> Result<Plan, PlanError> {
5948 if object_type == mz_sql_parser::ast::ObjectType::Func {
5949 bail_unsupported!("DROP FUNCTION");
5950 }
5951 let object_type = object_type.into();
5952
5953 let mut referenced_ids = Vec::new();
5954 for name in names {
5955 let id = match &name {
5956 UnresolvedObjectName::Cluster(name) => {
5957 plan_drop_cluster(scx, if_exists, name, cascade)?.map(ObjectId::Cluster)
5958 }
5959 UnresolvedObjectName::ClusterReplica(name) => {
5960 plan_drop_cluster_replica(scx, if_exists, name)?.map(ObjectId::ClusterReplica)
5961 }
5962 UnresolvedObjectName::Database(name) => {
5963 plan_drop_database(scx, if_exists, name, cascade)?.map(ObjectId::Database)
5964 }
5965 UnresolvedObjectName::Schema(name) => {
5966 plan_drop_schema(scx, if_exists, name, cascade)?.map(ObjectId::Schema)
5967 }
5968 UnresolvedObjectName::Role(name) => {
5969 plan_drop_role(scx, if_exists, name)?.map(ObjectId::Role)
5970 }
5971 UnresolvedObjectName::Item(name) => {
5972 plan_drop_item_name(scx, object_type, if_exists, name.clone())?.map(ObjectId::Item)
5976 }
5977 UnresolvedObjectName::NetworkPolicy(name) => {
5978 plan_drop_network_policy(scx, if_exists, name)?.map(ObjectId::NetworkPolicy)
5979 }
5980 };
5981 match id {
5982 Some(id) => referenced_ids.push(id),
5983 None => scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
5984 name: name.to_ast_string_simple(),
5985 object_type,
5986 }),
5987 }
5988 }
5989
5990 if !cascade {
5994 let dropped_items: BTreeSet<CatalogItemId> = referenced_ids
5995 .iter()
5996 .filter_map(|id| match id {
5997 ObjectId::Item(id) => Some(*id),
5998 _ => None,
5999 })
6000 .collect();
6001 for id in &dropped_items {
6002 let catalog_item = scx.catalog.get_item(id);
6003 ensure_no_blocking_dependents(scx, object_type, catalog_item, &dropped_items)?;
6004 }
6005 }
6006
6007 let drop_ids = scx.catalog.object_dependents(&referenced_ids);
6008
6009 Ok(Plan::DropObjects(DropObjectsPlan {
6010 referenced_ids,
6011 drop_ids,
6012 object_type,
6013 }))
6014}
6015
6016fn plan_drop_schema(
6017 scx: &StatementContext,
6018 if_exists: bool,
6019 name: &UnresolvedSchemaName,
6020 cascade: bool,
6021) -> Result<Option<(ResolvedDatabaseSpecifier, SchemaSpecifier)>, PlanError> {
6022 let normalized = normalize::unresolved_schema_name(name.clone())?;
6026 if normalized.database.is_none() && normalized.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA {
6027 sql_bail!("cannot drop schema {name} because it is a temporary schema",)
6028 }
6029
6030 Ok(match resolve_schema(scx, name.clone(), if_exists)? {
6031 Some((database_spec, schema_spec)) => {
6032 if let ResolvedDatabaseSpecifier::Ambient = database_spec {
6033 sql_bail!(
6034 "cannot drop schema {name} because it is required by the database system",
6035 );
6036 }
6037 if let SchemaSpecifier::Temporary = schema_spec {
6038 sql_bail!("cannot drop schema {name} because it is a temporary schema",)
6039 }
6040 let schema = scx.get_schema(&database_spec, &schema_spec);
6041 if !cascade && schema.has_items() {
6042 let full_schema_name = scx.catalog.resolve_full_schema_name(schema.name());
6043 sql_bail!(
6044 "schema '{}' cannot be dropped without CASCADE while it contains objects",
6045 full_schema_name
6046 );
6047 }
6048 Some((database_spec, schema_spec))
6049 }
6050 None => None,
6051 })
6052}
6053
6054fn plan_drop_role(
6055 scx: &StatementContext,
6056 if_exists: bool,
6057 name: &Ident,
6058) -> Result<Option<RoleId>, PlanError> {
6059 match scx.catalog.resolve_role(name.as_str()) {
6060 Ok(role) => {
6061 let id = role.id();
6062 if &id == scx.catalog.active_role_id() {
6063 sql_bail!("current role cannot be dropped");
6064 }
6065 for role in scx.catalog.get_roles() {
6066 for (member_id, grantor_id) in role.membership() {
6067 if &id == grantor_id {
6068 let member_role = scx.catalog.get_role(member_id);
6069 sql_bail!(
6070 "cannot drop role {}: still depended up by membership of role {} in role {}",
6071 name.as_str(),
6072 role.name(),
6073 member_role.name()
6074 );
6075 }
6076 }
6077 }
6078 Ok(Some(role.id()))
6079 }
6080 Err(_) if if_exists => Ok(None),
6081 Err(e) => Err(e.into()),
6082 }
6083}
6084
6085fn plan_drop_cluster(
6086 scx: &StatementContext,
6087 if_exists: bool,
6088 name: &Ident,
6089 cascade: bool,
6090) -> Result<Option<ClusterId>, PlanError> {
6091 Ok(match resolve_cluster(scx, name, if_exists)? {
6092 Some(cluster) => {
6093 if !cascade && !cluster.bound_objects().is_empty() {
6094 return Err(PlanError::DependentObjectsStillExist {
6095 object_type: "cluster".to_string(),
6096 object_name: cluster.name().to_string(),
6097 dependents: Vec::new(),
6098 });
6099 }
6100 Some(cluster.id())
6101 }
6102 None => None,
6103 })
6104}
6105
6106fn plan_drop_network_policy(
6107 scx: &StatementContext,
6108 if_exists: bool,
6109 name: &Ident,
6110) -> Result<Option<NetworkPolicyId>, PlanError> {
6111 match scx.catalog.resolve_network_policy(name.as_str()) {
6112 Ok(policy) => {
6113 if scx.catalog.system_vars().default_network_policy_name() == policy.name() {
6116 Err(PlanError::NetworkPolicyInUse)
6117 } else {
6118 Ok(Some(policy.id()))
6119 }
6120 }
6121 Err(_) if if_exists => Ok(None),
6122 Err(e) => Err(e.into()),
6123 }
6124}
6125
6126fn plan_drop_cluster_replica(
6127 scx: &StatementContext,
6128 if_exists: bool,
6129 name: &QualifiedReplica,
6130) -> Result<Option<(ClusterId, ReplicaId)>, PlanError> {
6131 let cluster = resolve_cluster_replica(scx, name, if_exists)?;
6132 Ok(cluster.map(|(cluster, replica_id)| (cluster.id(), replica_id)))
6133}
6134
6135fn plan_drop_item(
6137 scx: &StatementContext,
6138 object_type: ObjectType,
6139 if_exists: bool,
6140 name: UnresolvedItemName,
6141 cascade: bool,
6142) -> Result<Option<CatalogItemId>, PlanError> {
6143 let Some(id) = plan_drop_item_name(scx, object_type, if_exists, name)? else {
6144 return Ok(None);
6145 };
6146 if !cascade {
6147 let catalog_item = scx.catalog.get_item(&id);
6148 ensure_no_blocking_dependents(scx, object_type, catalog_item, &BTreeSet::new())?;
6149 }
6150 Ok(Some(id))
6151}
6152
6153fn plan_drop_item_name(
6157 scx: &StatementContext,
6158 object_type: ObjectType,
6159 if_exists: bool,
6160 name: UnresolvedItemName,
6161) -> Result<Option<CatalogItemId>, PlanError> {
6162 let resolved = match resolve_item_or_type(scx, object_type, name, if_exists) {
6163 Ok(r) => r,
6164 Err(PlanError::MismatchedObjectType {
6166 name,
6167 is_type: ObjectType::MaterializedView,
6168 expected_type: ObjectType::View,
6169 }) => {
6170 return Err(PlanError::DropViewOnMaterializedView(name.to_string()));
6171 }
6172 e => e?,
6173 };
6174
6175 Ok(match resolved {
6176 Some(catalog_item) => {
6177 if catalog_item.id().is_system() {
6178 sql_bail!(
6179 "cannot drop {} {} because it is required by the database system",
6180 catalog_item.item_type(),
6181 scx.catalog.minimal_qualification(catalog_item.name()),
6182 );
6183 }
6184 Some(catalog_item.id())
6185 }
6186 None => None,
6187 })
6188}
6189
6190fn ensure_no_blocking_dependents(
6195 scx: &StatementContext,
6196 object_type: ObjectType,
6197 catalog_item: &dyn CatalogItem,
6198 also_dropped: &BTreeSet<CatalogItemId>,
6199) -> Result<(), PlanError> {
6200 for id in catalog_item.used_by() {
6201 if also_dropped.contains(id) {
6202 continue;
6203 }
6204 let dep = scx.catalog.get_item(id);
6205 if dependency_prevents_drop(object_type, dep) {
6206 return Err(PlanError::DependentObjectsStillExist {
6207 object_type: catalog_item.item_type().to_string(),
6208 object_name: scx
6209 .catalog
6210 .minimal_qualification(catalog_item.name())
6211 .to_string(),
6212 dependents: vec![(
6213 dep.item_type().to_string(),
6214 scx.catalog.minimal_qualification(dep.name()).to_string(),
6215 )],
6216 });
6217 }
6218 }
6219 Ok(())
6222}
6223
6224fn dependency_prevents_drop(object_type: ObjectType, dep: &dyn CatalogItem) -> bool {
6226 match object_type {
6227 ObjectType::Type => true,
6228 ObjectType::Table
6229 | ObjectType::View
6230 | ObjectType::MaterializedView
6231 | ObjectType::Source
6232 | ObjectType::Sink
6233 | ObjectType::MetricSink
6234 | ObjectType::Index
6235 | ObjectType::Role
6236 | ObjectType::Cluster
6237 | ObjectType::ClusterReplica
6238 | ObjectType::Secret
6239 | ObjectType::Connection
6240 | ObjectType::Database
6241 | ObjectType::Schema
6242 | ObjectType::Func
6243 | ObjectType::NetworkPolicy => match dep.item_type() {
6244 CatalogItemType::Func
6245 | CatalogItemType::Table
6246 | CatalogItemType::Source
6247 | CatalogItemType::View
6248 | CatalogItemType::MaterializedView
6249 | CatalogItemType::Sink
6250 | CatalogItemType::MetricSink
6251 | CatalogItemType::Type
6252 | CatalogItemType::Secret
6253 | CatalogItemType::Connection => true,
6254 CatalogItemType::Index => false,
6255 },
6256 }
6257}
6258
6259pub fn describe_alter_index_options(
6260 _: &StatementContext,
6261 _: AlterIndexStatement<Aug>,
6262) -> Result<StatementDesc, PlanError> {
6263 Ok(StatementDesc::new(None))
6264}
6265
6266pub fn describe_drop_owned(
6267 _: &StatementContext,
6268 _: DropOwnedStatement<Aug>,
6269) -> Result<StatementDesc, PlanError> {
6270 Ok(StatementDesc::new(None))
6271}
6272
6273pub fn plan_drop_owned(
6274 scx: &StatementContext,
6275 drop: DropOwnedStatement<Aug>,
6276) -> Result<Plan, PlanError> {
6277 let cascade = drop.cascade();
6278 let role_ids: BTreeSet<_> = drop.role_names.into_iter().map(|role| role.id).collect();
6279 let mut drop_ids = Vec::new();
6280 let mut privilege_revokes = Vec::new();
6281 let mut default_privilege_revokes = Vec::new();
6282
6283 fn update_privilege_revokes(
6284 object_id: SystemObjectId,
6285 privileges: &PrivilegeMap,
6286 role_ids: &BTreeSet<RoleId>,
6287 privilege_revokes: &mut Vec<(SystemObjectId, MzAclItem)>,
6288 ) {
6289 privilege_revokes.extend(iter::zip(
6290 iter::repeat(object_id),
6291 privileges
6292 .all_values()
6293 .filter(|privilege| role_ids.contains(&privilege.grantee))
6294 .cloned(),
6295 ));
6296 }
6297
6298 for replica in scx.catalog.get_cluster_replicas() {
6300 if role_ids.contains(&replica.owner_id()) {
6301 drop_ids.push((replica.cluster_id(), replica.replica_id()).into());
6302 }
6303 }
6304
6305 for cluster in scx.catalog.get_clusters() {
6307 if role_ids.contains(&cluster.owner_id()) {
6308 if !cascade {
6310 let non_owned_bound_objects: Vec<_> = cluster
6311 .bound_objects()
6312 .into_iter()
6313 .map(|item_id| scx.catalog.get_item(item_id))
6314 .filter(|item| !role_ids.contains(&item.owner_id()))
6315 .collect();
6316 if !non_owned_bound_objects.is_empty() {
6317 let names: Vec<_> = non_owned_bound_objects
6318 .into_iter()
6319 .map(|item| {
6320 (
6321 item.item_type().to_string(),
6322 scx.catalog.resolve_full_name(item.name()).to_string(),
6323 )
6324 })
6325 .collect();
6326 return Err(PlanError::DependentObjectsStillExist {
6327 object_type: "cluster".to_string(),
6328 object_name: cluster.name().to_string(),
6329 dependents: names,
6330 });
6331 }
6332 }
6333 drop_ids.push(cluster.id().into());
6334 }
6335 update_privilege_revokes(
6336 SystemObjectId::Object(cluster.id().into()),
6337 cluster.privileges(),
6338 &role_ids,
6339 &mut privilege_revokes,
6340 );
6341 }
6342
6343 for item in scx.catalog.get_items() {
6345 if role_ids.contains(&item.owner_id()) {
6346 if !cascade {
6347 let check_if_dependents_exist = |used_by: &[CatalogItemId]| {
6349 let non_owned_dependencies: Vec<_> = used_by
6350 .into_iter()
6351 .map(|item_id| scx.catalog.get_item(item_id))
6352 .filter(|item| dependency_prevents_drop(item.item_type().into(), *item))
6353 .filter(|item| !role_ids.contains(&item.owner_id()))
6354 .collect();
6355 if !non_owned_dependencies.is_empty() {
6356 let names: Vec<_> = non_owned_dependencies
6357 .into_iter()
6358 .map(|item| {
6359 let item_typ = item.item_type().to_string();
6360 let item_name =
6361 scx.catalog.resolve_full_name(item.name()).to_string();
6362 (item_typ, item_name)
6363 })
6364 .collect();
6365 Err(PlanError::DependentObjectsStillExist {
6366 object_type: item.item_type().to_string(),
6367 object_name: scx
6368 .catalog
6369 .resolve_full_name(item.name())
6370 .to_string()
6371 .to_string(),
6372 dependents: names,
6373 })
6374 } else {
6375 Ok(())
6376 }
6377 };
6378
6379 if let Some(id) = item.progress_id() {
6382 let progress_item = scx.catalog.get_item(&id);
6383 check_if_dependents_exist(progress_item.used_by())?;
6384 }
6385 check_if_dependents_exist(item.used_by())?;
6386 }
6387 drop_ids.push(item.id().into());
6388 }
6389 update_privilege_revokes(
6390 SystemObjectId::Object(item.id().into()),
6391 item.privileges(),
6392 &role_ids,
6393 &mut privilege_revokes,
6394 );
6395 }
6396
6397 for schema in scx.catalog.get_schemas() {
6399 if !schema.id().is_temporary() {
6400 if role_ids.contains(&schema.owner_id()) {
6401 if !cascade {
6402 let non_owned_dependencies: Vec<_> = schema
6403 .item_ids()
6404 .map(|item_id| scx.catalog.get_item(&item_id))
6405 .filter(|item| dependency_prevents_drop(item.item_type().into(), *item))
6406 .filter(|item| !role_ids.contains(&item.owner_id()))
6407 .collect();
6408 if !non_owned_dependencies.is_empty() {
6409 let full_schema_name = scx.catalog.resolve_full_schema_name(schema.name());
6410 sql_bail!(
6411 "schema {} cannot be dropped without CASCADE while it contains non-owned objects",
6412 full_schema_name.to_string().quoted()
6413 );
6414 }
6415 }
6416 drop_ids.push((*schema.database(), *schema.id()).into())
6417 }
6418 update_privilege_revokes(
6419 SystemObjectId::Object((*schema.database(), *schema.id()).into()),
6420 schema.privileges(),
6421 &role_ids,
6422 &mut privilege_revokes,
6423 );
6424 }
6425 }
6426
6427 for database in scx.catalog.get_databases() {
6429 if role_ids.contains(&database.owner_id()) {
6430 if !cascade {
6431 let non_owned_schemas: Vec<_> = database
6432 .schemas()
6433 .into_iter()
6434 .filter(|schema| !role_ids.contains(&schema.owner_id()))
6435 .collect();
6436 if !non_owned_schemas.is_empty() {
6437 sql_bail!(
6438 "database {} cannot be dropped without CASCADE while it contains non-owned schemas",
6439 database.name().quoted(),
6440 );
6441 }
6442 }
6443 drop_ids.push(database.id().into());
6444 }
6445 update_privilege_revokes(
6446 SystemObjectId::Object(database.id().into()),
6447 database.privileges(),
6448 &role_ids,
6449 &mut privilege_revokes,
6450 );
6451 }
6452
6453 for network_policy in scx.catalog.get_network_policies() {
6455 if role_ids.contains(&network_policy.owner_id()) {
6456 drop_ids.push(ObjectId::NetworkPolicy(network_policy.id()));
6457 }
6458 update_privilege_revokes(
6459 SystemObjectId::Object(ObjectId::NetworkPolicy(network_policy.id())),
6460 network_policy.privileges(),
6461 &role_ids,
6462 &mut privilege_revokes,
6463 );
6464 }
6465
6466 update_privilege_revokes(
6468 SystemObjectId::System,
6469 scx.catalog.get_system_privileges(),
6470 &role_ids,
6471 &mut privilege_revokes,
6472 );
6473
6474 for (default_privilege_object, default_privilege_acl_items) in
6475 scx.catalog.get_default_privileges()
6476 {
6477 for default_privilege_acl_item in default_privilege_acl_items {
6478 if role_ids.contains(&default_privilege_object.role_id)
6479 || role_ids.contains(&default_privilege_acl_item.grantee)
6480 {
6481 default_privilege_revokes.push((
6482 default_privilege_object.clone(),
6483 default_privilege_acl_item.clone(),
6484 ));
6485 }
6486 }
6487 }
6488
6489 let drop_ids = scx.catalog.object_dependents(&drop_ids);
6490
6491 let system_ids: Vec<_> = drop_ids.iter().filter(|id| id.is_system()).collect();
6492 if !system_ids.is_empty() {
6493 let mut owners = system_ids
6494 .into_iter()
6495 .filter_map(|object_id| scx.catalog.get_owner_id(object_id))
6496 .collect::<BTreeSet<_>>()
6497 .into_iter()
6498 .map(|role_id| scx.catalog.get_role(&role_id).name().quoted());
6499 sql_bail!(
6500 "cannot drop objects owned by role {} because they are required by the database system",
6501 owners.join(", "),
6502 );
6503 }
6504
6505 Ok(Plan::DropOwned(DropOwnedPlan {
6506 role_ids: role_ids.into_iter().collect(),
6507 drop_ids,
6508 privilege_revokes,
6509 default_privilege_revokes,
6510 }))
6511}
6512
6513fn plan_retain_history_option(
6514 scx: &StatementContext,
6515 retain_history: Option<OptionalDuration>,
6516) -> Result<Option<CompactionWindow>, PlanError> {
6517 if let Some(OptionalDuration(lcw)) = retain_history {
6518 Ok(Some(plan_retain_history(scx, lcw)?))
6519 } else {
6520 Ok(None)
6521 }
6522}
6523
6524fn plan_retain_history(
6530 scx: &StatementContext,
6531 lcw: Option<Duration>,
6532) -> Result<CompactionWindow, PlanError> {
6533 scx.require_feature_flag(&vars::ENABLE_LOGICAL_COMPACTION_WINDOW)?;
6534 match lcw {
6535 Some(Duration::ZERO) => Err(PlanError::InvalidOptionValue {
6540 option_name: "RETAIN HISTORY".to_string(),
6541 err: Box::new(PlanError::Unstructured(
6542 "internal error: unexpectedly zero".to_string(),
6543 )),
6544 }),
6545 Some(duration) => {
6546 if duration < DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION
6549 && scx
6550 .require_feature_flag(&vars::ENABLE_UNLIMITED_RETAIN_HISTORY)
6551 .is_err()
6552 {
6553 return Err(PlanError::RetainHistoryLow {
6554 limit: DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION,
6555 });
6556 }
6557 Ok(duration.try_into()?)
6558 }
6559 None => {
6562 if scx
6563 .require_feature_flag(&vars::ENABLE_UNLIMITED_RETAIN_HISTORY)
6564 .is_err()
6565 {
6566 Err(PlanError::RetainHistoryRequired)
6567 } else {
6568 Ok(CompactionWindow::DisableCompaction)
6569 }
6570 }
6571 }
6572}
6573
6574generate_extracted_config!(IndexOption, (RetainHistory, OptionalDuration));
6575
6576fn plan_index_options(
6577 scx: &StatementContext,
6578 with_opts: Vec<IndexOption<Aug>>,
6579) -> Result<Vec<crate::plan::IndexOption>, PlanError> {
6580 if !with_opts.is_empty() {
6581 scx.require_feature_flag(&vars::ENABLE_INDEX_OPTIONS)?;
6583 }
6584
6585 let IndexOptionExtracted { retain_history, .. }: IndexOptionExtracted = with_opts.try_into()?;
6586
6587 let mut out = Vec::with_capacity(1);
6588 if let Some(cw) = plan_retain_history_option(scx, retain_history)? {
6589 out.push(crate::plan::IndexOption::RetainHistory(cw));
6590 }
6591 Ok(out)
6592}
6593
6594generate_extracted_config!(
6595 TableOption,
6596 (PartitionBy, Vec<Ident>),
6597 (RetainHistory, OptionalDuration),
6598 (RedactedTest, String)
6599);
6600
6601fn plan_table_options(
6602 scx: &StatementContext,
6603 desc: &RelationDesc,
6604 with_opts: Vec<TableOption<Aug>>,
6605) -> Result<Vec<crate::plan::TableOption>, PlanError> {
6606 let TableOptionExtracted {
6607 partition_by,
6608 retain_history,
6609 redacted_test,
6610 ..
6611 }: TableOptionExtracted = with_opts.try_into()?;
6612
6613 if let Some(partition_by) = partition_by {
6614 scx.require_feature_flag(&ENABLE_COLLECTION_PARTITION_BY)?;
6615 check_partition_by(desc, partition_by)?;
6616 }
6617
6618 if redacted_test.is_some() {
6619 scx.require_feature_flag(&vars::ENABLE_REDACTED_TEST_OPTION)?;
6620 }
6621
6622 let mut out = Vec::with_capacity(1);
6623 if let Some(cw) = plan_retain_history_option(scx, retain_history)? {
6624 out.push(crate::plan::TableOption::RetainHistory(cw));
6625 }
6626 Ok(out)
6627}
6628
6629pub fn plan_alter_index_options(
6630 scx: &mut StatementContext,
6631 AlterIndexStatement {
6632 index_name,
6633 if_exists,
6634 action,
6635 }: AlterIndexStatement<Aug>,
6636) -> Result<Plan, PlanError> {
6637 let object_type = ObjectType::Index;
6638 match action {
6639 AlterIndexAction::ResetOptions(options) => {
6640 let mut options = options.into_iter();
6641 if let Some(opt) = options.next() {
6642 match opt {
6643 IndexOptionName::RetainHistory => {
6644 if options.next().is_some() {
6645 sql_bail!("RETAIN HISTORY must be only option");
6646 }
6647 return alter_retain_history(
6648 scx,
6649 object_type,
6650 if_exists,
6651 UnresolvedObjectName::Item(index_name),
6652 None,
6653 );
6654 }
6655 }
6656 }
6657 sql_bail!("expected option");
6658 }
6659 AlterIndexAction::SetOptions(options) => {
6660 let mut options = options.into_iter();
6661 if let Some(opt) = options.next() {
6662 match opt.name {
6663 IndexOptionName::RetainHistory => {
6664 if options.next().is_some() {
6665 sql_bail!("RETAIN HISTORY must be only option");
6666 }
6667 return alter_retain_history(
6668 scx,
6669 object_type,
6670 if_exists,
6671 UnresolvedObjectName::Item(index_name),
6672 opt.value,
6673 );
6674 }
6675 }
6676 }
6677 sql_bail!("expected option");
6678 }
6679 }
6680}
6681
6682pub fn describe_alter_cluster_set_options(
6683 _: &StatementContext,
6684 _: AlterClusterStatement<Aug>,
6685) -> Result<StatementDesc, PlanError> {
6686 Ok(StatementDesc::new(None))
6687}
6688
6689pub fn plan_alter_cluster(
6690 scx: &mut StatementContext,
6691 AlterClusterStatement {
6692 name,
6693 action,
6694 if_exists,
6695 }: AlterClusterStatement<Aug>,
6696) -> Result<Plan, PlanError> {
6697 let cluster = match resolve_cluster(scx, &name, if_exists)? {
6698 Some(entry) => entry,
6699 None => {
6700 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
6701 name: name.to_ast_string_simple(),
6702 object_type: ObjectType::Cluster,
6703 });
6704
6705 return Ok(Plan::AlterNoop(AlterNoopPlan {
6706 object_type: ObjectType::Cluster,
6707 }));
6708 }
6709 };
6710
6711 let mut options: PlanClusterOption = Default::default();
6712 let mut alter_strategy: AlterClusterPlanStrategy = AlterClusterPlanStrategy::None;
6713
6714 match action {
6715 AlterClusterAction::SetOptions {
6716 options: set_options,
6717 with_options,
6718 } => {
6719 let ClusterOptionExtracted {
6720 auto_scaling_strategy,
6721 availability_zones,
6722 experimental_arrangement_compression,
6723 introspection_debugging,
6724 introspection_interval,
6725 managed,
6726 replicas: replica_defs,
6727 replication_factor,
6728 seen: _,
6729 size,
6730 disk,
6731 schedule,
6732 workload_class,
6733 }: ClusterOptionExtracted = set_options.try_into()?;
6734
6735 if !scx.catalog.active_role_id().is_system() {
6736 if workload_class.is_some() {
6737 sql_bail!("WORKLOAD CLASS not supported for non-system users");
6738 }
6739 }
6740
6741 match managed.unwrap_or_else(|| cluster.is_managed()) {
6742 true => {
6743 let alter_strategy_extracted =
6744 ClusterAlterOptionExtracted::try_from(with_options)?;
6745 alter_strategy = AlterClusterPlanStrategy::try_from(alter_strategy_extracted)?;
6746
6747 if !matches!(alter_strategy, AlterClusterPlanStrategy::None)
6751 && size.is_none()
6752 && availability_zones.is_none()
6753 && introspection_debugging.is_none()
6754 && introspection_interval.is_none()
6755 && experimental_arrangement_compression.is_none()
6756 {
6757 sql_bail!(
6758 "WAIT can only be used together with a SIZE, AVAILABILITY ZONES, \
6759 INTROSPECTION, or EXPERIMENTAL ARRANGEMENT COMPRESSION change"
6760 );
6761 }
6762
6763 if replica_defs.is_some() {
6764 sql_bail!("REPLICAS not supported for managed clusters");
6765 }
6766 if schedule.is_some()
6767 && !matches!(schedule, Some(ClusterScheduleOptionValue::Manual))
6768 {
6769 scx.require_feature_flag(&ENABLE_CLUSTER_SCHEDULE_REFRESH)?;
6770
6771 if replication_factor.is_none()
6780 && cluster.replication_factor().is_some_and(|rf| rf > 1)
6781 {
6782 sql_bail!(
6783 "SCHEDULE cannot be set to anything other than MANUAL while the \
6784 cluster's REPLICATION FACTOR is greater than 1; \
6785 set the REPLICATION FACTOR to 1 first"
6786 );
6787 }
6788 }
6789
6790 if replication_factor.is_some() {
6791 if schedule.is_some()
6792 && !matches!(schedule, Some(ClusterScheduleOptionValue::Manual))
6793 {
6794 sql_bail!(
6795 "REPLICATION FACTOR cannot be given together with any SCHEDULE other than MANUAL"
6796 );
6797 }
6798 if let Some(current_schedule) = cluster.schedule() {
6799 if !matches!(current_schedule, ClusterSchedule::Manual) {
6800 sql_bail!(
6801 "REPLICATION FACTOR cannot be set if the cluster SCHEDULE is anything other than MANUAL"
6802 );
6803 }
6804 }
6805 }
6806
6807 if let Some(value) = auto_scaling_strategy {
6808 scx.require_feature_flag(&ENABLE_AUTO_SCALING_STRATEGY)?;
6809 let strategy = plan_auto_scaling_strategy(value)?;
6810 options.auto_scaling_strategy = AlterOptionParameter::Set(strategy);
6811 }
6812
6813 let effective_strategy = match &options.auto_scaling_strategy {
6817 AlterOptionParameter::Set(s) => s.clone(),
6818 AlterOptionParameter::Reset => None,
6819 AlterOptionParameter::Unchanged => cluster.auto_scaling_strategy().cloned(),
6820 };
6821 if let Some(effective_strategy) = &effective_strategy {
6822 let effective_size = size.as_deref().or_else(|| cluster.managed_size());
6823 let schedule_non_manual = match &schedule {
6824 Some(s) => !matches!(s, ClusterScheduleOptionValue::Manual),
6825 None => cluster
6826 .schedule()
6827 .is_some_and(|s| !matches!(s, ClusterSchedule::Manual)),
6828 };
6829 validate_auto_scaling_strategy(
6830 effective_strategy,
6831 effective_size,
6832 schedule_non_manual,
6833 )?;
6834 }
6835 }
6836 false => {
6837 if !with_options.is_empty() {
6838 sql_bail!("ALTER... WITH not supported for unmanaged clusters");
6839 }
6840 if auto_scaling_strategy.is_some() {
6841 sql_bail!("AUTO SCALING STRATEGY not supported for unmanaged clusters");
6842 }
6843 if availability_zones.is_some() {
6844 sql_bail!("AVAILABILITY ZONES not supported for unmanaged clusters");
6845 }
6846 if replication_factor.is_some() {
6847 sql_bail!("REPLICATION FACTOR not supported for unmanaged clusters");
6848 }
6849 if introspection_debugging.is_some() {
6850 sql_bail!("INTROSPECTION DEBUGGING not supported for unmanaged clusters");
6851 }
6852 if introspection_interval.is_some() {
6853 sql_bail!("INTROSPECTION INTERVAL not supported for unmanaged clusters");
6854 }
6855 if experimental_arrangement_compression.is_some() {
6856 sql_bail!(
6857 "EXPERIMENTAL ARRANGEMENT COMPRESSION not supported for unmanaged clusters"
6858 );
6859 }
6860 if size.is_some() {
6861 sql_bail!("SIZE not supported for unmanaged clusters");
6862 }
6863 if disk.is_some() {
6864 sql_bail!("DISK not supported for unmanaged clusters");
6865 }
6866 if schedule.is_some()
6867 && !matches!(schedule, Some(ClusterScheduleOptionValue::Manual))
6868 {
6869 sql_bail!(
6870 "cluster schedules other than MANUAL are not supported for unmanaged clusters"
6871 );
6872 }
6873 if let Some(current_schedule) = cluster.schedule() {
6874 if !matches!(current_schedule, ClusterSchedule::Manual)
6875 && schedule.is_none()
6876 {
6877 sql_bail!(
6878 "when switching a cluster to unmanaged, if the managed \
6879 cluster's SCHEDULE is anything other than MANUAL, you have to \
6880 explicitly set the SCHEDULE to MANUAL"
6881 );
6882 }
6883 }
6884 }
6885 }
6886
6887 let mut replicas = vec![];
6888 for ReplicaDefinition { name, options } in
6889 replica_defs.into_iter().flat_map(Vec::into_iter)
6890 {
6891 replicas.push((normalize::ident(name), plan_replica_config(scx, options)?));
6892 }
6893
6894 if let Some(managed) = managed {
6895 options.managed = AlterOptionParameter::Set(managed);
6896 }
6897 if let Some(replication_factor) = replication_factor {
6898 options.replication_factor = AlterOptionParameter::Set(replication_factor);
6899 } else if schedule
6900 .as_ref()
6901 .is_some_and(|s| !matches!(s, ClusterScheduleOptionValue::Manual))
6902 && managed != Some(true)
6903 {
6904 options.replication_factor = AlterOptionParameter::Set(0);
6917 }
6918 if let Some(size) = &size {
6919 options.size = AlterOptionParameter::Set(size.clone());
6920 }
6921 if let Some(availability_zones) = availability_zones {
6922 options.availability_zones = AlterOptionParameter::Set(availability_zones);
6923 }
6924 if let Some(introspection_debugging) = introspection_debugging {
6925 options.introspection_debugging =
6926 AlterOptionParameter::Set(introspection_debugging);
6927 }
6928 if let Some(introspection_interval) = introspection_interval {
6929 options.introspection_interval = AlterOptionParameter::Set(introspection_interval);
6930 }
6931 if let Some(experimental_arrangement_compression) = experimental_arrangement_compression
6932 {
6933 options.arrangement_compression =
6934 AlterOptionParameter::Set(experimental_arrangement_compression);
6935 }
6936 if disk.is_some() {
6937 let size = match size.as_deref() {
6941 Some(s) => s,
6942 None => cluster
6943 .managed_size()
6944 .ok_or_else(|| sql_err!("cluster is not managed"))?,
6945 };
6946 if scx.catalog.is_cluster_size_cc(size) {
6947 sql_bail!(
6948 "DISK option not supported for modern cluster sizes because disk is always enabled"
6949 );
6950 }
6951
6952 scx.catalog
6953 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated);
6954 }
6955 if !replicas.is_empty() {
6956 options.replicas = AlterOptionParameter::Set(replicas);
6957 }
6958 if let Some(schedule) = schedule {
6959 options.schedule = AlterOptionParameter::Set(plan_cluster_schedule(schedule)?);
6960 }
6961 if let Some(workload_class) = workload_class {
6962 options.workload_class = AlterOptionParameter::Set(workload_class.0);
6963 }
6964 }
6965 AlterClusterAction::ResetOptions(reset_options) => {
6966 use AlterOptionParameter::Reset;
6967 use ClusterOptionName::*;
6968
6969 if !scx.catalog.active_role_id().is_system() {
6970 if reset_options.contains(&WorkloadClass) {
6971 sql_bail!("WORKLOAD CLASS not supported for non-system users");
6972 }
6973 }
6974
6975 for option in reset_options {
6981 match option {
6982 AutoScalingStrategy => options.auto_scaling_strategy = Reset,
6983 AvailabilityZones => options.availability_zones = Reset,
6984 Disk => scx
6985 .catalog
6986 .add_notice(PlanNotice::ReplicaDiskOptionDeprecated),
6987 IntrospectionInterval => options.introspection_interval = Reset,
6988 IntrospectionDebugging => options.introspection_debugging = Reset,
6989 ExperimentalArrangementCompression => options.arrangement_compression = Reset,
6990 Managed => options.managed = Reset,
6991 Replicas => options.replicas = Reset,
6992 ReplicationFactor => options.replication_factor = Reset,
6993 Size => options.size = Reset,
6994 Schedule => options.schedule = Reset,
6995 WorkloadClass => options.workload_class = Reset,
6996 }
6997 }
6998 }
6999 }
7000 Ok(Plan::AlterCluster(AlterClusterPlan {
7001 id: cluster.id(),
7002 name: cluster.name().to_string(),
7003 options,
7004 strategy: alter_strategy,
7005 }))
7006}
7007
7008pub fn describe_alter_set_cluster(
7009 _: &StatementContext,
7010 _: AlterSetClusterStatement<Aug>,
7011) -> Result<StatementDesc, PlanError> {
7012 Ok(StatementDesc::new(None))
7013}
7014
7015pub fn plan_alter_item_set_cluster(
7016 scx: &StatementContext,
7017 AlterSetClusterStatement {
7018 if_exists,
7019 set_cluster: in_cluster_name,
7020 name,
7021 object_type,
7022 }: AlterSetClusterStatement<Aug>,
7023) -> Result<Plan, PlanError> {
7024 scx.require_feature_flag(&vars::ENABLE_ALTER_SET_CLUSTER)?;
7025
7026 let object_type = object_type.into();
7027
7028 match object_type {
7030 ObjectType::MaterializedView => {}
7031 ObjectType::Index | ObjectType::Sink | ObjectType::MetricSink | ObjectType::Source => {
7032 bail_unsupported!(29606, format!("ALTER {object_type} SET CLUSTER"))
7033 }
7034 ObjectType::Table
7035 | ObjectType::View
7036 | ObjectType::Type
7037 | ObjectType::Role
7038 | ObjectType::Cluster
7039 | ObjectType::ClusterReplica
7040 | ObjectType::Secret
7041 | ObjectType::Connection
7042 | ObjectType::Database
7043 | ObjectType::Schema
7044 | ObjectType::Func
7045 | ObjectType::NetworkPolicy => {
7046 bail_never_supported!(
7047 format!("ALTER {object_type} SET CLUSTER"),
7048 "sql/alter-set-cluster/",
7049 format!("{object_type} has no associated cluster")
7050 )
7051 }
7052 }
7053
7054 let in_cluster = scx.catalog.get_cluster(in_cluster_name.id);
7055
7056 match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
7057 Some(entry) => {
7058 let current_cluster = entry.cluster_id();
7059 let Some(current_cluster) = current_cluster else {
7060 sql_bail!("No cluster associated with {name}");
7061 };
7062
7063 if current_cluster == in_cluster.id() {
7064 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7065 } else {
7066 Ok(Plan::AlterSetCluster(AlterSetClusterPlan {
7067 id: entry.id(),
7068 set_cluster: in_cluster.id(),
7069 }))
7070 }
7071 }
7072 None => {
7073 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7074 name: name.to_ast_string_simple(),
7075 object_type,
7076 });
7077
7078 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7079 }
7080 }
7081}
7082
7083pub fn describe_alter_object_rename(
7084 _: &StatementContext,
7085 _: AlterObjectRenameStatement,
7086) -> Result<StatementDesc, PlanError> {
7087 Ok(StatementDesc::new(None))
7088}
7089
7090pub fn plan_alter_object_rename(
7091 scx: &mut StatementContext,
7092 AlterObjectRenameStatement {
7093 name,
7094 object_type,
7095 to_item_name,
7096 if_exists,
7097 }: AlterObjectRenameStatement,
7098) -> Result<Plan, PlanError> {
7099 let object_type = object_type.into();
7100 match (object_type, name) {
7101 (
7102 ObjectType::View
7103 | ObjectType::MaterializedView
7104 | ObjectType::Table
7105 | ObjectType::Source
7106 | ObjectType::Index
7107 | ObjectType::Sink
7108 | ObjectType::Secret
7109 | ObjectType::Connection,
7110 UnresolvedObjectName::Item(name),
7111 ) => plan_alter_item_rename(scx, object_type, name, to_item_name, if_exists),
7112 (ObjectType::Cluster, UnresolvedObjectName::Cluster(name)) => {
7113 plan_alter_cluster_rename(scx, object_type, name, to_item_name, if_exists)
7114 }
7115 (ObjectType::ClusterReplica, UnresolvedObjectName::ClusterReplica(name)) => {
7116 plan_alter_cluster_replica_rename(scx, object_type, name, to_item_name, if_exists)
7117 }
7118 (ObjectType::Schema, UnresolvedObjectName::Schema(name)) => {
7119 plan_alter_schema_rename(scx, name, to_item_name, if_exists)
7120 }
7121 (object_type, name) => {
7122 bail_internal!("invalid object type '{object_type}' for ALTER RENAME with name {name}")
7125 }
7126 }
7127}
7128
7129pub fn plan_alter_schema_rename(
7130 scx: &mut StatementContext,
7131 name: UnresolvedSchemaName,
7132 to_schema_name: Ident,
7133 if_exists: bool,
7134) -> Result<Plan, PlanError> {
7135 let normalized = normalize::unresolved_schema_name(name.clone())?;
7139 if normalized.database.is_none() && normalized.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA {
7140 sql_bail!(
7141 "cannot rename schemas in the ambient database: {:?}",
7142 mz_repr::namespaces::MZ_TEMP_SCHEMA
7143 );
7144 }
7145
7146 let Some((db_spec, schema_spec)) = resolve_schema(scx, name.clone(), if_exists)? else {
7147 let object_type = ObjectType::Schema;
7148 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7149 name: name.to_ast_string_simple(),
7150 object_type,
7151 });
7152 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7153 };
7154
7155 if scx
7157 .resolve_schema_in_database(&db_spec, &to_schema_name)
7158 .is_ok()
7159 {
7160 return Err(PlanError::Catalog(CatalogError::SchemaAlreadyExists(
7161 to_schema_name.clone().into_string(),
7162 )));
7163 }
7164
7165 let schema = scx.catalog.get_schema(&db_spec, &schema_spec);
7167 if schema.id().is_system() {
7168 bail_never_supported!(format!("renaming the {} schema", schema.name().schema))
7169 }
7170
7171 Ok(Plan::AlterSchemaRename(AlterSchemaRenamePlan {
7172 cur_schema_spec: (db_spec, schema_spec),
7173 new_schema_name: to_schema_name.into_string(),
7174 }))
7175}
7176
7177pub fn plan_alter_schema_swap<F>(
7178 scx: &mut StatementContext,
7179 name_a: UnresolvedSchemaName,
7180 name_b: Ident,
7181 if_exists: bool,
7182 gen_temp_suffix: F,
7183) -> Result<Plan, PlanError>
7184where
7185 F: Fn(&dyn Fn(&str) -> bool) -> Result<String, PlanError>,
7186{
7187 let normalized_a = normalize::unresolved_schema_name(name_a.clone())?;
7191 if normalized_a.database.is_none() && normalized_a.schema == mz_repr::namespaces::MZ_TEMP_SCHEMA
7192 {
7193 sql_bail!("cannot swap schemas that are in the ambient database");
7194 }
7195 let name_b_str = normalize::ident_ref(&name_b);
7197 if name_b_str == mz_repr::namespaces::MZ_TEMP_SCHEMA {
7198 sql_bail!("cannot swap schemas that are in the ambient database");
7199 }
7200
7201 let schema_a = match scx.resolve_schema(name_a.clone()) {
7202 Ok(schema) => schema,
7203 Err(_) if if_exists => {
7204 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7205 name: name_a.to_ast_string_simple(),
7206 object_type: ObjectType::Schema,
7207 });
7208 return Ok(Plan::AlterNoop(AlterNoopPlan {
7209 object_type: ObjectType::Schema,
7210 }));
7211 }
7212 Err(e) => return Err(e),
7213 };
7214
7215 let db_spec = schema_a.database().clone();
7216 if matches!(db_spec, ResolvedDatabaseSpecifier::Ambient) {
7217 sql_bail!("cannot swap schemas that are in the ambient database");
7218 };
7219 let schema_b = scx.resolve_schema_in_database(&db_spec, &name_b)?;
7220
7221 if schema_a.id().is_system() || schema_b.id().is_system() {
7223 bail_never_supported!("swapping a system schema".to_string())
7224 }
7225
7226 const SCHEMA_SWAP_PREFIX: &str = "mz_schema_swap_";
7230 let check = |temp_suffix: &str| {
7231 let mut temp_name = ident!(SCHEMA_SWAP_PREFIX);
7232 temp_name.append_lossy(temp_suffix);
7233 scx.resolve_schema_in_database(&db_spec, &temp_name)
7234 .is_err()
7235 };
7236 let temp_suffix = gen_temp_suffix(&check)?;
7237 let name_temp = format!("{SCHEMA_SWAP_PREFIX}{temp_suffix}");
7238
7239 Ok(Plan::AlterSchemaSwap(AlterSchemaSwapPlan {
7240 schema_a_spec: (*schema_a.database(), *schema_a.id()),
7241 schema_a_name: schema_a.name().schema.to_string(),
7242 schema_b_spec: (*schema_b.database(), *schema_b.id()),
7243 schema_b_name: schema_b.name().schema.to_string(),
7244 name_temp,
7245 }))
7246}
7247
7248pub fn plan_alter_item_rename(
7249 scx: &mut StatementContext,
7250 object_type: ObjectType,
7251 name: UnresolvedItemName,
7252 to_item_name: Ident,
7253 if_exists: bool,
7254) -> Result<Plan, PlanError> {
7255 let resolved = match resolve_item_or_type(scx, object_type, name.clone(), if_exists) {
7256 Ok(r) => r,
7257 Err(PlanError::MismatchedObjectType {
7259 name,
7260 is_type: ObjectType::MaterializedView,
7261 expected_type: ObjectType::View,
7262 }) => {
7263 return Err(PlanError::AlterViewOnMaterializedView(name.to_string()));
7264 }
7265 e => e?,
7266 };
7267
7268 match resolved {
7269 Some(entry) => {
7270 let full_name = scx.catalog.resolve_full_name(entry.name());
7271 let item_type = entry.item_type();
7272
7273 let proposed_name = QualifiedItemName {
7274 qualifiers: entry.name().qualifiers.clone(),
7275 item: to_item_name.clone().into_string(),
7276 };
7277
7278 let conflicting_type_exists;
7282 let conflicting_item_exists;
7283 if item_type == CatalogItemType::Type {
7284 conflicting_type_exists = scx.catalog.get_type_by_name(&proposed_name).is_some();
7285 conflicting_item_exists = scx
7286 .catalog
7287 .get_item_by_name(&proposed_name)
7288 .map(|item| item.item_type().conflicts_with_type())
7289 .unwrap_or(false);
7290 } else {
7291 conflicting_type_exists = item_type.conflicts_with_type()
7292 && scx.catalog.get_type_by_name(&proposed_name).is_some();
7293 conflicting_item_exists = scx.catalog.get_item_by_name(&proposed_name).is_some();
7294 };
7295 if conflicting_type_exists || conflicting_item_exists {
7296 sql_bail!("catalog item '{}' already exists", to_item_name);
7297 }
7298
7299 Ok(Plan::AlterItemRename(AlterItemRenamePlan {
7300 id: entry.id(),
7301 current_full_name: full_name,
7302 to_name: normalize::ident(to_item_name),
7303 object_type,
7304 }))
7305 }
7306 None => {
7307 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7308 name: name.to_ast_string_simple(),
7309 object_type,
7310 });
7311
7312 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7313 }
7314 }
7315}
7316
7317pub fn plan_alter_cluster_rename(
7318 scx: &mut StatementContext,
7319 object_type: ObjectType,
7320 name: Ident,
7321 to_name: Ident,
7322 if_exists: bool,
7323) -> Result<Plan, PlanError> {
7324 match resolve_cluster(scx, &name, if_exists)? {
7325 Some(entry) => Ok(Plan::AlterClusterRename(AlterClusterRenamePlan {
7326 id: entry.id(),
7327 name: entry.name().to_string(),
7328 to_name: ident(to_name),
7329 })),
7330 None => {
7331 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7332 name: name.to_ast_string_simple(),
7333 object_type,
7334 });
7335
7336 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7337 }
7338 }
7339}
7340
7341pub fn plan_alter_cluster_swap<F>(
7342 scx: &mut StatementContext,
7343 name_a: Ident,
7344 name_b: Ident,
7345 if_exists: bool,
7346 gen_temp_suffix: F,
7347) -> Result<Plan, PlanError>
7348where
7349 F: Fn(&dyn Fn(&str) -> bool) -> Result<String, PlanError>,
7350{
7351 let cluster_a = match scx.resolve_cluster(Some(&name_a)) {
7352 Ok(cluster) => cluster,
7353 Err(_) if if_exists => {
7354 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7355 name: name_a.to_ast_string_simple(),
7356 object_type: ObjectType::Cluster,
7357 });
7358 return Ok(Plan::AlterNoop(AlterNoopPlan {
7359 object_type: ObjectType::Cluster,
7360 }));
7361 }
7362 Err(e) => return Err(e),
7363 };
7364 let cluster_b = scx.resolve_cluster(Some(&name_b))?;
7365
7366 const CLUSTER_SWAP_PREFIX: &str = "mz_cluster_swap_";
7367 let check = |temp_suffix: &str| {
7368 let mut temp_name = ident!(CLUSTER_SWAP_PREFIX);
7369 temp_name.append_lossy(temp_suffix);
7370 match scx.catalog.resolve_cluster(Some(temp_name.as_str())) {
7371 Err(CatalogError::UnknownCluster(_)) => true,
7373 Ok(_) | Err(_) => false,
7375 }
7376 };
7377 let temp_suffix = gen_temp_suffix(&check)?;
7378 let name_temp = format!("{CLUSTER_SWAP_PREFIX}{temp_suffix}");
7379
7380 Ok(Plan::AlterClusterSwap(AlterClusterSwapPlan {
7381 id_a: cluster_a.id(),
7382 id_b: cluster_b.id(),
7383 name_a: name_a.into_string(),
7384 name_b: name_b.into_string(),
7385 name_temp,
7386 }))
7387}
7388
7389pub fn plan_alter_cluster_replica_rename(
7390 scx: &mut StatementContext,
7391 object_type: ObjectType,
7392 name: QualifiedReplica,
7393 to_item_name: Ident,
7394 if_exists: bool,
7395) -> Result<Plan, PlanError> {
7396 match resolve_cluster_replica(scx, &name, if_exists)? {
7397 Some((cluster, replica)) => {
7398 ensure_cluster_is_not_managed(scx, cluster.id())?;
7399 Ok(Plan::AlterClusterReplicaRename(
7400 AlterClusterReplicaRenamePlan {
7401 cluster_id: cluster.id(),
7402 replica_id: replica,
7403 name: QualifiedReplica {
7404 cluster: Ident::new(cluster.name())?,
7405 replica: name.replica,
7406 },
7407 to_name: normalize::ident(to_item_name),
7408 },
7409 ))
7410 }
7411 None => {
7412 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7413 name: name.to_ast_string_simple(),
7414 object_type,
7415 });
7416
7417 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7418 }
7419 }
7420}
7421
7422pub fn describe_alter_object_swap(
7423 _: &StatementContext,
7424 _: AlterObjectSwapStatement,
7425) -> Result<StatementDesc, PlanError> {
7426 Ok(StatementDesc::new(None))
7427}
7428
7429pub fn plan_alter_object_swap(
7430 scx: &mut StatementContext,
7431 stmt: AlterObjectSwapStatement,
7432) -> Result<Plan, PlanError> {
7433 scx.require_feature_flag(&vars::ENABLE_ALTER_SWAP)?;
7434
7435 let AlterObjectSwapStatement {
7436 object_type,
7437 if_exists,
7438 name_a,
7439 name_b,
7440 } = stmt;
7441 let object_type = object_type.into();
7442
7443 let gen_temp_suffix = |check_fn: &dyn Fn(&str) -> bool| {
7445 let mut attempts = 0;
7446 let name_temp = loop {
7447 attempts += 1;
7448 if attempts > 10 {
7449 tracing::warn!("Unable to generate temp id for swapping");
7450 sql_bail!("unable to swap!");
7451 }
7452
7453 let short_id = mz_ore::id_gen::temp_id();
7455 if check_fn(&short_id) {
7456 break short_id;
7457 }
7458 };
7459
7460 Ok(name_temp)
7461 };
7462
7463 match (object_type, name_a, name_b) {
7464 (ObjectType::Schema, UnresolvedObjectName::Schema(name_a), name_b) => {
7465 plan_alter_schema_swap(scx, name_a, name_b, if_exists, gen_temp_suffix)
7466 }
7467 (ObjectType::Cluster, UnresolvedObjectName::Cluster(name_a), name_b) => {
7468 plan_alter_cluster_swap(scx, name_a, name_b, if_exists, gen_temp_suffix)
7469 }
7470 (ObjectType::Schema | ObjectType::Cluster, _, _) => {
7471 bail_internal!("name type does not match object type for ALTER SWAP")
7472 }
7473 (
7474 ObjectType::Table
7475 | ObjectType::View
7476 | ObjectType::MaterializedView
7477 | ObjectType::Source
7478 | ObjectType::Sink
7479 | ObjectType::MetricSink
7480 | ObjectType::Index
7481 | ObjectType::Type
7482 | ObjectType::Role
7483 | ObjectType::ClusterReplica
7484 | ObjectType::Secret
7485 | ObjectType::Connection
7486 | ObjectType::Database
7487 | ObjectType::Func
7488 | ObjectType::NetworkPolicy,
7489 _,
7490 _,
7491 ) => Err(PlanError::Unsupported {
7492 feature: format!("ALTER {object_type} .. SWAP WITH ..."),
7493 discussion_no: None,
7494 }),
7495 }
7496}
7497
7498pub fn describe_alter_retain_history(
7499 _: &StatementContext,
7500 _: AlterRetainHistoryStatement<Aug>,
7501) -> Result<StatementDesc, PlanError> {
7502 Ok(StatementDesc::new(None))
7503}
7504
7505pub fn plan_alter_retain_history(
7506 scx: &StatementContext,
7507 AlterRetainHistoryStatement {
7508 object_type,
7509 if_exists,
7510 name,
7511 history,
7512 }: AlterRetainHistoryStatement<Aug>,
7513) -> Result<Plan, PlanError> {
7514 alter_retain_history(scx, object_type.into(), if_exists, name, history)
7515}
7516
7517fn alter_retain_history(
7518 scx: &StatementContext,
7519 object_type: ObjectType,
7520 if_exists: bool,
7521 name: UnresolvedObjectName,
7522 history: Option<WithOptionValue<Aug>>,
7523) -> Result<Plan, PlanError> {
7524 let name = match (object_type, name) {
7525 (
7526 ObjectType::View
7528 | ObjectType::MaterializedView
7529 | ObjectType::Table
7530 | ObjectType::Source
7531 | ObjectType::Index,
7532 UnresolvedObjectName::Item(name),
7533 ) => name,
7534 (object_type, _) => {
7535 bail_unsupported!(format!("RETAIN HISTORY on {object_type}"))
7536 }
7537 };
7538 match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
7539 Some(entry) => {
7540 let full_name = scx.catalog.resolve_full_name(entry.name());
7541 let item_type = entry.item_type();
7542
7543 if object_type == ObjectType::View && item_type == CatalogItemType::MaterializedView {
7545 return Err(PlanError::AlterViewOnMaterializedView(
7546 full_name.to_string(),
7547 ));
7548 } else if object_type == ObjectType::View {
7549 sql_bail!("{object_type} does not support RETAIN HISTORY")
7550 } else if object_type != item_type {
7551 sql_bail!(
7552 "\"{}\" is a {} not a {}",
7553 full_name,
7554 entry.item_type(),
7555 format!("{object_type}").to_lowercase()
7556 )
7557 }
7558
7559 let (value, lcw) = match &history {
7561 Some(WithOptionValue::RetainHistoryFor(value)) => {
7562 let window = OptionalDuration::try_from_value(value.clone())?;
7563 (Some(value.clone()), window.0)
7564 }
7565 None => (None, Some(DEFAULT_LOGICAL_COMPACTION_WINDOW_DURATION)),
7567 _ => sql_bail!("unexpected value type for RETAIN HISTORY"),
7568 };
7569 let window = plan_retain_history(scx, lcw)?;
7570
7571 Ok(Plan::AlterRetainHistory(AlterRetainHistoryPlan {
7572 id: entry.id(),
7573 value,
7574 window,
7575 object_type,
7576 }))
7577 }
7578 None => {
7579 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7580 name: name.to_ast_string_simple(),
7581 object_type,
7582 });
7583
7584 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7585 }
7586 }
7587}
7588
7589fn alter_source_timestamp_interval(
7590 scx: &StatementContext,
7591 if_exists: bool,
7592 source_name: UnresolvedItemName,
7593 value: Option<WithOptionValue<Aug>>,
7594) -> Result<Plan, PlanError> {
7595 let object_type = ObjectType::Source;
7596 match resolve_item_or_type(scx, object_type, source_name.clone(), if_exists)? {
7597 Some(entry) => {
7598 let full_name = scx.catalog.resolve_full_name(entry.name());
7599 if entry.item_type() != CatalogItemType::Source {
7600 sql_bail!(
7601 "\"{}\" is a {} not a {}",
7602 full_name,
7603 entry.item_type(),
7604 format!("{object_type}").to_lowercase()
7605 )
7606 }
7607
7608 match value {
7609 Some(val) => {
7610 let val = match val {
7611 WithOptionValue::Value(v) => v,
7612 _ => sql_bail!("TIMESTAMP INTERVAL requires an interval value"),
7613 };
7614 let duration = Duration::try_from_value(val.clone())?;
7615
7616 let min = scx.catalog.system_vars().min_timestamp_interval();
7617 let max = scx.catalog.system_vars().max_timestamp_interval();
7618 if duration < min || duration > max {
7619 return Err(PlanError::InvalidTimestampInterval {
7620 min,
7621 max,
7622 requested: duration,
7623 });
7624 }
7625
7626 Ok(Plan::AlterSourceTimestampInterval(
7627 AlterSourceTimestampIntervalPlan {
7628 id: entry.id(),
7629 value: Some(val),
7630 interval: duration,
7631 },
7632 ))
7633 }
7634 None => {
7635 let interval = scx.catalog.system_vars().default_timestamp_interval();
7636 Ok(Plan::AlterSourceTimestampInterval(
7637 AlterSourceTimestampIntervalPlan {
7638 id: entry.id(),
7639 value: None,
7640 interval,
7641 },
7642 ))
7643 }
7644 }
7645 }
7646 None => {
7647 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7648 name: source_name.to_ast_string_simple(),
7649 object_type,
7650 });
7651
7652 Ok(Plan::AlterNoop(AlterNoopPlan { object_type }))
7653 }
7654 }
7655}
7656
7657pub fn describe_alter_secret_options(
7658 _: &StatementContext,
7659 _: AlterSecretStatement<Aug>,
7660) -> Result<StatementDesc, PlanError> {
7661 Ok(StatementDesc::new(None))
7662}
7663
7664pub fn plan_alter_secret(
7665 scx: &mut StatementContext,
7666 stmt: AlterSecretStatement<Aug>,
7667) -> Result<Plan, PlanError> {
7668 let AlterSecretStatement {
7669 name,
7670 if_exists,
7671 value,
7672 } = stmt;
7673 let object_type = ObjectType::Secret;
7674 let id = match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
7675 Some(entry) => entry.id(),
7676 None => {
7677 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7678 name: name.to_string(),
7679 object_type,
7680 });
7681
7682 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7683 }
7684 };
7685
7686 let secret_as = query::plan_secret_as(scx, value)?;
7687
7688 Ok(Plan::AlterSecret(AlterSecretPlan { id, secret_as }))
7689}
7690
7691pub fn describe_alter_connection(
7692 _: &StatementContext,
7693 _: AlterConnectionStatement<Aug>,
7694) -> Result<StatementDesc, PlanError> {
7695 Ok(StatementDesc::new(None))
7696}
7697
7698generate_extracted_config!(AlterConnectionOption, (Validate, bool));
7699
7700pub fn plan_alter_connection(
7701 scx: &StatementContext,
7702 stmt: AlterConnectionStatement<Aug>,
7703) -> Result<Plan, PlanError> {
7704 let AlterConnectionStatement {
7705 name,
7706 if_exists,
7707 actions,
7708 with_options,
7709 } = stmt;
7710 let conn_name = normalize::unresolved_item_name(name)?;
7711 let entry = match scx.catalog.resolve_item(&conn_name) {
7712 Ok(entry) => entry,
7713 Err(_) if if_exists => {
7714 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7715 name: conn_name.to_string(),
7716 object_type: ObjectType::Connection,
7717 });
7718
7719 return Ok(Plan::AlterNoop(AlterNoopPlan {
7720 object_type: ObjectType::Connection,
7721 }));
7722 }
7723 Err(e) => return Err(e.into()),
7724 };
7725
7726 let connection = entry.connection()?;
7727
7728 if actions
7729 .iter()
7730 .any(|action| matches!(action, AlterConnectionAction::RotateKeys))
7731 {
7732 if actions.len() > 1 {
7733 sql_bail!("cannot specify any other actions alongside ALTER CONNECTION...ROTATE KEYS");
7734 }
7735
7736 if !with_options.is_empty() {
7737 sql_bail!(
7738 "ALTER CONNECTION...ROTATE KEYS does not support WITH ({})",
7739 with_options
7740 .iter()
7741 .map(|o| o.to_ast_string_simple())
7742 .join(", ")
7743 );
7744 }
7745
7746 if !matches!(connection, Connection::Ssh(_)) {
7747 sql_bail!(
7748 "{} is not an SSH connection",
7749 scx.catalog.resolve_full_name(entry.name())
7750 )
7751 }
7752
7753 return Ok(Plan::AlterConnection(AlterConnectionPlan {
7754 id: entry.id(),
7755 action: crate::plan::AlterConnectionAction::RotateKeys,
7756 }));
7757 }
7758
7759 let options = AlterConnectionOptionExtracted::try_from(with_options)?;
7760 if options.validate.is_some() {
7761 scx.require_feature_flag(&vars::ENABLE_CONNECTION_VALIDATION_SYNTAX)?;
7762 }
7763
7764 let validate = match options.validate {
7765 Some(val) => val,
7766 None => {
7767 scx.catalog
7768 .system_vars()
7769 .enable_default_connection_validation()
7770 && connection.validate_by_default()
7771 }
7772 };
7773
7774 let connection_type = match connection {
7775 Connection::Aws(_) => CreateConnectionType::Aws,
7776 Connection::AwsPrivatelink(_) => CreateConnectionType::AwsPrivatelink,
7777 Connection::Gcp(_) => CreateConnectionType::Gcp,
7778 Connection::Kafka(_) => CreateConnectionType::Kafka,
7779 Connection::Csr(_) => CreateConnectionType::Csr,
7780 Connection::GlueSchemaRegistry(_) => CreateConnectionType::GlueSchemaRegistry,
7781 Connection::Postgres(_) => CreateConnectionType::Postgres,
7782 Connection::Ssh(_) => CreateConnectionType::Ssh,
7783 Connection::MySql(_) => CreateConnectionType::MySql,
7784 Connection::SqlServer(_) => CreateConnectionType::SqlServer,
7785 Connection::IcebergCatalog(_) => CreateConnectionType::IcebergCatalog,
7786 };
7787
7788 let specified_options: BTreeSet<_> = actions
7790 .iter()
7791 .map(|action: &AlterConnectionAction<Aug>| match action {
7792 AlterConnectionAction::SetOption(option) => Ok(option.name.clone()),
7793 AlterConnectionAction::DropOption(name) => Ok(name.clone()),
7794 AlterConnectionAction::RotateKeys => {
7795 Err(internal_err!("RotateKeys is handled separately above"))
7796 }
7797 })
7798 .collect::<Result<_, PlanError>>()?;
7799
7800 for invalid in INALTERABLE_OPTIONS {
7801 if specified_options.contains(invalid) {
7802 sql_bail!("cannot ALTER {} option {}", connection_type, invalid);
7803 }
7804 }
7805
7806 connection::validate_options_per_connection_type(connection_type, specified_options)?;
7807
7808 let mut set_options_vec: Vec<_> = Vec::new();
7810 let mut drop_options: BTreeSet<_> = BTreeSet::new();
7811 for action in actions {
7812 match action {
7813 AlterConnectionAction::SetOption(option) => set_options_vec.push(option),
7814 AlterConnectionAction::DropOption(name) => {
7815 drop_options.insert(name);
7816 }
7817 AlterConnectionAction::RotateKeys => {
7818 bail_internal!("RotateKeys is handled separately above")
7819 }
7820 }
7821 }
7822
7823 let set_options: BTreeMap<_, _> = set_options_vec
7824 .clone()
7825 .into_iter()
7826 .map(|option| (option.name, option.value))
7827 .collect();
7828
7829 let connection_options_extracted =
7833 connection::ConnectionOptionExtracted::try_from(set_options_vec)?;
7834
7835 let duplicates: Vec<_> = connection_options_extracted
7836 .seen
7837 .intersection(&drop_options)
7838 .collect();
7839
7840 if !duplicates.is_empty() {
7841 sql_bail!(
7842 "cannot both SET and DROP/RESET options {}",
7843 duplicates
7844 .iter()
7845 .map(|option| option.to_string())
7846 .join(", ")
7847 )
7848 }
7849
7850 for mutually_exclusive_options in MUTUALLY_EXCLUSIVE_SETS {
7851 let set_options_count = mutually_exclusive_options
7852 .iter()
7853 .filter(|o| set_options.contains_key(o))
7854 .count();
7855 let drop_options_count = mutually_exclusive_options
7856 .iter()
7857 .filter(|o| drop_options.contains(o))
7858 .count();
7859
7860 if set_options_count > 0 && drop_options_count > 0 {
7862 sql_bail!(
7863 "cannot both SET and DROP/RESET mutually exclusive {} options {}",
7864 connection_type,
7865 mutually_exclusive_options
7866 .iter()
7867 .map(|option| option.to_string())
7868 .join(", ")
7869 )
7870 }
7871
7872 if set_options_count > 0 || drop_options_count > 0 {
7877 drop_options.extend(mutually_exclusive_options.iter().cloned());
7878 }
7879
7880 }
7883
7884 Ok(Plan::AlterConnection(AlterConnectionPlan {
7885 id: entry.id(),
7886 action: crate::plan::AlterConnectionAction::AlterOptions {
7887 set_options,
7888 drop_options,
7889 validate,
7890 },
7891 }))
7892}
7893
7894pub fn describe_alter_sink(
7895 _: &StatementContext,
7896 _: AlterSinkStatement<Aug>,
7897) -> Result<StatementDesc, PlanError> {
7898 Ok(StatementDesc::new(None))
7899}
7900
7901pub fn plan_alter_sink(
7902 scx: &mut StatementContext,
7903 stmt: AlterSinkStatement<Aug>,
7904) -> Result<Plan, PlanError> {
7905 let AlterSinkStatement {
7906 sink_name,
7907 if_exists,
7908 action,
7909 } = stmt;
7910
7911 let object_type = ObjectType::Sink;
7912 let item = resolve_item_or_type(scx, object_type, sink_name.clone(), if_exists)?;
7913
7914 let Some(item) = item else {
7915 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
7916 name: sink_name.to_string(),
7917 object_type,
7918 });
7919
7920 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7921 };
7922 let item = item.at_version(RelationVersionSelector::Latest);
7924
7925 let create_sql = item.create_sql();
7927 let stmts = mz_sql_parser::parser::parse_statements(create_sql)?;
7928 let [stmt]: [StatementParseResult; 1] = stmts
7929 .try_into()
7930 .map_err(|_| internal_err!("create SQL of sink was not exactly one statement"))?;
7931 let Statement::CreateSink(stmt) = stmt.ast else {
7932 bail_internal!("create SQL of sink is not a CREATE SINK statement");
7933 };
7934 let (mut stmt, _) = crate::names::resolve(scx.catalog, stmt)?;
7935
7936 let mut set_options = vec![];
7938 let mut reset_options = vec![];
7939 match action {
7940 AlterSinkAction::ChangeRelation(new_from) => {
7941 stmt.from = new_from;
7942 }
7943 AlterSinkAction::SetOptions(options) => {
7944 for option in &options {
7945 match &option.name {
7946 CreateSinkOptionName::CommitInterval => {}
7947 name => bail_unsupported!(format!(
7948 "ALTER SINK ... SET ({})",
7949 name.to_ast_string_simple()
7950 )),
7951 }
7952 }
7953 if options.iter().all(|o| stmt.with_options.contains(o)) {
7969 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
7970 }
7971 set_options = options;
7972 }
7973 AlterSinkAction::ResetOptions(names) => {
7974 for name in &names {
7975 match name {
7976 CreateSinkOptionName::CommitInterval => {}
7977 name => bail_unsupported!(format!(
7978 "ALTER SINK ... RESET ({})",
7979 name.to_ast_string_simple()
7980 )),
7981 }
7982 if !stmt.with_options.iter().any(|o| o.name == *name) {
7985 sql_bail!(
7986 "cannot RESET {}: option is not set",
7987 name.to_ast_string_simple()
7988 );
7989 }
7990 }
7991 reset_options = names;
7992 }
7993 }
7994 crate::plan::apply_sink_option_edits(&mut stmt.with_options, &set_options, &reset_options);
7995
7996 let Plan::CreateSink(mut plan) = plan_sink(scx, stmt)? else {
7998 bail_internal!("plan_sink did not produce a CreateSink plan");
7999 };
8000
8001 plan.sink.version += 1;
8002
8003 Ok(Plan::AlterSink(AlterSinkPlan {
8004 item_id: item.id(),
8005 global_id: item.global_id(),
8006 sink: plan.sink,
8007 with_snapshot: plan.with_snapshot,
8008 in_cluster: plan.in_cluster,
8009 set_options,
8010 reset_options,
8011 }))
8012}
8013
8014pub fn describe_alter_source(
8015 _: &StatementContext,
8016 _: AlterSourceStatement<Aug>,
8017) -> Result<StatementDesc, PlanError> {
8018 Ok(StatementDesc::new(None))
8020}
8021
8022generate_extracted_config!(
8023 AlterSourceAddSubsourceOption,
8024 (TextColumns, Vec::<UnresolvedItemName>, Default(vec![])),
8025 (ExcludeColumns, Vec::<UnresolvedItemName>, Default(vec![])),
8026 (Details, String)
8027);
8028
8029pub fn plan_alter_source(
8030 scx: &mut StatementContext,
8031 stmt: AlterSourceStatement<Aug>,
8032) -> Result<Plan, PlanError> {
8033 let AlterSourceStatement {
8034 source_name,
8035 if_exists,
8036 action,
8037 } = stmt;
8038 let object_type = ObjectType::Source;
8039
8040 if resolve_item_or_type(scx, object_type, source_name.clone(), if_exists)?.is_none() {
8041 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
8042 name: source_name.to_string(),
8043 object_type,
8044 });
8045
8046 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
8047 }
8048
8049 match action {
8050 AlterSourceAction::SetOptions(options) => {
8051 let mut options = options.into_iter();
8052 let option = options
8053 .next()
8054 .ok_or_else(|| sql_err!("ALTER SOURCE SET requires at least one option"))?;
8055 if option.name == CreateSourceOptionName::RetainHistory {
8056 if options.next().is_some() {
8057 sql_bail!("RETAIN HISTORY must be only option");
8058 }
8059 return alter_retain_history(
8060 scx,
8061 object_type,
8062 if_exists,
8063 UnresolvedObjectName::Item(source_name),
8064 option.value,
8065 );
8066 }
8067 if option.name == CreateSourceOptionName::TimestampInterval {
8068 if options.next().is_some() {
8069 sql_bail!("TIMESTAMP INTERVAL must be only option");
8070 }
8071 return alter_source_timestamp_interval(scx, if_exists, source_name, option.value);
8072 }
8073 sql_bail!(
8076 "Cannot modify the {} of a SOURCE.",
8077 option.name.to_ast_string_simple()
8078 );
8079 }
8080 AlterSourceAction::ResetOptions(reset) => {
8081 let mut options = reset.into_iter();
8082 let option = options
8083 .next()
8084 .ok_or_else(|| sql_err!("ALTER SOURCE RESET requires at least one option"))?;
8085 if option == CreateSourceOptionName::RetainHistory {
8086 if options.next().is_some() {
8087 sql_bail!("RETAIN HISTORY must be only option");
8088 }
8089 return alter_retain_history(
8090 scx,
8091 object_type,
8092 if_exists,
8093 UnresolvedObjectName::Item(source_name),
8094 None,
8095 );
8096 }
8097 if option == CreateSourceOptionName::TimestampInterval {
8098 if options.next().is_some() {
8099 sql_bail!("TIMESTAMP INTERVAL must be only option");
8100 }
8101 return alter_source_timestamp_interval(scx, if_exists, source_name, None);
8102 }
8103 sql_bail!(
8104 "Cannot modify the {} of a SOURCE.",
8105 option.to_ast_string_simple()
8106 );
8107 }
8108 AlterSourceAction::DropSubsources { .. } => {
8109 sql_bail!("ALTER SOURCE...DROP SUBSOURCE no longer supported; use DROP SOURCE")
8110 }
8111 AlterSourceAction::AddSubsources { .. } => {
8112 sql_bail!("ALTER SOURCE...ADD SUBSOURCE must be purified before planning")
8113 }
8114 AlterSourceAction::RefreshReferences => {
8115 sql_bail!("ALTER SOURCE...REFRESH REFERENCES must be purified before planning")
8116 }
8117 };
8118}
8119
8120pub fn describe_alter_system_set(
8121 _: &StatementContext,
8122 _: AlterSystemSetStatement,
8123) -> Result<StatementDesc, PlanError> {
8124 Ok(StatementDesc::new(None))
8125}
8126
8127pub fn plan_alter_system_set(
8128 _: &StatementContext,
8129 AlterSystemSetStatement { name, to }: AlterSystemSetStatement,
8130) -> Result<Plan, PlanError> {
8131 let name = name.to_string();
8132 Ok(Plan::AlterSystemSet(AlterSystemSetPlan {
8133 name,
8134 value: scl::plan_set_variable_to(to)?,
8135 }))
8136}
8137
8138pub fn describe_alter_system_reset(
8139 _: &StatementContext,
8140 _: AlterSystemResetStatement,
8141) -> Result<StatementDesc, PlanError> {
8142 Ok(StatementDesc::new(None))
8143}
8144
8145pub fn plan_alter_system_reset(
8146 _: &StatementContext,
8147 AlterSystemResetStatement { name }: AlterSystemResetStatement,
8148) -> Result<Plan, PlanError> {
8149 let name = name.to_string();
8150 Ok(Plan::AlterSystemReset(AlterSystemResetPlan { name }))
8151}
8152
8153pub fn describe_alter_system_reset_all(
8154 _: &StatementContext,
8155 _: AlterSystemResetAllStatement,
8156) -> Result<StatementDesc, PlanError> {
8157 Ok(StatementDesc::new(None))
8158}
8159
8160pub fn plan_alter_system_reset_all(
8161 _: &StatementContext,
8162 _: AlterSystemResetAllStatement,
8163) -> Result<Plan, PlanError> {
8164 Ok(Plan::AlterSystemResetAll(AlterSystemResetAllPlan {}))
8165}
8166
8167pub fn describe_alter_role(
8168 _: &StatementContext,
8169 _: AlterRoleStatement<Aug>,
8170) -> Result<StatementDesc, PlanError> {
8171 Ok(StatementDesc::new(None))
8172}
8173
8174pub fn plan_alter_role(
8175 scx: &StatementContext,
8176 AlterRoleStatement { name, option }: AlterRoleStatement<Aug>,
8177) -> Result<Plan, PlanError> {
8178 let option = match option {
8179 AlterRoleOption::Attributes(attrs) => {
8180 let attrs = plan_role_attributes(attrs, scx)?;
8181 PlannedAlterRoleOption::Attributes(attrs)
8182 }
8183 AlterRoleOption::Variable(variable) => {
8184 let var = plan_role_variable(scx, variable)?;
8185 PlannedAlterRoleOption::Variable(var)
8186 }
8187 };
8188
8189 Ok(Plan::AlterRole(AlterRolePlan {
8190 id: name.id,
8191 name: name.name,
8192 option,
8193 }))
8194}
8195
8196pub fn describe_alter_table_add_column(
8197 _: &StatementContext,
8198 _: AlterTableAddColumnStatement<Aug>,
8199) -> Result<StatementDesc, PlanError> {
8200 Ok(StatementDesc::new(None))
8201}
8202
8203pub fn plan_alter_table_add_column(
8204 scx: &StatementContext,
8205 stmt: AlterTableAddColumnStatement<Aug>,
8206) -> Result<Plan, PlanError> {
8207 let AlterTableAddColumnStatement {
8208 if_exists,
8209 name,
8210 if_col_not_exist,
8211 column_name,
8212 data_type,
8213 } = stmt;
8214 let object_type = ObjectType::Table;
8215
8216 scx.require_feature_flag(&vars::ENABLE_ALTER_TABLE_ADD_COLUMN)?;
8217
8218 let (relation_id, item_name, desc) =
8219 match resolve_item_or_type(scx, object_type, name.clone(), if_exists)? {
8220 Some(item) => {
8221 let item_name = scx.catalog.resolve_full_name(item.name());
8223 let item = item.at_version(RelationVersionSelector::Latest);
8224 let desc = item
8225 .relation_desc()
8226 .ok_or_else(|| sql_err!("item does not have a relation description"))?
8227 .into_owned();
8228 (item.id(), item_name, desc)
8229 }
8230 None => {
8231 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
8232 name: name.to_ast_string_simple(),
8233 object_type,
8234 });
8235 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
8236 }
8237 };
8238
8239 let column_name = ColumnName::from(column_name.as_str());
8240 if desc.get_by_name(&column_name).is_some() {
8241 if if_col_not_exist {
8242 scx.catalog.add_notice(PlanNotice::ColumnAlreadyExists {
8243 column_name: column_name.to_string(),
8244 object_name: item_name.item,
8245 });
8246 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
8247 } else {
8248 return Err(PlanError::ColumnAlreadyExists {
8249 column_name,
8250 object_name: item_name.item,
8251 });
8252 }
8253 }
8254
8255 let scalar_type = scalar_type_from_sql(scx, &data_type)?;
8256 let column_type = scalar_type.nullable(true);
8258 let raw_sql_type = mz_sql_parser::parser::parse_data_type(&data_type.to_ast_string_stable())?;
8260
8261 Ok(Plan::AlterTableAddColumn(AlterTablePlan {
8262 relation_id,
8263 column_name,
8264 column_type,
8265 raw_sql_type,
8266 }))
8267}
8268
8269pub fn describe_alter_materialized_view_apply_replacement(
8270 _: &StatementContext,
8271 _: AlterMaterializedViewApplyReplacementStatement,
8272) -> Result<StatementDesc, PlanError> {
8273 Ok(StatementDesc::new(None))
8274}
8275
8276pub fn plan_alter_materialized_view_apply_replacement(
8277 scx: &StatementContext,
8278 stmt: AlterMaterializedViewApplyReplacementStatement,
8279) -> Result<Plan, PlanError> {
8280 let AlterMaterializedViewApplyReplacementStatement {
8281 if_exists,
8282 name,
8283 replacement_name,
8284 } = stmt;
8285
8286 scx.require_feature_flag(&vars::ENABLE_REPLACEMENT_MATERIALIZED_VIEWS)?;
8287
8288 let object_type = ObjectType::MaterializedView;
8289 let Some(mv) = resolve_item_or_type(scx, object_type, name.clone(), if_exists)? else {
8290 scx.catalog.add_notice(PlanNotice::ObjectDoesNotExist {
8291 name: name.to_ast_string_simple(),
8292 object_type,
8293 });
8294 return Ok(Plan::AlterNoop(AlterNoopPlan { object_type }));
8295 };
8296
8297 let replacement = resolve_item_or_type(scx, object_type, replacement_name, false)?
8298 .ok_or_else(|| sql_err!("replacement materialized view does not exist"))?;
8299
8300 if replacement.replacement_target() != Some(mv.id()) {
8301 return Err(PlanError::InvalidReplacement {
8302 item_type: mv.item_type(),
8303 item_name: scx.catalog.minimal_qualification(mv.name()),
8304 replacement_type: replacement.item_type(),
8305 replacement_name: scx.catalog.minimal_qualification(replacement.name()),
8306 });
8307 }
8308
8309 Ok(Plan::AlterMaterializedViewApplyReplacement(
8310 AlterMaterializedViewApplyReplacementPlan {
8311 id: mv.id(),
8312 replacement_id: replacement.id(),
8313 },
8314 ))
8315}
8316
8317pub fn describe_comment(
8318 _: &StatementContext,
8319 _: CommentStatement<Aug>,
8320) -> Result<StatementDesc, PlanError> {
8321 Ok(StatementDesc::new(None))
8322}
8323
8324pub fn plan_comment(
8325 scx: &mut StatementContext,
8326 stmt: CommentStatement<Aug>,
8327) -> Result<Plan, PlanError> {
8328 const MAX_COMMENT_LENGTH: usize = 1024;
8329
8330 let CommentStatement { object, comment } = stmt;
8331
8332 if let Some(c) = &comment {
8334 if c.len() > 1024 {
8335 return Err(PlanError::CommentTooLong {
8336 length: c.len(),
8337 max_size: MAX_COMMENT_LENGTH,
8338 });
8339 }
8340 }
8341
8342 let (object_id, column_pos) = match &object {
8343 com_ty @ CommentObjectType::Table { name }
8344 | com_ty @ CommentObjectType::View { name }
8345 | com_ty @ CommentObjectType::MaterializedView { name }
8346 | com_ty @ CommentObjectType::Index { name }
8347 | com_ty @ CommentObjectType::Func { name }
8348 | com_ty @ CommentObjectType::Connection { name }
8349 | com_ty @ CommentObjectType::Source { name }
8350 | com_ty @ CommentObjectType::Sink { name }
8351 | com_ty @ CommentObjectType::Secret { name } => {
8352 let item = scx.get_item_by_resolved_name(name)?;
8353 match (com_ty, item.item_type()) {
8354 (CommentObjectType::Table { .. }, CatalogItemType::Table) => {
8355 (CommentObjectId::Table(item.id()), None)
8356 }
8357 (CommentObjectType::View { .. }, CatalogItemType::View) => {
8358 (CommentObjectId::View(item.id()), None)
8359 }
8360 (CommentObjectType::MaterializedView { .. }, CatalogItemType::MaterializedView) => {
8361 (CommentObjectId::MaterializedView(item.id()), None)
8362 }
8363 (CommentObjectType::Index { .. }, CatalogItemType::Index) => {
8364 (CommentObjectId::Index(item.id()), None)
8365 }
8366 (CommentObjectType::Func { .. }, CatalogItemType::Func) => {
8367 (CommentObjectId::Func(item.id()), None)
8368 }
8369 (CommentObjectType::Connection { .. }, CatalogItemType::Connection) => {
8370 (CommentObjectId::Connection(item.id()), None)
8371 }
8372 (CommentObjectType::Source { .. }, CatalogItemType::Source) => {
8373 (CommentObjectId::Source(item.id()), None)
8374 }
8375 (CommentObjectType::Sink { .. }, CatalogItemType::Sink) => {
8376 (CommentObjectId::Sink(item.id()), None)
8377 }
8378 (CommentObjectType::Secret { .. }, CatalogItemType::Secret) => {
8379 (CommentObjectId::Secret(item.id()), None)
8380 }
8381 (com_ty, cat_ty) => {
8382 let expected_type = match com_ty {
8383 CommentObjectType::Table { .. } => ObjectType::Table,
8384 CommentObjectType::View { .. } => ObjectType::View,
8385 CommentObjectType::MaterializedView { .. } => ObjectType::MaterializedView,
8386 CommentObjectType::Index { .. } => ObjectType::Index,
8387 CommentObjectType::Func { .. } => ObjectType::Func,
8388 CommentObjectType::Connection { .. } => ObjectType::Connection,
8389 CommentObjectType::Source { .. } => ObjectType::Source,
8390 CommentObjectType::Sink { .. } => ObjectType::Sink,
8391 CommentObjectType::Secret { .. } => ObjectType::Secret,
8392 _ => sql_bail!("cannot comment on this object type"),
8393 };
8394
8395 return Err(PlanError::InvalidObjectType {
8396 expected_type: SystemObjectType::Object(expected_type),
8397 actual_type: SystemObjectType::Object(cat_ty.into()),
8398 object_name: item.name().item.clone(),
8399 });
8400 }
8401 }
8402 }
8403 CommentObjectType::Type { ty } => match ty {
8404 ResolvedDataType::AnonymousList(_) | ResolvedDataType::AnonymousMap { .. } => {
8405 sql_bail!("cannot comment on anonymous list or map type");
8406 }
8407 ResolvedDataType::Named { id, modifiers, .. } => {
8408 if !modifiers.is_empty() {
8409 sql_bail!("cannot comment on type with modifiers");
8410 }
8411 (CommentObjectId::Type(*id), None)
8412 }
8413 ResolvedDataType::Error => bail_internal!("unresolved data type"),
8414 },
8415 CommentObjectType::Column { name } => {
8416 let (item, pos) = scx.get_column_by_resolved_name(name)?;
8417 match item.item_type() {
8418 CatalogItemType::Table => (CommentObjectId::Table(item.id()), Some(pos + 1)),
8419 CatalogItemType::Source => (CommentObjectId::Source(item.id()), Some(pos + 1)),
8420 CatalogItemType::View => (CommentObjectId::View(item.id()), Some(pos + 1)),
8421 CatalogItemType::MaterializedView => {
8422 (CommentObjectId::MaterializedView(item.id()), Some(pos + 1))
8423 }
8424 CatalogItemType::Type => (CommentObjectId::Type(item.id()), Some(pos + 1)),
8425 r => {
8426 return Err(PlanError::Unsupported {
8427 feature: format!("Specifying comments on a column of {r}"),
8428 discussion_no: None,
8429 });
8430 }
8431 }
8432 }
8433 CommentObjectType::Role { name } => (CommentObjectId::Role(name.id), None),
8434 CommentObjectType::Database { name } => {
8435 (CommentObjectId::Database(*name.database_id()), None)
8436 }
8437 CommentObjectType::Schema { name } => {
8438 if matches!(name.schema_spec(), SchemaSpecifier::Temporary) {
8442 sql_bail!(
8443 "cannot comment on schema {} because it is a temporary schema",
8444 mz_repr::namespaces::MZ_TEMP_SCHEMA
8445 );
8446 }
8447 (
8448 CommentObjectId::Schema((*name.database_spec(), *name.schema_spec())),
8449 None,
8450 )
8451 }
8452 CommentObjectType::Cluster { name } => (CommentObjectId::Cluster(name.id), None),
8453 CommentObjectType::ClusterReplica { name } => {
8454 let replica = scx.catalog.resolve_cluster_replica(name)?;
8455 (
8456 CommentObjectId::ClusterReplica((replica.cluster_id(), replica.replica_id())),
8457 None,
8458 )
8459 }
8460 CommentObjectType::NetworkPolicy { name } => {
8461 (CommentObjectId::NetworkPolicy(name.id), None)
8462 }
8463 };
8464
8465 if let Some(p) = column_pos {
8471 i32::try_from(p).map_err(|_| PlanError::TooManyColumns {
8472 max_num_columns: MAX_NUM_COLUMNS,
8473 req_num_columns: p,
8474 })?;
8475 }
8476
8477 Ok(Plan::Comment(CommentPlan {
8478 object_id,
8479 sub_component: column_pos,
8480 comment,
8481 }))
8482}
8483
8484pub(crate) fn resolve_cluster<'a>(
8485 scx: &'a StatementContext,
8486 name: &'a Ident,
8487 if_exists: bool,
8488) -> Result<Option<&'a dyn CatalogCluster<'a>>, PlanError> {
8489 match scx.resolve_cluster(Some(name)) {
8490 Ok(cluster) => Ok(Some(cluster)),
8491 Err(_) if if_exists => Ok(None),
8492 Err(e) => Err(e),
8493 }
8494}
8495
8496pub(crate) fn resolve_cluster_replica<'a>(
8497 scx: &'a StatementContext,
8498 name: &QualifiedReplica,
8499 if_exists: bool,
8500) -> Result<Option<(&'a dyn CatalogCluster<'a>, ReplicaId)>, PlanError> {
8501 match scx.resolve_cluster(Some(&name.cluster)) {
8502 Ok(cluster) => match cluster.replica_ids().get(name.replica.as_str()) {
8503 Some(replica_id) => Ok(Some((cluster, *replica_id))),
8504 None if if_exists => Ok(None),
8505 None => Err(sql_err!(
8506 "CLUSTER {} has no CLUSTER REPLICA named {}",
8507 cluster.name(),
8508 name.replica.as_str().quoted(),
8509 )),
8510 },
8511 Err(_) if if_exists => Ok(None),
8512 Err(e) => Err(e),
8513 }
8514}
8515
8516pub(crate) fn resolve_database<'a>(
8517 scx: &'a StatementContext,
8518 name: &'a UnresolvedDatabaseName,
8519 if_exists: bool,
8520) -> Result<Option<&'a dyn CatalogDatabase>, PlanError> {
8521 match scx.resolve_database(name) {
8522 Ok(database) => Ok(Some(database)),
8523 Err(_) if if_exists => Ok(None),
8524 Err(e) => Err(e),
8525 }
8526}
8527
8528pub(crate) fn resolve_schema<'a>(
8529 scx: &'a StatementContext,
8530 name: UnresolvedSchemaName,
8531 if_exists: bool,
8532) -> Result<Option<(ResolvedDatabaseSpecifier, SchemaSpecifier)>, PlanError> {
8533 match scx.resolve_schema(name) {
8534 Ok(schema) => Ok(Some((schema.database().clone(), schema.id().clone()))),
8535 Err(_) if if_exists => Ok(None),
8536 Err(e) => Err(e),
8537 }
8538}
8539
8540pub(crate) fn resolve_network_policy<'a>(
8541 scx: &'a StatementContext,
8542 name: Ident,
8543 if_exists: bool,
8544) -> Result<Option<ResolvedNetworkPolicyName>, PlanError> {
8545 match scx
8546 .catalog
8547 .resolve_network_policy(normalize::ident_ref(&name))
8548 {
8549 Ok(policy) => Ok(Some(ResolvedNetworkPolicyName {
8550 id: policy.id(),
8551 name: policy.name().to_string(),
8552 })),
8553 Err(_) if if_exists => Ok(None),
8554 Err(e) => Err(e.into()),
8555 }
8556}
8557
8558pub(crate) fn resolve_item_or_type<'a>(
8559 scx: &'a StatementContext,
8560 object_type: ObjectType,
8561 name: UnresolvedItemName,
8562 if_exists: bool,
8563) -> Result<Option<&'a dyn CatalogItem>, PlanError> {
8564 let name = normalize::unresolved_item_name(name)?;
8565 let catalog_item = match object_type {
8566 ObjectType::Type => scx.catalog.resolve_type(&name),
8567 ObjectType::Table
8568 | ObjectType::View
8569 | ObjectType::MaterializedView
8570 | ObjectType::Source
8571 | ObjectType::Sink
8572 | ObjectType::MetricSink
8573 | ObjectType::Index
8574 | ObjectType::Role
8575 | ObjectType::Cluster
8576 | ObjectType::ClusterReplica
8577 | ObjectType::Secret
8578 | ObjectType::Connection
8579 | ObjectType::Database
8580 | ObjectType::Schema
8581 | ObjectType::Func
8582 | ObjectType::NetworkPolicy => scx.catalog.resolve_item(&name),
8583 };
8584
8585 match catalog_item {
8586 Ok(item) => {
8587 let is_type = ObjectType::from(item.item_type());
8588 if object_type == is_type {
8589 Ok(Some(item))
8590 } else {
8591 Err(PlanError::MismatchedObjectType {
8592 name: scx.catalog.minimal_qualification(item.name()),
8593 is_type,
8594 expected_type: object_type,
8595 })
8596 }
8597 }
8598 Err(_) if if_exists => Ok(None),
8599 Err(e) => Err(e.into()),
8600 }
8601}
8602
8603fn ensure_cluster_is_not_managed(
8605 scx: &StatementContext,
8606 cluster_id: ClusterId,
8607) -> Result<(), PlanError> {
8608 let cluster = scx.catalog.get_cluster(cluster_id);
8609 if cluster.is_managed() {
8610 Err(PlanError::ManagedCluster {
8611 cluster_name: cluster.name().to_string(),
8612 })
8613 } else {
8614 Ok(())
8615 }
8616}
8617
8618#[cfg(test)]
8619mod tests {
8620 use super::{
8621 METRIC_SINK_CURATED_PREFIX_MARKER, METRIC_SINK_PREFIX_MARKER,
8622 validate_user_metric_sink_prefix,
8623 };
8624
8625 #[mz_ore::test]
8626 fn user_metric_sink_prefix_cannot_reach_the_curated_prefix() {
8627 assert!(validate_user_metric_sink_prefix(METRIC_SINK_CURATED_PREFIX_MARKER).is_err());
8629 assert!(validate_user_metric_sink_prefix("mz_metric_sink_curated_x").is_err());
8630 assert!(validate_user_metric_sink_prefix(METRIC_SINK_PREFIX_MARKER).is_err());
8632 assert!(validate_user_metric_sink_prefix("mz_metric_sink_cur").is_err());
8633 assert!(validate_user_metric_sink_prefix("myapp_").is_err());
8635 assert!(validate_user_metric_sink_prefix("mz_metric_sink_myapp_").is_ok());
8638 assert!(validate_user_metric_sink_prefix("mz_metric_sink_curatex_").is_ok());
8639 assert!(validate_user_metric_sink_prefix("mz_metric_sink_my_curated_").is_ok());
8640 }
8641}