1use crate::object_id::DependencySortable;
2use crate::parallel_runner::ParallelRunner;
3use crate::quoting::IdentifierQuoter;
4use crate::storage::DataFormat;
5use crate::storage::{CopyDestination, CopySource, CopyTransaction};
6use crate::*;
7use itertools::Itertools;
8use std::num::NonZeroUsize;
9use tracing::{debug, info, instrument};
10
11#[derive(Debug, Default)]
12pub struct CopyDataOptions {
13 pub data_format: Option<DataFormat>,
15 pub max_parallel: Option<NonZeroUsize>,
17
18 pub target_schema: Option<String>,
20
21 pub rename_schema_to: Option<String>,
24
25 pub schema_only: bool,
27
28 pub differential: bool,
33}
34
35const NON_ZERO_USIZE1: NonZeroUsize = NonZeroUsize::new(1).unwrap();
36
37impl CopyDataOptions {
38 fn get_max_parallel_or_1(&self) -> NonZeroUsize {
39 self.max_parallel.unwrap_or(NON_ZERO_USIZE1)
40 }
41}
42
43#[instrument(skip_all)]
47pub async fn copy_data<'d, S: CopySourceFactory, D: CopyDestinationFactory<'d>>(
48 source: &S,
49 destination: &'d mut D,
50 options: CopyDataOptions,
51) -> Result<()> {
52 let data_format = get_data_type(source, destination, &options).await?;
53
54 let expected_parallelism = if options.get_max_parallel_or_1() == NON_ZERO_USIZE1 {
55 SupportedParallelism::Sequential
56 } else {
57 source
58 .supported_parallelism()
59 .negotiate_parallelism(destination.supported_parallelism())
60 };
61
62 let definition = source.get_introspection().await?;
65
66 let (mut source, mut destination) = match expected_parallelism {
67 SupportedParallelism::Sequential => (
68 SequentialOrParallel::Sequential(source.create_sequential_source().await?),
69 SequentialOrParallel::Sequential(destination.create_sequential_destination().await?),
70 ),
71 SupportedParallelism::Parallel => (
72 source.create_source().await?,
73 destination.create_destination().await?,
74 ),
75 };
76
77 let destination_definition = if options.differential {
78 destination
79 .try_get_introspeciton()
80 .await?
81 .unwrap_or_default()
82 } else {
83 default()
84 };
85
86 let source_definition = if let Some(target_schema) = &options.target_schema {
87 definition.filtered_to_schema(target_schema)
88 } else {
89 definition
90 };
91
92 let target_definition = if let (Some(target_schema), Some(rename_to)) =
93 (&options.target_schema, &options.rename_schema_to)
94 {
95 source_definition.with_renamed_schema(target_schema, rename_to)
96 } else {
97 source_definition.clone()
98 };
99
100 if let Some(target_schema) = &options.target_schema {
101 destination_definition.filtered_to_schema(target_schema);
102 }
103
104 with_both!(&mut destination, |d| {
105 apply_pre_copy_in_txn(d, &target_definition, &destination_definition).await
106 })?;
107
108 if !options.schema_only {
109 let mut parallel_runner = ParallelRunner::new(options.get_max_parallel_or_1());
110
111 for target_schema in &target_definition.schemas {
112 let source_schema = source_definition
113 .schemas
114 .iter()
115 .find(|s| s.object_id == target_schema.object_id);
116 let source_schema = match source_schema {
117 Some(s) => s,
118 None => {
119 continue;
120 }
121 };
122
123 for target_table in &target_schema.tables {
124 if let TableTypeDetails::PartitionedParentTable { .. } = &target_table.table_type {
125 continue;
126 }
127
128 let source_table = source_schema
129 .tables
130 .iter()
131 .find(|t| t.object_id == target_table.object_id);
132 let source_table = match source_table {
133 Some(s) => s,
134 None => {
135 continue;
136 }
137 };
138
139 match &mut source {
140 SequentialOrParallel::Sequential(ref mut source) => match &mut destination {
141 SequentialOrParallel::Sequential(ref mut destination) => {
142 do_copy(
143 source,
144 destination,
145 target_schema,
146 target_table,
147 source_schema,
148 source_table,
149 &data_format,
150 &options,
151 )
152 .await?
153 }
154 SequentialOrParallel::Parallel(ref mut destination) => {
155 do_copy(
156 source,
157 destination,
158 target_schema,
159 target_table,
160 source_schema,
161 source_table,
162 &data_format,
163 &options,
164 )
165 .await?
166 }
167 },
168 SequentialOrParallel::Parallel(ref source) => match &mut destination {
169 SequentialOrParallel::Sequential(ref mut destination) => {
170 let mut source = source.clone();
171 do_copy(
172 &mut source,
173 destination,
174 target_schema,
175 target_table,
176 source_schema,
177 source_table,
178 &data_format,
179 &options,
180 )
181 .await?
182 }
183 SequentialOrParallel::Parallel(ref mut destination) => {
184 let mut source = source.clone();
185 let mut destination = destination.clone();
186 let df = data_format.clone();
187 let opt = &options;
188 parallel_runner
189 .enqueue(async move {
190 do_copy(
191 &mut source,
192 &mut destination,
193 target_schema,
194 target_table,
195 source_schema,
196 source_table,
197 &df,
198 opt,
199 )
200 .await
201 })
202 .await?;
203 }
204 },
205 }
206 }
207 }
208
209 parallel_runner.run_remaining().await?;
210 }
211
212 match &mut destination {
213 SequentialOrParallel::Sequential(ref mut destination) => {
214 apply_post_copy_structure_sequential(
215 destination,
216 &target_definition,
217 &destination_definition,
218 )
219 .await?;
220 }
221 SequentialOrParallel::Parallel(ref mut destination) => {
222 apply_post_copy_structure_parallel(
223 destination,
224 &target_definition,
225 &options,
226 &destination_definition,
227 )
228 .await?;
229 }
230 }
231
232 destination.finish().await?;
233
234 Ok(())
235}
236
237#[instrument(skip_all)]
244async fn apply_pre_copy_structure<T: CopyTransaction>(
245 txn: &mut T,
246 identifier_quoter: &IdentifierQuoter,
247 definition: &PostgresDatabase,
248 target_definition: &PostgresDatabase,
249) -> Result<()> {
250 for schema in &definition.schemas {
251 let target_schema = target_definition.try_get_schema(&schema.name);
252 if target_schema.is_none() {
253 txn.apply_statement(&schema.get_create_statement(identifier_quoter))
254 .await?;
255 }
256
257 if let Some(comment_statement) = schema.get_set_comment_statement(identifier_quoter) {
258 txn.apply_statement(&comment_statement).await?;
259 }
260 }
261
262 for ext in &definition.enabled_extensions {
263 if target_definition
264 .enabled_extensions
265 .iter()
266 .any(|e| e.name == ext.name)
267 {
268 debug!("Extension {} already exists in destination", ext.name);
269 continue;
270 }
271
272 txn.apply_statement(&ext.get_create_statement(identifier_quoter))
273 .await?;
274 }
275
276 for schema in &definition.schemas {
277 let target_schema = target_definition.try_get_schema(&schema.name);
278
279 for enumeration in &schema.enums {
280 if target_schema.is_some_and(|s| s.enums.iter().any(|e| e.name == enumeration.name)) {
281 debug!("Enum {} already exists in destination", enumeration.name);
282 continue;
283 }
284
285 txn.apply_statement(&enumeration.get_create_statement(identifier_quoter))
286 .await?;
287 }
288 }
289
290 let mut tables_and_functions: Vec<PostgresThingWithDependencies> = Vec::new();
291
292 for schema in &definition.schemas {
293 let target_schema = target_definition.try_get_schema(&schema.name);
294
295 for function in &schema.functions {
296 if target_schema.is_some_and(|s| {
297 s.functions
298 .iter()
299 .any(|f| f.function_name == function.function_name)
300 }) {
301 debug!(
302 "Function {} already exists in destination",
303 function.function_name
304 );
305 continue;
306 }
307
308 tables_and_functions.push(PostgresThingWithDependencies::Function(function, schema));
309 }
310
311 for aggregate_function in &schema.aggregate_functions {
312 if target_schema.is_some_and(|s| {
313 s.aggregate_functions
314 .iter()
315 .any(|f| f.function_name == aggregate_function.function_name)
316 }) {
317 debug!(
318 "Aggregate function {} already exists in destination",
319 aggregate_function.function_name
320 );
321 continue;
322 }
323
324 tables_and_functions.push(PostgresThingWithDependencies::AggregateFunction(
325 aggregate_function,
326 schema,
327 ));
328 }
329
330 for table in &schema.tables {
331 if target_schema
332 .and_then(|s| s.try_get_table(&table.name))
333 .is_some()
334 {
335 debug!("Table {} already exists in destination", table.name);
336 continue;
337 }
338
339 tables_and_functions.push(PostgresThingWithDependencies::Table(table, schema));
340 }
341
342 for view in &schema.views {
343 if target_schema.is_some_and(|s| s.views.iter().any(|v| v.name == view.name)) {
344 debug!("View {} already exists in destination", view.name);
345 continue;
346 }
347
348 tables_and_functions.push(PostgresThingWithDependencies::View(view, schema));
349 }
350
351 for domain in &schema.domains {
352 if target_schema.is_some_and(|s| s.domains.iter().any(|d| d.name == domain.name)) {
353 debug!("Domain {} already exists in destination", domain.name);
354 continue;
355 }
356
357 tables_and_functions.push(PostgresThingWithDependencies::Domain(domain, schema));
358 }
359 }
360
361 let sorted = tables_and_functions.iter().sort_by_dependencies();
362
363 for thing in sorted {
364 let sql = thing.get_create_sql(identifier_quoter);
365 txn.apply_statement(&sql).await?;
366 }
367
368 Ok(())
369}
370
371async fn apply_pre_copy_in_txn(
372 dest: &mut impl CopyDestination,
373 target_definition: &PostgresDatabase,
374 destination_definition: &PostgresDatabase,
375) -> Result<()> {
376 let identifier_quoter = dest.get_identifier_quoter();
377 let mut txn = dest.begin_transaction().await?;
378 apply_pre_copy_structure(
379 &mut txn,
380 &identifier_quoter,
381 target_definition,
382 destination_definition,
383 )
384 .await?;
385 txn.commit().await
386}
387
388#[instrument(skip_all)]
390#[allow(clippy::too_many_arguments)]
391async fn do_copy<S: CopySource, D: CopyDestination>(
392 source: &mut S,
393 destination: &mut D,
394 target_schema: &PostgresSchema,
395 target_table: &PostgresTable,
396 source_schema: &PostgresSchema,
397 source_table: &PostgresTable,
398 data_format: &DataFormat,
399 options: &CopyDataOptions,
400) -> Result<()> {
401 let has_data = options.differential
402 && destination
403 .has_data_in_table(target_schema, target_table)
404 .await?;
405
406 if !has_data {
407 info!(
408 "Skipping table {} as it already has data in the destination",
409 target_table.name
410 );
411 let data = source
412 .get_data(source_schema, source_table, data_format)
413 .await?;
414
415 destination
416 .apply_data(target_schema, target_table, data)
417 .await?;
418 }
419
420 Ok(())
421}
422
423#[instrument(skip_all)]
429fn get_post_apply_statement_groups(
430 definition: &PostgresDatabase,
431 identifier_quoter: &IdentifierQuoter,
432 target_definition: &PostgresDatabase,
433) -> Vec<Vec<String>> {
434 let mut statements = Vec::new();
435
436 for schema in &definition.schemas {
437 let existing_schema = target_definition.try_get_schema(&schema.name);
438
439 let mut group_1 = Vec::new();
440 let mut group_2 = Vec::new();
441 for table in &schema.tables {
442 let existing_table = existing_schema.and_then(|s| s.try_get_table(&table.name));
443
444 for index in &table.indices {
445 if index.index_constraint_type == PostgresIndexType::PrimaryKey {
446 continue;
447 }
448
449 if existing_table.is_some_and(|t| t.indices.iter().any(|i| i.name == index.name)) {
450 debug!(
451 "Index {} on table {} already exists in destination",
452 index.name, table.name
453 );
454 continue;
455 }
456
457 let is_temporal_unique_backing = table.constraints.iter().any(|c| {
460 matches!(c, PostgresConstraint::Unique(uk)
461 if uk.unique_index_name == index.name && uk.constraint_definition.is_some())
462 });
463 if is_temporal_unique_backing {
464 continue;
465 }
466
467 if !table.is_timescale_table() {
468 let sql = index.get_create_index_command(schema, table, identifier_quoter);
469 group_1.push(sql);
470 }
471 }
472 }
473
474 for sequence in &schema.sequences {
475 let existing_sequence = existing_schema
476 .and_then(|s| s.sequences.iter().find(|seq| seq.name == sequence.name));
477
478 if existing_sequence.is_none() || sequence.is_internally_created {
479 group_1.push(sequence.get_create_statement(schema, identifier_quoter));
480 } else {
481 debug!("Sequence {} already exists in destination", sequence.name);
482 }
483 if existing_sequence.is_none()
484 || existing_sequence.is_some_and(|s| s.last_value != sequence.last_value)
485 {
486 if let Some(sql) = sequence.get_set_value_statement(schema, identifier_quoter) {
487 group_2.push(sql);
488 }
489 }
490 }
491
492 for table in &schema.tables {
493 let existing_table = existing_schema.and_then(|s| s.try_get_table(&table.name));
494
495 for column in &table.columns {
496 let target_column =
497 existing_table.and_then(|t| t.columns.iter().find(|c| c.name == column.name));
498
499 if target_column.is_some_and(|c| c.default_value == column.default_value) {
500 debug!(
501 "Default value for column {} on table {} already matches destination",
502 column.name, table.name
503 );
504 continue;
505 }
506
507 if let Some(sql) =
508 column.get_alter_table_set_default_statement(table, schema, identifier_quoter)
509 {
510 group_2.push(sql);
511 }
512 }
513 }
514
515 statements.push(group_1);
516 statements.push(group_2);
517 }
518
519 for schema in &definition.schemas {
520 let existing_schema = target_definition.try_get_schema(&schema.name);
521
522 let mut group_3 = Vec::new();
523 for table in &schema.tables {
524 let existing_table = existing_schema.and_then(|s| s.try_get_table(&table.name));
525 for constraint in &table.constraints {
526 if existing_table
527 .is_some_and(|t| t.constraints.iter().any(|c| c.name() == constraint.name()))
528 {
529 continue;
530 }
531
532 if let PostgresConstraint::Unique(uk) = constraint {
533 if !table.is_timescale_table() {
534 let sql = uk.get_create_statement(table, schema, identifier_quoter);
535 group_3.push(sql);
536 }
537 }
538
539 if let PostgresConstraint::NotNull(nn) = constraint {
540 let sql = nn.get_create_statement(table, schema, identifier_quoter);
541 group_3.push(sql);
542 }
543 }
544 }
545 statements.push(group_3);
546 }
547
548 for schema in &definition.schemas {
549 let existing_schema = target_definition.try_get_schema(&schema.name);
550 for table in &schema.tables {
551 let existing_table = existing_schema.and_then(|s| s.try_get_table(&table.name));
552 for constraint in &table.constraints {
553 if existing_table
554 .is_some_and(|t| t.constraints.iter().any(|c| c.name() == constraint.name()))
555 {
556 debug!(
557 "Foreign key constraint {} on table {} already exists in destination",
558 constraint.name(),
559 table.name
560 );
561 continue;
562 }
563
564 if let PostgresConstraint::ForeignKey(fk) = constraint {
565 let sql = fk.get_create_statement(table, schema, identifier_quoter);
566 statements.push(vec![sql]);
567 }
568 }
569 }
570 }
571
572 let mut group_4 = Vec::new();
573 for schema in &definition.schemas {
574 let existing_schema = target_definition.try_get_schema(&schema.name);
575
576 for trigger in &schema.triggers {
577 if existing_schema.is_some_and(|s| s.triggers.iter().any(|t| t.name == trigger.name)) {
578 debug!(
579 "Trigger {} on table {} already exists in destination",
580 trigger.name, trigger.table_name
581 );
582 continue;
583 }
584
585 let sql = trigger.get_create_statement(schema, identifier_quoter);
586 group_4.push(sql);
587 }
588 }
589 statements.push(group_4);
590
591 for schema in &definition.schemas {
592 for view in schema.views.iter().sort_by_dependencies() {
593 if let Some(sql) = view.get_refresh_sql(schema, identifier_quoter) {
594 statements.push(vec![sql]);
595 }
596 }
597 }
598
599 let mut group_5 = Vec::new();
600 for job in &definition.timescale_support.user_defined_jobs {
601 if target_definition
602 .timescale_support
603 .user_defined_jobs
604 .iter()
605 .any(|j| {
606 j.function_schema == job.function_schema
607 && j.function_name == job.function_name
608 && j.config == job.config
609 })
610 {
611 debug!(
612 "Timescale job {} already exists in destination",
613 job.function_name
614 );
615 continue;
616 }
617
618 group_5.push(job.get_create_sql(identifier_quoter));
619 }
620
621 for schema in &definition.schemas {
622 let existing_schema = target_definition.try_get_schema(&schema.name);
623
624 for table in &schema.tables {
625 if let TableTypeDetails::TimescaleHypertable {
626 compression: existing_compression,
627 retention: existing_retention,
628 ..
629 } = &table.table_type
630 {
631 let existing_table = existing_schema.and_then(|s| s.try_get_table(&table.name));
632
633 if existing_table.is_some_and(|t| {
634 if let TableTypeDetails::TimescaleHypertable {
635 compression,
636 retention,
637 ..
638 } = &t.table_type
639 {
640 compression == existing_compression && retention == existing_retention
641 } else {
642 false
643 }
644 }) {
645 debug!(
646 "Timescale hypertable {} already exists in destination",
647 table.name
648 );
649 continue;
650 }
651 }
652
653 if let Some(timescale_post) =
654 table.get_timescale_post_settings(schema, identifier_quoter)
655 {
656 group_5.push(timescale_post);
657 }
658 }
659 }
660
661 statements.push(group_5);
662
663 statements
664}
665
666#[instrument(skip_all)]
668async fn apply_post_copy_structure_sequential<D: CopyDestination>(
669 destination: &mut D,
670 definition: &PostgresDatabase,
671 target_definition: &PostgresDatabase,
672) -> Result<()> {
673 let identifier_quoter = destination.get_identifier_quoter();
674
675 let statement_groups =
676 get_post_apply_statement_groups(definition, &identifier_quoter, target_definition);
677
678 for group in statement_groups {
679 for statement in group {
680 destination
681 .apply_non_transactional_statement(&statement)
682 .await?;
683 }
684 }
685
686 Ok(())
687}
688
689#[instrument(skip_all)]
691async fn apply_post_copy_structure_parallel<D: CopyDestination + Clone>(
692 destination: &mut D,
693 definition: &PostgresDatabase,
694 options: &CopyDataOptions,
695 target_definition: &PostgresDatabase,
696) -> Result<()> {
697 let identifier_quoter = destination.get_identifier_quoter();
698
699 let statement_groups =
700 get_post_apply_statement_groups(definition, &identifier_quoter, target_definition);
701
702 for group in statement_groups {
703 if group.is_empty() {
704 continue;
705 }
706
707 if group.len() == 1 {
708 destination
709 .apply_non_transactional_statement(&group[0])
710 .await?;
711 } else {
712 let mut join_handles = ParallelRunner::new(options.get_max_parallel_or_1());
713
714 for statement in group {
715 let mut destination = destination.clone();
716 join_handles
717 .enqueue(async move {
718 destination
719 .apply_non_transactional_statement(&statement)
720 .await
721 })
722 .await?;
723 }
724
725 join_handles.run_remaining().await?;
726 }
727 }
728
729 Ok(())
730}
731
732#[instrument(skip_all)]
735async fn get_data_type(
736 source: &impl CopySourceFactory,
737 destination: &impl CopyDestinationFactory<'_>,
738 options: &CopyDataOptions,
739) -> Result<DataFormat> {
740 let source_formats = source.supported_data_format().await?;
741 let destination_formats = destination.supported_data_format().await?;
742
743 let overlap = source_formats
744 .iter()
745 .filter(|f| destination_formats.contains(f))
746 .collect_vec();
747
748 if overlap.is_empty()
749 || options
750 .data_format
751 .as_ref()
752 .is_some_and(|d| !overlap.contains(&d))
753 {
754 Err(ElefantToolsError::DataFormatsNotCompatible {
755 supported_by_source: source_formats,
756 supported_by_target: destination_formats,
757 required_format: options.data_format.clone(),
758 })
759 } else {
760 for format in &overlap {
761 if let DataFormat::PostgresBinary { .. } = format {
762 return Ok((*format).clone());
763 }
764 }
765
766 Ok(overlap[0].clone())
767 }
768}