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 =
366 optimize::view::Optimizer::new_with_prep(optimizer_config.clone(), None, prep)
367 .without_fold_constants_limit();
368
369 let hir = mz_sql::plan::plan_copy_from(
370 &pcx,
371 &conn_catalog,
372 target_id,
373 target_name.clone(),
374 columns.clone(),
375 vec![dummy_row],
376 )?;
377 let mir = optimize::Optimize::optimize(&mut optimizer, hir)?;
378 let mir_expr = mir.into_inner();
379 let (result_ref, _) = mir_expr
380 .as_const()
381 .expect("optimizer should produce constant");
382 let result_rows = result_ref
383 .clone()
384 .map_err(|e| AdapterError::Unstructured(anyhow::anyhow!("eval error: {e}")))?;
385
386 let (full_row, _) = result_rows.into_iter().next().expect("should have one row");
387 let full_datums: Vec<Datum> = full_row.unpack();
388
389 let col_to_source: std::collections::BTreeMap<ColumnIndex, usize> =
390 columns.iter().enumerate().map(|(a, b)| (*b, a)).collect();
391
392 let mut sources: Vec<ColumnSource> = Vec::with_capacity(target_desc.arity());
393 let mut default_datums: Vec<Datum> = Vec::new();
394
395 for i in 0..target_desc.arity() {
396 let col_idx = ColumnIndex::from_raw(i);
397 if let Some(&src_idx) = col_to_source.get(&col_idx) {
398 sources.push(ColumnSource::Input(src_idx));
399 } else {
400 sources.push(ColumnSource::Default(default_datums.len()));
401 default_datums.push(full_datums[i]);
402 }
403 }
404
405 let defaults_row = Row::pack(&default_datums);
406
407 Some(ColumnTransform {
408 sources,
409 defaults_row,
410 })
411 };
412
413 let column_types: Arc<[mz_pgrepr::Type]> = row_desc
415 .typ()
416 .column_types
417 .iter()
418 .map(|x| &x.scalar_type)
419 .map(mz_pgrepr::Type::from)
420 .collect::<Vec<_>>()
421 .into();
422
423 let num_workers = std::cmp::min(
426 std::thread::available_parallelism()
427 .map(|n| n.get())
428 .unwrap_or(1),
429 COPY_FROM_STDIN_MAX_WORKERS,
430 );
431 tracing::info!(
432 %target_id, num_workers,
433 "starting parallel COPY FROM STDIN batch builders"
434 );
435
436 let column_transform = Arc::new(column_transform);
438 let target_desc = Arc::new(target_desc);
439 let collection_desc = Arc::new(collection_desc);
440 let persist_client = self.persist_client.clone();
441
442 let mut batch_txs = Vec::with_capacity(num_workers);
449 let mut worker_handles = Vec::with_capacity(num_workers);
450
451 let first_chunk_has_header = params.requires_header();
457 let mut worker_params = params;
458 if let CopyFormatParams::Csv(ref mut csv) = worker_params {
459 csv.header = false;
460 }
461
462 for worker_id in 0..num_workers {
463 let (batch_tx, batch_rx) = mpsc::channel::<Vec<u8>>(1);
466 batch_txs.push(batch_tx);
467
468 let persist_client = persist_client.clone();
469 let column_types = Arc::clone(&column_types);
470 let column_transform = Arc::clone(&column_transform);
471 let target_desc = Arc::clone(&target_desc);
472 let collection_desc = Arc::clone(&collection_desc);
473 let params = worker_params.clone();
474 let skip_header_on_first_chunk = worker_id == 0 && first_chunk_has_header;
477
478 let handle = mz_ore::task::spawn(
479 || format!("copy_from_stdin_worker:{target_id}:{worker_id}"),
480 Self::copy_from_stdin_batch_builder(
481 persist_client,
482 shard_id,
483 collection_id,
484 collection_desc,
485 target_desc,
486 column_transform,
487 column_types,
488 params,
489 skip_header_on_first_chunk,
490 batch_rx,
491 ),
492 );
493 worker_handles.push(handle);
494 }
495
496 let (completion_tx, completion_rx) = oneshot::channel();
498 mz_ore::task::spawn(
499 || format!("copy_from_stdin_collector:{target_id}"),
500 async move {
501 let mut all_batches = Vec::with_capacity(num_workers);
502 let mut total_rows: u64 = 0;
503
504 for handle in worker_handles {
505 match handle.await {
506 Ok((proto_batches, count)) => {
507 all_batches.extend(proto_batches);
508 total_rows += count;
509 }
510 Err(e) => {
511 let _ = completion_tx.send(Err(e));
512 return;
513 }
514 }
515 }
516
517 let _ = completion_tx.send(Ok((all_batches, total_rows)));
518 },
519 );
520
521 Ok(CopyFromStdinWriter {
522 batch_txs,
523 completion_rx,
524 })
525 }
526
527 async fn copy_from_stdin_batch_builder(
530 persist_client: mz_persist_client::PersistClient,
531 shard_id: mz_persist_client::ShardId,
532 collection_id: mz_repr::GlobalId,
533 collection_desc: Arc<RelationDesc>,
534 target_desc: Arc<RelationDesc>,
535 column_transform: Arc<Option<ColumnTransform>>,
536 column_types: Arc<[mz_pgrepr::Type]>,
537 params: CopyFormatParams<'static>,
538 skip_header_on_first_chunk: bool,
539 mut batch_rx: mpsc::Receiver<Vec<u8>>,
540 ) -> Result<(Vec<ProtoBatch>, u64), AdapterError> {
541 let persist_diagnostics = Diagnostics {
542 shard_name: collection_id.to_string(),
543 handle_purpose: "CopyFromStdin::batch_builder".to_string(),
544 };
545 let write_handle = persist_client
546 .open_writer::<SourceData, (), mz_repr::Timestamp, StorageDiff>(
547 shard_id,
548 collection_desc,
549 Arc::new(UnitSchema),
550 persist_diagnostics,
551 )
552 .await
553 .map_err(|e| AdapterError::Unstructured(anyhow::anyhow!("persist open: {e}")))?;
554
555 let lower = mz_repr::Timestamp::MIN;
558 let upper = Antichain::from_elem(lower.step_forward());
559 let mut batch_builder = write_handle.builder(Antichain::from_elem(lower));
560 let mut row_count: u64 = 0;
561 let mut row_count_in_batch: u64 = 0;
562 let mut batch_bytes: usize = 0;
563 let mut proto_batches = Vec::new();
564
565 let rt = tokio::runtime::Handle::current();
566 let mut is_first_chunk = true;
567 while let Some(raw_bytes) = batch_rx.recv().await {
568 let chunk_params = if is_first_chunk && skip_header_on_first_chunk {
571 let mut p = params.clone();
572 if let CopyFormatParams::Csv(ref mut csv) = p {
573 csv.header = true;
574 }
575 p
576 } else {
577 params.clone()
578 };
579 is_first_chunk = false;
580 let raw_len = raw_bytes.len();
581
582 let chunk_column_types = Arc::clone(&column_types);
596 let chunk_transform = Arc::clone(&column_transform);
597 let chunk_target_desc = Arc::clone(&target_desc);
598 let chunk_rt = rt.clone();
599 let (returned_builder, added_rows) = mz_ore::task::spawn_blocking(
600 || "copy_from_stdin_process_chunk",
601 move || {
602 let rows = mz_pgcopy::decode_copy_format(
603 &raw_bytes,
604 &chunk_column_types,
605 chunk_params,
606 )
607 .map_err(|e| AdapterError::CopyFormatError(e.to_string()))?;
608
609 chunk_rt.block_on(async move {
610 let mut added: u64 = 0;
611 for row in rows {
612 let full_row = if let Some(ref transform) = *chunk_transform {
614 transform.apply(&row)
615 } else {
616 row
617 };
618
619 for (i, datum) in full_row.iter().enumerate() {
621 chunk_target_desc.constraints_met(i, &datum).map_err(|e| {
622 AdapterError::Unstructured(anyhow::anyhow!(
623 "constraint violation: {e}"
624 ))
625 })?;
626 }
627
628 let data = SourceData(Ok(full_row));
629 batch_builder
630 .add(&data, &(), &lower, &1)
631 .await
632 .map_err(|e| {
633 AdapterError::Unstructured(anyhow::anyhow!("persist add: {e}"))
634 })?;
635 added += 1;
636 }
637 Ok::<_, AdapterError>((batch_builder, added))
638 })
639 },
640 )
641 .await?;
642 batch_builder = returned_builder;
643 row_count += added_rows;
644 row_count_in_batch += added_rows;
645
646 batch_bytes = batch_bytes.saturating_add(raw_len);
647 if batch_bytes >= COPY_FROM_STDIN_MAX_BATCH_BYTES {
648 let batch = batch_builder.finish(upper.clone()).await.map_err(|e| {
649 AdapterError::Unstructured(anyhow::anyhow!("persist finish: {e}"))
650 })?;
651 proto_batches.push(batch.into_transmittable_batch());
652
653 batch_builder = write_handle.builder(Antichain::from_elem(lower));
654 row_count_in_batch = 0;
655 batch_bytes = 0;
656 }
657 }
658
659 if row_count_in_batch > 0 || proto_batches.is_empty() {
660 let batch = batch_builder
661 .finish(upper)
662 .await
663 .map_err(|e| AdapterError::Unstructured(anyhow::anyhow!("persist finish: {e}")))?;
664 proto_batches.push(batch.into_transmittable_batch());
665 }
666
667 Ok((proto_batches, row_count))
668 }
669
670 pub(crate) fn commit_staged_batches(
671 &mut self,
672 conn_id: ConnectionId,
673 table_id: CatalogItemId,
674 batches: Vec<Result<ProtoBatch, String>>,
675 ) {
676 let Some(active_copy) = self.active_copies.remove(&conn_id) else {
677 tracing::warn!(%conn_id, ?batches, "got response for canceled COPY FROM");
679 return;
680 };
681
682 let ActiveCopyFrom {
683 ingestion_id,
684 cluster_id: _,
685 table_id: _,
686 mut ctx,
687 } = active_copy;
688 tracing::info!(%ingestion_id, num_batches = ?batches.len(), "received batches to append");
689
690 let mut all_batches = SmallVec::with_capacity(batches.len());
691 let mut all_errors = SmallVec::<[String; 1]>::with_capacity(batches.len());
692 let mut row_count = 0u64;
693
694 for maybe_batch in batches {
695 match maybe_batch {
696 Ok(batch) => {
697 let count = batch.batch.as_ref().map(|b| b.len).unwrap_or(0);
698 all_batches.push(batch);
699 row_count = row_count.saturating_add(count);
700 }
701 Err(err) => all_errors.push(err),
702 }
703 }
704
705 if let Some(error) = all_errors.pop() {
707 tracing::warn!(?error, ?all_errors, "failed COPY FROM");
708
709 ctx.retire(Err(AdapterError::Unstructured(anyhow::anyhow!(
713 "COPY FROM: {error}"
714 ))));
715
716 return;
717 }
718
719 let stage_write = ctx
724 .session_mut()
725 .add_transaction_ops(TransactionOps::Writes(vec![WriteOp {
726 id: table_id,
727 rows: TableData::Batches(all_batches),
728 }]));
729
730 if let Err(err) = stage_write {
731 ctx.retire(Err(err));
732 } else {
733 ctx.retire(Ok(ExecuteResponse::Copied(row_count.cast_into())));
734 }
735 }
736
737 #[mz_ore::instrument(level = "debug")]
739 pub(crate) fn cancel_pending_copy(&mut self, conn_id: &ConnectionId) {
740 if let Some(ActiveCopyFrom {
741 ingestion_id,
742 cluster_id: _,
743 table_id: _,
744 ctx,
745 }) = self.active_copies.remove(conn_id)
746 {
747 let cancel_result = self
748 .controller
749 .storage
750 .cancel_oneshot_ingestion(ingestion_id);
751 if let Err(err) = cancel_result {
752 tracing::error!(?err, "failed to cancel OneshotIngestion");
753 }
754
755 ctx.retire(Err(AdapterError::Canceled));
756 }
757 }
758}
759
760struct ColumnTransform {
763 sources: Vec<ColumnSource>,
765 defaults_row: Row,
768}
769
770enum ColumnSource {
771 Input(usize),
773 Default(usize),
775}
776
777impl ColumnTransform {
778 fn apply(&self, input: &Row) -> Row {
780 let input_datums: Vec<Datum> = input.unpack();
781 let default_datums: Vec<Datum> = self.defaults_row.unpack();
782 let mut output_datums = Vec::with_capacity(self.sources.len());
783 for source in &self.sources {
784 match source {
785 ColumnSource::Input(idx) => output_datums.push(input_datums[*idx]),
786 ColumnSource::Default(idx) => output_datums.push(default_datums[*idx]),
787 }
788 }
789 Row::pack(&output_datums)
790 }
791}