1use std::collections::{HashMap, HashSet};
40
41use rich_diagram::er::{Cardinality, Column, Entity, Relationship};
42use rich_diagram::{ErDiagram, ErModel};
43use rich_ext::schema::sql::{self, Note, SqlError};
44use rich_ext::schema::{ForeignKey, Schema};
45
46pub fn model(schema: &Schema) -> ErModel {
48 let tables: Vec<(String, &Schema)> = if schema.tables().is_empty() {
49 vec![(schema.name().unwrap_or("schema").to_string(), schema)]
50 } else {
51 schema
52 .tables()
53 .iter()
54 .enumerate()
55 .map(|(index, table)| {
56 let name = table
57 .name()
58 .map_or_else(|| format!("table {}", index + 1), str::to_string);
59 (name, table)
60 })
61 .collect()
62 };
63 let names = Names::new(tables.iter().map(|(name, _)| name.as_str()));
64 let mut er = ErModel::new();
65 for (name, table) in &tables {
66 let keys = table.foreign_keys();
67 let marks = Marks::new(table, &keys);
69 let entity = Entity::new(name.as_str()).columns(table.fields().iter().map(|field| {
70 let mut column = Column::new(field.name()).data_type(field.type_label());
71 if marks.primary.contains(field.name()) {
72 column = column.primary_key();
73 }
74 if marks.foreign.contains(field.name()) {
75 column = column.foreign_key();
76 }
77 if field.is_unique() {
78 column = column.unique();
79 }
80 if !field.is_required() && !field.is_primary_key() {
81 column = column.nullable();
82 }
83 column
84 }));
85 er = er.entity(entity);
86 for key in &keys {
87 er = er.relationship(relationship(name, &marks, key, &names));
88 }
89 }
90 er
91}
92
93pub fn from_sql(ddl: &str) -> Result<(ErDiagram, Vec<Note>), SqlError> {
96 let parsed = sql::parse(ddl)?;
97 Ok((ErDiagram::new(model(&parsed.schema)), parsed.notes))
98}
99
100struct Marks<'a> {
102 key: Vec<&'a str>,
104 primary: HashSet<&'a str>,
105 foreign: HashSet<&'a str>,
106 unique: HashSet<&'a str>,
107}
108
109impl<'a> Marks<'a> {
110 fn new(table: &'a Schema, keys: &'a [ForeignKey]) -> Self {
111 let key = table.primary_key();
112 Marks {
113 primary: key.iter().copied().collect(),
114 key,
115 foreign: keys
116 .iter()
117 .flat_map(|k| k.columns.iter().map(String::as_str))
118 .collect(),
119 unique: table
120 .fields()
121 .iter()
122 .filter(|f| f.is_unique())
123 .map(|f| f.name())
124 .collect(),
125 }
126 }
127}
128
129fn relationship(from: &str, marks: &Marks, key: &ForeignKey, names: &Names) -> Relationship {
131 let to = names.resolve(&key.table);
132 let one = (!marks.key.is_empty()
133 && marks.key.len() == key.columns.len()
134 && key
135 .columns
136 .iter()
137 .all(|c| marks.primary.contains(c.as_str())))
138 || (key.columns.len() == 1 && marks.unique.contains(key.columns[0].as_str()));
139 let mut edge = Relationship::new(from, to).cardinality(if one {
140 Cardinality::OneToOne
141 } else {
142 Cardinality::ManyToOne
143 });
144 if !key.columns.is_empty() {
145 edge = edge.columns(key.columns.clone(), key.references.clone());
146 }
147 edge
148}
149
150struct Names<'a> {
152 all: HashSet<&'a str>,
153 last: HashMap<&'a str, Option<&'a str>>,
155}
156
157fn last_part(name: &str) -> &str {
159 name.rsplit('.').next().unwrap_or(name)
160}
161
162impl<'a> Names<'a> {
163 fn new(names: impl IntoIterator<Item = &'a str>) -> Self {
164 let mut all = HashSet::new();
165 let mut last: HashMap<&str, Option<&str>> = HashMap::new();
166 for name in names {
167 all.insert(name);
168 last.entry(last_part(name))
169 .and_modify(|only| *only = None)
170 .or_insert(Some(name));
171 }
172 Names { all, last }
173 }
174
175 fn resolve(&self, name: &str) -> String {
179 if self.all.contains(name) {
180 return name.to_string();
181 }
182 match self.last.get(last_part(name)) {
183 Some(Some(only)) => (*only).to_string(),
184 _ => name.to_string(),
185 }
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn keys_and_cardinality_come_from_the_ddl() {
195 let (diagram, notes) = from_sql(
196 "CREATE TABLE public.users (id INT PRIMARY KEY, email TEXT UNIQUE NOT NULL, bio TEXT);
197 CREATE TABLE profiles (user_id INT PRIMARY KEY REFERENCES users (id));
198 CREATE TABLE posts (id INT, author INT, PRIMARY KEY (id),
199 FOREIGN KEY (author) REFERENCES public.users (id));
200 CREATE INDEX posts_author ON posts (author);",
201 )
202 .unwrap();
203 assert_eq!(notes.len(), 1, "{notes:?}");
204 let model = diagram.model();
205 let users = &model.entities[0];
206 assert_eq!(users.name, "public.users");
207 assert!(users.columns[0].primary_key && !users.columns[0].nullable);
208 assert!(users.columns[1].unique && !users.columns[1].nullable);
209 assert!(users.columns[2].nullable);
210 let edges: Vec<_> = model
211 .relationships
212 .iter()
213 .map(|r| (r.from.as_str(), r.to.as_str(), r.cardinality))
214 .collect();
215 assert_eq!(
217 edges,
218 [
219 ("profiles", "public.users", Some(Cardinality::OneToOne)),
220 ("posts", "public.users", Some(Cardinality::ManyToOne)),
221 ]
222 );
223 assert!(model.entities[2].columns[1].foreign_key);
224 }
225
226 #[test]
227 fn a_schema_without_tables_is_one_entity() {
228 let schema = Schema::new(vec![rich_ext::schema::Field::new(
229 "id",
230 rich_ext::schema::DataType::Integer,
231 )]);
232 let er = model(&schema);
233 assert_eq!(er.entities.len(), 1);
234 assert_eq!(er.entities[0].name, "schema");
235 assert!(er.relationships.is_empty());
236 }
237
238 #[test]
241 fn many_keys_resolve_in_linear_time() {
242 let mut ddl = String::new();
243 for i in 0..50_000 {
244 ddl.push_str(&format!("CREATE TABLE s.t{i} (a INT REFERENCES ghost);\n"));
245 }
246 ddl.push_str("CREATE TABLE u (b INT REFERENCES t7);");
247 let schema = sql::parse(&ddl).unwrap().schema;
248 let started = std::time::Instant::now();
249 let er = model(&schema);
250 assert!(started.elapsed() < std::time::Duration::from_secs(20));
251 assert_eq!(er.relationships.len(), 50_001);
252 assert_eq!(er.relationships[0].to, "ghost");
253 assert_eq!(er.relationships[50_000].to, "s.t7");
254 }
255
256 #[test]
259 fn wide_tables_with_many_keys_are_linear() {
260 let columns: Vec<String> = (0..2_000).map(|i| format!("c{i} INT UNIQUE")).collect();
261 let keys: Vec<String> = (0..2_000)
262 .map(|i| format!("FOREIGN KEY (c{i}) REFERENCES t{}", i % 7))
263 .collect();
264 let table = format!("({}, {})", columns.join(", "), keys.join(", "));
265 let mut ddl = String::new();
266 for t in 0..100 {
267 ddl.push_str(&format!("CREATE TABLE t{t} {table};\n"));
268 }
269 let schema = sql::parse(&ddl).unwrap().schema;
270 let started = std::time::Instant::now();
271 let er = model(&schema);
272 assert!(started.elapsed() < std::time::Duration::from_secs(5));
273 assert_eq!(er.relationships.len(), 200_000);
274 assert_eq!(er.relationships[0].cardinality, Some(Cardinality::OneToOne));
275 let column = &er.entities[99].columns[1_999];
276 assert!(column.foreign_key && column.unique && column.nullable);
277 }
278
279 #[test]
280 fn an_ambiguous_or_missing_table_is_kept_as_written() {
281 let resolve = |name: &str, names: &[&str]| Names::new(names.iter().copied()).resolve(name);
282 assert_eq!(resolve("users", &["a.users", "b.users"]), "users");
283 assert_eq!(resolve("ghost", &["users"]), "ghost");
284 assert_eq!(resolve("users", &["users", "a.users"]), "users");
285 assert_eq!(resolve("x.users", &["a.users"]), "a.users");
286 }
287}