-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Add print-schema option allow_tables_in_same_query
#4500
base: master
Are you sure you want to change the base?
Changes from all commits
e575f1c
8b945ab
472ed39
758b090
1889f31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,10 +3,11 @@ use crate::database::{Backend, InferConnection}; | |
use crate::infer_schema_internals::*; | ||
|
||
use serde::{Deserialize, Serialize}; | ||
use std::collections::HashSet; | ||
use std::collections::{BTreeSet, HashSet}; | ||
use std::fmt::{self, Display, Formatter, Write}; | ||
use std::io::Write as IoWrite; | ||
use std::process; | ||
use std::str; | ||
|
||
const SCHEMA_HEADER: &str = "// @generated automatically by Diesel CLI.\n"; | ||
|
||
|
@@ -42,6 +43,24 @@ impl Default for DocConfig { | |
} | ||
} | ||
|
||
/// How to group tables in `allow_tables_to_appear_in_same_query!()`. | ||
#[derive(Debug, Deserialize, Serialize, Clone, Copy)] | ||
pub enum AllowTablesInSameQuery { | ||
/// Group by foreign key relations | ||
#[serde(rename = "fk_related_tables")] | ||
FkRelatedTables, | ||
/// List all tables in invocation | ||
#[serde(rename = "all_tables")] | ||
AllTables, | ||
} | ||
|
||
#[allow(clippy::derivable_impls)] // that's not supported on rust 1.65 | ||
impl Default for AllowTablesInSameQuery { | ||
fn default() -> Self { | ||
AllowTablesInSameQuery::AllTables | ||
} | ||
} | ||
Comment on lines
+57
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should be fine to use the derive now, as our minimal supported rust version is currently 1.78 |
||
|
||
pub fn run_print_schema<W: IoWrite>( | ||
connection: &mut InferConnection, | ||
config: &config::PrintSchema, | ||
|
@@ -237,6 +256,7 @@ pub fn output_schema( | |
tables: table_data, | ||
fk_constraints: foreign_keys, | ||
with_docs: config.with_docs, | ||
allow_tables_in_same_query: config.allow_tables_in_same_query, | ||
custom_types_for_tables: columns_custom_types.map(|custom_types_sorted| { | ||
CustomTypesForTables { | ||
backend, | ||
|
@@ -538,6 +558,7 @@ struct TableDefinitions<'a> { | |
tables: Vec<TableData>, | ||
fk_constraints: Vec<ForeignKeyConstraint>, | ||
with_docs: DocConfig, | ||
allow_tables_in_same_query: AllowTablesInSameQuery, | ||
import_types: Option<&'a [String]>, | ||
custom_types_for_tables: Option<CustomTypesForTables>, | ||
} | ||
|
@@ -574,17 +595,31 @@ impl Display for TableDefinitions<'_> { | |
writeln!(f, "{}", Joinable(foreign_key))?; | ||
} | ||
|
||
if self.tables.len() > 1 { | ||
write!(f, "\ndiesel::allow_tables_to_appear_in_same_query!(")?; | ||
let table_groups = match self.allow_tables_in_same_query { | ||
AllowTablesInSameQuery::FkRelatedTables => { | ||
foreign_key_table_groups(&self.tables, &self.fk_constraints) | ||
} | ||
AllowTablesInSameQuery::AllTables => { | ||
vec![self.tables.iter().map(|table| &table.name).collect()] | ||
} | ||
}; | ||
for (table_group_index, table_group) in table_groups | ||
.into_iter() | ||
.filter(|table_group| table_group.len() >= 2) | ||
.enumerate() | ||
{ | ||
if table_group_index == 0 { | ||
writeln!(f)?; | ||
} | ||
write!(f, "diesel::allow_tables_to_appear_in_same_query!(")?; | ||
{ | ||
let mut out = PadAdapter::new(f); | ||
writeln!(out)?; | ||
for table in &self.tables { | ||
if table.name.rust_name == table.name.sql_name { | ||
writeln!(out, "{},", table.name.sql_name)?; | ||
} else { | ||
writeln!(out, "{},", table.name.rust_name)?; | ||
for (table_index, table) in table_group.into_iter().enumerate() { | ||
if table_index != 0 { | ||
write!(out, ", ")?; | ||
} | ||
write!(out, "{}", table.rust_name)?; | ||
} | ||
} | ||
writeln!(f, ");")?; | ||
|
@@ -594,6 +629,71 @@ impl Display for TableDefinitions<'_> { | |
} | ||
} | ||
|
||
/// Calculates groups of tables that are related by foreign key. | ||
/// | ||
/// Given the graph of all tables and their foreign key relations, this returns the set of connected | ||
/// components of that graph. | ||
fn foreign_key_table_groups<'a>( | ||
tables: &'a [TableData], | ||
fk_constraints: &'a [ForeignKeyConstraint], | ||
) -> Vec<Vec<&'a TableName>> { | ||
let mut visited = BTreeSet::new(); | ||
let mut components = vec![]; | ||
|
||
// Find connected components in table graph. For the intended purpose of this function, we treat | ||
// the foreign key relation as being symmetrical, i.e. we are operating on the undirected graph. | ||
// | ||
// The algorithm is not optimized and suffers from repeated lookups in the foreign key list, but | ||
// it should be sufficient for typical table counts from a few dozen up to a few hundred tables. | ||
sgoll marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for table in tables { | ||
let name = &table.name; | ||
if visited.contains(name) { | ||
// This table is already part of another connected component. | ||
continue; | ||
} | ||
|
||
visited.insert(name); | ||
let mut component = vec![]; | ||
let mut pending = vec![name]; | ||
|
||
// Start a depth-first search with the current table name, walking the foreign key relations | ||
// in both directions. | ||
while let Some(name) = pending.pop() { | ||
component.push(name); | ||
|
||
let mut visit = |related_name: &'a TableName| { | ||
if visited.insert(related_name) { | ||
pending.push(related_name); | ||
} | ||
}; | ||
|
||
// Visit all remaining child tables that have this table as parent. | ||
for foreign_key in fk_constraints.iter().filter(|fk| fk.parent_table == *name) { | ||
visit(&foreign_key.child_table); | ||
} | ||
|
||
// Visit all remaining parent tables that have this table as child. | ||
for foreign_key in fk_constraints.iter().filter(|fk| fk.child_table == *name) { | ||
visit(&foreign_key.parent_table); | ||
} | ||
} | ||
|
||
// The component contains all tables that are reachable in either direction from the current | ||
// table. Sort that list by table name to ensure a stable output that does not depend on the | ||
// algorithm's specific implementation. | ||
component.sort(); | ||
|
||
components.push(component); | ||
} | ||
|
||
// Sort the list of components to ensure a stable output that does not depend on the algorithm's | ||
// specific implementation. This sorts the list of components by the name of the first tables in | ||
// each component. | ||
components.sort(); | ||
|
||
components | ||
} | ||
|
||
struct TableDefinition<'a> { | ||
table: &'a TableData, | ||
with_docs: DocConfig, | ||
|
@@ -832,6 +932,7 @@ impl DocConfig { | |
"no-doc-comments", | ||
]; | ||
} | ||
|
||
impl<'de> Deserialize<'de> for DocConfig { | ||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||
where | ||
|
@@ -881,7 +982,8 @@ impl<'de> Deserialize<'de> for DocConfig { | |
deserializer.deserialize_any(DocConfigVisitor) | ||
} | ||
} | ||
impl std::str::FromStr for DocConfig { | ||
|
||
impl str::FromStr for DocConfig { | ||
type Err = &'static str; | ||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
Ok(match s { | ||
|
@@ -899,3 +1001,21 @@ impl std::str::FromStr for DocConfig { | |
}) | ||
} | ||
} | ||
|
||
impl str::FromStr for AllowTablesInSameQuery { | ||
type Err = &'static str; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
Ok(match s { | ||
"fk_related_tables" => AllowTablesInSameQuery::FkRelatedTables, | ||
"all_tables" => AllowTablesInSameQuery::AllTables, | ||
_ => { | ||
return Err( | ||
"Unknown variant for `allow_tables_in_same_query` config, expected one of: \ | ||
`fk_related_tables`, \ | ||
`all_tables`", | ||
) | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[print_schema] | ||
file = "src/schema.rs" | ||
allow_tables_in_same_query = "fk_related_tables" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. test should probably be named around that it's testing specifically There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is correct. I had this originally, but then I ran into PostgreSQL's limit on the length of database names (63 characters) for the databases created by the test suite. This may be another hint that the option name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the suggestion here is to use |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
--- | ||
source: diesel_cli/tests/print_schema.rs | ||
assertion_line: 500 | ||
description: "Test: print_schema_allow_tables_in_same_query" | ||
snapshot_kind: text | ||
--- | ||
// @generated automatically by Diesel CLI. | ||
|
||
diesel::table! { | ||
bikes (id) { | ||
id -> Int4, | ||
} | ||
} | ||
|
||
diesel::table! { | ||
cars (id) { | ||
id -> Int4, | ||
} | ||
} | ||
|
||
diesel::table! { | ||
comments (id) { | ||
id -> Int4, | ||
post_id -> Int4, | ||
} | ||
} | ||
|
||
diesel::table! { | ||
posts (id) { | ||
id -> Int4, | ||
user_id -> Int4, | ||
} | ||
} | ||
|
||
diesel::table! { | ||
sessions (id) { | ||
id -> Int4, | ||
} | ||
} | ||
|
||
diesel::table! { | ||
transactions (id) { | ||
id -> Int4, | ||
session_id -> Int4, | ||
} | ||
} | ||
|
||
diesel::table! { | ||
users (id) { | ||
id -> Int4, | ||
} | ||
} | ||
|
||
diesel::joinable!(comments -> posts (post_id)); | ||
diesel::joinable!(posts -> users (user_id)); | ||
diesel::joinable!(transactions -> sessions (session_id)); | ||
|
||
diesel::allow_tables_to_appear_in_same_query!(comments, posts, users); | ||
diesel::allow_tables_to_appear_in_same_query!(sessions, transactions); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
-- Three related tables. | ||
CREATE TABLE users (id SERIAL PRIMARY KEY); | ||
CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users); | ||
CREATE TABLE comments (id SERIAL PRIMARY KEY, post_id INTEGER NOT NULL REFERENCES posts); | ||
|
||
-- Two related tables. | ||
CREATE TABLE sessions (id SERIAL PRIMARY KEY); | ||
CREATE TABLE transactions (id SERIAL PRIMARY KEY, session_id INTEGER NOT NULL REFERENCES sessions); | ||
|
||
-- Unrelated tables. | ||
CREATE TABLE cars (id SERIAL PRIMARY KEY); | ||
CREATE TABLE bikes (id SERIAL PRIMARY KEY); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming:
given the name of this argument, at first glance I would expect that I can do:
and that that would merge the connected components of table1 and table2 together for generation.
Considering that this may even be something that makes sense and we might want to add later on, how about calling this
allow_tables_to_appear_in_same_query_generation_mode
or something along those lines ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct. Initially, I had
--allow-tables-to-appear-in-same-query
, which suffers from the same problem but seemed to be too verbose to me. Adding a suffix-mode
or-generation-mode
would make it even longer.There is precedent for the suffix
-config
, as in--with-docs-config
, so I think I would prefer that over-mode
and-generation-mode
. What do you think?The full name then could be
--allow-tables-to-appear-in-same-query-config
. Still pretty long, but as long as that is okay, I can change it to that.