Skip to main content

elefant_tools/
copy_data.rs

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    /// Force this data format to be used
14    pub data_format: Option<DataFormat>,
15    /// How many tables to copy in parallel at most
16    pub max_parallel: Option<NonZeroUsize>,
17
18    /// The schema to inspect
19    pub target_schema: Option<String>,
20
21    /// If `target_schema` is specified it will be renamed to this
22    /// when applied to the destination.
23    pub rename_schema_to: Option<String>,
24
25    /// Only the schema will be copied, but not any data
26    pub schema_only: bool,
27
28    /// Only the structures missing in the destination will be copied.
29    /// Data copy is only checked against "empty table" vs "non-empty table".
30    /// This only works with data sources that supports structural inspections, aka
31    /// not sql-files.
32    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/// Copies data and structures from the provided source to the destination.
44///
45/// This is probably the main function you want to deal with when using Elefant Tools as a library.
46#[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    // Get introspection from the factory before creating sources.
63    // This is called on the factory (&self) which is Sync-safe.
64    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/// Applies all structures needed to be able to actually insert data. This includes:
238/// * Creating schemas
239/// * Creating tables
240/// * Creating functions
241/// * Creating views
242/// * Creating custom types
243#[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/// Actually copies data between two tables.
389#[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/// Get instructions to apply after the data has been copied. This includes:
424/// * Creating indexes
425/// * Creating constraints
426/// * Creating triggers
427/// * Refreshing materialized views
428#[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                // Skip indexes that back temporal unique constraints — the backing
458                // index is created implicitly when the temporal constraint is added.
459                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/// Applies the structures generated in [get_post_apply_statement_groups] to the destination sequentially.
667#[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/// Applies the structures generated in [get_post_apply_statement_groups] to the destination in parallel.
690#[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/// Get the data format to use when copying data from the source to the destination, that both
733/// source and destination supports.
734#[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}