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