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