Skip to main content

elefant_tools/storage/
mod.rs

1use crate::models::PostgresDatabase;
2use crate::*;
3use std::sync::Arc;
4
5mod data_format;
6mod elefant_file;
7mod postgres;
8mod sql_file;
9mod table_data;
10
11// pub use elefant_file::ElefantFileDestinationStorage;
12use crate::models::PostgresSchema;
13use crate::models::PostgresTable;
14use crate::quoting::IdentifierQuoter;
15pub use data_format::*;
16pub use postgres::PostgresInstanceStorage;
17pub use sql_file::{apply_sql_file, apply_sql_string, SqlDataMode, SqlFile, SqlFileOptions};
18pub use table_data::*;
19
20/// A trait for thing that are either a CopyDestination or CopySource.
21pub trait BaseCopyTarget {
22    /// Which data format is supported by this destination/source.
23    fn supported_data_format(&self) -> impl std::future::Future<Output = Result<Vec<DataFormat>>>;
24}
25
26/// A factory for providing copy sources. This is used to create a source that can be used to read data from.
27pub trait CopySourceFactory: BaseCopyTarget {
28    /// A type that can be used to read data from the source. This type has to support
29    /// single threaded reading, but can support multiple threads reading at the same time.
30    type SequentialSource: CopySource;
31
32    /// A type that can be used to read data from the source. This type has to support
33    /// multiple threads reading at the same time.
34    type ParallelSource: CopySource + Clone;
35
36    /// Should provide introspection data of the source. This means poking the `pg_catalog` tables when
37    /// working with Postgres, for example.
38    fn get_introspection(&self) -> impl std::future::Future<Output = Result<PostgresDatabase>>;
39
40    /// Should create whatever type is needed to be able to read data from the source.
41    fn create_source(
42        &self,
43    ) -> impl std::future::Future<
44        Output = Result<SequentialOrParallel<Self::SequentialSource, Self::ParallelSource>>,
45    >;
46
47    /// Should create a datasource that works with single threaded reading.
48    fn create_sequential_source(
49        &self,
50    ) -> impl std::future::Future<Output = Result<Self::SequentialSource>>;
51
52    /// Should return what kind of parallelism is supported by the source. This is used
53    /// for negotiation with the destination.
54    fn supported_parallelism(&self) -> SupportedParallelism;
55}
56
57/// A copy source is something that can be used to read data from a source.
58pub trait CopySource: Send {
59    /// The type of the specific data reader provided when reading data
60    type DataReader<'a>: TableDataReader + 'a
61    where
62        Self: 'a;
63
64    /// The type of the cleanup that is returned when reading data. Can be `()` if no cleanup is needed.
65    type Cleanup: AsyncCleanup;
66
67    /// Should return a data-reader for the specified type in the specified format.
68    fn get_data<'a>(
69        &'a mut self,
70        schema: &'a PostgresSchema,
71        table: &'a PostgresTable,
72        data_format: &'a DataFormat,
73    ) -> impl std::future::Future<Output = Result<TableData<Self::DataReader<'a>, Self::Cleanup>>> + 'a;
74}
75
76/// A factory for providing copy destinations. This is used to create a destination that can be used to write data to.
77pub trait CopyDestinationFactory<'a>: BaseCopyTarget {
78    /// The implementation type when dealing with single-threaded workloads. The can optionally
79    /// support multi-threading, but it is not needed.
80    type SequentialDestination: CopyDestination;
81
82    /// The implementation type when dealing with multithreaded workloads. This type has to support
83    /// multi-threading.
84    type ParallelDestination: CopyDestination + Clone;
85
86    /// Should create whatever type is needed to be able to write data to the destination.
87    fn create_destination(
88        &'a mut self,
89    ) -> impl std::future::Future<
90        Output = Result<
91            SequentialOrParallel<Self::SequentialDestination, Self::ParallelDestination>,
92        >,
93    >;
94
95    /// Should create a destination that works with single threaded writing.
96    fn create_sequential_destination(
97        &'a mut self,
98    ) -> impl std::future::Future<Output = Result<Self::SequentialDestination>>;
99
100    /// Should return what kind of parallelism is supported by the destination. This is used
101    /// for negotiation with the source.
102    fn supported_parallelism(&self) -> SupportedParallelism;
103}
104
105/// A transaction on a copy destination. Returned by `CopyDestination::begin_transaction`.
106/// All transactional DDL statements should be applied through this type, and the transaction
107/// must be committed when done.
108pub trait CopyTransaction: Send {
109    /// Apply a DDL statement within this transaction.
110    fn apply_statement(&mut self, statement: &str)
111        -> impl std::future::Future<Output = Result<()>>;
112
113    /// Commit the transaction.
114    fn commit(self) -> impl std::future::Future<Output = Result<()>>;
115}
116
117pub trait CopyDestination: Send {
118    /// The transaction type returned by `begin_transaction`.
119    type Transaction<'a>: CopyTransaction + 'a
120    where
121        Self: 'a;
122
123    /// This should apply the data to the destination. The data is expected to be in the
124    /// format returned by `supported_data_format`, if possible.
125    fn apply_data<R: TableDataReader, C: AsyncCleanup>(
126        &mut self,
127        schema: &PostgresSchema,
128        table: &PostgresTable,
129        data: TableData<R, C>,
130    ) -> impl std::future::Future<Output = Result<()>>;
131
132    /// This should apply the DDL statements to the destination.
133    /// These commands has to be run outside a transaction, as they might fail otherwise.
134    fn apply_non_transactional_statement(
135        &mut self,
136        statement: &str,
137    ) -> impl std::future::Future<Output = Result<()>>;
138
139    /// Should begin a new transaction and return a handle to it.
140    fn begin_transaction(
141        &mut self,
142    ) -> impl std::future::Future<Output = Result<Self::Transaction<'_>>>;
143
144    /// Should get the identifier quoter that works with this destination. This ensures
145    /// quoting respects the rules of the destination, not the source.
146    fn get_identifier_quoter(&self) -> Arc<IdentifierQuoter>;
147
148    fn finish(&mut self) -> impl std::future::Future<Output = Result<()>> {
149        async { Ok(()) }
150    }
151
152    /// Should try to introspect the destination. If introspection is not supported, this should return `Ok(None)`,
153    /// not an error. Errors should only be returned if introspection is supported, but failed.
154    fn try_introspect(
155        &self,
156    ) -> impl std::future::Future<Output = Result<Option<PostgresDatabase>>> {
157        async { Ok(None) }
158    }
159
160    fn has_data_in_table(
161        &self,
162        _schema: &PostgresSchema,
163        _table: &PostgresTable,
164    ) -> impl std::future::Future<Output = Result<bool>> {
165        async { Ok(false) }
166    }
167}
168
169/// A type that can be either a sequential or parallel source or destination.
170pub enum SequentialOrParallel<S: Send, P: Send + Clone> {
171    Sequential(S),
172    Parallel(P),
173}
174
175/// Indicates if parallelism is supported.
176#[derive(Clone, Debug, Eq, PartialEq)]
177pub enum SupportedParallelism {
178    /// Only sequential single-threaded operations are available.
179    Sequential,
180    /// Parallel multithreaded operations are available.
181    Parallel,
182}
183
184impl SupportedParallelism {
185    /// Negotiate the parallelism between two sources or destinations.
186    pub fn negotiate_parallelism(&self, other: SupportedParallelism) -> SupportedParallelism {
187        match (self, other) {
188            (SupportedParallelism::Parallel, SupportedParallelism::Parallel) => {
189                SupportedParallelism::Parallel
190            }
191            _ => SupportedParallelism::Sequential,
192        }
193    }
194}
195
196impl<S: CopyDestination, P: CopyDestination + Clone> SequentialOrParallel<S, P> {
197    pub(crate) async fn finish(&mut self) -> Result<()> {
198        with_both!(self, |d| d.finish().await)
199    }
200
201    pub(crate) async fn try_get_introspeciton(&self) -> Result<Option<PostgresDatabase>> {
202        with_both!(self, |d| d.try_introspect().await)
203    }
204}
205
206/// A CopyDestination that panics when used.
207/// Cannot be constructed outside this module, but is available for type reference
208/// to indicate Parallel copy is not supported.
209#[derive(Copy, Clone)]
210pub struct ParallelCopyDestinationNotAvailable {
211    _private: (),
212}
213
214/// A CopyTransaction that panics when used.
215pub struct ParallelCopyTransactionNotAvailable {
216    _private: (),
217}
218
219impl CopyTransaction for ParallelCopyTransactionNotAvailable {
220    async fn apply_statement(&mut self, _statement: &str) -> Result<()> {
221        unreachable!("Parallel copy destination not available")
222    }
223
224    async fn commit(self) -> Result<()> {
225        unreachable!("Parallel copy destination not available")
226    }
227}
228
229impl CopyDestination for ParallelCopyDestinationNotAvailable {
230    type Transaction<'a> = ParallelCopyTransactionNotAvailable;
231
232    async fn apply_data<R: TableDataReader, C: AsyncCleanup>(
233        &mut self,
234        _schema: &PostgresSchema,
235        _table: &PostgresTable,
236        _data: TableData<R, C>,
237    ) -> Result<()> {
238        unreachable!("Parallel copy destination not available")
239    }
240
241    async fn apply_non_transactional_statement(&mut self, _statement: &str) -> Result<()> {
242        unreachable!("Parallel copy destination not available")
243    }
244
245    async fn begin_transaction(&mut self) -> Result<ParallelCopyTransactionNotAvailable> {
246        unreachable!("Parallel copy destination not available")
247    }
248
249    fn get_identifier_quoter(&self) -> Arc<IdentifierQuoter> {
250        unreachable!("Parallel copy destination not available")
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use crate::test_helpers::TestHelper;
257
258    pub fn get_copy_source_database_create_script(version: i32) -> &'static str {
259        if version >= 150 {
260            r#"
261        create extension btree_gin;
262
263        create table people(
264            id serial primary key,
265            name text not null unique,
266            age int not null check (age > 0),
267            constraint multi_check check (name != 'fsgsdfgsdf' and age < 9999)
268        );
269
270        create index people_age_idx on people (age desc) include (name, id) where (age % 2 = 0);
271        create index people_age_brin_idx on people using brin (age);
272        create index people_name_lower_idx on people (lower(name));
273
274        insert into people(name, age)
275        values
276            ('foo', 42),
277            ('bar', 89),
278            ('nice', 69),
279            (E'str\nange', 420),
280            (E't\t\tap', 421),
281            (E'q''t', 12)
282            ;
283
284        create table field(
285            id serial primary key
286        );
287
288        create table tree_node(
289            id serial primary key,
290            field_id int not null references field(id),
291            name text not null,
292            parent_id int,
293            constraint field_id_id_unique unique (field_id, id),
294            foreign key (field_id, parent_id) references tree_node(field_id, id),
295            constraint unique_name_per_level unique nulls not distinct (field_id, parent_id, name)
296        );
297
298        create view people_who_cant_drink as select * from people where age < 18;
299
300        create table ext_test_table(
301            id serial primary key,
302            name text not null,
303            search_vector tsvector generated always as (to_tsvector('english', name)) stored
304        );
305
306        create index ext_test_table_name_idx on ext_test_table using gin (id, search_vector);
307
308        create table array_test(
309            name text[] not null
310        );
311
312        insert into array_test(name)
313        values
314            ('{"foo", "bar"}'),
315            ('{"baz", "qux"}'),
316            ('{"quux", "corge"}');
317
318        create table my_partitioned_table(
319            value int not null
320        ) partition by range (value);
321
322        create table my_partitioned_table_1 partition of my_partitioned_table for values from (1) to (10);
323        create table my_partitioned_table_2 partition of my_partitioned_table for values from (10) to (20);
324
325        insert into my_partitioned_table(value)
326        values (1), (9), (11), (19);
327
328        create table pets (
329            id serial primary key,
330            name text not null check(length(name) > 1)
331        );
332
333        create table dogs(
334            breed text not null check(length(breed) > 1)
335        ) inherits (pets);
336
337        create table cats(
338            color text not null
339        ) inherits (pets);
340
341        insert into dogs(name, breed) values('Fido', 'beagle');
342        insert into cats(name, color) values('Fluffy', 'white');
343        insert into pets(name) values('Remy');
344            "#
345        } else {
346            r#"
347        create extension btree_gin;
348
349        create table people(
350            id serial primary key,
351            name text not null unique,
352            age int not null check (age > 0),
353            constraint multi_check check (name != 'fsgsdfgsdf' and age < 9999)
354        );
355
356        create index people_age_idx on people (age desc) include (name, id) where (age % 2 = 0);
357        create index people_age_brin_idx on people using brin (age);
358        create index people_name_lower_idx on people (lower(name));
359
360        insert into people(name, age)
361        values
362            ('foo', 42),
363            ('bar', 89),
364            ('nice', 69),
365            (E'str\nange', 420),
366            (E't\t\tap', 421),
367            (E'q''t', 12)
368            ;
369
370        create table field(
371            id serial primary key
372        );
373
374        create table tree_node(
375            id serial primary key,
376            field_id int not null references field(id),
377            name text not null,
378            parent_id int,
379            constraint field_id_id_unique unique (field_id, id),
380            foreign key (field_id, parent_id) references tree_node(field_id, id),
381            constraint unique_name_per_level unique (field_id, parent_id, name)
382        );
383
384        create view people_who_cant_drink as select * from people where age < 18;
385
386        create table ext_test_table(
387            id serial primary key,
388            name text not null,
389            search_vector tsvector generated always as (to_tsvector('english', name)) stored
390        );
391
392        create index ext_test_table_name_idx on ext_test_table using gin (id, search_vector);
393
394        create table array_test(
395            name text[] not null
396        );
397
398        insert into array_test(name)
399        values
400            ('{"foo", "bar"}'),
401            ('{"baz", "qux"}'),
402            ('{"quux", "corge"}');
403
404        create table my_partitioned_table(
405            value int not null
406        ) partition by range (value);
407
408        create table my_partitioned_table_1 partition of my_partitioned_table for values from (1) to (10);
409        create table my_partitioned_table_2 partition of my_partitioned_table for values from (10) to (20);
410
411        insert into my_partitioned_table(value)
412        values (1), (9), (11), (19);
413
414        create table pets (
415            id serial primary key,
416            name text not null check(length(name) > 1)
417        );
418
419        create table dogs(
420            breed text not null check(length(breed) > 1)
421        ) inherits (pets);
422
423        create table cats(
424            color text not null
425        ) inherits (pets);
426
427        insert into dogs(name, breed) values('Fido', 'beagle');
428        insert into cats(name, color) values('Fluffy', 'white');
429        insert into pets(name) values('Remy');
430            "#
431        }
432    }
433
434    pub fn get_expected_people_data() -> Vec<(i32, String, i32)> {
435        vec![
436            (1, "foo".to_string(), 42),
437            (2, "bar".to_string(), 89),
438            (3, "nice".to_string(), 69),
439            (4, "str\nange".to_string(), 420),
440            (5, "t\t\tap".to_string(), 421),
441            (6, "q't".to_string(), 12),
442        ]
443    }
444
445    pub fn get_expected_array_test_data() -> Vec<(Vec<String>,)> {
446        vec![
447            (vec!["foo".to_string(), "bar".to_string()],),
448            (vec!["baz".to_string(), "qux".to_string()],),
449            (vec!["quux".to_string(), "corge".to_string()],),
450        ]
451    }
452
453    pub async fn validate_pets(connection: &TestHelper) {
454        let pets = connection
455            .get_results::<(i32, String)>("select id, name from pets order by id")
456            .await;
457        assert_eq!(
458            pets,
459            vec![
460                (1, "Fido".to_string()),
461                (2, "Fluffy".to_string()),
462                (3, "Remy".to_string()),
463            ]
464        );
465
466        let dogs = connection
467            .get_results::<(i32, String, String)>("select id, name, breed from dogs order by id")
468            .await;
469        assert_eq!(dogs, vec![(1, "Fido".to_string(), "beagle".to_string()),]);
470
471        let cats = connection
472            .get_results::<(i32, String, String)>("select id, name, color from cats order by id")
473            .await;
474        assert_eq!(cats, vec![(2, "Fluffy".to_string(), "white".to_string()),]);
475    }
476
477    pub async fn validate_copy_state(destination: &TestHelper) {
478        let items = destination
479            .get_results::<(i32, String, i32)>("select id, name, age from people;")
480            .await;
481
482        assert_eq!(items, get_expected_people_data());
483
484        let result = destination
485            .get_conn()
486            .execute_non_query("insert into people (name, age) values ('new-value', 10000)")
487            .await;
488        assert!(result.is_err(), "Expected CHECK_VIOLATION error");
489        let err_msg = format!("{}", result.unwrap_err());
490        assert!(
491            err_msg.contains("check") || err_msg.contains("CHECK") || err_msg.contains("violates"),
492            "Expected check violation, got: {err_msg}"
493        );
494
495        let result = destination
496            .get_conn()
497            .execute_non_query("insert into people (name, age) values ('foo', 100)")
498            .await;
499        assert!(result.is_err(), "Expected UNIQUE_VIOLATION error");
500        let err_msg = format!("{}", result.unwrap_err());
501        assert!(
502            err_msg.contains("unique")
503                || err_msg.contains("UNIQUE")
504                || err_msg.contains("duplicate"),
505            "Expected unique violation, got: {err_msg}"
506        );
507
508        destination
509            .execute_not_query("insert into field (id) values (1);")
510            .await;
511
512        destination.execute_not_query("insert into tree_node(id, field_id, name, parent_id) values (1, 1, 'foo', null), (2, 1, 'bar', 1)").await;
513        if destination.get_conn().version() >= 150 {
514            let result = destination.get_conn().execute_non_query("insert into tree_node(id, field_id, name, parent_id) values (3, 1, 'foo', null)").await;
515            assert!(result.is_err(), "Expected UNIQUE_VIOLATION error");
516        }
517
518        let result = destination.get_conn().execute_non_query("insert into tree_node(id, field_id, name, parent_id) values (9999, 9999, 'foobarbaz', null)").await;
519        assert!(result.is_err(), "Expected FOREIGN_KEY_VIOLATION error");
520
521        let people_who_cant_drink = destination
522            .get_results::<(i32, String, i32)>("select id, name, age from people_who_cant_drink;")
523            .await;
524        assert_eq!(people_who_cant_drink, vec![(6, "q't".to_string(), 12)]);
525
526        let array_test_data = destination
527            .get_results::<(Vec<String>,)>("select name from array_test;")
528            .await;
529
530        assert_eq!(array_test_data, get_expected_array_test_data());
531
532        let partition_test_data = destination
533            .get_results::<(i32,)>("select value from my_partitioned_table order by value;")
534            .await;
535
536        assert_eq!(partition_test_data, vec![(1,), (9,), (11,), (19,)]);
537
538        validate_pets(destination).await;
539    }
540}