1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use itertools::Itertools;
6use postgres::types::ToSql;
7use postgres::Row;
8
9use crate::psql::connection::PsqlConnection;
10use crate::psql::dto::*;
11use lezeh_common::types::ResultAnyError;
12
13pub type PsqlParamValue = Box<dyn ToSql + Sync>;
14
15const TABLE_WITH_FK_QUERY: &'static str = "
16 SELECT
17 tc.constraint_name,
18 tc.table_schema,
19 tc.table_name,
20 kcu.column_name,
21 c.data_type AS column_data_type,
22 ccu.table_schema AS foreign_table_schema,
23 ccu.table_name AS foreign_table_name,
24 ccu.column_name AS foreign_column_name,
25 foreign_c_meta.data_type AS foreign_column_data_type
26 FROM
27 information_schema.table_constraints AS tc
28 JOIN information_schema.key_column_usage AS kcu ON
29 tc.constraint_name = kcu.constraint_name AND
30 tc.table_schema = kcu.table_schema
31 JOIN information_schema.constraint_column_usage AS ccu ON
32 ccu.constraint_name = tc.constraint_name
33 JOIN information_schema.columns as c ON
34 c.table_name = tc.table_name AND
35 c.column_name = kcu.column_name
36 JOIN information_schema.columns as foreign_c_meta ON
37 foreign_c_meta.table_schema = ccu.table_schema AND
38 foreign_c_meta.table_name = ccu.table_name AND
39 foreign_c_meta.column_name = ccu.column_name
40 WHERE tc.constraint_type = 'FOREIGN KEY';
41";
42
43#[derive(PartialEq, Debug)]
44pub struct ForeignKeyInformationRow {
45 constraint_name: String,
46
47 table_schema: String,
49 table_name: String,
50 column_name: String,
51 column_data_type: String,
52
53 foreign_table_schema: String,
55 foreign_table_name: String,
56 foreign_column_name: String,
57 foreign_column_data_type: String,
58}
59
60pub struct Query {
61 connection: Rc<RefCell<PsqlConnection>>,
62}
63
64impl Query {
65 fn fetch_fk_info(&mut self, _schema: &str) -> ResultAnyError<Vec<ForeignKeyInformationRow>> {
66 let rows: Vec<Row> = self
69 .connection
70 .borrow_mut()
71 .get()
72 .query(TABLE_WITH_FK_QUERY, &[])?;
73
74 let fk_info_rows: Vec<ForeignKeyInformationRow> = rows
75 .into_iter()
76 .map(|row: Row| -> ForeignKeyInformationRow {
77 return ForeignKeyInformationRow {
78 table_schema: row.get("table_schema"),
79 constraint_name: row.get("constraint_name"),
80 table_name: row.get("table_name"),
81 column_name: row.get("column_name"),
82 column_data_type: row.get("column_data_type"),
83 foreign_table_schema: row.get("foreign_table_schema"),
84 foreign_table_name: row.get("foreign_table_name"),
85 foreign_column_name: row.get("foreign_column_name"),
86 foreign_column_data_type: row.get("foreign_column_data_type"),
87 };
88 })
89 .collect();
90
91 return Ok(fk_info_rows);
92 }
93
94 fn get_table_by_id(&mut self) -> ResultAnyError<HashMap<PsqlTableIdentity, PsqlTable>> {
95 let rows: Vec<Row> = self.connection.borrow_mut().get().query(
96 "
97 SELECT
98 tc.constraint_name,
99 tc.table_schema,
100 tc.table_name,
101 kcu.column_name as primary_column_name,
102 c.data_type AS primary_column_data_type
103 FROM
104 information_schema.table_constraints AS tc
105 JOIN information_schema.key_column_usage AS kcu
106 ON tc.constraint_name = kcu.constraint_name
107 AND tc.table_schema = kcu.table_schema
108 JOIN information_schema.columns as c
109 ON c.table_schema = tc.table_schema
110 AND c.table_name = tc.table_name
111 AND c.column_name = kcu.column_name
112 WHERE tc.constraint_type = 'PRIMARY KEY' and
113 tc.table_schema not in ('pg_catalog', 'information_schema')
114 ",
115 &[],
116 )?;
117
118 let psql_table_by_id: HashMap<PsqlTableIdentity, PsqlTable> = rows
119 .into_iter()
120 .map(|row| {
121 let psql_table = PsqlTable::new(
122 row.get::<_, String>("table_schema"),
123 row.get::<_, String>("table_name"),
124 PsqlTableColumn::new(
125 row.get::<_, String>("primary_column_name"),
126 row.get::<_, String>("primary_column_data_type"),
127 ),
128 Default::default(),
129 Default::default(),
130 Default::default(),
131 );
132
133 return (psql_table.id.clone(), psql_table);
134 })
135 .collect();
136
137 return Ok(psql_table_by_id);
138 }
139}
140
141pub struct DbMetadata {
142 query: RefCell<Query>,
145}
146
147impl DbMetadata {
148 pub fn new(psql_connection: Rc<RefCell<PsqlConnection>>) -> DbMetadata {
149 return DbMetadata {
150 query: RefCell::new(Query {
151 connection: psql_connection,
152 }),
153 };
154 }
155}
156
157impl DbMetadata {
158 pub fn load_table_structure(
159 &self,
160 schema: &str,
161 ) -> ResultAnyError<HashMap<PsqlTableIdentity, PsqlTable>> {
162 let fk_info_rows = self.query.borrow_mut().fetch_fk_info(schema)?;
163
164 let mut table_by_id = self.query.borrow_mut().get_table_by_id()?;
165
166 psql_table_map_from_foreign_key_info_rows(&mut table_by_id, &fk_info_rows);
167
168 return Ok(table_by_id);
169 }
170}
171
172fn psql_table_map_from_foreign_key_info_rows(
173 table_by_id: &mut HashMap<PsqlTableIdentity, PsqlTable>,
174 rows: &Vec<ForeignKeyInformationRow>,
175) {
176 let fk_info_rows_by_foreign_table_id: HashMap<PsqlTableIdentity, Vec<&ForeignKeyInformationRow>> =
177 rows.iter().into_group_map_by(|row| {
178 return PsqlTableIdentity::new(&row.foreign_table_schema, &row.foreign_table_name);
179 });
180
181 let fk_info_rows_by_table_id: HashMap<PsqlTableIdentity, Vec<&ForeignKeyInformationRow>> =
182 rows.iter().into_group_map_by(|row| {
183 return PsqlTableIdentity::new(&row.table_schema, &row.table_name);
184 });
185
186 for (table_id, table) in table_by_id.into_iter() {
187 let referencing_fk_rows = fk_info_rows_by_table_id.get(&table_id);
188
189 if referencing_fk_rows.is_some() {
190 let referencing_fk_rows = referencing_fk_rows.unwrap();
191
192 table.referencing_fk_by_constraint_name = referencing_fk_rows
193 .iter()
194 .map(|fk_row| {
195 return (
196 fk_row.constraint_name.clone(),
197 PsqlForeignKey::new(
198 fk_row.constraint_name.clone(),
199 PsqlTableColumn::new(fk_row.column_name.clone(), fk_row.column_data_type.clone()),
200 fk_row.foreign_table_schema.clone(),
201 fk_row.foreign_table_name.clone(),
202 ),
203 );
204 })
205 .collect();
206 }
207
208 let referenced_fk_rows = fk_info_rows_by_foreign_table_id.get(&table_id);
209
210 if referenced_fk_rows.is_some() {
211 let referenced_fk_rows = referenced_fk_rows.unwrap();
212
213 table.referenced_fk_by_constraint_name = referenced_fk_rows
214 .iter()
215 .map(|fk_row| {
216 return (
217 fk_row.constraint_name.clone(),
218 PsqlForeignKey::new(
219 fk_row.constraint_name.clone(),
220 PsqlTableColumn::new(fk_row.column_name.clone(), fk_row.column_data_type.clone()),
221 fk_row.table_schema.clone(),
222 fk_row.table_name.clone(),
223 ),
224 );
225 })
226 .collect();
227 }
228 }
229}
230
231#[cfg(test)]
232mod test {
233 use super::*;
234 use std::borrow::Cow;
235 use std::collections::HashSet;
236
237 impl PsqlTable {
238 fn basic<'a, S>(schema: S, name: S, primary_column: PsqlTableColumn) -> PsqlTable
239 where
240 S: Into<Cow<'a, str>>,
241 {
242 return PsqlTable {
243 id: PsqlTableIdentity::new(schema, name),
244 primary_column,
245 columns: Default::default(),
246 referenced_fk_by_constraint_name: Default::default(),
247 referencing_fk_by_constraint_name: Default::default(),
248 };
249 }
250 }
251
252 mod psql_tables_from_foreign_key_info_rows {
253 use super::*;
254 use lezeh_common::macros::hashmap_literal;
255
256 #[test]
257 fn it_should_load_rows() {
258 let fk_info_rows = vec![
260 ForeignKeyInformationRow {
261 table_schema: "public".into(),
262 constraint_name: "orders_store_id_foreign".into(),
263 table_name: "orders".into(),
264 column_name: "store_id".into(),
265 column_data_type: "integer".into(),
266 foreign_table_schema: "public".into(),
267 foreign_table_name: "stores".into(),
268 foreign_column_name: "id".into(),
269 foreign_column_data_type: "integer".into(),
270 },
271 ForeignKeyInformationRow {
272 table_schema: "public".into(),
273 constraint_name: "order_statuses_store_id_foreign".into(),
274 table_name: "order_statuses".into(),
275 column_name: "store_id".into(),
276 column_data_type: "integer".into(),
277 foreign_table_schema: "public".into(),
278 foreign_table_name: "stores".into(),
279 foreign_column_name: "id".into(),
280 foreign_column_data_type: "integer".into(),
281 },
282 ForeignKeyInformationRow {
283 table_schema: "public".into(),
284 constraint_name: "product_images_product_id_foreign".into(),
285 table_name: "product_images".into(),
286 column_name: "product_id".into(),
287 column_data_type: "integer".into(),
288 foreign_table_schema: "public".into(),
289 foreign_table_name: "products".into(),
290 foreign_column_name: "id".into(),
291 foreign_column_data_type: "integer".into(),
292 },
293 ForeignKeyInformationRow {
294 table_schema: "public".into(),
295 constraint_name: "product_stock_ledgers_product_id_foreign".into(),
296 table_name: "product_stock_ledgers".into(),
297 column_name: "product_id".into(),
298 column_data_type: "integer".into(),
299 foreign_table_schema: "public".into(),
300 foreign_table_name: "products".into(),
301 foreign_column_name: "id".into(),
302 foreign_column_data_type: "integer".into(),
303 },
304 ForeignKeyInformationRow {
305 table_schema: "public".into(),
306 constraint_name: "store_customers_store_id_foreign".into(),
307 table_name: "store_customers".into(),
308 column_name: "store_id".into(),
309 column_data_type: "integer".into(),
310 foreign_table_schema: "public".into(),
311 foreign_table_name: "stores".into(),
312 foreign_column_name: "id".into(),
313 foreign_column_data_type: "integer".into(),
314 },
315 ForeignKeyInformationRow {
316 table_schema: "public".into(),
317 constraint_name: "store_staffs_stores_store_staff_role_id_foreign".into(),
318 table_name: "store_staffs_stores".into(),
319 column_name: "store_staff_role_id".into(),
320 column_data_type: "uuid".into(),
321 foreign_table_schema: "public".into(),
322 foreign_table_name: "store_staff_roles".into(),
323 foreign_column_name: "id".into(),
324 foreign_column_data_type: "uuid".into(),
325 },
326 ForeignKeyInformationRow {
327 table_schema: "public".into(),
328 constraint_name: "store_staffs_stores_store_staff_id_foreign".into(),
329 table_name: "store_staffs_stores".into(),
330 column_name: "store_staff_id".into(),
331 column_data_type: "integer".into(),
332 foreign_table_schema: "public".into(),
333 foreign_table_name: "store_staffs".into(),
334 foreign_column_name: "id".into(),
335 foreign_column_data_type: "integer".into(),
336 },
337 ForeignKeyInformationRow {
338 table_schema: "public".into(),
339 constraint_name: "store_staffs_stores_store_id_foreign".into(),
340 table_name: "store_staffs_stores".into(),
341 column_name: "store_id".into(),
342 column_data_type: "integer".into(),
343 foreign_table_schema: "public".into(),
344 foreign_table_name: "stores".into(),
345 foreign_column_name: "id".into(),
346 foreign_column_data_type: "integer".into(),
347 },
348 ForeignKeyInformationRow {
349 table_schema: "public".into(),
350 constraint_name: "products_store_id_foreign".into(),
351 table_name: "products".into(),
352 column_name: "store_id".into(),
353 column_data_type: "integer".into(),
354 foreign_table_schema: "public".into(),
355 foreign_table_name: "stores".into(),
356 foreign_column_name: "id".into(),
357 foreign_column_data_type: "integer".into(),
358 },
359 ForeignKeyInformationRow {
360 table_schema: "public".into(),
361 constraint_name: "order_items_order_id_foreign".into(),
362 table_name: "order_items".into(),
363 column_name: "order_id".into(),
364 column_data_type: "integer".into(),
365 foreign_table_schema: "public".into(),
366 foreign_table_name: "orders".into(),
367 foreign_column_name: "id".into(),
368 foreign_column_data_type: "integer".into(),
369 },
370 ForeignKeyInformationRow {
371 table_schema: "public".into(),
372 constraint_name: "order_items_product_id_foreign".into(),
373 table_name: "order_items".into(),
374 column_name: "product_id".into(),
375 column_data_type: "integer".into(),
376 foreign_table_schema: "public".into(),
377 foreign_table_name: "products".into(),
378 foreign_column_name: "id".into(),
379 foreign_column_data_type: "integer".into(),
380 },
381 ];
382
383 let mut psql_table_by_id: HashMap<PsqlTableIdentity, PsqlTable> = hashmap_literal! {
384 PsqlTableIdentity::new("public", "stores") => PsqlTable::basic("public", "stores", PsqlTableColumn{
385 name: "id".into(),
386 data_type: "integer".into(),
387 }),
388 PsqlTableIdentity::new("public", "orders") => PsqlTable::basic("public", "orders", PsqlTableColumn{
389 name: "id".into(),
390 data_type: "integer".into(),
391 }),
392 PsqlTableIdentity::new("public", "order_items") => PsqlTable::basic("public", "order_items", PsqlTableColumn{
393 name: "id".into(),
394 data_type: "integer".into(),
395 }),
396 PsqlTableIdentity::new("public", "order_statuses") => PsqlTable::basic("public", "order_statuses", PsqlTableColumn{
397 name: "id".into(),
398 data_type: "integer".into(),
399 }),
400 PsqlTableIdentity::new("public", "products") => PsqlTable::basic("public", "products", PsqlTableColumn{
401 name: "id".into(),
402 data_type: "integer".into(),
403 }),
404 PsqlTableIdentity::new("public", "product_images") => PsqlTable::basic("public", "product_images", PsqlTableColumn{
405 name: "id".into(),
406 data_type: "integer".into(),
407 }),
408 PsqlTableIdentity::new("public", "product_stock_ledgers") => PsqlTable::basic("public", "product_stock_ledgers", PsqlTableColumn{
409 name: "id".into(),
410 data_type: "integer".into(),
411 }),
412 PsqlTableIdentity::new("public", "store_customers") => PsqlTable::basic("public", "store_customers", PsqlTableColumn{
413 name: "id".into(),
414 data_type: "integer".into(),
415 }),
416 PsqlTableIdentity::new("public", "store_staffs_stores") => PsqlTable::basic("public", "store_staffs_stores", PsqlTableColumn{
417 name: "id".into(),
418 data_type: "uuid".into(),
419 }),
420 PsqlTableIdentity::new("public", "store_staff_roles") => PsqlTable::basic("public", "store_staff_roles", PsqlTableColumn{
421 name: "id".into(),
422 data_type: "uuid".into(),
423 }),
424 PsqlTableIdentity::new("public", "store_staffs") => PsqlTable::basic("public", "store_staffs", PsqlTableColumn{
425 name: "id".into(),
426 data_type: "integer".into(),
427 }),
428 };
429
430 psql_table_map_from_foreign_key_info_rows(&mut psql_table_by_id, &fk_info_rows);
432
433 let order_items_table: &PsqlTable = psql_table_by_id
437 .get(&PsqlTableIdentity::new("public", "order_items"))
438 .unwrap();
439
440 assert_eq!(
441 order_items_table.id,
442 PsqlTableIdentity::new("public", "order_items")
443 );
444 assert_eq!(order_items_table.referencing_fk_by_constraint_name.len(), 2);
445 assert_eq!(order_items_table.referenced_fk_by_constraint_name.len(), 0);
446
447 let fk_to_orders_table_from_order_items = order_items_table
448 .referencing_fk_by_constraint_name
449 .get("order_items_order_id_foreign");
450
451 assert!(fk_to_orders_table_from_order_items.is_some());
452
453 let store_staffs_stores_table: &PsqlTable = psql_table_by_id
455 .get(&PsqlTableIdentity::new("public", "store_staffs_stores"))
456 .ok_or_else(|| "could not get store_staffs_stores")
457 .unwrap();
458
459 assert_eq!(
460 store_staffs_stores_table.id,
461 PsqlTableIdentity::new("public", "store_staffs_stores")
462 );
463 assert_eq!(
464 store_staffs_stores_table
465 .referencing_fk_by_constraint_name
466 .len(),
467 3
468 );
469
470 let products_table: &PsqlTable = psql_table_by_id
472 .get(&PsqlTableIdentity::new("public", "products"))
473 .unwrap();
474
475 assert_eq!(
476 products_table.id,
477 PsqlTableIdentity::new("public", "products")
478 );
479
480 assert_eq!(products_table.referencing_fk_by_constraint_name.len(), 1);
481 assert_eq!(products_table.referenced_fk_by_constraint_name.len(), 3);
482
483 let _available_tables: HashSet<&String> =
487 fk_info_rows.iter().map(|row| &row.table_name).collect();
488
489 assert_eq!(psql_table_by_id.len(), 11)
490 }
491 }
492}