1use std::collections::{BTreeMap, BTreeSet};
13
14use mz_postgres_util::desc::PostgresTableDesc;
15use mz_proto::RustType;
16use mz_repr::{Datum, ReprColumnType, ReprScalarType, Row, SqlScalarType};
17use mz_sql_parser::ast::display::AstDisplay;
18use mz_sql_parser::ast::{
19 ColumnDef, CreateSubsourceOption, CreateSubsourceOptionName, CreateSubsourceStatement,
20 ExternalReferences, Ident, PgConfigOptionName, TableConstraint, UnresolvedItemName, Value,
21 WithOptionValue,
22};
23use mz_storage_types::sources::casts::{CastFunc, StorageScalarExpr};
24use mz_storage_types::sources::postgres::CastType;
25use mz_storage_types::sources::{MzOffset, SourceExportStatementDetails};
26use prost::Message;
27use tokio_postgres::Client;
28use tokio_postgres::types::Oid;
29
30use crate::names::{Aug, ResolvedItemName};
31use crate::normalize;
32use crate::plan::{PlanError, StatementContext};
33
34use super::error::PgSourcePurificationError;
35use super::references::RetrievedSourceReferences;
36use super::{
37 FilterConstraints, PartialItemName, PurifiedExportDetails, PurifiedSourceExport,
38 SourceReferencePolicy,
39};
40
41pub(super) async fn validate_requested_references_privileges(
45 client: &Client,
46 table_oids: &[Oid],
47) -> Result<(), PlanError> {
48 privileges::check_table_privileges(client, table_oids).await?;
49 privileges::check_rls_privileges(client, table_oids).await?;
50 replica_identity::check_replica_identity_full(client, table_oids).await?;
51
52 Ok(())
53}
54
55pub(super) fn map_column_refs(
60 retrieved_references: &RetrievedSourceReferences,
61 columns: &mut [UnresolvedItemName],
62 option_type: PgConfigOptionName,
63) -> Result<BTreeMap<u32, BTreeSet<String>>, PlanError> {
64 let mut cols_map: BTreeMap<u32, BTreeSet<String>> = BTreeMap::new();
65
66 for name in columns {
67 let (qual, col) = match name.0.split_last().expect("must have at least one element") {
68 (col, []) => {
69 return Err(PlanError::InvalidOptionValue {
70 option_name: option_type.to_ast_string_simple(),
71 err: Box::new(PlanError::UnderqualifiedColumnName(
72 col.as_str().to_string(),
73 )),
74 });
75 }
76 (col, qual) => (qual.to_vec(), col.as_str().to_string()),
77 };
78
79 let resolved_reference = retrieved_references.resolve_name(&qual)?;
80 let mut fully_qualified_name =
81 resolved_reference
82 .external_reference()
83 .map_err(|e| PlanError::InvalidOptionValue {
84 option_name: option_type.to_ast_string_simple(),
85 err: Box::new(e.into()),
86 })?;
87
88 let desc = resolved_reference
89 .postgres_desc()
90 .expect("known to be postgres");
91
92 if !desc.columns.iter().any(|column| column.name == col) {
93 let column = mz_repr::ColumnName::from(col);
94 let similar = desc
95 .columns
96 .iter()
97 .filter_map(|c| {
98 let c_name = mz_repr::ColumnName::from(c.name.clone());
99 c_name.is_similar(&column).then_some(c_name)
100 })
101 .collect();
102 return Err(PlanError::InvalidOptionValue {
103 option_name: option_type.to_ast_string_simple(),
104 err: Box::new(PlanError::UnknownColumn {
105 table: Some(
106 normalize::unresolved_item_name(fully_qualified_name)
107 .expect("known to be of valid len"),
108 ),
109 column,
110 similar,
111 }),
112 });
113 }
114
115 let col_ident = Ident::new(col.as_str().to_string())?;
117 fully_qualified_name.0.push(col_ident);
118 *name = fully_qualified_name;
119
120 let new = cols_map
121 .entry(desc.oid)
122 .or_default()
123 .insert(col.as_str().to_string());
124
125 if !new {
126 return Err(PlanError::InvalidOptionValue {
127 option_name: option_type.to_ast_string_simple(),
128 err: Box::new(PlanError::UnexpectedDuplicateReference { name: name.clone() }),
129 });
130 }
131 }
132
133 Ok(cols_map)
134}
135
136pub fn generate_create_subsource_statements(
137 scx: &StatementContext,
138 source_name: ResolvedItemName,
139 requested_subsources: BTreeMap<UnresolvedItemName, PurifiedSourceExport>,
140) -> Result<Vec<CreateSubsourceStatement<Aug>>, PlanError> {
141 let mut unsupported_cols = vec![];
143
144 let mut subsources = Vec::with_capacity(requested_subsources.len());
146
147 for (subsource_name, purified_export) in requested_subsources {
148 let PostgresExportStatementValues {
149 columns,
150 constraints,
151 text_columns,
152 exclude_columns,
153 details,
154 external_reference,
155 } = generate_source_export_statement_values(scx, purified_export, &mut unsupported_cols)?;
156
157 let mut with_options = vec![
158 CreateSubsourceOption {
159 name: CreateSubsourceOptionName::ExternalReference,
160 value: Some(WithOptionValue::UnresolvedItemName(external_reference)),
161 },
162 CreateSubsourceOption {
163 name: CreateSubsourceOptionName::Details,
164 value: Some(WithOptionValue::Value(Value::String(hex::encode(
165 details.into_proto().encode_to_vec(),
166 )))),
167 },
168 ];
169
170 if let Some(text_columns) = text_columns {
171 with_options.push(CreateSubsourceOption {
172 name: CreateSubsourceOptionName::TextColumns,
173 value: Some(WithOptionValue::Sequence(text_columns)),
174 });
175 }
176
177 if let Some(exclude_columns) = exclude_columns {
178 with_options.push(CreateSubsourceOption {
179 name: CreateSubsourceOptionName::ExcludeColumns,
180 value: Some(WithOptionValue::Sequence(exclude_columns)),
181 });
182 }
183
184 let subsource = CreateSubsourceStatement {
186 name: subsource_name,
187 columns,
188 of_source: Some(source_name.clone()),
191 constraints,
200 if_not_exists: false,
201 with_options,
202 };
203 subsources.push(subsource);
204 }
205
206 if !unsupported_cols.is_empty() {
207 unsupported_cols.sort();
208 Err(PgSourcePurificationError::UnrecognizedTypes {
209 cols: unsupported_cols,
210 })?;
211 }
212
213 Ok(subsources)
214}
215
216pub(super) struct PostgresExportStatementValues {
217 pub(super) columns: Vec<ColumnDef<Aug>>,
218 pub(super) constraints: Vec<TableConstraint<Aug>>,
219 pub(super) text_columns: Option<Vec<WithOptionValue<Aug>>>,
220 pub(super) exclude_columns: Option<Vec<WithOptionValue<Aug>>>,
221 pub(super) details: SourceExportStatementDetails,
222 pub(super) external_reference: UnresolvedItemName,
223}
224
225pub(super) fn generate_source_export_statement_values(
226 scx: &StatementContext,
227 purified_export: PurifiedSourceExport,
228 unsupported_cols: &mut Vec<(String, mz_repr::adt::system::Oid)>,
229) -> Result<PostgresExportStatementValues, PlanError> {
230 let PurifiedExportDetails::Postgres {
231 table,
232 text_columns,
233 exclude_columns,
234 initial_lsn,
235 } = purified_export.details
236 else {
237 bail_internal!("purified export details must be postgres");
238 };
239
240 let text_column_set = BTreeSet::from_iter(text_columns.iter().flatten().map(Ident::as_str));
241 let exclude_column_set =
242 BTreeSet::from_iter(exclude_columns.iter().flatten().map(Ident::as_str));
243
244 let mut columns = vec![];
246 for c in table.columns.iter() {
247 let name = Ident::new(c.name.clone())?;
248
249 if exclude_column_set.contains(c.name.as_str()) {
250 continue;
251 }
252
253 let ty = if text_column_set.contains(c.name.as_str()) {
254 mz_pgrepr::Type::Text
255 } else {
256 match mz_pgrepr::Type::from_oid_and_typmod(c.type_oid, c.type_mod) {
257 Ok(t) => t,
258 Err(_) => {
259 let mut full_name = purified_export.external_reference.0.clone();
260 full_name.push(name);
261 unsupported_cols.push((
262 UnresolvedItemName(full_name).to_ast_string_simple(),
263 mz_repr::adt::system::Oid(c.type_oid),
264 ));
265 continue;
266 }
267 }
268 };
269
270 let data_type = scx.resolve_type(ty)?;
271 let mut options = vec![];
272
273 if !c.nullable {
274 options.push(mz_sql_parser::ast::ColumnOptionDef {
275 name: None,
276 option: mz_sql_parser::ast::ColumnOption::NotNull,
277 });
278 }
279
280 columns.push(ColumnDef {
281 name,
282 data_type,
283 collation: None,
284 options,
285 });
286 }
287
288 let mut constraints = vec![];
289 for key in table.keys.clone() {
290 let mut key_columns = vec![];
291 let mut all_key_cols_included = true;
292
293 for col_num in key.cols {
294 match table.columns.iter().find(|col| col.col_num == col_num) {
295 Some(col) => {
296 let ident = Ident::new(col.name.clone())?;
297 key_columns.push(ident);
298 }
299 None => {
300 all_key_cols_included = false;
301 break;
302 }
303 }
304 }
305 if !all_key_cols_included {
306 continue;
307 }
308
309 let constraint = mz_sql_parser::ast::TableConstraint::Unique {
310 name: Some(Ident::new(key.name)?),
311 columns: key_columns,
312 is_primary: key.is_primary,
313 nulls_not_distinct: key.nulls_not_distinct,
314 };
315
316 if key.is_primary {
318 constraints.insert(0, constraint);
319 } else {
320 constraints.push(constraint);
321 }
322 }
323 let details = SourceExportStatementDetails::Postgres {
328 table,
329 cast_oid_full_range: true,
330 initial_lsn: Some(initial_lsn),
331 };
332
333 let text_columns = text_columns.map(|mut columns| {
334 columns.sort();
335 columns
336 .into_iter()
337 .map(WithOptionValue::Ident::<Aug>)
338 .collect()
339 });
340
341 let exclude_columns = exclude_columns.map(|mut columns| {
342 columns.sort();
343 columns
344 .into_iter()
345 .map(WithOptionValue::Ident::<Aug>)
346 .collect()
347 });
348
349 Ok(PostgresExportStatementValues {
350 columns,
351 constraints,
352 text_columns,
353 exclude_columns,
354 details,
355 external_reference: purified_export.external_reference,
356 })
357}
358
359pub(super) struct PurifiedSourceExports {
360 pub(super) source_exports: BTreeMap<UnresolvedItemName, PurifiedSourceExport>,
361 pub(super) normalized_text_columns: Vec<WithOptionValue<Aug>>,
368}
369
370pub(super) async fn purify_source_exports(
383 client: &Client,
384 retrieved_references: &RetrievedSourceReferences,
385 requested_references: &Option<ExternalReferences>,
386 mut text_columns: Vec<UnresolvedItemName>,
387 mut exclude_columns: Vec<UnresolvedItemName>,
388 filter_constraints: &FilterConstraints,
389 exclude_all_not_null_constraints: bool,
390 unresolved_source_name: &UnresolvedItemName,
391 reference_policy: &SourceReferencePolicy,
392 initial_lsn: MzOffset,
393) -> Result<PurifiedSourceExports, PlanError> {
394 let requested_exports = match requested_references.as_ref() {
395 Some(requested) if matches!(reference_policy, SourceReferencePolicy::NotAllowed) => {
396 Err(PlanError::UseTablesForSources(requested.to_string()))?
397 }
398 Some(requested) => retrieved_references
399 .requested_source_exports(Some(requested), unresolved_source_name)?,
400 None => {
401 if matches!(reference_policy, SourceReferencePolicy::Required) {
402 Err(PgSourcePurificationError::RequiresExternalReferences)?
403 }
404
405 if !text_columns.is_empty() {
408 Err(
409 PgSourcePurificationError::UnnecessaryOptionsWithoutReferences(
410 "TEXT COLUMNS".to_string(),
411 ),
412 )?
413 }
414
415 if !exclude_columns.is_empty() {
418 Err(
419 PgSourcePurificationError::UnnecessaryOptionsWithoutReferences(
420 "EXCLUDE COLUMNS".to_string(),
421 ),
422 )?
423 }
424
425 return Ok(PurifiedSourceExports {
426 source_exports: BTreeMap::new(),
427 normalized_text_columns: vec![],
428 });
429 }
430 };
431
432 if requested_exports.is_empty() {
433 sql_bail!(
434 "[internal error]: Postgres reference {} did not match any tables",
435 requested_references
436 .as_ref()
437 .unwrap()
438 .to_ast_string_simple()
439 );
440 }
441
442 super::validate_source_export_names(&requested_exports)?;
443
444 if filter_constraints.excludes_any() && requested_exports.len() != 1 {
445 sql_bail!(
446 "EXCLUDE CONSTRAINTS and EXCLUDE ALL CONSTRAINTS apply to exactly one table, \
447 but {} tables were referenced",
448 requested_exports.len()
449 );
450 }
451
452 let table_oids: Vec<_> = requested_exports
453 .iter()
454 .map(|r| r.meta.postgres_desc().expect("is postgres").oid)
455 .collect();
456
457 validate_requested_references_privileges(client, &table_oids).await?;
458
459 let mut text_column_map = map_column_refs(
460 retrieved_references,
461 &mut text_columns,
462 PgConfigOptionName::TextColumns,
463 )?;
464 let mut exclude_column_map = map_column_refs(
465 retrieved_references,
466 &mut exclude_columns,
467 PgConfigOptionName::ExcludeColumns,
468 )?;
469
470 text_columns.sort();
472 text_columns.dedup();
473 let normalized_text_columns: Vec<_> = text_columns
474 .into_iter()
475 .map(WithOptionValue::UnresolvedItemName)
476 .collect();
477
478 let source_exports = requested_exports
479 .into_iter()
480 .map(|r| {
481 let mut desc = r.meta.postgres_desc().expect("known postgres").clone();
482 let text_columns = text_column_map.remove(&desc.oid);
483 let exclude_columns = exclude_column_map.remove(&desc.oid);
484
485 if let FilterConstraints::Exclude(names) = filter_constraints {
486 let missing: Vec<_> = names
487 .iter()
488 .filter(|n| !desc.keys.iter().any(|k| &&k.name == n))
489 .cloned()
490 .collect();
491 if !missing.is_empty() {
492 return Err(PgSourcePurificationError::ConstraintsNotFound {
493 table: PartialItemName {
494 database: None,
495 schema: Some(desc.namespace.clone()),
496 item: desc.name.clone(),
497 },
498 constraints: missing,
499 });
500 }
501 }
502
503 if let Some(exclude_cols) = &exclude_columns {
504 let excluded_col_nums: BTreeSet<u16> = desc
505 .columns
506 .iter()
507 .filter(|c| exclude_cols.contains(&c.name))
508 .map(|c| c.col_num)
509 .collect();
510 desc.columns.retain(|c| !exclude_cols.contains(&c.name));
511 desc.keys
517 .retain(|k| k.cols.iter().all(|c| !excluded_col_nums.contains(c)));
518 }
519
520 match filter_constraints {
521 FilterConstraints::Exclude(names) => {
522 desc.keys.retain(|k| !names.contains(&k.name));
523 }
524 FilterConstraints::ExcludeAll => {
525 desc.keys.clear();
528 for c in &mut desc.columns {
529 c.nullable = true;
530 }
531 }
532 }
533 if exclude_all_not_null_constraints {
534 for c in &mut desc.columns {
535 c.nullable = true;
536 }
537 }
538
539 if let (Some(text_cols), Some(exclude_cols)) = (&text_columns, &exclude_columns) {
540 let intersection: Vec<_> = text_cols.intersection(exclude_cols).collect();
541 if !intersection.is_empty() {
542 return Err(PgSourcePurificationError::DuplicatedColumnNames(
543 intersection.iter().map(|s| (*s).to_string()).collect(),
544 ));
545 }
546 }
547 Ok((
548 r.name,
549 PurifiedSourceExport {
550 external_reference: r.external_reference,
551 details: PurifiedExportDetails::Postgres {
552 text_columns: text_columns.map(|v| {
553 v.into_iter()
554 .map(|s| Ident::new(s).expect("validated above"))
555 .collect()
556 }),
557 exclude_columns: exclude_columns.map(|v| {
558 v.into_iter()
559 .map(|s| Ident::new(s).expect("validated above"))
560 .collect()
561 }),
562 table: desc,
563 initial_lsn,
564 },
565 },
566 ))
567 })
568 .collect::<Result<BTreeMap<_, _>, _>>()?;
569
570 if !text_column_map.is_empty() {
571 let mut dangling_text_column_refs = vec![];
574 let all_references = retrieved_references.all_references();
575
576 for id in text_column_map.keys() {
577 let desc = all_references
578 .iter()
579 .find_map(|reference| {
580 let desc = reference.postgres_desc().expect("is postgres");
581 if desc.oid == *id { Some(desc) } else { None }
582 })
583 .expect("validated when generating text columns");
584
585 dangling_text_column_refs.push(PartialItemName {
586 database: None,
587 schema: Some(desc.namespace.clone()),
588 item: desc.name.clone(),
589 });
590 }
591
592 dangling_text_column_refs.sort();
593 return Err(PlanError::from(
594 PgSourcePurificationError::DanglingTextColumns {
595 items: dangling_text_column_refs,
596 },
597 ));
598 }
599
600 if !exclude_column_map.is_empty() {
601 let mut dangling_exclude_column_refs = vec![];
604 let all_references = retrieved_references.all_references();
605
606 for id in exclude_column_map.keys() {
607 let desc = all_references
608 .iter()
609 .find_map(|reference| {
610 let desc = reference.postgres_desc().expect("is postgres");
611 if desc.oid == *id { Some(desc) } else { None }
612 })
613 .expect("validated when generating exclude columns");
614
615 dangling_exclude_column_refs.push(PartialItemName {
616 database: None,
617 schema: Some(desc.namespace.clone()),
618 item: desc.name.clone(),
619 });
620 }
621
622 dangling_exclude_column_refs.sort();
623 return Err(PlanError::from(
624 PgSourcePurificationError::DanglingExcludeColumns {
625 items: dangling_exclude_column_refs,
626 },
627 ));
628 }
629
630 Ok(PurifiedSourceExports {
631 source_exports,
632 normalized_text_columns,
633 })
634}
635
636pub(crate) fn generate_column_casts(
637 scx: &StatementContext,
638 table: &PostgresTableDesc,
639 text_columns: &Vec<Ident>,
640 cast_oid_full_range: bool,
641) -> Result<Vec<(CastType, StorageScalarExpr)>, PlanError> {
642 let text_columns = BTreeSet::from_iter(text_columns.iter().map(Ident::as_str));
647
648 let mut table_cast = vec![];
649 for (i, column) in table.columns.iter().enumerate() {
650 let (cast_type, ty) = if text_columns.contains(column.name.as_str()) {
651 (CastType::Text, mz_pgrepr::Type::Text)
657 } else {
658 match mz_pgrepr::Type::from_oid_and_typmod(column.type_oid, column.type_mod) {
659 Ok(t) => (CastType::Natural, t),
660 Err(_) => {
665 table_cast.push((
666 CastType::Natural,
667 StorageScalarExpr::ErrorIfNull(
668 Box::new(StorageScalarExpr::Literal(
669 Row::pack_slice(&[Datum::Null]),
670 ReprColumnType {
671 nullable: true,
672 scalar_type: ReprScalarType::String,
673 },
674 )),
675 format!("Unsupported type with OID {}", column.type_oid),
676 ),
677 ));
678 continue;
679 }
680 }
681 };
682
683 let cast_expr = match pg_type_to_cast_func(scx, &ty, cast_oid_full_range) {
684 Ok(None) => {
685 StorageScalarExpr::Column(i)
687 }
688 Ok(Some(cast_func)) => {
689 StorageScalarExpr::CallUnary(cast_func, Box::new(StorageScalarExpr::Column(i)))
690 }
691 Err(PlanError::TableContainsUningestableTypes { type_, .. }) => {
692 return Err(PlanError::TableContainsUningestableTypes {
698 name: table.name.to_string(),
699 type_,
700 column: column.name.to_string(),
701 });
702 }
703 Err(e) => return Err(e),
704 };
705
706 let cast = if column.nullable {
707 cast_expr
708 } else {
709 let message = format!(
715 "PG column {}.{}.{} contained NULL data, despite having NOT NULL constraint",
716 table.namespace, table.name, column.name
717 );
718 StorageScalarExpr::ErrorIfNull(Box::new(cast_expr), message)
719 };
720
721 table_cast.push((cast_type, cast));
722 }
723 Ok(table_cast)
724}
725
726fn resolve_pg_type_to_scalar_type(
728 scx: &StatementContext,
729 ty: &mz_pgrepr::Type,
730) -> Result<SqlScalarType, PlanError> {
731 let data_type = scx.resolve_type(ty.clone())?;
732 crate::plan::query::scalar_type_from_sql(scx, &data_type)
733}
734
735fn pg_type_to_cast_func(
743 scx: &StatementContext,
744 ty: &mz_pgrepr::Type,
745 cast_oid_full_range: bool,
746) -> Result<Option<CastFunc>, PlanError> {
747 use mz_pgrepr::Type;
748
749 let cast_func = match ty {
750 Type::Bool => CastFunc::CastStringToBool,
751 Type::Bytea => CastFunc::CastStringToBytes,
752 Type::Char => CastFunc::CastStringToPgLegacyChar,
753 Type::Date => CastFunc::CastStringToDate,
754 Type::Float4 => CastFunc::CastStringToFloat32,
755 Type::Float8 => CastFunc::CastStringToFloat64,
756 Type::Int2 => CastFunc::CastStringToInt16,
757 Type::Int4 => CastFunc::CastStringToInt32,
758 Type::Int8 => CastFunc::CastStringToInt64,
759 Type::UInt2 => CastFunc::CastStringToUint16,
760 Type::UInt4 => CastFunc::CastStringToUint32,
761 Type::UInt8 => CastFunc::CastStringToUint64,
762 Type::Interval { .. } => CastFunc::CastStringToInterval,
763 Type::Jsonb => CastFunc::CastStringToJsonb,
764 Type::Name => CastFunc::CastStringToPgLegacyName,
765 Type::Numeric { .. } => {
766 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
768 match scalar_type {
769 SqlScalarType::Numeric { max_scale } => CastFunc::CastStringToNumeric(max_scale),
770 _ => unreachable!("Numeric must resolve to Numeric"),
771 }
772 }
773 Type::Oid => {
774 if cast_oid_full_range {
775 CastFunc::CastStringToOidFullRange
776 } else {
777 CastFunc::CastStringToOid
778 }
779 }
780 Type::Text => return Ok(None),
781 Type::BpChar { .. } => {
782 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
784 match scalar_type {
785 SqlScalarType::Char { length } => CastFunc::CastStringToChar {
786 length,
787 fail_on_len: true,
788 },
789 _ => unreachable!("BpChar must resolve to Char"),
790 }
791 }
792 Type::VarChar { .. } => {
793 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
795 match scalar_type {
796 SqlScalarType::VarChar { max_length } => CastFunc::CastStringToVarChar {
797 length: max_length,
798 fail_on_len: true,
799 },
800 _ => unreachable!("VarChar must resolve to VarChar"),
801 }
802 }
803 Type::Time { .. } => {
804 CastFunc::CastStringToTime
806 }
807 Type::Timestamp { .. } => {
808 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
810 match scalar_type {
811 SqlScalarType::Timestamp { precision } => {
812 CastFunc::CastStringToTimestamp(precision)
813 }
814 _ => unreachable!("Timestamp must resolve to Timestamp"),
815 }
816 }
817 Type::TimestampTz { .. } => {
818 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
820 match scalar_type {
821 SqlScalarType::TimestampTz { precision } => {
822 CastFunc::CastStringToTimestampTz(precision)
823 }
824 _ => unreachable!("TimestampTz must resolve to TimestampTz"),
825 }
826 }
827 Type::Uuid => CastFunc::CastStringToUuid,
828 Type::Int2Vector => CastFunc::CastStringToInt2Vector,
829 Type::MzTimestamp => CastFunc::CastStringToMzTimestamp,
830 Type::Json => CastFunc::CastStringToJsonb,
832 Type::Array(elem) => {
833 let return_ty = resolve_pg_type_to_scalar_type(scx, ty)?;
834 let elem_cast = build_element_cast_expr(scx, elem, cast_oid_full_range)?;
835 CastFunc::CastStringToArray {
836 return_ty,
837 cast_expr: Box::new(elem_cast),
838 }
839 }
840 Type::List(elem) => {
841 let return_ty = resolve_pg_type_to_scalar_type(scx, ty)?;
842 let elem_cast = build_element_cast_expr(scx, elem, cast_oid_full_range)?;
843 CastFunc::CastStringToList {
844 return_ty,
845 cast_expr: Box::new(elem_cast),
846 }
847 }
848 Type::Map { value_type } => {
849 let return_ty = resolve_pg_type_to_scalar_type(scx, ty)?;
850 let value_cast = build_element_cast_expr(scx, value_type, cast_oid_full_range)?;
851 CastFunc::CastStringToMap {
852 return_ty,
853 cast_expr: Box::new(value_cast),
854 }
855 }
856 Type::Range { element_type } => {
857 let return_ty = resolve_pg_type_to_scalar_type(scx, ty)?;
858 let elem_cast = build_element_cast_expr(scx, element_type, cast_oid_full_range)?;
859 CastFunc::CastStringToRange {
860 return_ty,
861 cast_expr: Box::new(elem_cast),
862 }
863 }
864 Type::RegType | Type::RegClass | Type::RegProc => {
867 return Err(PlanError::TableContainsUningestableTypes {
868 name: String::new(),
869 type_: ty.name().to_string(),
870 column: String::new(),
871 });
872 }
873 other => {
874 return Err(PlanError::TableContainsUningestableTypes {
875 name: String::new(),
876 type_: other.name().to_string(),
877 column: String::new(),
878 });
879 }
880 };
881 Ok(Some(cast_func))
882}
883
884fn build_element_cast_expr(
888 scx: &StatementContext,
889 elem_ty: &mz_pgrepr::Type,
890 cast_oid_full_range: bool,
891) -> Result<StorageScalarExpr, PlanError> {
892 match pg_type_to_cast_func(scx, elem_ty, cast_oid_full_range)? {
893 None => Ok(StorageScalarExpr::Column(0)),
894 Some(cast_func) => Ok(StorageScalarExpr::CallUnary(
895 cast_func,
896 Box::new(StorageScalarExpr::Column(0)),
897 )),
898 }
899}
900
901mod privileges {
902 use mz_postgres_util::{PostgresError, query, sql};
903
904 use super::*;
905 use crate::plan::PlanError;
906 use crate::pure::PgSourcePurificationError;
907
908 async fn check_schema_privileges(client: &Client, table_oids: &[Oid]) -> Result<(), PlanError> {
909 let invalid_schema_privileges_rows = query(
910 client,
911 sql!(
912 "
913 WITH distinct_namespace AS (
914 SELECT
915 DISTINCT n.oid, n.nspname AS schema_name
916 FROM unnest($1::OID[]) AS oids (oid)
917 JOIN pg_class AS c ON c.oid = oids.oid
918 JOIN pg_namespace AS n ON c.relnamespace = n.oid
919 )
920 SELECT d.schema_name
921 FROM distinct_namespace AS d
922 WHERE
923 NOT has_schema_privilege(CURRENT_USER::TEXT, d.oid, 'usage')"
924 ),
925 &[&table_oids],
926 )
927 .await?;
928
929 let mut invalid_schema_privileges = invalid_schema_privileges_rows
930 .into_iter()
931 .map(|row| row.get("schema_name"))
932 .collect::<Vec<String>>();
933
934 if invalid_schema_privileges.is_empty() {
935 Ok(())
936 } else {
937 invalid_schema_privileges.sort();
938 Err(PgSourcePurificationError::UserLacksUsageOnSchemas {
939 schemas: invalid_schema_privileges,
940 })?
941 }
942 }
943
944 pub async fn check_table_privileges(
955 client: &Client,
956 table_oids: &[Oid],
957 ) -> Result<(), PlanError> {
958 check_schema_privileges(client, table_oids).await?;
959
960 let invalid_table_privileges_rows = query(
961 client,
962 sql!(
963 "
964 SELECT
965 format('%I.%I', n.nspname, c.relname) AS schema_qualified_table_name
966 FROM unnest($1::oid[]) AS oids (oid)
967 JOIN
968 pg_class c ON c.oid = oids.oid
969 JOIN
970 pg_namespace n ON c.relnamespace = n.oid
971 WHERE NOT has_table_privilege(CURRENT_USER::text, c.oid, 'select')"
972 ),
973 &[&table_oids],
974 )
975 .await?;
976
977 let mut invalid_table_privileges = invalid_table_privileges_rows
978 .into_iter()
979 .map(|row| row.get("schema_qualified_table_name"))
980 .collect::<Vec<String>>();
981
982 if invalid_table_privileges.is_empty() {
983 Ok(())
984 } else {
985 invalid_table_privileges.sort();
986 Err(PgSourcePurificationError::UserLacksSelectOnTables {
987 tables: invalid_table_privileges,
988 })?
989 }
990 }
991
992 pub async fn check_rls_privileges(
997 client: &Client,
998 table_oids: &[Oid],
999 ) -> Result<(), PlanError> {
1000 match mz_postgres_util::validate_no_rls_policies(client, table_oids).await {
1001 Ok(_) => Ok(()),
1002 Err(err) => match err {
1003 PostgresError::BypassRLSRequired(tables) => {
1007 Err(PgSourcePurificationError::BypassRLSRequired { tables })?
1008 }
1009 _ => Err(err)?,
1010 },
1011 }
1012 }
1013}
1014
1015mod replica_identity {
1016 use mz_postgres_util::{query, sql};
1017
1018 use super::*;
1019 use crate::plan::PlanError;
1020 use crate::pure::PgSourcePurificationError;
1021
1022 pub async fn check_replica_identity_full(
1024 client: &Client,
1025 table_oids: &[Oid],
1026 ) -> Result<(), PlanError> {
1027 let invalid_replica_identity_rows = query(
1028 client,
1029 sql!(
1030 "
1031 SELECT
1032 format('%I.%I', n.nspname, c.relname) AS schema_qualified_table_name
1033 FROM unnest($1::oid[]) AS oids (oid)
1034 JOIN
1035 pg_class c ON c.oid = oids.oid
1036 JOIN
1037 pg_namespace n ON c.relnamespace = n.oid
1038 WHERE relreplident != 'f' OR relreplident IS NULL;"
1039 ),
1040 &[&table_oids],
1041 )
1042 .await?;
1043
1044 let mut invalid_replica_identity = invalid_replica_identity_rows
1045 .into_iter()
1046 .map(|row| row.get("schema_qualified_table_name"))
1047 .collect::<Vec<String>>();
1048
1049 if invalid_replica_identity.is_empty() {
1050 Ok(())
1051 } else {
1052 invalid_replica_identity.sort();
1053 Err(PgSourcePurificationError::NotTablesWReplicaIdentityFull {
1054 items: invalid_replica_identity,
1055 })?
1056 }
1057 }
1058}