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