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::SourceExportStatementDetails;
24use mz_storage_types::sources::casts::{CastFunc, StorageScalarExpr};
25use mz_storage_types::sources::postgres::CastType;
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 } = purified_export.details
232 else {
233 bail_internal!("purified export details must be postgres");
234 };
235
236 let text_column_set = BTreeSet::from_iter(text_columns.iter().flatten().map(Ident::as_str));
237 let exclude_column_set =
238 BTreeSet::from_iter(exclude_columns.iter().flatten().map(Ident::as_str));
239
240 let mut columns = vec![];
242 for c in table.columns.iter() {
243 let name = Ident::new(c.name.clone())?;
244
245 if exclude_column_set.contains(c.name.as_str()) {
246 continue;
247 }
248
249 let ty = if text_column_set.contains(c.name.as_str()) {
250 mz_pgrepr::Type::Text
251 } else {
252 match mz_pgrepr::Type::from_oid_and_typmod(c.type_oid, c.type_mod) {
253 Ok(t) => t,
254 Err(_) => {
255 let mut full_name = purified_export.external_reference.0.clone();
256 full_name.push(name);
257 unsupported_cols.push((
258 UnresolvedItemName(full_name).to_ast_string_simple(),
259 mz_repr::adt::system::Oid(c.type_oid),
260 ));
261 continue;
262 }
263 }
264 };
265
266 let data_type = scx.resolve_type(ty)?;
267 let mut options = vec![];
268
269 if !c.nullable {
270 options.push(mz_sql_parser::ast::ColumnOptionDef {
271 name: None,
272 option: mz_sql_parser::ast::ColumnOption::NotNull,
273 });
274 }
275
276 columns.push(ColumnDef {
277 name,
278 data_type,
279 collation: None,
280 options,
281 });
282 }
283
284 let mut constraints = vec![];
285 for key in table.keys.clone() {
286 let mut key_columns = vec![];
287 let mut all_key_cols_included = true;
288
289 for col_num in key.cols {
290 match table.columns.iter().find(|col| col.col_num == col_num) {
291 Some(col) => {
292 let ident = Ident::new(col.name.clone())?;
293 key_columns.push(ident);
294 }
295 None => {
296 all_key_cols_included = false;
297 break;
298 }
299 }
300 }
301 if !all_key_cols_included {
302 continue;
303 }
304
305 let constraint = mz_sql_parser::ast::TableConstraint::Unique {
306 name: Some(Ident::new(key.name)?),
307 columns: key_columns,
308 is_primary: key.is_primary,
309 nulls_not_distinct: key.nulls_not_distinct,
310 };
311
312 if key.is_primary {
314 constraints.insert(0, constraint);
315 } else {
316 constraints.push(constraint);
317 }
318 }
319 let details = SourceExportStatementDetails::Postgres {
324 table,
325 cast_oid_full_range: true,
326 };
327
328 let text_columns = text_columns.map(|mut columns| {
329 columns.sort();
330 columns
331 .into_iter()
332 .map(WithOptionValue::Ident::<Aug>)
333 .collect()
334 });
335
336 let exclude_columns = exclude_columns.map(|mut columns| {
337 columns.sort();
338 columns
339 .into_iter()
340 .map(WithOptionValue::Ident::<Aug>)
341 .collect()
342 });
343
344 Ok(PostgresExportStatementValues {
345 columns,
346 constraints,
347 text_columns,
348 exclude_columns,
349 details,
350 external_reference: purified_export.external_reference,
351 })
352}
353
354pub(super) struct PurifiedSourceExports {
355 pub(super) source_exports: BTreeMap<UnresolvedItemName, PurifiedSourceExport>,
356 pub(super) normalized_text_columns: Vec<WithOptionValue<Aug>>,
363}
364
365pub(super) async fn purify_source_exports(
369 client: &Client,
370 retrieved_references: &RetrievedSourceReferences,
371 requested_references: &Option<ExternalReferences>,
372 mut text_columns: Vec<UnresolvedItemName>,
373 mut exclude_columns: Vec<UnresolvedItemName>,
374 exclude_constraints: &BTreeSet<String>,
379 exclude_all_constraints: bool,
380 unresolved_source_name: &UnresolvedItemName,
381 reference_policy: &SourceReferencePolicy,
382) -> Result<PurifiedSourceExports, PlanError> {
383 let requested_exports = match requested_references.as_ref() {
384 Some(requested) if matches!(reference_policy, SourceReferencePolicy::NotAllowed) => {
385 Err(PlanError::UseTablesForSources(requested.to_string()))?
386 }
387 Some(requested) => retrieved_references
388 .requested_source_exports(Some(requested), unresolved_source_name)?,
389 None => {
390 if matches!(reference_policy, SourceReferencePolicy::Required) {
391 Err(PgSourcePurificationError::RequiresExternalReferences)?
392 }
393
394 if !text_columns.is_empty() {
397 Err(
398 PgSourcePurificationError::UnnecessaryOptionsWithoutReferences(
399 "TEXT COLUMNS".to_string(),
400 ),
401 )?
402 }
403
404 if !exclude_columns.is_empty() {
407 Err(
408 PgSourcePurificationError::UnnecessaryOptionsWithoutReferences(
409 "EXCLUDE COLUMNS".to_string(),
410 ),
411 )?
412 }
413
414 return Ok(PurifiedSourceExports {
415 source_exports: BTreeMap::new(),
416 normalized_text_columns: vec![],
417 });
418 }
419 };
420
421 if requested_exports.is_empty() {
422 sql_bail!(
423 "[internal error]: Postgres reference {} did not match any tables",
424 requested_references
425 .as_ref()
426 .unwrap()
427 .to_ast_string_simple()
428 );
429 }
430
431 super::validate_source_export_names(&requested_exports)?;
432
433 let table_oids: Vec<_> = requested_exports
434 .iter()
435 .map(|r| r.meta.postgres_desc().expect("is postgres").oid)
436 .collect();
437
438 validate_requested_references_privileges(client, &table_oids).await?;
439
440 let mut text_column_map = map_column_refs(
441 retrieved_references,
442 &mut text_columns,
443 PgConfigOptionName::TextColumns,
444 )?;
445 let mut exclude_column_map = map_column_refs(
446 retrieved_references,
447 &mut exclude_columns,
448 PgConfigOptionName::ExcludeColumns,
449 )?;
450
451 text_columns.sort();
453 text_columns.dedup();
454 let normalized_text_columns: Vec<_> = text_columns
455 .into_iter()
456 .map(WithOptionValue::UnresolvedItemName)
457 .collect();
458
459 let source_exports = requested_exports
460 .into_iter()
461 .map(|r| {
462 let mut desc = r.meta.postgres_desc().expect("known postgres").clone();
463 let text_columns = text_column_map.remove(&desc.oid);
464 let exclude_columns = exclude_column_map.remove(&desc.oid);
465
466 let missing_exclude_constraints: Vec<_> = exclude_constraints
467 .iter()
468 .filter(|n| !desc.keys.iter().any(|k| &&k.name == n))
469 .cloned()
470 .collect();
471 if !missing_exclude_constraints.is_empty() {
472 return Err(PgSourcePurificationError::ConstraintsNotFound {
473 table: PartialItemName {
474 database: None,
475 schema: Some(desc.namespace.clone()),
476 item: desc.name.clone(),
477 },
478 constraints: missing_exclude_constraints,
479 });
480 }
481
482 if let Some(exclude_cols) = &exclude_columns {
483 let excluded_col_nums: BTreeSet<u16> = desc
484 .columns
485 .iter()
486 .filter(|c| exclude_cols.contains(&c.name))
487 .map(|c| c.col_num)
488 .collect();
489 desc.columns.retain(|c| !exclude_cols.contains(&c.name));
490 desc.keys
496 .retain(|k| k.cols.iter().all(|c| !excluded_col_nums.contains(c)));
497 }
498
499 desc.keys.retain(|k| !exclude_constraints.contains(&k.name));
500
501 if exclude_all_constraints {
502 desc.keys.clear();
505 for c in &mut desc.columns {
506 c.nullable = true;
507 }
508 }
509
510 if let (Some(text_cols), Some(exclude_cols)) = (&text_columns, &exclude_columns) {
511 let intersection: Vec<_> = text_cols.intersection(exclude_cols).collect();
512 if !intersection.is_empty() {
513 return Err(PgSourcePurificationError::DuplicatedColumnNames(
514 intersection.iter().map(|s| (*s).to_string()).collect(),
515 ));
516 }
517 }
518 Ok((
519 r.name,
520 PurifiedSourceExport {
521 external_reference: r.external_reference,
522 details: PurifiedExportDetails::Postgres {
523 text_columns: text_columns.map(|v| {
524 v.into_iter()
525 .map(|s| Ident::new(s).expect("validated above"))
526 .collect()
527 }),
528 exclude_columns: exclude_columns.map(|v| {
529 v.into_iter()
530 .map(|s| Ident::new(s).expect("validated above"))
531 .collect()
532 }),
533 table: desc,
534 },
535 },
536 ))
537 })
538 .collect::<Result<BTreeMap<_, _>, _>>()?;
539
540 if !text_column_map.is_empty() {
541 let mut dangling_text_column_refs = vec![];
544 let all_references = retrieved_references.all_references();
545
546 for id in text_column_map.keys() {
547 let desc = all_references
548 .iter()
549 .find_map(|reference| {
550 let desc = reference.postgres_desc().expect("is postgres");
551 if desc.oid == *id { Some(desc) } else { None }
552 })
553 .expect("validated when generating text columns");
554
555 dangling_text_column_refs.push(PartialItemName {
556 database: None,
557 schema: Some(desc.namespace.clone()),
558 item: desc.name.clone(),
559 });
560 }
561
562 dangling_text_column_refs.sort();
563 return Err(PlanError::from(
564 PgSourcePurificationError::DanglingTextColumns {
565 items: dangling_text_column_refs,
566 },
567 ));
568 }
569
570 if !exclude_column_map.is_empty() {
571 let mut dangling_exclude_column_refs = vec![];
574 let all_references = retrieved_references.all_references();
575
576 for id in exclude_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 exclude columns");
584
585 dangling_exclude_column_refs.push(PartialItemName {
586 database: None,
587 schema: Some(desc.namespace.clone()),
588 item: desc.name.clone(),
589 });
590 }
591
592 dangling_exclude_column_refs.sort();
593 return Err(PlanError::from(
594 PgSourcePurificationError::DanglingExcludeColumns {
595 items: dangling_exclude_column_refs,
596 },
597 ));
598 }
599
600 Ok(PurifiedSourceExports {
601 source_exports,
602 normalized_text_columns,
603 })
604}
605
606pub(crate) fn generate_column_casts(
607 scx: &StatementContext,
608 table: &PostgresTableDesc,
609 text_columns: &Vec<Ident>,
610 cast_oid_full_range: bool,
611) -> Result<Vec<(CastType, StorageScalarExpr)>, PlanError> {
612 let text_columns = BTreeSet::from_iter(text_columns.iter().map(Ident::as_str));
617
618 let mut table_cast = vec![];
619 for (i, column) in table.columns.iter().enumerate() {
620 let (cast_type, ty) = if text_columns.contains(column.name.as_str()) {
621 (CastType::Text, mz_pgrepr::Type::Text)
627 } else {
628 match mz_pgrepr::Type::from_oid_and_typmod(column.type_oid, column.type_mod) {
629 Ok(t) => (CastType::Natural, t),
630 Err(_) => {
635 table_cast.push((
636 CastType::Natural,
637 StorageScalarExpr::ErrorIfNull(
638 Box::new(StorageScalarExpr::Literal(
639 Row::pack_slice(&[Datum::Null]),
640 ReprColumnType {
641 nullable: true,
642 scalar_type: ReprScalarType::String,
643 },
644 )),
645 format!("Unsupported type with OID {}", column.type_oid),
646 ),
647 ));
648 continue;
649 }
650 }
651 };
652
653 let cast_expr = match pg_type_to_cast_func(scx, &ty, cast_oid_full_range) {
654 Ok(None) => {
655 StorageScalarExpr::Column(i)
657 }
658 Ok(Some(cast_func)) => {
659 StorageScalarExpr::CallUnary(cast_func, Box::new(StorageScalarExpr::Column(i)))
660 }
661 Err(PlanError::TableContainsUningestableTypes { type_, .. }) => {
662 return Err(PlanError::TableContainsUningestableTypes {
668 name: table.name.to_string(),
669 type_,
670 column: column.name.to_string(),
671 });
672 }
673 Err(e) => return Err(e),
674 };
675
676 let cast = if column.nullable {
677 cast_expr
678 } else {
679 let message = format!(
685 "PG column {}.{}.{} contained NULL data, despite having NOT NULL constraint",
686 table.namespace, table.name, column.name
687 );
688 StorageScalarExpr::ErrorIfNull(Box::new(cast_expr), message)
689 };
690
691 table_cast.push((cast_type, cast));
692 }
693 Ok(table_cast)
694}
695
696fn resolve_pg_type_to_scalar_type(
698 scx: &StatementContext,
699 ty: &mz_pgrepr::Type,
700) -> Result<SqlScalarType, PlanError> {
701 let data_type = scx.resolve_type(ty.clone())?;
702 crate::plan::query::scalar_type_from_sql(scx, &data_type)
703}
704
705fn pg_type_to_cast_func(
713 scx: &StatementContext,
714 ty: &mz_pgrepr::Type,
715 cast_oid_full_range: bool,
716) -> Result<Option<CastFunc>, PlanError> {
717 use mz_pgrepr::Type;
718
719 let cast_func = match ty {
720 Type::Bool => CastFunc::CastStringToBool,
721 Type::Bytea => CastFunc::CastStringToBytes,
722 Type::Char => CastFunc::CastStringToPgLegacyChar,
723 Type::Date => CastFunc::CastStringToDate,
724 Type::Float4 => CastFunc::CastStringToFloat32,
725 Type::Float8 => CastFunc::CastStringToFloat64,
726 Type::Int2 => CastFunc::CastStringToInt16,
727 Type::Int4 => CastFunc::CastStringToInt32,
728 Type::Int8 => CastFunc::CastStringToInt64,
729 Type::UInt2 => CastFunc::CastStringToUint16,
730 Type::UInt4 => CastFunc::CastStringToUint32,
731 Type::UInt8 => CastFunc::CastStringToUint64,
732 Type::Interval { .. } => CastFunc::CastStringToInterval,
733 Type::Jsonb => CastFunc::CastStringToJsonb,
734 Type::Name => CastFunc::CastStringToPgLegacyName,
735 Type::Numeric { .. } => {
736 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
738 match scalar_type {
739 SqlScalarType::Numeric { max_scale } => CastFunc::CastStringToNumeric(max_scale),
740 _ => unreachable!("Numeric must resolve to Numeric"),
741 }
742 }
743 Type::Oid => {
744 if cast_oid_full_range {
745 CastFunc::CastStringToOidFullRange
746 } else {
747 CastFunc::CastStringToOid
748 }
749 }
750 Type::Text => return Ok(None),
751 Type::BpChar { .. } => {
752 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
754 match scalar_type {
755 SqlScalarType::Char { length } => CastFunc::CastStringToChar {
756 length,
757 fail_on_len: true,
758 },
759 _ => unreachable!("BpChar must resolve to Char"),
760 }
761 }
762 Type::VarChar { .. } => {
763 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
765 match scalar_type {
766 SqlScalarType::VarChar { max_length } => CastFunc::CastStringToVarChar {
767 length: max_length,
768 fail_on_len: true,
769 },
770 _ => unreachable!("VarChar must resolve to VarChar"),
771 }
772 }
773 Type::Time { .. } => {
774 CastFunc::CastStringToTime
776 }
777 Type::Timestamp { .. } => {
778 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
780 match scalar_type {
781 SqlScalarType::Timestamp { precision } => {
782 CastFunc::CastStringToTimestamp(precision)
783 }
784 _ => unreachable!("Timestamp must resolve to Timestamp"),
785 }
786 }
787 Type::TimestampTz { .. } => {
788 let scalar_type = resolve_pg_type_to_scalar_type(scx, ty)?;
790 match scalar_type {
791 SqlScalarType::TimestampTz { precision } => {
792 CastFunc::CastStringToTimestampTz(precision)
793 }
794 _ => unreachable!("TimestampTz must resolve to TimestampTz"),
795 }
796 }
797 Type::Uuid => CastFunc::CastStringToUuid,
798 Type::Int2Vector => CastFunc::CastStringToInt2Vector,
799 Type::MzTimestamp => CastFunc::CastStringToMzTimestamp,
800 Type::Json => CastFunc::CastStringToJsonb,
802 Type::Array(elem) => {
803 let return_ty = resolve_pg_type_to_scalar_type(scx, ty)?;
804 let elem_cast = build_element_cast_expr(scx, elem, cast_oid_full_range)?;
805 CastFunc::CastStringToArray {
806 return_ty,
807 cast_expr: Box::new(elem_cast),
808 }
809 }
810 Type::List(elem) => {
811 let return_ty = resolve_pg_type_to_scalar_type(scx, ty)?;
812 let elem_cast = build_element_cast_expr(scx, elem, cast_oid_full_range)?;
813 CastFunc::CastStringToList {
814 return_ty,
815 cast_expr: Box::new(elem_cast),
816 }
817 }
818 Type::Map { value_type } => {
819 let return_ty = resolve_pg_type_to_scalar_type(scx, ty)?;
820 let value_cast = build_element_cast_expr(scx, value_type, cast_oid_full_range)?;
821 CastFunc::CastStringToMap {
822 return_ty,
823 cast_expr: Box::new(value_cast),
824 }
825 }
826 Type::Range { element_type } => {
827 let return_ty = resolve_pg_type_to_scalar_type(scx, ty)?;
828 let elem_cast = build_element_cast_expr(scx, element_type, cast_oid_full_range)?;
829 CastFunc::CastStringToRange {
830 return_ty,
831 cast_expr: Box::new(elem_cast),
832 }
833 }
834 Type::RegType | Type::RegClass | Type::RegProc => {
837 return Err(PlanError::TableContainsUningestableTypes {
838 name: String::new(),
839 type_: ty.name().to_string(),
840 column: String::new(),
841 });
842 }
843 other => {
844 return Err(PlanError::TableContainsUningestableTypes {
845 name: String::new(),
846 type_: other.name().to_string(),
847 column: String::new(),
848 });
849 }
850 };
851 Ok(Some(cast_func))
852}
853
854fn build_element_cast_expr(
858 scx: &StatementContext,
859 elem_ty: &mz_pgrepr::Type,
860 cast_oid_full_range: bool,
861) -> Result<StorageScalarExpr, PlanError> {
862 match pg_type_to_cast_func(scx, elem_ty, cast_oid_full_range)? {
863 None => Ok(StorageScalarExpr::Column(0)),
864 Some(cast_func) => Ok(StorageScalarExpr::CallUnary(
865 cast_func,
866 Box::new(StorageScalarExpr::Column(0)),
867 )),
868 }
869}
870
871mod privileges {
872 use mz_postgres_util::{PostgresError, query, sql};
873
874 use super::*;
875 use crate::plan::PlanError;
876 use crate::pure::PgSourcePurificationError;
877
878 async fn check_schema_privileges(client: &Client, table_oids: &[Oid]) -> Result<(), PlanError> {
879 let invalid_schema_privileges_rows = query(
880 client,
881 sql!(
882 "
883 WITH distinct_namespace AS (
884 SELECT
885 DISTINCT n.oid, n.nspname AS schema_name
886 FROM unnest($1::OID[]) AS oids (oid)
887 JOIN pg_class AS c ON c.oid = oids.oid
888 JOIN pg_namespace AS n ON c.relnamespace = n.oid
889 )
890 SELECT d.schema_name
891 FROM distinct_namespace AS d
892 WHERE
893 NOT has_schema_privilege(CURRENT_USER::TEXT, d.oid, 'usage')"
894 ),
895 &[&table_oids],
896 )
897 .await?;
898
899 let mut invalid_schema_privileges = invalid_schema_privileges_rows
900 .into_iter()
901 .map(|row| row.get("schema_name"))
902 .collect::<Vec<String>>();
903
904 if invalid_schema_privileges.is_empty() {
905 Ok(())
906 } else {
907 invalid_schema_privileges.sort();
908 Err(PgSourcePurificationError::UserLacksUsageOnSchemas {
909 schemas: invalid_schema_privileges,
910 })?
911 }
912 }
913
914 pub async fn check_table_privileges(
925 client: &Client,
926 table_oids: &[Oid],
927 ) -> Result<(), PlanError> {
928 check_schema_privileges(client, table_oids).await?;
929
930 let invalid_table_privileges_rows = query(
931 client,
932 sql!(
933 "
934 SELECT
935 format('%I.%I', n.nspname, c.relname) AS schema_qualified_table_name
936 FROM unnest($1::oid[]) AS oids (oid)
937 JOIN
938 pg_class c ON c.oid = oids.oid
939 JOIN
940 pg_namespace n ON c.relnamespace = n.oid
941 WHERE NOT has_table_privilege(CURRENT_USER::text, c.oid, 'select')"
942 ),
943 &[&table_oids],
944 )
945 .await?;
946
947 let mut invalid_table_privileges = invalid_table_privileges_rows
948 .into_iter()
949 .map(|row| row.get("schema_qualified_table_name"))
950 .collect::<Vec<String>>();
951
952 if invalid_table_privileges.is_empty() {
953 Ok(())
954 } else {
955 invalid_table_privileges.sort();
956 Err(PgSourcePurificationError::UserLacksSelectOnTables {
957 tables: invalid_table_privileges,
958 })?
959 }
960 }
961
962 pub async fn check_rls_privileges(
967 client: &Client,
968 table_oids: &[Oid],
969 ) -> Result<(), PlanError> {
970 match mz_postgres_util::validate_no_rls_policies(client, table_oids).await {
971 Ok(_) => Ok(()),
972 Err(err) => match err {
973 PostgresError::BypassRLSRequired(tables) => {
977 Err(PgSourcePurificationError::BypassRLSRequired { tables })?
978 }
979 _ => Err(err)?,
980 },
981 }
982 }
983}
984
985mod replica_identity {
986 use mz_postgres_util::{query, sql};
987
988 use super::*;
989 use crate::plan::PlanError;
990 use crate::pure::PgSourcePurificationError;
991
992 pub async fn check_replica_identity_full(
994 client: &Client,
995 table_oids: &[Oid],
996 ) -> Result<(), PlanError> {
997 let invalid_replica_identity_rows = query(
998 client,
999 sql!(
1000 "
1001 SELECT
1002 format('%I.%I', n.nspname, c.relname) AS schema_qualified_table_name
1003 FROM unnest($1::oid[]) AS oids (oid)
1004 JOIN
1005 pg_class c ON c.oid = oids.oid
1006 JOIN
1007 pg_namespace n ON c.relnamespace = n.oid
1008 WHERE relreplident != 'f' OR relreplident IS NULL;"
1009 ),
1010 &[&table_oids],
1011 )
1012 .await?;
1013
1014 let mut invalid_replica_identity = invalid_replica_identity_rows
1015 .into_iter()
1016 .map(|row| row.get("schema_qualified_table_name"))
1017 .collect::<Vec<String>>();
1018
1019 if invalid_replica_identity.is_empty() {
1020 Ok(())
1021 } else {
1022 invalid_replica_identity.sort();
1023 Err(PgSourcePurificationError::NotTablesWReplicaIdentityFull {
1024 items: invalid_replica_identity,
1025 })?
1026 }
1027 }
1028}