1use std::str::FromStr;
11use std::sync::Arc;
12
13use mz_adapter_types::connection::ConnectionId;
14use mz_expr::Eval;
15use mz_ore::cast::CastInto;
16use mz_persist_client::Diagnostics;
17use mz_persist_client::batch::ProtoBatch;
18use mz_persist_types::codec_impls::UnitSchema;
19use mz_pgcopy::CopyFormatParams;
20use mz_repr::{CatalogItemId, ColumnIndex, Datum, RelationDesc, Row, RowArena};
21use mz_sql::catalog::SessionCatalog;
22use mz_sql::plan::{self, CopyFromFilter, CopyFromSource, HirScalarExpr};
23use mz_sql::session::metadata::SessionMetadata;
24use mz_storage_client::client::TableData;
25use mz_storage_types::StorageDiff;
26use mz_storage_types::oneshot_sources::{ContentShape, OneshotIngestionRequest};
27use mz_storage_types::sources::SourceData;
28use smallvec::SmallVec;
29use timely::progress::Antichain;
30use tokio::sync::{mpsc, oneshot};
31use url::Url;
32use uuid::Uuid;
33
34use crate::command::CopyFromStdinWriter;
35use crate::coord::sequencer::inner::return_if_err;
36use crate::coord::{ActiveCopyFrom, Coordinator, TargetCluster};
37use crate::optimize;
38use crate::optimize::dataflows::{EvalTime, ExprPrep, ExprPrepOneShot};
39use crate::session::{Session, TransactionOps, WriteOp};
40use crate::{AdapterError, ExecuteContext, ExecuteResponse};
41
42const COPY_FROM_STDIN_MAX_BATCH_BYTES: usize = 32 * 1024 * 1024;
45
46const COPY_FROM_STDIN_MAX_WORKERS: usize = 8;
51
52impl Coordinator {
53 pub(crate) async fn sequence_copy_from(
54 &mut self,
55 ctx: ExecuteContext,
56 plan: plan::CopyFromPlan,
57 target_cluster: TargetCluster,
58 ) {
59 if ctx
60 .session()
61 .vars()
62 .transaction_isolation()
63 .is_bounded_staleness()
64 {
65 return ctx.retire(Err(AdapterError::BoundedStalenessReadOnly));
66 }
67
68 if let CopyFromSource::Stdin = plan.source {
72 let (tx, _, session, ctx_extra, response_barriers) = ctx.into_parts();
73 let response = Ok(ExecuteResponse::CopyFrom {
74 target_id: plan.target_id,
75 target_name: plan.target_name,
76 columns: plan.columns,
77 params: plan.params,
78 ctx_extra,
79 });
80 if response_barriers.is_empty() {
81 tx.send(response, session);
82 } else {
83 mz_ore::task::spawn(|| "copy_from_stdin_after_response_barriers", async move {
84 for barrier in response_barriers {
85 barrier.await;
86 }
87 tx.send(response, session);
88 });
89 }
90 return;
91 }
92
93 let plan::CopyFromPlan {
94 target_name: _,
95 target_id,
96 source,
97 columns: _,
98 source_desc,
99 mfp,
100 params,
101 filter,
102 } = plan;
103
104 let eval_uri = |from: HirScalarExpr| -> Result<String, AdapterError> {
105 let style = ExprPrepOneShot {
106 logical_time: EvalTime::NotAvailable,
107 session: ctx.session(),
108 catalog_state: self.catalog().state(),
109 };
110 let mut from = from.lower_uncorrelated(self.catalog().state().system_config())?;
111 style.prep_scalar_expr(&mut from)?;
112
113 let temp_storage = RowArena::new();
116 let eval_result = from.eval(&[], &temp_storage)?;
117 let eval_string = match eval_result {
118 Datum::Null => coord_bail!("COPY FROM target value cannot be NULL"),
119 Datum::String(url_str) => url_str,
120 other => coord_bail!("programming error! COPY FROM target cannot be {other}"),
121 };
122
123 Ok(eval_string.to_string())
124 };
125
126 let Some(entry) = self.catalog().try_get_entry(&target_id) else {
128 return ctx.retire(Err(AdapterError::ConcurrentDependencyDrop {
129 dependency_kind: "table",
130 dependency_id: target_id.to_string(),
131 }));
132 };
133 let Some(dest_table) = entry.table() else {
134 let typ = entry.item().typ();
135 let msg = format!("programming error: expected a Table found {typ:?}");
136 return ctx.retire(Err(AdapterError::Unstructured(anyhow::anyhow!(msg))));
137 };
138
139 let ingestion_id = Uuid::new_v4();
141 let collection_id = dest_table.global_id_writes();
142
143 let format = match params {
144 CopyFormatParams::Csv(csv) => {
145 mz_storage_types::oneshot_sources::ContentFormat::Csv(csv.to_owned())
146 }
147 CopyFormatParams::Parquet => mz_storage_types::oneshot_sources::ContentFormat::Parquet,
148 CopyFormatParams::Text(_) | CopyFormatParams::Binary => {
149 mz_ore::soft_panic_or_log!("unsupported formats should be rejected in planning");
150 ctx.retire(Err(AdapterError::Unsupported("COPY FROM URL/S3 format")));
151 return;
152 }
153 };
154
155 let source = match source {
156 CopyFromSource::Url(from_expr) => {
157 let url = return_if_err!(eval_uri(from_expr), ctx);
158 let result = Url::parse(&url)
160 .map_err(|err| AdapterError::Unstructured(anyhow::anyhow!("{err}")));
161 let url = return_if_err!(result, ctx);
162
163 match url.scheme() {
170 "http" | "https" => {}
171 other => {
172 return ctx.retire(Err(AdapterError::Unstructured(anyhow::anyhow!(
173 "only 'http://' and 'https://' urls are supported as COPY FROM \
174 target, got '{other}://'"
175 ))));
176 }
177 }
178 let enforce_external_addresses =
179 mz_storage_types::dyncfgs::ENFORCE_EXTERNAL_ADDRESSES
180 .get(self.controller.storage.config().config_set());
181 if enforce_external_addresses {
182 if let Err(err) = mz_ore::netio::ensure_url_ip_global(&url) {
183 return ctx
184 .retire(Err(AdapterError::Unstructured(anyhow::anyhow!("{err}"))));
185 }
186 }
187 mz_storage_types::oneshot_sources::ContentSource::Http {
188 url: mz_ore::url::SensitiveUrl(url),
189 }
190 }
191 CopyFromSource::AwsS3 {
192 uri,
193 connection,
194 connection_id,
195 } => {
196 let uri = return_if_err!(eval_uri(uri), ctx);
197
198 let result = http::Uri::from_str(&uri)
203 .map_err(|err| {
204 AdapterError::Unstructured(anyhow::anyhow!("expected S3 uri: {err}"))
205 })
206 .and_then(|uri| {
207 if uri.scheme_str() != Some("s3") && uri.scheme_str() != Some("gs") {
208 coord_bail!("only 's3://...' and 'gs://...' urls are supported as COPY FROM target");
209 }
210 Ok(uri)
211 })
212 .and_then(|uri| {
213 if uri.host().is_none() {
214 coord_bail!("missing bucket name from 's3://...' url");
215 }
216 Ok(uri)
217 });
218 let uri = return_if_err!(result, ctx);
219
220 mz_storage_types::oneshot_sources::ContentSource::AwsS3 {
221 connection,
222 connection_id,
223 uri: uri.to_string(),
224 }
225 }
226 CopyFromSource::Stdin => {
227 unreachable!("STDIN handled by the early return above")
228 }
229 };
230
231 let filter = match filter {
232 None => mz_storage_types::oneshot_sources::ContentFilter::None,
233 Some(CopyFromFilter::Files(files)) => {
234 mz_storage_types::oneshot_sources::ContentFilter::Files(files)
235 }
236 Some(CopyFromFilter::Pattern(pattern)) => {
237 mz_storage_types::oneshot_sources::ContentFilter::Pattern(pattern)
238 }
239 };
240
241 let source_mfp = mfp
242 .into_plan()
243 .map_err(|s| AdapterError::internal("copy_from", s))
244 .and_then(|mfp| {
245 mfp.into_nontemporal().map_err(|_| {
246 AdapterError::internal("copy_from", "temporal MFP not allowed in copy from")
247 })
248 });
249 let source_mfp = return_if_err!(source_mfp, ctx);
250
251 let shape = ContentShape {
252 source_desc,
253 source_mfp,
254 };
255
256 let request = OneshotIngestionRequest {
257 source,
258 format,
259 filter,
260 shape,
261 };
262
263 let target_cluster = match self
264 .catalog()
265 .resolve_target_cluster(target_cluster, ctx.session())
266 {
267 Ok(cluster) => cluster,
268 Err(err) => {
269 return ctx.retire(Err(err));
270 }
271 };
272 let cluster_id = target_cluster.id;
273
274 let command_tx = self.internal_cmd_tx.clone();
277 let conn_id = ctx.session().conn_id().clone();
278 let closure = Box::new(move |batches| {
279 let _ = command_tx.send(crate::coord::Message::StagedBatches {
280 conn_id,
281 table_id: target_id,
282 batches,
283 });
284 });
285 let conn_id = ctx.session().conn_id().clone();
287 self.active_copies.insert(
288 conn_id,
289 ActiveCopyFrom {
290 ingestion_id,
291 cluster_id,
292 table_id: target_id,
293 ctx,
294 },
295 );
296
297 let _result = self
298 .controller
299 .storage
300 .create_oneshot_ingestion(ingestion_id, collection_id, cluster_id, request, closure)
301 .await;
302 }
303
304 pub(crate) fn setup_copy_from_stdin(
311 &self,
312 session: &Session,
313 target_id: CatalogItemId,
314 target_name: String,
315 columns: Vec<ColumnIndex>,
316 row_desc: RelationDesc,
317 params: CopyFormatParams<'static>,
318 ) -> Result<CopyFromStdinWriter, AdapterError> {
319 let Some(entry) = self.catalog().try_get_entry(&target_id) else {
321 return Err(AdapterError::ConcurrentDependencyDrop {
322 dependency_kind: "table",
323 dependency_id: target_id.to_string(),
324 });
325 };
326 let Some(dest_table) = entry.table() else {
327 let typ = entry.item().typ();
328 return Err(AdapterError::Unstructured(anyhow::anyhow!(
329 "programming error: expected a Table found {typ:?}"
330 )));
331 };
332 let collection_id = dest_table.global_id_writes();
333
334 let collection_meta = self
335 .controller
336 .storage
337 .collection_metadata(collection_id)
338 .map_err(|e| AdapterError::Unstructured(anyhow::anyhow!("{e}")))?;
339 let shard_id = collection_meta.data_shard;
340 let collection_desc = collection_meta.relation_desc.clone();
341
342 let pcx = session.pcx().clone();
344 let session_meta = session.meta();
345 let catalog = self.catalog().clone();
346 let conn_catalog = catalog.for_session(session);
347 let catalog_state = conn_catalog.state();
348 let optimizer_config = optimize::OptimizerConfig::from(conn_catalog.system_vars());
349
350 let target_desc = catalog
352 .try_get_entry(&target_id)
353 .expect("table must exist")
354 .relation_desc_latest()
355 .expect("table has desc")
356 .into_owned();
357 let all_columns_in_order = columns.len() == target_desc.arity()
358 && columns.iter().enumerate().all(|(i, c)| c.to_raw() == i);
359
360 let column_transform = if all_columns_in_order {
363 None
364 } else {
365 let dummy_datums: Vec<Datum> = columns.iter().map(|_| Datum::Null).collect();
366 let dummy_row = Row::pack(&dummy_datums);
367
368 let prep = ExprPrepOneShot {
369 logical_time: EvalTime::NotAvailable,
370 session: &session_meta,
371 catalog_state,
372 };
373 let mut optimizer = optimize::view::Optimizer::new_with_prep_no_limit(
374 optimizer_config.clone(),
375 None,
376 prep,
377 );
378
379 let hir = mz_sql::plan::plan_copy_from(
380 &pcx,
381 &conn_catalog,
382 target_id,
383 target_name.clone(),
384 columns.clone(),
385 vec![dummy_row],
386 )?;
387 let mir = optimize::Optimize::optimize(&mut optimizer, hir)?;
388 let mir_expr = mir.into_inner();
389 let (result_ref, _) = mir_expr
390 .as_const()
391 .expect("optimizer should produce constant");
392 let result_rows = result_ref
393 .clone()
394 .map_err(|e| AdapterError::Unstructured(anyhow::anyhow!("eval error: {e}")))?;
395
396 let (full_row, _) = result_rows.into_iter().next().expect("should have one row");
397 let full_datums: Vec<Datum> = full_row.unpack();
398
399 let col_to_source: std::collections::BTreeMap<ColumnIndex, usize> =
400 columns.iter().enumerate().map(|(a, b)| (*b, a)).collect();
401
402 let mut sources: Vec<ColumnSource> = Vec::with_capacity(target_desc.arity());
403 let mut default_datums: Vec<Datum> = Vec::new();
404
405 for i in 0..target_desc.arity() {
406 let col_idx = ColumnIndex::from_raw(i);
407 if let Some(&src_idx) = col_to_source.get(&col_idx) {
408 sources.push(ColumnSource::Input(src_idx));
409 } else {
410 sources.push(ColumnSource::Default(default_datums.len()));
411 default_datums.push(full_datums[i]);
412 }
413 }
414
415 let defaults_row = Row::pack(&default_datums);
416
417 Some(ColumnTransform {
418 sources,
419 defaults_row,
420 })
421 };
422
423 let column_types: Arc<[mz_pgrepr::Type]> = row_desc
425 .typ()
426 .column_types
427 .iter()
428 .map(|x| &x.scalar_type)
429 .map(mz_pgrepr::Type::from)
430 .collect::<Vec<_>>()
431 .into();
432
433 let num_workers = std::cmp::min(
436 std::thread::available_parallelism()
437 .map(|n| n.get())
438 .unwrap_or(1),
439 COPY_FROM_STDIN_MAX_WORKERS,
440 );
441 tracing::info!(
442 %target_id, num_workers,
443 "starting parallel COPY FROM STDIN batch builders"
444 );
445
446 let column_transform = Arc::new(column_transform);
448 let target_desc = Arc::new(target_desc);
449 let collection_desc = Arc::new(collection_desc);
450 let persist_client = self.persist_client.clone();
451
452 let mut batch_txs = Vec::with_capacity(num_workers);
459 let mut worker_handles = Vec::with_capacity(num_workers);
460
461 let first_chunk_has_header = params.requires_header();
467 let mut worker_params = params;
468 if let CopyFormatParams::Csv(ref mut csv) = worker_params {
469 csv.header = false;
470 }
471
472 for worker_id in 0..num_workers {
473 let (batch_tx, batch_rx) = mpsc::channel::<Vec<u8>>(1);
476 batch_txs.push(batch_tx);
477
478 let persist_client = persist_client.clone();
479 let column_types = Arc::clone(&column_types);
480 let column_transform = Arc::clone(&column_transform);
481 let target_desc = Arc::clone(&target_desc);
482 let collection_desc = Arc::clone(&collection_desc);
483 let params = worker_params.clone();
484 let skip_header_on_first_chunk = worker_id == 0 && first_chunk_has_header;
487
488 let handle = mz_ore::task::spawn(
489 || format!("copy_from_stdin_worker:{target_id}:{worker_id}"),
490 Self::copy_from_stdin_batch_builder(
491 persist_client,
492 shard_id,
493 collection_id,
494 collection_desc,
495 target_desc,
496 column_transform,
497 column_types,
498 params,
499 skip_header_on_first_chunk,
500 batch_rx,
501 ),
502 );
503 worker_handles.push(handle);
504 }
505
506 let (completion_tx, completion_rx) = oneshot::channel();
508 mz_ore::task::spawn(
509 || format!("copy_from_stdin_collector:{target_id}"),
510 async move {
511 let mut all_batches = Vec::with_capacity(num_workers);
512 let mut total_rows: u64 = 0;
513
514 for handle in worker_handles {
515 match handle.await {
516 Ok((proto_batches, count)) => {
517 all_batches.extend(proto_batches);
518 total_rows += count;
519 }
520 Err(e) => {
521 let _ = completion_tx.send(Err(e));
522 return;
523 }
524 }
525 }
526
527 let _ = completion_tx.send(Ok((all_batches, total_rows)));
528 },
529 );
530
531 Ok(CopyFromStdinWriter {
532 batch_txs,
533 completion_rx,
534 })
535 }
536
537 async fn copy_from_stdin_batch_builder(
540 persist_client: mz_persist_client::PersistClient,
541 shard_id: mz_persist_client::ShardId,
542 collection_id: mz_repr::GlobalId,
543 collection_desc: Arc<RelationDesc>,
544 target_desc: Arc<RelationDesc>,
545 column_transform: Arc<Option<ColumnTransform>>,
546 column_types: Arc<[mz_pgrepr::Type]>,
547 params: CopyFormatParams<'static>,
548 skip_header_on_first_chunk: bool,
549 mut batch_rx: mpsc::Receiver<Vec<u8>>,
550 ) -> Result<(Vec<ProtoBatch>, u64), AdapterError> {
551 let persist_diagnostics = Diagnostics {
552 shard_name: collection_id.to_string(),
553 handle_purpose: "CopyFromStdin::batch_builder".to_string(),
554 };
555 let write_handle = persist_client
556 .open_writer::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
557 shard_id,
558 collection_desc,
559 Arc::new(UnitSchema),
560 persist_diagnostics,
561 )
562 .await
563 .map_err(|e| AdapterError::Unstructured(anyhow::anyhow!("persist open: {e}")))?;
564
565 let lower = mz_repr::Timestamp::MIN;
568 let upper = Antichain::from_elem(lower.step_forward());
569 let mut batch_builder = write_handle.builder(Antichain::from_elem(lower));
570 let mut row_count: u64 = 0;
571 let mut row_count_in_batch: u64 = 0;
572 let mut batch_bytes: usize = 0;
573 let mut proto_batches = Vec::new();
574
575 let rt = tokio::runtime::Handle::current();
576 let mut is_first_chunk = true;
577 while let Some(raw_bytes) = batch_rx.recv().await {
578 let chunk_params = if is_first_chunk && skip_header_on_first_chunk {
581 let mut p = params.clone();
582 if let CopyFormatParams::Csv(ref mut csv) = p {
583 csv.header = true;
584 }
585 p
586 } else {
587 params.clone()
588 };
589 is_first_chunk = false;
590 let raw_len = raw_bytes.len();
591
592 let chunk_column_types = Arc::clone(&column_types);
606 let chunk_transform = Arc::clone(&column_transform);
607 let chunk_target_desc = Arc::clone(&target_desc);
608 let chunk_rt = rt.clone();
609 let (returned_builder, added_rows) = mz_ore::task::spawn_blocking(
610 || "copy_from_stdin_process_chunk",
611 move || {
612 let rows = mz_pgcopy::decode_copy_format(
613 &raw_bytes,
614 &chunk_column_types,
615 chunk_params,
616 )
617 .map_err(|e| AdapterError::CopyFormatError(e.to_string()))?;
618
619 chunk_rt.block_on(async move {
620 let mut added: u64 = 0;
621 for row in rows {
622 let full_row = if let Some(ref transform) = *chunk_transform {
624 transform.apply(&row)
625 } else {
626 row
627 };
628
629 for (i, datum) in full_row.iter().enumerate() {
631 chunk_target_desc.constraints_met(i, &datum).map_err(|e| {
632 AdapterError::Unstructured(anyhow::anyhow!(
633 "constraint violation: {e}"
634 ))
635 })?;
636 }
637
638 let data = SourceData(Ok(full_row));
639 batch_builder
640 .add(&data, &(), &lower, &1)
641 .await
642 .map_err(|e| {
643 AdapterError::Unstructured(anyhow::anyhow!("persist add: {e}"))
644 })?;
645 added += 1;
646 }
647 Ok::<_, AdapterError>((batch_builder, added))
648 })
649 },
650 )
651 .await?;
652 batch_builder = returned_builder;
653 row_count += added_rows;
654 row_count_in_batch += added_rows;
655
656 batch_bytes = batch_bytes.saturating_add(raw_len);
657 if batch_bytes >= COPY_FROM_STDIN_MAX_BATCH_BYTES {
658 let batch = batch_builder.finish(upper.clone()).await.map_err(|e| {
659 AdapterError::Unstructured(anyhow::anyhow!("persist finish: {e}"))
660 })?;
661 proto_batches.push(batch.into_transmittable_batch());
662
663 batch_builder = write_handle.builder(Antichain::from_elem(lower));
664 row_count_in_batch = 0;
665 batch_bytes = 0;
666 }
667 }
668
669 if row_count_in_batch > 0 || proto_batches.is_empty() {
670 let batch = batch_builder
671 .finish(upper)
672 .await
673 .map_err(|e| AdapterError::Unstructured(anyhow::anyhow!("persist finish: {e}")))?;
674 proto_batches.push(batch.into_transmittable_batch());
675 }
676
677 Ok((proto_batches, row_count))
678 }
679
680 pub(crate) fn commit_staged_batches(
681 &mut self,
682 conn_id: ConnectionId,
683 table_id: CatalogItemId,
684 batches: Vec<Result<ProtoBatch, String>>,
685 ) {
686 let Some(active_copy) = self.active_copies.remove(&conn_id) else {
687 tracing::warn!(%conn_id, ?batches, "got response for canceled COPY FROM");
689 return;
690 };
691
692 let ActiveCopyFrom {
693 ingestion_id,
694 cluster_id: _,
695 table_id: _,
696 mut ctx,
697 } = active_copy;
698 tracing::info!(%ingestion_id, num_batches = ?batches.len(), "received batches to append");
699
700 let mut all_batches = SmallVec::with_capacity(batches.len());
701 let mut all_errors = SmallVec::<[String; 1]>::with_capacity(batches.len());
702 let mut row_count = 0u64;
703
704 for maybe_batch in batches {
705 match maybe_batch {
706 Ok(batch) => {
707 let count = batch.batch.as_ref().map(|b| b.len).unwrap_or(0);
708 all_batches.push(batch);
709 row_count = row_count.saturating_add(count);
710 }
711 Err(err) => all_errors.push(err),
712 }
713 }
714
715 if let Some(error) = all_errors.pop() {
717 tracing::warn!(?error, ?all_errors, "failed COPY FROM");
718
719 ctx.retire(Err(AdapterError::Unstructured(anyhow::anyhow!(
723 "COPY FROM: {error}"
724 ))));
725
726 return;
727 }
728
729 let stage_write = ctx
734 .session_mut()
735 .add_transaction_ops(TransactionOps::Writes(vec![WriteOp {
736 id: table_id,
737 rows: TableData::Batches(all_batches),
738 }]));
739
740 if let Err(err) = stage_write {
741 ctx.retire(Err(err));
742 } else {
743 ctx.retire(Ok(ExecuteResponse::Copied(row_count.cast_into())));
744 }
745 }
746
747 #[mz_ore::instrument(level = "debug")]
749 pub(crate) fn cancel_pending_copy(&mut self, conn_id: &ConnectionId) {
750 if let Some(ActiveCopyFrom {
751 ingestion_id,
752 cluster_id: _,
753 table_id: _,
754 ctx,
755 }) = self.active_copies.remove(conn_id)
756 {
757 let cancel_result = self
758 .controller
759 .storage
760 .cancel_oneshot_ingestion(ingestion_id);
761 if let Err(err) = cancel_result {
762 tracing::error!(?err, "failed to cancel OneshotIngestion");
763 }
764
765 ctx.retire(Err(AdapterError::Canceled));
766 }
767 }
768}
769
770struct ColumnTransform {
773 sources: Vec<ColumnSource>,
775 defaults_row: Row,
778}
779
780enum ColumnSource {
781 Input(usize),
783 Default(usize),
785}
786
787impl ColumnTransform {
788 fn apply(&self, input: &Row) -> Row {
790 let input_datums: Vec<Datum> = input.unpack();
791 let default_datums: Vec<Datum> = self.defaults_row.unpack();
792 let mut output_datums = Vec::with_capacity(self.sources.len());
793 for source in &self.sources {
794 match source {
795 ColumnSource::Input(idx) => output_datums.push(input_datums[*idx]),
796 ColumnSource::Default(idx) => output_datums.push(default_datums[*idx]),
797 }
798 }
799 Row::pack(&output_datums)
800 }
801}