Repository: datanymizer/datanymizer (language: rust)

Issue:
Fetch tables metadata takes too long
`Fetch tables metadata...`
takes more time than a full database dump.

I have filters in place for only 4 tables I need out of 100+ but this still takes longer than I expect.


Discussion / hints:
I see `SELECT pg_catalog.pg_get_serial_sequence ( ? )` millions of times on the db
> `Fetch tables metadata...` takes more time than a full database dump.

I came here today to say exactly this. I have 20-something schemas. Each with about 50 tables. The metadata fetch takes over an hour. the actual db dumping part takes probably 10 min to a local disk.

TY for the tool, it does the thing, I just wish it were quicker.


Hello! I think that it is a some kind of bug, we will check this.
Let me know how I can help

On Thu, Feb 3, 2022, 10:55 AM Evgeniy Rashchepkin ***@***.***>
wrote:

> Hello! I think that it is some kind of bug, we will check this.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/datanymizer/datanymizer/issues/145#issuecomment-1029133799>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ACWRLSHM2YM422LQT53SIGTUZKQO7ANCNFSM5NNBFGCA>
> .
> Triage notifications on the go with GitHub Mobile for iOS
> <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
> or Android
> <https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
>
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

I have some ideas, could you please provide the database schema (the dump without data)?
angryjohnnie <https://github.com/angryjohnnie>, are you able to easily?
this is a production database, I can't just share our schema, sorry.

On Fri, Feb 4, 2022 at 8:09 AM Evgeniy Rashchepkin ***@***.***>
wrote:

> I have some ideas, could you please provide the database schema (the dump
> without data)?
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/datanymizer/datanymizer/issues/145#issuecomment-1029971312>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ACWRLSBZSADUPV35GKTIRJTUZPFXNANCNFSM5NNBFGCA>
> .
> Triage notifications on the go with GitHub Mobile for iOS
> <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
> or Android
> <https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
>
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

> angryjohnnie <https://github.com/angryjohnnie>, are you able to easily? this is a production database, I can't just share our schema, sorry.
> […](#)

im not sure what our policy is regarding sharing that schema. I can see if I can find out though.
It looks like I am not allowed to share our schema either, unfortunately.
OK, then we will try to fix this without these schemas.
if you have branch or changes you would like to test, I can run it and
supply logs

On Mon, Feb 7, 2022 at 11:50 AM Evgeniy Rashchepkin <
***@***.***> wrote:

> OK, then we will try to fix this without these schemas.
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/datanymizer/datanymizer/issues/145#issuecomment-1031686064>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ACWRLSCGX7ZZZHVLPOSMG2TUZ7Z3TANCNFSM5NNBFGCA>
> .
> Triage notifications on the go with GitHub Mobile for iOS
> <https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
> or Android
> <https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
>
> You are receiving this because you authored the thread.Message ID:
> ***@***.***>
>

I have a similar kind of issue, I have 4 schemas and around 20 tables in each of the schema. I also tried passing filter args using the [cli <PG_DUMP_ARGS>](https://github.com/datanymizer/datanymizer/blob/main/docs/pg_datanymizer.md#args). But it still takes more than 20 mins to fetch the metadata.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5b9e3a2..5178a2d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   ([@evgeniy-r](https://github.com/evgeniy-r))
 
 ### ⚙️ Changed
+- Speed up loading of metadata [#170](https://github.com/datanymizer/datanymizer/pull/170)
+  ([@evgeniy-r](https://github.com/evgeniy-r))
 - Refactoring (more generic `Dumper` trait) [#159](https://github.com/datanymizer/datanymizer/pull/159)
   ([@evgeniy-r](https://github.com/evgeniy-r))
 - Remove the dependency from `crono` ([security issue](https://github.com/chronotope/chrono/pull/578)).
diff --git a/datanymizer_dumper/src/lib.rs b/datanymizer_dumper/src/lib.rs
index 575436d..a8db73f 100644
--- a/datanymizer_dumper/src/lib.rs
+++ b/datanymizer_dumper/src/lib.rs
@@ -29,7 +29,7 @@ pub trait Dumper: 'static + Sized + Send {
 
     /// Preparation (load table list, set table ordering, initialize table filter)
     fn prepare(&mut self, connection: &mut Self::Connection) -> Result<()> {
-        let mut tables = self.schema_inspector().ordered_tables(connection);
+        let mut tables = self.schema_inspector().ordered_tables(connection)?;
         sort_tables(&mut tables, &self.settings().table_order);
         let tables: Vec<_> = tables.into_iter().map(|(t, _)| t).collect();
         self.filter_mut()
@@ -76,6 +76,7 @@ pub trait SchemaInspector: 'static + Sized + Send + Clone {
     type Connection;
     type Table: Table<Self::Type>;
     type Column: ColumnData<Self::Type>;
+    type ForeignKey;
 
     /// Get all tables in the database
     fn get_tables(&self, connection: &mut Self::Connection) -> Result<Vec<Self::Table>>;
@@ -84,37 +85,44 @@ pub trait SchemaInspector: 'static + Sized + Send + Clone {
     fn get_table_size(&self, connection: &mut Self::Connection, table: &Self::Table)
         -> Result<i64>;
 
-    /// Get all dependencies (by FK) for `table` in database
-    fn get_dependencies(
+    /// Get foreign keys for table
+    fn get_foreign_keys(
         &self,
         connection: &mut Self::Connection,
         table: &Self::Table,
-    ) -> Result<Vec<Self::Table>>;
-
-    fn ordered_tables(&self, connection: &mut Self::Connection) -> Vec<(Self::Table, i32)> {
-        let mut res: HashMap<Self::Table, i32> = HashMap::new();
-        let mut depgraph: DepGraph<Self::Table> = DepGraph::new();
-        if let Ok(tables) = self.get_tables(connection) {
-            for table in tables.iter() {
-                let deps: Vec<Self::Table> = self
-                    .get_dependencies(connection, table)
-                    .unwrap_or_default()
-                    .into_iter()
-                    .collect();
-                depgraph.register_dependencies(table.clone(), deps);
-            }
+    ) -> Result<Vec<Self::ForeignKey>>;
+
+    fn ordered_tables(&self, connection: &mut Self::Connection) -> Result<Vec<(Self::Table, i32)>> {
+        let mut depgraph: DepGraph<String> = DepGraph::new();
 
-            for table in tables.iter() {
-                let _ = res.entry(table.clone()).or_insert(0);
-                if let Ok(nodes) = depgraph.dependencies_of(table) {
-                    for node in nodes.flatten() {
-                        let counter = res.entry(node.clone()).or_insert(0);
-                        *counter += 1;
-                    }
+        let tables = self.get_tables(connection)?;
+        let mut weight_map: HashMap<String, i32> = HashMap::with_capacity(tables.len());
+
+        for table in tables.iter() {
+            depgraph.register_dependencies(table.get_full_name(), table.get_dep_table_names());
+        }
+
+        for table in tables.iter() {
+            let name = table.get_full_name();
+            weight_map.entry(name.clone()).or_insert(0);
+            if let Ok(dep_names) = depgraph.dependencies_of(&name) {
+                for dep_name in dep_names.flatten() {
+                    let weight = weight_map.entry(dep_name.clone()).or_insert(0);
+                    *weight += 1;
                 }
             }
         }
-        res.iter().map(|(k, b)| (k.clone(), *b)).collect()
+
+        Ok(tables
+            .into_iter()
+            .map(|t| {
+                let name = t.get_full_name();
+                (
+                    t,
+                    weight_map.get(name.as_str()).copied().unwrap_or_default(),
+                )
+            })
+            .collect())
     }
 
     /// Get columns for table
@@ -144,6 +152,8 @@ pub trait Table<T>: Sized + Send + Clone + Eq + Hash {
     fn get_size(&self) -> i64;
     /// Get column name - index map
     fn get_column_indexes(&self) -> &HashMap<String, usize>;
+    /// Get depended table names
+    fn get_dep_table_names(&self) -> Vec<String>;
 }
 
 pub trait ColumnData<T> {
@@ -178,15 +188,32 @@ mod tests {
     struct MockTable {
         schema: &'static str,
         name: &'static str,
+        dep_table_names: Vec<&'static str>,
         col_map: HashMap<String, usize>,
     }
 
     impl MockTable {
         fn new(schema: &'static str, name: &'static str) -> Self {
+            let dep_table_names = vec![];
+            let col_map = HashMap::new();
+            Self {
+                schema,
+                name,
+                dep_table_names,
+                col_map,
+            }
+        }
+
+        fn with_deps(
+            schema: &'static str,
+            name: &'static str,
+            dep_table_names: Vec<&'static str>,
+        ) -> Self {
             let col_map = HashMap::new();
             Self {
                 schema,
                 name,
+                dep_table_names,
                 col_map,
             }
         }
@@ -230,26 +257,34 @@ mod tests {
         fn get_column_indexes(&self) -> &HashMap<String, usize> {
             &self.col_map
         }
+
+        fn get_dep_table_names(&self) -> Vec<String> {
+            self.dep_table_names.iter().map(|s| s.to_string()).collect()
+        }
     }
 
     struct MockConnection;
 
     #[derive(Clone)]
-    struct MockSchemaInspector;
+    struct MockSchemaInspector {
+        tables: Vec<MockTable>,
+    }
+
+    impl MockSchemaInspector {
+        fn new(tables: Vec<MockTable>) -> Self {
+            Self { tables }
+        }
+    }
 
     impl SchemaInspector for MockSchemaInspector {
         type Type = ();
         type Connection = MockConnection;
         type Table = MockTable;
         type Column = MockColumn;
+        type ForeignKey = ();
 
         fn get_tables(&self, _connection: &mut Self::Connection) -> Result<Vec<Self::Table>> {
-            Ok(vec![
-                MockTable::new("public", "table1"),
-                MockTable::new("other", "table1"),
-                MockTable::new("public", "table2"),
-                MockTable::new("public", "table3"),
-            ])
+            Ok(self.tables.clone())
         }
 
         fn get_table_size(
@@ -257,14 +292,14 @@ mod tests {
             _connection: &mut Self::Connection,
             _table: &Self::Table,
         ) -> Result<i64> {
-            Ok(4)
+            Ok(self.tables.len() as i64)
         }
 
-        fn get_dependencies(
+        fn get_foreign_keys(
             &self,
             _connection: &mut Self::Connection,
             _table: &Self::Table,
-        ) -> Result<Vec<Self::Table>> {
+        ) -> Result<Vec<Self::ForeignKey>> {
             Ok(vec![])
         }
 
@@ -278,15 +313,20 @@ mod tests {
     }
 
     struct MockDumper {
+        inspector: MockSchemaInspector,
         pub tables: Vec<MockTable>,
         pub settings: Settings,
     }
 
     impl MockDumper {
-        fn new(cfg: &str) -> Self {
+        fn new(cfg: &str, inspector: MockSchemaInspector) -> Self {
             let tables = vec![];
             let settings = Settings::from_yaml(cfg).unwrap();
-            Self { tables, settings }
+            Self {
+                inspector,
+                tables,
+                settings,
+            }
         }
     }
 
@@ -307,7 +347,7 @@ mod tests {
         }
 
         fn schema_inspector(&self) -> Self::SchemaInspector {
-            MockSchemaInspector
+            self.inspector.clone()
         }
 
         fn set_tables(&mut self, tables: Vec<<Self::SchemaInspector as SchemaInspector>::Table>) {
@@ -361,43 +401,131 @@ mod tests {
         )
     }
 
-    #[test]
-    fn prepare() {
-        let cfg = r#"
-            table_order:
-              - table3
-              - other.table1
-              - public.table1
-            filter:
-              schema:
-                except:
-                  - other.*
-              data:
-                only:
-                  - table1
-                  - table2
-            "#;
-        let mut dumper = MockDumper::new(cfg);
-
-        assert!(dumper.prepare(&mut MockConnection).is_ok());
-        // it loads sorted tables
-        assert_eq!(
-            dumper
-                .tables
-                .iter()
-                .map(|t| t.get_full_name())
-                .collect::<Vec<_>>(),
-            vec![
-                "public.table2",
-                "public.table3",
-                "other.table1",
-                "public.table1"
-            ]
-        );
-        // it inits filter
-        assert_eq!(
-            dumper.settings.filter.schema_match_list(),
-            &TableList::Except(vec![String::from("other.table1")])
-        );
+    mod dumper {
+        use super::*;
+
+        #[test]
+        fn prepare() {
+            let cfg = r#"
+                table_order:
+                  - table3
+                  - other.table1
+                  - public.table1
+                filter:
+                  schema:
+                    except:
+                      - other.*
+                  data:
+                    only:
+                      - table1
+                      - table2
+                "#;
+            let inspector = MockSchemaInspector::new(vec![
+                MockTable::new("public", "table1"),
+                MockTable::new("other", "table1"),
+                MockTable::new("public", "table2"),
+                MockTable::new("public", "table3"),
+            ]);
+            let mut dumper = MockDumper::new(cfg, inspector);
+
+            assert!(dumper.prepare(&mut MockConnection).is_ok());
+            // it loads sorted tables
+            assert_eq!(
+                dumper
+                    .tables
+                    .iter()
+                    .map(|t| t.get_full_name())
+                    .collect::<Vec<_>>(),
+                vec![
+                    "public.table2",
+                    "public.table3",
+                    "other.table1",
+                    "public.table1"
+                ]
+            );
+            // it inits filter
+            assert_eq!(
+                dumper.settings.filter.schema_match_list(),
+                &TableList::Except(vec![String::from("other.table1")])
+            );
+        }
+    }
+
+    mod inspector {
+        use super::*;
+
+        mod ordered_tables {
+            use super::*;
+
+            fn get_weight_map(ts: Vec<MockTable>) -> HashMap<String, i32> {
+                let inspector = MockSchemaInspector::new(ts);
+
+                let tables: Vec<_> = inspector.ordered_tables(&mut MockConnection).unwrap();
+                let mut weight_map = HashMap::new();
+                for (t, i) in tables.iter() {
+                    weight_map.insert(t.get_full_name(), *i);
+                }
+
+                weight_map
+            }
+
+            #[test]
+            fn plain() {
+                let weight_map = get_weight_map(vec![
+                    MockTable::with_deps("public", "table1", vec![]),
+                    MockTable::with_deps("public", "table2", vec![]),
+                    MockTable::with_deps("public", "table3", vec![]),
+                ]);
+
+                assert_eq!(weight_map["public.table1"], 1);
+                assert_eq!(weight_map["public.table2"], 1);
+                assert_eq!(weight_map["public.table3"], 1);
+            }
+
+            #[test]
+            fn simple() {
+                let weight_map = get_weight_map(vec![
+                    MockTable::with_deps("public", "table1", vec![]),
+                    MockTable::with_deps("public", "table2", vec!["public.table1"]),
+                    MockTable::with_deps("public", "table3", vec!["public.table1"]),
+                ]);
+
+                assert_eq!(weight_map["public.table1"], 3);
+                assert_eq!(weight_map["public.table2"], 1);
+                assert_eq!(weight_map["public.table3"], 1);
+            }
+
+            #[test]
+            fn chain() {
+                let weight_map = get_weight_map(vec![
+                    MockTable::with_deps("public", "table1", vec![]),
+                    MockTable::with_deps("public", "table2", vec!["public.table1"]),
+                    MockTable::with_deps(
+                        "public",
+                        "table3",
+                        vec!["public.table1", "public.table2"],
+                    ),
+                    MockTable::with_deps("public", "table4", vec!["public.table3"]),
+                ]);
+
+                assert_eq!(weight_map["public.table1"], 4);
+                assert_eq!(weight_map["public.table2"], 3);
+                assert_eq!(weight_map["public.table3"], 2);
+                assert_eq!(weight_map["public.table4"], 1);
+            }
+
+            #[test]
+            fn cycle() {
+                let weight_map = get_weight_map(vec![
+                    MockTable::with_deps("public", "table1", vec!["public.table2"]),
+                    MockTable::with_deps("public", "table2", vec!["public.table3"]),
+                    MockTable::with_deps("public", "table3", vec!["public.table1"]),
+                ]);
+
+                assert_eq!(weight_map["public.table1"], 0);
+                assert_eq!(weight_map["public.table2"], 0);
+                assert_eq!(weight_map["public.table3"], 0);
+            }
+        }
     }
 }
diff --git a/datanymizer_dumper/src/postgres/foreign_key.rs b/datanymizer_dumper/src/postgres/foreign_key.rs
index ce79fc8..08e4ca9 100644
--- a/datanymizer_dumper/src/postgres/foreign_key.rs
+++ b/datanymizer_dumper/src/postgres/foreign_key.rs
@@ -1,7 +1,7 @@
 use postgres::Row as PostgresRow;
 
-#[derive(Debug)]
-pub struct ForeignKey {
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct PgForeignKey {
     // Source
     pub table_schema: String,
     pub table_name: String,
@@ -15,7 +15,7 @@ pub struct ForeignKey {
     pub foreign_column_name: String,
 }
 
-impl From<PostgresRow> for ForeignKey {
+impl From<PostgresRow> for PgForeignKey {
     fn from(row: PostgresRow) -> Self {
         Self {
             table_schema: row.get("table_schema"),
diff --git a/datanymizer_dumper/src/postgres/schema_inspector.rs b/datanymizer_dumper/src/postgres/schema_inspector.rs
index 0737df9..627ec18 100644
--- a/datanymizer_dumper/src/postgres/schema_inspector.rs
+++ b/datanymizer_dumper/src/postgres/schema_inspector.rs
@@ -1,42 +1,36 @@
 use super::{
-    column::PgColumn, connector, foreign_key::ForeignKey, sequence::PgSequence, table::PgTable,
+    column::PgColumn, connector, foreign_key::PgForeignKey, sequence::PgSequence, table::PgTable,
     SchemaInspector,
 };
-use crate::Table;
 use anyhow::Result;
 use postgres::types::Type;
 
-const PG_CATALOG_SCHEMA: &str = "SELECT tablename, schemaname
-                                 FROM pg_catalog.pg_tables
-                                 WHERE schemaname != 'pg_catalog'
-                                 AND schemaname != 'information_schema'";
-
-const TABLE_FOREIGN_KEYS: &str = "SELECT
-                                    tc.table_schema,
-                                    tc.constraint_name,
-                                    tc.table_name,
-                                    kcu.column_name,
-                                    ccu.table_schema AS foreign_table_schema,
-                                    ccu.table_name AS foreign_table_name,
-                                    ccu.column_name AS foreign_column_name
-                                FROM
-                                    information_schema.table_constraints AS tc
-                                    JOIN information_schema.key_column_usage AS kcu
-                                    ON tc.constraint_name = kcu.constraint_name
-                                    AND tc.table_schema = kcu.table_schema
-                                    JOIN information_schema.constraint_column_usage AS ccu
-                                    ON ccu.constraint_name = tc.constraint_name
-                                    AND ccu.table_schema = tc.table_schema
-                                WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name = $1";
+const PG_CATALOG_SCHEMA_QUERY: &str = "SELECT tablename, schemaname
+    FROM pg_catalog.pg_tables
+    WHERE schemaname != 'pg_catalog' AND schemaname != 'information_schema'";
+
+const TABLE_FK_QUERY: &str = "SELECT tc.table_schema,
+        tc.constraint_name,
+        tc.table_name,
+        kcu.column_name,
+        ccu.table_schema AS foreign_table_schema,
+        ccu.table_name AS foreign_table_name,
+        ccu.column_name AS foreign_column_name
+    FROM information_schema.table_constraints AS tc
+    JOIN information_schema.key_column_usage AS kcu
+    ON tc.constraint_name = kcu.constraint_name AND  tc.table_schema = kcu.table_schema
+    JOIN information_schema.constraint_column_usage AS ccu
+    ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
+    WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = $1 AND tc.table_name = $2";
 
 const TABLE_COLUMNS_QUERY: &str = "SELECT cc.column_name, cc.ordinal_position, cc.data_type, pt.oid
-                                   FROM information_schema.columns as cc
-                                        JOIN pg_catalog.pg_namespace as pn
-                                        ON cc.udt_schema = pn.nspname
-                                        JOIN pg_catalog.pg_type as pt
-                                        ON cc.udt_name = pt.typname AND pn.oid = pt.typnamespace
-                                   WHERE cc.table_schema = $1 and cc.table_name = $2                                   
-                                   ORDER BY cc.ordinal_position ASC";
+    FROM information_schema.columns as cc
+    JOIN pg_catalog.pg_namespace as pn
+    ON cc.udt_schema = pn.nspname
+    JOIN pg_catalog.pg_type as pt
+    ON cc.udt_name = pt.typname AND pn.oid = pt.typnamespace
+    WHERE cc.table_schema = $1 and cc.table_name = $2
+    ORDER BY cc.ordinal_position ASC";
 
 const TABLE_SIZE_QUERY: &str =
     "SELECT
@@ -46,7 +40,9 @@ const TABLE_SIZE_QUERY: &str =
     )::bigint AS len
     FROM pg_catalog.pg_class
     INNER JOIN pg_catalog.pg_namespace ON pg_catalog.pg_class.relnamespace = pg_catalog.pg_namespace.oid
-    WHERE pg_catalog.pg_class.relname = $1 AND pg_catalog.pg_namespace.nspname = $2";
+    WHERE pg_catalog.pg_namespace.nspname = $1 AND pg_catalog.pg_class.relname = $2";
+
+const SEQUENCE_QUERY: &str = "SELECT pg_catalog.pg_get_serial_sequence($1, $2)";
 
 #[derive(Clone)]
 pub struct PgSchemaInspector;
@@ -56,13 +52,14 @@ impl SchemaInspector for PgSchemaInspector {
     type Connection = connector::Connection;
     type Table = PgTable;
     type Column = PgColumn;
+    type ForeignKey = PgForeignKey;
 
     // Get all tables in the database
     fn get_tables(&self, connection: &mut Self::Connection) -> Result<Vec<Self::Table>> {
         let mut counter = 0;
         let items: Vec<Self::Table> = connection
             .client
-            .query(PG_CATALOG_SCHEMA, &[])?
+            .query(PG_CATALOG_SCHEMA_QUERY, &[])?
             .into_iter()
             .map(|row| row.into())
             .map(|mut table| {
@@ -72,6 +69,9 @@ impl SchemaInspector for PgSchemaInspector {
                 if let Ok(sequences) = self.get_sequences(connection, &table) {
                     table.set_sequences(sequences);
                 };
+                if let Ok(foreign_keys) = self.get_foreign_keys(connection, &table) {
+                    table.set_foreign_keys(foreign_keys);
+                };
 
                 match self.get_table_size(connection, &table) {
                     Ok(size) => table.size = size as i64,
@@ -94,46 +94,23 @@ impl SchemaInspector for PgSchemaInspector {
     ) -> Result<i64> {
         let row = connection
             .client
-            .query_one(TABLE_SIZE_QUERY, &[&table.tablename, &table.schemaname])?;
+            .query_one(TABLE_SIZE_QUERY, &[&table.schemaname, &table.tablename])?;
         let size: i64 = row.get("len");
         Ok(size)
     }
 
-    // Get all dependencies (by FK) for `table` in database
-    fn get_dependencies(
+    /// Get foreign keys for table
+    fn get_foreign_keys(
         &self,
         connection: &mut Self::Connection,
         table: &Self::Table,
-    ) -> Result<Vec<Self::Table>> {
-        let fkeys_iterator = connection
+    ) -> Result<Vec<Self::ForeignKey>> {
+        Ok(connection
             .client
-            .query(TABLE_FOREIGN_KEYS, &[&table.get_name()])?
+            .query(TABLE_FK_QUERY, &[&table.schemaname, &table.tablename])?
             .into_iter()
-            .map(|row| row.into());
-
-        let tables: Vec<Self::Table> = fkeys_iterator
-            // Table from foreign key
-            .map(|fkey: ForeignKey| {
-                PgTable::new(fkey.foreign_table_name, fkey.foreign_table_schema)
-            })
-            // Columns for table
-            .map(|mut table| {
-                if let Ok(columns) = self.get_columns(connection, &table) {
-                    table.set_columns(columns);
-                };
-                if let Ok(sequences) = self.get_sequences(connection, &table) {
-                    table.set_sequences(sequences);
-                };
-
-                match self.get_table_size(connection, &table) {
-                    Ok(size) => table.size = size as i64,
-                    Err(e) => println!("ERR: {}", e),
-                }
-
-                table
-            })
-            .collect();
-        Ok(tables)
+            .map(|row| row.into())
+            .collect())
     }
 
     /// Get columns for table
@@ -162,10 +139,7 @@ impl PgSchemaInspector {
         for col in table.columns.iter() {
             let full_name: Option<String> = connection
                 .client
-                .query_one(
-                    "SELECT pg_catalog.pg_get_serial_sequence($1, $2)",
-                    &[&table.quoted_full_name(), &col.name],
-                )?
+                .query_one(SEQUENCE_QUERY, &[&table.quoted_full_name(), &col.name])?
                 .get(0);
             if let Some(full_name) = full_name {
                 sequences.push(PgSequence { full_name });
diff --git a/datanymizer_dumper/src/postgres/table.rs b/datanymizer_dumper/src/postgres/table.rs
index c08ef41..a8a6a8a 100644
--- a/datanymizer_dumper/src/postgres/table.rs
+++ b/datanymizer_dumper/src/postgres/table.rs
@@ -1,4 +1,4 @@
-use super::{column::PgColumn, row::PgRow, sequence::PgSequence};
+use super::{column::PgColumn, foreign_key::PgForeignKey, row::PgRow, sequence::PgSequence};
 use crate::Table;
 use anyhow::{anyhow, Result};
 use datanymizer_engine::{Query as QueryCfg, Table as TableCfg};
@@ -14,6 +14,7 @@ pub struct PgTable {
     pub schemaname: String,
     pub columns: Vec<PgColumn>,
     pub sequences: Vec<PgSequence>,
+    pub foreign_keys: Vec<PgForeignKey>,
     column_indexes: HashMap<String, usize>,
     pub size: i64,
 }
@@ -64,6 +65,13 @@ impl Table<Type> for PgTable {
     fn get_column_indexes(&self) -> &HashMap<String, usize> {
         &self.column_indexes
     }
+
+    fn get_dep_table_names(&self) -> Vec<String> {
+        self.foreign_keys
+            .iter()
+            .map(|fk| format!("{}.{}", fk.foreign_table_schema, fk.foreign_table_name))
+            .collect()
+    }
 }
 
 impl PgTable {
@@ -73,6 +81,7 @@ impl PgTable {
             schemaname,
             columns: vec![],
             sequences: vec![],
+            foreign_keys: vec![],
             column_indexes: HashMap::new(),
             size: 0,
         }
@@ -107,6 +116,10 @@ impl PgTable {
         self.sequences = sequences;
     }
 
+    pub fn set_foreign_keys(&mut self, foreign_keys: Vec<PgForeignKey>) {
+        self.foreign_keys = foreign_keys;
+    }
+
     pub fn transformed_query_to(
         &self,
         cfg: Option<&TableCfg>,


Test patch:
diff --git a/datanymizer_dumper/tests/postgres/schema_inspector.rs b/datanymizer_dumper/tests/postgres/schema_inspector.rs
index caa8096..5c507a4 100644
--- a/datanymizer_dumper/tests/postgres/schema_inspector.rs
+++ b/datanymizer_dumper/tests/postgres/schema_inspector.rs
@@ -12,6 +12,24 @@ fn find_table<'a>(tables: &'a [PgTable], full_name: &str) -> &'a PgTable {
         .unwrap()
 }
 
+fn assert_cols(table: &PgTable, expected: Vec<&'static str>) {
+    let mut cols: Vec<_> = table
+        .columns
+        .iter()
+        .map(|c| (c.position, c.name.clone()))
+        .collect();
+    cols.sort_by_key(|(p, _)| *p);
+    let cols: Vec<_> = cols.iter().map(|(_, name)| name.clone()).collect();
+    assert_eq!(cols, expected);
+}
+
+fn assert_fks(table: &PgTable, expected: Vec<&'static str>) {
+    let deps = table.get_dep_table_names();
+    for name in expected {
+        assert!(deps.contains(&name.to_string()));
+    }
+}
+
 #[test]
 fn get_tables() {
     let mut connection = Connection::new(helpers::src_client(), helpers::src_database_url());
@@ -21,4 +39,46 @@ fn get_tables() {
     let table = find_table(&tables, "public.actor");
     assert_eq!(table.tablename, "actor");
     assert_eq!(table.schemaname, "public");
+    assert_cols(
+        table,
+        vec!["actor_id", "first_name", "last_name", "last_update"],
+    );
+    assert_fks(table, vec![]);
+
+    let table = find_table(&tables, "public.address");
+    assert_eq!(table.tablename, "address");
+    assert_eq!(table.schemaname, "public");
+    assert_cols(
+        table,
+        vec![
+            "address_id",
+            "address",
+            "address2",
+            "district",
+            "city_id",
+            "postal_code",
+            "phone",
+            "last_update",
+        ],
+    );
+    assert_fks(table, vec!["public.city"]);
+
+    let table = find_table(&tables, "public.payment");
+    assert_eq!(table.tablename, "payment");
+    assert_eq!(table.schemaname, "public");
+    assert_cols(
+        table,
+        vec![
+            "payment_id",
+            "customer_id",
+            "staff_id",
+            "rental_id",
+            "amount",
+            "payment_date",
+        ],
+    );
+    assert_fks(
+        table,
+        vec!["public.customer", "public.rental", "public.staff"],
+    );
 }