Repository: diesel-rs/diesel (language: rust)

Issue:
Infer_schema! generates joinable for composite Foreign Key
## Setup
The following repository was used to setup and reproduce the problem: https://github.com/lochetti/diesel_joinable_composite_fk

The problem is reproduced just by running the migration with the print_schema enabled.

### Versions

- **Rust**: 1.65
- **Diesel**: 2.0.2
- **Database**: Postgresql
- **Operating System**: Fedora 37

### Feature Flags

- **diesel:**["32-column-tables", "chrono", "numeric", "postgres", "r2d2", "without-deprecated"]

## Problem Description
According to [the documentation](https://docs.diesel.rs/diesel/macro.joinable.html)  we didn't expect to have a joinable being created between the second and third tables, since the only relationship between them is the composite foreign key.

A more dangerous implication of this problem is that it seems that diesel chooses the first column to be the joinable which could lead to wrong joining strategies. This can be verified by changing the order of the columns in the definition of the foreign key, in the migration file, which generates a different join strategy.

### What are you trying to accomplish?
I'm trying to be sure that the infer_schema only creates joinables that are correct, and in that case the joinable created could not be correct.

### What is the expected output?
I expect to don't have a joinable between those tables, according to the documentation provided.

### What is the actual output?
See the schema.rs file in the [setup repository](https://github.com/lochetti/diesel_joinable_composite_fk/blob/main/src/schema.rs).

### Are you seeing any additional errors?
No.

### Steps to reproduce
1. clone the repository
2. configure a postgres database_url in the .env file
3. Run `diesel setup` .
4. See the outputed schema.rs file

## Checklist

- [X] I have already looked over the [issue tracker](https://github.com/diesel-rs/diesel/issues)  and the [disussion forum](https://github.com/diesel-rs/diesel/discussions)  for similar possible closed issues. 
- [X] This issue can be reproduced on Rust's stable channel. (Your issue will be
  closed if this is not the case)
- [X] This issue can be reproduced without requiring a third party crate


Discussion / hints:
Thanks for opening this issue. I've tried to reproduce this based on the provided repository, but for me it does not output 2 foreign keys.

Can you please verify that you:
* Use the latest version of diesel_cli
* That your example actually reproduces the issue
* Add more information about your environment (postgres version, …)
I tried to reproduce here with @lochetti 's repo (me and him found this weird behavior together), and got the same results. The image I'm using is `docker.io/postgres:11.5-alpine`. The schema committed to repository is unchanged after db setup.
I have tested in various docker postgres images.

- docker.io/postgres:10 -> I was able to reproduce the problem
- docker.io/postgres:11 -> I was able to reproduce the problem
- docker.io/postgres:12 -> Don't show the error. Looks like it's ok.
- docker.io/postgres:13 -> Don't show the error. Looks like it's ok.
- docker.io/postgres:14 -> Don't show the error. Looks like it's ok.
- docker.io/postgres:15 -> Don't show the error. Looks like it's ok.

So, it looks like the problem only happens in versions older than 12.
Thanks for coming back to report that this is problem depending on the used postgres version. As this does only affect older postgres versions I do not consider that as a critical bug. I would accept a fix nevertheless as long as it does not break newer postgres versions.

The relevant code is here: 
https://github.com/diesel-rs/diesel/blob/027be9b155a3439ede6c947bd1de30cd8954c3c4/diesel_cli/src/infer_schema_internals/information_schema.rs#L191

This code loads the foreign key constraints from the information schema. We likely miss some condition there to make this work on older postgres version. It might be helpful to just dump the query via `diesel::debug_query` and try to modify the relevant parts in a local postgres command line. You don't need to run `diesel setup` anytime to reproduce the issue. `diesel print-schema` has the same behavior. 
Thank you, for your reply!
Unfortunately, I could not only reproduce the bug on Postgres 15, but I could also show that the column chosen for the joinable definition also varies "arbitrarily".

I updated [the repository](https://github.com/lochetti/diesel_joinable_composite_fk), to show this in a clear manner. With this version, one could reproduce the problem in Postgres 15 (and 14 as well), which might show that the problem is bigger than antecipated.

Would you kindly try to reproduce with this new setup? 

Fix patch:
diff --git a/diesel_cli/src/infer_schema_internals/data_structures.rs b/diesel_cli/src/infer_schema_internals/data_structures.rs
index d71086e9d3c0..4aa678168087 100644
--- a/diesel_cli/src/infer_schema_internals/data_structures.rs
+++ b/diesel_cli/src/infer_schema_internals/data_structures.rs
@@ -119,9 +119,9 @@ where
 pub struct ForeignKeyConstraint {
     pub child_table: TableName,
     pub parent_table: TableName,
-    pub foreign_key: String,
-    pub foreign_key_rust_name: String,
-    pub primary_key: String,
+    pub foreign_key_columns: Vec<String>,
+    pub foreign_key_columns_rust: Vec<String>,
+    pub primary_key_columns: Vec<String>,
 }
 
 impl ForeignKeyConstraint {
diff --git a/diesel_cli/src/infer_schema_internals/foreign_keys.rs b/diesel_cli/src/infer_schema_internals/foreign_keys.rs
index 1a13fa9f09c1..4991a716c7da 100644
--- a/diesel_cli/src/infer_schema_internals/foreign_keys.rs
+++ b/diesel_cli/src/infer_schema_internals/foreign_keys.rs
@@ -30,12 +30,13 @@ pub fn remove_unsafe_foreign_keys_for_codegen(
         .filter(|fk| fk.parent_table != fk.child_table)
         .filter(|fk| safe_tables.contains(&fk.parent_table))
         .filter(|fk| safe_tables.contains(&fk.child_table))
+        .filter(|fk| fk.foreign_key_columns.len() == 1)
         .filter(|fk| {
             let pk_columns = get_primary_keys(&mut conn, &fk.parent_table).expect(&format!(
                 "Error loading primary keys for `{}`",
                 fk.parent_table
             ));
-            pk_columns.len() == 1 && pk_columns[0] == fk.primary_key
+            pk_columns.len() == 1 && Some(&pk_columns[0]) == fk.primary_key_columns.get(0)
         })
         .filter(|fk| !duplicates.contains(&fk.ordered_tables()))
         .cloned()
diff --git a/diesel_cli/src/infer_schema_internals/inference.rs b/diesel_cli/src/infer_schema_internals/inference.rs
index 87d7680343cd..36253c106eb5 100644
--- a/diesel_cli/src/infer_schema_internals/inference.rs
+++ b/diesel_cli/src/infer_schema_internals/inference.rs
@@ -222,8 +222,7 @@ pub fn load_foreign_key_constraints(
         }
         #[cfg(feature = "postgres")]
         InferConnection::Pg(ref mut c) => {
-            super::information_schema::load_foreign_key_constraints(c, schema_name)
-                .map_err(Into::into)
+            super::pg::load_foreign_key_constraints(c, schema_name).map_err(Into::into)
         }
         #[cfg(feature = "mysql")]
         InferConnection::Mysql(ref mut c) => {
@@ -234,9 +233,10 @@ pub fn load_foreign_key_constraints(
     constraints.map(|mut ct| {
         ct.sort();
         ct.iter_mut().for_each(|foreign_key_constraint| {
-            if is_reserved_name(&foreign_key_constraint.foreign_key_rust_name) {
-                foreign_key_constraint.foreign_key_rust_name =
-                    format!("{}_", foreign_key_constraint.foreign_key_rust_name);
+            for name in &mut foreign_key_constraint.foreign_key_columns_rust {
+                if is_reserved_name(name) {
+                    *name = format!("{name}_");
+                }
             }
         });
         ct
diff --git a/diesel_cli/src/infer_schema_internals/information_schema.rs b/diesel_cli/src/infer_schema_internals/information_schema.rs
index 2cb533f791eb..2448a6559f9d 100644
--- a/diesel_cli/src/infer_schema_internals/information_schema.rs
+++ b/diesel_cli/src/infer_schema_internals/information_schema.rs
@@ -1,8 +1,6 @@
 use std::borrow::Cow;
 use std::error::Error;
 
-#[cfg(feature = "postgres")]
-use super::ForeignKeyConstraint;
 use diesel::backend::Backend;
 use diesel::connection::LoadConnection;
 use diesel::deserialize::FromSql;
@@ -186,66 +184,6 @@ where
         .collect())
 }
 
-#[allow(clippy::similar_names)]
-#[cfg(feature = "postgres")]
-pub fn load_foreign_key_constraints(
-    connection: &mut PgConnection,
-    schema_name: Option<&str>,
-) -> QueryResult<Vec<ForeignKeyConstraint>> {
-    use self::information_schema::key_column_usage as kcu;
-    use self::information_schema::referential_constraints as rc;
-    use self::information_schema::table_constraints as tc;
-
-    let default_schema = Pg::default_schema(connection)?;
-    let schema_name = schema_name.unwrap_or(&default_schema);
-
-    let constraint_names = tc::table
-        .filter(tc::constraint_type.eq("FOREIGN KEY"))
-        .filter(tc::table_schema.eq(schema_name))
-        .inner_join(
-            rc::table.on(tc::constraint_schema
-                .eq(rc::constraint_schema)
-                .and(tc::constraint_name.eq(rc::constraint_name))),
-        )
-        .select((
-            rc::constraint_schema,
-            rc::constraint_name,
-            rc::unique_constraint_schema,
-            rc::unique_constraint_name,
-        ))
-        .load::<(String, String, Option<String>, Option<String>)>(connection)?;
-
-    constraint_names
-        .into_iter()
-        .map(
-            |(foreign_key_schema, foreign_key_name, primary_key_schema, primary_key_name)| {
-                let (mut foreign_key_table, foreign_key_column) = kcu::table
-                    .filter(kcu::constraint_schema.eq(&foreign_key_schema))
-                    .filter(kcu::constraint_name.eq(&foreign_key_name))
-                    .select(((kcu::table_name, kcu::table_schema), kcu::column_name))
-                    .first::<(TableName, String)>(connection)?;
-                let (mut primary_key_table, primary_key_column) = kcu::table
-                    .filter(kcu::constraint_schema.nullable().eq(primary_key_schema))
-                    .filter(kcu::constraint_name.nullable().eq(primary_key_name))
-                    .select(((kcu::table_name, kcu::table_schema), kcu::column_name))
-                    .first::<(TableName, _)>(connection)?;
-
-                foreign_key_table.strip_schema_if_matches(&default_schema);
-                primary_key_table.strip_schema_if_matches(&default_schema);
-
-                Ok(ForeignKeyConstraint {
-                    child_table: foreign_key_table,
-                    parent_table: primary_key_table,
-                    foreign_key: foreign_key_column.clone(),
-                    foreign_key_rust_name: foreign_key_column,
-                    primary_key: primary_key_column,
-                })
-            },
-        )
-        .filter(|e| !matches!(e, Err(NotFound)))
-        .collect()
-}
-
 #[cfg(all(test, feature = "postgres"))]
 mod tests {
     extern crate dotenvy;
@@ -400,46 +338,4 @@ mod tests {
             get_primary_keys(&mut connection, &table_2).unwrap()
         );
     }
-
-    #[test]
-    fn get_foreign_keys_loads_foreign_keys() {
-        let mut connection = connection();
-
-        diesel::sql_query("CREATE SCHEMA test_schema")
-            .execute(&mut connection)
-            .unwrap();
-        diesel::sql_query("CREATE TABLE test_schema.table_1 (id SERIAL PRIMARY KEY)")
-            .execute(&mut connection)
-            .unwrap();
-        diesel::sql_query(
-                "CREATE TABLE test_schema.table_2 (id SERIAL PRIMARY KEY, fk_one INTEGER NOT NULL REFERENCES test_schema.table_1)",
-            ).execute(&mut connection)
-            .unwrap();
-        diesel::sql_query(
-                "CREATE TABLE test_schema.table_3 (id SERIAL PRIMARY KEY, fk_two INTEGER NOT NULL REFERENCES test_schema.table_2)",
-            ).execute(&mut connection)
-            .unwrap();
-
-        let table_1 = TableName::new("table_1", "test_schema");
-        let table_2 = TableName::new("table_2", "test_schema");
-        let table_3 = TableName::new("table_3", "test_schema");
-        let fk_one = ForeignKeyConstraint {
-            child_table: table_2.clone(),
-            parent_table: table_1,
-            foreign_key: "fk_one".into(),
-            foreign_key_rust_name: "fk_one".into(),
-            primary_key: "id".into(),
-        };
-        let fk_two = ForeignKeyConstraint {
-            child_table: table_3,
-            parent_table: table_2,
-            foreign_key: "fk_two".into(),
-            foreign_key_rust_name: "fk_two".into(),
-            primary_key: "id".into(),
-        };
-        assert_eq!(
-            Ok(vec![fk_one, fk_two]),
-            load_foreign_key_constraints(&mut connection, Some("test_schema"))
-        );
-    }
 }
diff --git a/diesel_cli/src/infer_schema_internals/mysql.rs b/diesel_cli/src/infer_schema_internals/mysql.rs
index 8ed5822bd938..fdd63d33891e 100644
--- a/diesel_cli/src/infer_schema_internals/mysql.rs
+++ b/diesel_cli/src/infer_schema_internals/mysql.rs
@@ -1,6 +1,7 @@
 use diesel::mysql::{Mysql, MysqlConnection};
 use diesel::*;
 use heck::ToUpperCamelCase;
+use std::collections::HashMap;
 use std::{borrow::Cow, error::Error};
 
 use super::data_structures::*;
@@ -123,20 +124,33 @@ pub fn load_foreign_key_constraints(
             (kcu::referenced_table_name, kcu::referenced_table_schema),
             kcu::column_name,
             kcu::referenced_column_name,
+            kcu::constraint_name,
         ))
-        .load::<(TableName, TableName, String, _)>(connection)?
+        .load::<(TableName, TableName, String, String, String)>(connection)?
         .into_iter()
+        .fold(
+            HashMap::new(),
+            |mut acc, (child_table, parent_table, foreign_key, primary_key, fk_constraint_name)| {
+                let entry = acc
+                    .entry(fk_constraint_name)
+                    .or_insert_with(|| (child_table, parent_table, Vec::new(), Vec::new()));
+                entry.2.push(foreign_key);
+                entry.3.push(primary_key);
+                acc
+            },
+        )
+        .into_values()
         .map(
-            |(mut child_table, mut parent_table, foreign_key, primary_key)| {
+            |(mut child_table, mut parent_table, foreign_key_columns, primary_key_columns)| {
                 child_table.strip_schema_if_matches(&default_schema);
                 parent_table.strip_schema_if_matches(&default_schema);
 
                 ForeignKeyConstraint {
                     child_table,
                     parent_table,
-                    foreign_key: foreign_key.clone(),
-                    foreign_key_rust_name: foreign_key,
-                    primary_key,
+                    primary_key_columns,
+                    foreign_key_columns_rust: foreign_key_columns.clone(),
+                    foreign_key_columns,
                 }
             },
         )
diff --git a/diesel_cli/src/infer_schema_internals/pg.rs b/diesel_cli/src/infer_schema_internals/pg.rs
index b7c4021cbb0d..1cf7f5968649 100644
--- a/diesel_cli/src/infer_schema_internals/pg.rs
+++ b/diesel_cli/src/infer_schema_internals/pg.rs
@@ -115,6 +115,85 @@ mod information_schema {
     }
 }
 
+sql_function! {
+    #[aggregate]
+    fn array_agg(input: diesel::sql_types::Text) -> diesel::sql_types::Array<diesel::sql_types::Text>;
+}
+
+#[allow(clippy::similar_names)]
+pub fn load_foreign_key_constraints(
+    connection: &mut PgConnection,
+    schema_name: Option<&str>,
+) -> QueryResult<Vec<ForeignKeyConstraint>> {
+    use super::information_schema::information_schema::key_column_usage as kcu;
+    use super::information_schema::information_schema::referential_constraints as rc;
+    use super::information_schema::information_schema::table_constraints as tc;
+
+    let default_schema = Pg::default_schema(connection)?;
+    let schema_name = schema_name.unwrap_or(&default_schema);
+
+    let constraint_names = tc::table
+        .filter(tc::constraint_type.eq("FOREIGN KEY"))
+        .filter(tc::table_schema.eq(schema_name))
+        .inner_join(
+            rc::table.on(tc::constraint_schema
+                .eq(rc::constraint_schema)
+                .and(tc::constraint_name.eq(rc::constraint_name))),
+        )
+        .select((
+            rc::constraint_schema,
+            rc::constraint_name,
+            rc::unique_constraint_schema,
+            rc::unique_constraint_name,
+        ))
+        .load::<(String, String, Option<String>, Option<String>)>(connection)?;
+
+    constraint_names
+        .into_iter()
+        .map(
+            |(foreign_key_schema, foreign_key_name, primary_key_schema, primary_key_name)| {
+                let foreign_key = kcu::table
+                    .filter(kcu::constraint_schema.eq(&foreign_key_schema))
+                    .filter(kcu::constraint_name.eq(&foreign_key_name))
+                    .group_by((kcu::table_name, kcu::table_schema))
+                    .select((
+                        kcu::table_name,
+                        kcu::table_schema,
+                        array_agg(kcu::column_name),
+                    ))
+                    .first::<(String, String, Vec<String>)>(connection)?;
+                let primary_key = kcu::table
+                    .filter(kcu::constraint_schema.nullable().eq(primary_key_schema))
+                    .filter(kcu::constraint_name.nullable().eq(primary_key_name))
+                    .group_by((kcu::table_name, kcu::table_schema))
+                    .select((
+                        kcu::table_name,
+                        kcu::table_schema,
+                        array_agg(kcu::column_name),
+                    ))
+                    .first::<(String, String, Vec<String>)>(connection)?;
+
+                let mut primary_key_table = TableName::new(primary_key.0, primary_key.1);
+                primary_key_table.strip_schema_if_matches(&default_schema);
+                let mut foreign_key_table = TableName::new(foreign_key.0, foreign_key.1);
+                foreign_key_table.strip_schema_if_matches(&default_schema);
+
+                let primary_key_columns = primary_key.2;
+                let foreign_key_columns = foreign_key.2;
+
+                Ok(ForeignKeyConstraint {
+                    child_table: foreign_key_table,
+                    parent_table: primary_key_table,
+                    foreign_key_columns_rust: foreign_key_columns.clone(),
+                    foreign_key_columns,
+                    primary_key_columns,
+                })
+            },
+        )
+        .filter(|e| !matches!(e, Err(diesel::result::Error::NotFound)))
+        .collect()
+}
+
 #[cfg(test)]
 mod test {
     extern crate dotenvy;
@@ -202,4 +281,46 @@ mod test {
         );
         assert_eq!(Ok(None), get_table_comment(&mut connection, &table_2));
     }
+
+    #[test]
+    fn get_foreign_keys_loads_foreign_keys() {
+        let mut connection = connection();
+
+        diesel::sql_query("CREATE SCHEMA test_schema")
+            .execute(&mut connection)
+            .unwrap();
+        diesel::sql_query("CREATE TABLE test_schema.table_1 (id SERIAL PRIMARY KEY)")
+            .execute(&mut connection)
+            .unwrap();
+        diesel::sql_query(
+                "CREATE TABLE test_schema.table_2 (id SERIAL PRIMARY KEY, fk_one INTEGER NOT NULL REFERENCES test_schema.table_1)",
+            ).execute(&mut connection)
+            .unwrap();
+        diesel::sql_query(
+                "CREATE TABLE test_schema.table_3 (id SERIAL PRIMARY KEY, fk_two INTEGER NOT NULL REFERENCES test_schema.table_2)",
+            ).execute(&mut connection)
+            .unwrap();
+
+        let table_1 = TableName::new("table_1", "test_schema");
+        let table_2 = TableName::new("table_2", "test_schema");
+        let table_3 = TableName::new("table_3", "test_schema");
+        let fk_one = ForeignKeyConstraint {
+            child_table: table_2.clone(),
+            parent_table: table_1,
+            foreign_key_columns: vec!["fk_one".into()],
+            foreign_key_columns_rust: vec!["fk_one".into()],
+            primary_key_columns: vec!["id".into()],
+        };
+        let fk_two = ForeignKeyConstraint {
+            child_table: table_3,
+            parent_table: table_2,
+            foreign_key_columns: vec!["fk_two".into()],
+            foreign_key_columns_rust: vec!["fk_two".into()],
+            primary_key_columns: vec!["id".into()],
+        };
+        assert_eq!(
+            Ok(vec![fk_one, fk_two]),
+            load_foreign_key_constraints(&mut connection, Some("test_schema"))
+        );
+    }
 }
diff --git a/diesel_cli/src/infer_schema_internals/sqlite.rs b/diesel_cli/src/infer_schema_internals/sqlite.rs
index 144cdd50393f..aa7f41982031 100644
--- a/diesel_cli/src/infer_schema_internals/sqlite.rs
+++ b/diesel_cli/src/infer_schema_internals/sqlite.rs
@@ -77,28 +77,16 @@ pub fn load_foreign_key_constraints(
                 .map(|row| {
                     let parent_table = TableName::from_name(row.parent_table);
                     let primary_key = if let Some(primary_key) = row.primary_key {
-                        primary_key
+                        vec![primary_key]
                     } else {
-                        let mut primary_keys = get_primary_keys(connection, &parent_table)?;
-                        if primary_keys.len() == 1 {
-                            primary_keys
-                                .pop()
-                                .expect("There is exactly one primary key in this list")
-                        } else {
-                            return Err(diesel::result::Error::DatabaseError(
-                                diesel::result::DatabaseErrorKind::Unknown,
-                                Box::new(String::from(
-                                    "Found more than one primary key for an implicit reference",
-                                )),
-                            ));
-                        }
+                        get_primary_keys(connection, &parent_table)?
                     };
                     Ok(ForeignKeyConstraint {
                         child_table: child_table.clone(),
                         parent_table,
-                        foreign_key: row.foreign_key.clone(),
-                        foreign_key_rust_name: row.foreign_key,
-                        primary_key,
+                        foreign_key_columns: vec![row.foreign_key.clone()],
+                        foreign_key_columns_rust: vec![row.foreign_key.clone()],
+                        primary_key_columns: primary_key,
                     })
                 })
                 .collect::<Result<_, _>>()
@@ -386,17 +374,18 @@ fn load_foreign_key_constraints_loads_foreign_keys() {
     let fk_one = ForeignKeyConstraint {
         child_table: table_2.clone(),
         parent_table: table_1,
-        foreign_key: "fk_one".into(),
-        foreign_key_rust_name: "fk_one".into(),
-        primary_key: "id".into(),
+        foreign_key_columns: vec!["fk_one".into()],
+        foreign_key_columns_rust: vec!["fk_one".into()],
+        primary_key_columns: vec!["id".into()],
     };
     let fk_two = ForeignKeyConstraint {
         child_table: table_3,
         parent_table: table_2,
-        foreign_key: "fk_two".into(),
-        foreign_key_rust_name: "fk_two".into(),
-        primary_key: "id".into(),
+        foreign_key_columns: vec!["fk_two".into()],
+        foreign_key_columns_rust: vec!["fk_two".into()],
+        primary_key_columns: vec!["id".into()],
     };
+
     let fks = load_foreign_key_constraints(&mut connection, None).unwrap();
     assert_eq!(vec![fk_one, fk_two], fks);
 }
diff --git a/diesel_cli/src/print_schema.rs b/diesel_cli/src/print_schema.rs
index 241afb290c56..f702607cf185 100644
--- a/diesel_cli/src/print_schema.rs
+++ b/diesel_cli/src/print_schema.rs
@@ -519,7 +519,7 @@ impl<'a> Display for Joinable<'a> {
         write!(
             f,
             "diesel::joinable!({} -> {} ({}));",
-            child_table_name, parent_table_name, self.0.foreign_key_rust_name,
+            child_table_name, parent_table_name, self.0.foreign_key_columns_rust[0],
         )
     }
 }


Test patch:
diff --git a/diesel_cli/tests/print_schema.rs b/diesel_cli/tests/print_schema.rs
index 6ff32e51aba1..42de6d699794 100644
--- a/diesel_cli/tests/print_schema.rs
+++ b/diesel_cli/tests/print_schema.rs
@@ -270,6 +270,17 @@ fn print_schema_reserved_names() {
     test_print_schema("print_schema_reserved_name_mitigation_issue_3404", vec![])
 }
 
+#[test]
+#[cfg(feature = "postgres")]
+fn print_schema_regression_3446_ignore_compound_foreign_keys() {
+    test_print_schema("print_schema_regression_3446_compound_keys", vec![])
+}
+
+#[test]
+fn print_schema_several_keys_with_compound_key() {
+    test_print_schema("print_schema_several_keys_with_compound_key", vec![])
+}
+
 #[cfg(feature = "sqlite")]
 const BACKEND: &str = "sqlite";
 #[cfg(feature = "postgres")]
@@ -332,7 +343,6 @@ fn test_print_schema_config(test_name: &str, test_path: &Path, schema: String) {
 
     p.command("setup").run();
     p.create_migration("12345_create_schema", &schema, None);
-
     let result = p.command("migration").arg("run").run();
     assert!(result.is_success(), "Result was unsuccessful {:?}", result);
 
diff --git a/diesel_cli/tests/print_schema/print_schema_regression_3446_compound_keys/diesel.toml b/diesel_cli/tests/print_schema/print_schema_regression_3446_compound_keys/diesel.toml
new file mode 100644
index 000000000000..f57985adb185
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_regression_3446_compound_keys/diesel.toml
@@ -0,0 +1,2 @@
+[print_schema]
+file = "src/schema.rs"
diff --git a/diesel_cli/tests/print_schema/print_schema_regression_3446_compound_keys/postgres/expected.snap b/diesel_cli/tests/print_schema/print_schema_regression_3446_compound_keys/postgres/expected.snap
new file mode 100644
index 000000000000..ac72c0c75910
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_regression_3446_compound_keys/postgres/expected.snap
@@ -0,0 +1,38 @@
+---
+source: diesel_cli/tests/print_schema.rs
+description: "Test: print_schema_regression_3446_ignore_compound_foreign_keys"
+---
+// @generated automatically by Diesel CLI.
+
+diesel::table! {
+    payment_card (id) {
+        id -> Int4,
+        code -> Text,
+        holder_id -> Int4,
+    }
+}
+
+diesel::table! {
+    person (id) {
+        id -> Int4,
+        name -> Text,
+    }
+}
+
+diesel::table! {
+    transaction (id) {
+        id -> Int4,
+        executed_at -> Timestamptz,
+        payment_card_id -> Int4,
+        card_code -> Text,
+    }
+}
+
+diesel::joinable!(payment_card -> person (holder_id));
+
+diesel::allow_tables_to_appear_in_same_query!(
+    payment_card,
+    person,
+    transaction,
+);
+
diff --git a/diesel_cli/tests/print_schema/print_schema_regression_3446_compound_keys/postgres/schema.sql b/diesel_cli/tests/print_schema/print_schema_regression_3446_compound_keys/postgres/schema.sql
new file mode 100644
index 000000000000..c4326b153c4b
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_regression_3446_compound_keys/postgres/schema.sql
@@ -0,0 +1,19 @@
+CREATE TABLE person (
+    id SERIAL PRIMARY KEY,
+    name TEXT NOT NULL
+);
+
+CREATE TABLE payment_card (
+    id SERIAL PRIMARY KEY,
+    code TEXT NOT NULL,
+    holder_id INT NOT NULL REFERENCES person(id),
+    UNIQUE (id, code)
+);
+
+CREATE TABLE transaction (
+    id SERIAL PRIMARY KEY,
+    executed_at TIMESTAMPTZ NOT NULL,
+    payment_card_id INT NOT NULL,
+    card_code TEXT NOT NULL,
+    FOREIGN KEY (payment_card_id, card_code) REFERENCES payment_card (id, code)
+);
diff --git a/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/diesel.toml b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/diesel.toml
new file mode 100644
index 000000000000..f57985adb185
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/diesel.toml
@@ -0,0 +1,2 @@
+[print_schema]
+file = "src/schema.rs"
diff --git a/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/mysql/expected.snap b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/mysql/expected.snap
new file mode 100644
index 000000000000..ed9f20fc399e
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/mysql/expected.snap
@@ -0,0 +1,36 @@
+---
+source: diesel_cli/tests/print_schema.rs
+description: "Test: print_schema_several_keys_with_compound_key"
+---
+// @generated automatically by Diesel CLI.
+
+diesel::table! {
+    payment_card (id) {
+        id -> Integer,
+        code -> Varchar,
+    }
+}
+
+diesel::table! {
+    transaction_one (id) {
+        id -> Integer,
+        card_code -> Varchar,
+        payment_card_id -> Integer,
+        by_card_id -> Integer,
+    }
+}
+
+diesel::table! {
+    transaction_two (id) {
+        id -> Integer,
+        payment_card_id -> Integer,
+        card_code -> Varchar,
+    }
+}
+
+diesel::allow_tables_to_appear_in_same_query!(
+    payment_card,
+    transaction_one,
+    transaction_two,
+);
+
diff --git a/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/mysql/schema.sql b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/mysql/schema.sql
new file mode 100644
index 000000000000..792358a99406
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/mysql/schema.sql
@@ -0,0 +1,24 @@
+CREATE TABLE payment_card (
+    id INT PRIMARY KEY,
+    code VARCHAR(50) NOT NULL,
+    UNIQUE(id, code)
+);
+
+CREATE TABLE transaction_one (
+    id INT PRIMARY KEY,
+    card_code VARCHAR(50) NOT NULL,
+    payment_card_id INT NOT NULL,
+    by_card_id INT NOT NULL REFERENCES payment_card(id),
+    FOREIGN KEY (payment_card_id, card_code)
+    REFERENCES payment_card (id, code)
+);
+
+-- The only difference between transaction_one and transaction_two is the order of the 2nd and 3rd columns.
+-- Note that because of that, the joinable will be different!
+CREATE TABLE transaction_two (
+    id INT PRIMARY KEY,
+    payment_card_id INT NOT NULL,
+    card_code VARCHAR(50) NOT NULL,
+    FOREIGN KEY (payment_card_id, card_code)
+    REFERENCES payment_card (id, code)
+);
diff --git a/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/postgres/expected.snap b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/postgres/expected.snap
new file mode 100644
index 000000000000..067422c1e2e8
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/postgres/expected.snap
@@ -0,0 +1,36 @@
+---
+source: diesel_cli/tests/print_schema.rs
+description: "Test: print_schema_several_keys_with_compound_key"
+---
+// @generated automatically by Diesel CLI.
+
+diesel::table! {
+    payment_card (id) {
+        id -> Int4,
+        code -> Text,
+    }
+}
+
+diesel::table! {
+    transaction_one (id) {
+        id -> Int4,
+        card_code -> Text,
+        payment_card_id -> Int4,
+        by_card_id -> Int4,
+    }
+}
+
+diesel::table! {
+    transaction_two (id) {
+        id -> Int4,
+        payment_card_id -> Int4,
+        card_code -> Text,
+    }
+}
+
+diesel::allow_tables_to_appear_in_same_query!(
+    payment_card,
+    transaction_one,
+    transaction_two,
+);
+
diff --git a/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/postgres/schema.sql b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/postgres/schema.sql
new file mode 100644
index 000000000000..2767a28dc6c3
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/postgres/schema.sql
@@ -0,0 +1,24 @@
+CREATE TABLE payment_card (
+    id INT PRIMARY KEY,
+    code TEXT NOT NULL,
+    UNIQUE(id, code)
+);
+
+CREATE TABLE transaction_one (
+    id INT PRIMARY KEY,
+    card_code TEXT NOT NULL,
+    payment_card_id INT NOT NULL,
+    by_card_id INT NOT NULL REFERENCES payment_card(id),
+    FOREIGN KEY (payment_card_id, card_code)
+    REFERENCES payment_card (id, code)
+);
+
+-- The only difference between transaction_one and transaction_two is the order of the 2nd and 3rd columns.
+-- Note that because of that, the joinable will be different!
+CREATE TABLE transaction_two (
+    id INT PRIMARY KEY,
+    payment_card_id INT NOT NULL,
+    card_code TEXT NOT NULL,
+    FOREIGN KEY (payment_card_id, card_code)
+    REFERENCES payment_card (id, code)
+);
diff --git a/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/sqlite/expected.snap b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/sqlite/expected.snap
new file mode 100644
index 000000000000..607b4c75323b
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/sqlite/expected.snap
@@ -0,0 +1,36 @@
+---
+source: diesel_cli/tests/print_schema.rs
+description: "Test: print_schema_several_keys_with_compound_key"
+---
+// @generated automatically by Diesel CLI.
+
+diesel::table! {
+    payment_card (id) {
+        id -> Integer,
+        code -> Text,
+    }
+}
+
+diesel::table! {
+    transaction_one (id) {
+        id -> Integer,
+        card_code -> Text,
+        payment_card_id -> Integer,
+        by_card_id -> Integer,
+    }
+}
+
+diesel::table! {
+    transaction_two (id) {
+        id -> Integer,
+        payment_card_id -> Integer,
+        card_code -> Text,
+    }
+}
+
+diesel::allow_tables_to_appear_in_same_query!(
+    payment_card,
+    transaction_one,
+    transaction_two,
+);
+
diff --git a/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/sqlite/schema.sql b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/sqlite/schema.sql
new file mode 100644
index 000000000000..024210b546d1
--- /dev/null
+++ b/diesel_cli/tests/print_schema/print_schema_several_keys_with_compound_key/sqlite/schema.sql
@@ -0,0 +1,24 @@
+CREATE TABLE payment_card (
+    id INT NOT NULL PRIMARY KEY,
+    code TEXT NOT NULL,
+    UNIQUE(id, code)
+);
+
+CREATE TABLE transaction_one (
+    id INT NOT NULL PRIMARY KEY,
+    card_code TEXT NOT NULL,
+    payment_card_id INT NOT NULL,
+    by_card_id INT NOT NULL REFERENCES payment_card(id),
+    FOREIGN KEY (payment_card_id, card_code)
+    REFERENCES payment_card (id, code)
+);
+
+-- The only difference between transaction_one and transaction_two is the order of the 2nd and 3rd columns.
+-- Note that because of that, the joinable will be different!
+CREATE TABLE transaction_two (
+    id INT NOT NULL PRIMARY KEY,
+    payment_card_id INT NOT NULL,
+    card_code TEXT NOT NULL,
+    FOREIGN KEY (payment_card_id, card_code)
+    REFERENCES payment_card (id, code)
+);
diff --git a/diesel_cli/tests/support/command.rs b/diesel_cli/tests/support/command.rs
index 2e41980e56cd..3b932e014247 100644
--- a/diesel_cli/tests/support/command.rs
+++ b/diesel_cli/tests/support/command.rs
@@ -46,6 +46,8 @@ impl TestCommand {
             .build_command()
             .output()
             .expect("failed to execute process");
+        println!("STDOUT: {}", String::from_utf8_lossy(&output.stdout));
+        println!("STDERR: {}", String::from_utf8_lossy(&output.stderr));
         CommandResult { output }
     }