Skip to content

Commit 812c0d0

Browse files
authored
Add view and materialized view support (PlantUML) (#53)
* Version 0.6.0 * Add views to test_db * Add views to SqlERData * Add PlantUML view render * Add new puml lib link
1 parent 9b19d66 commit 812c0d0

File tree

11 files changed

+278
-22
lines changed

11 files changed

+278
-22
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ jobs:
8888
- run: ./target/release/sqlant $TEST_DATABASE_URL -s test_schema -o mermaid > input.mmd
8989
- run: mmdc -i input.mmd -o output.png
9090

91-
resource_class: small
91+
resource_class: medium
9292

9393
workflows:
9494
default:

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "sqlant"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
edition = "2021"
55
license = "MIT"
66
repository = "https://github.com/kurotych/sqlant"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ sqlant postgresql://sqlant_user:sqlant_pswd@localhost/sqlant_db --legend -e
2929
docker run --network host kurotych/sqlant postgresql://sqlant_user:sqlant_pswd@localhost/sqlant_db
3030
```
3131

32-
![example link ](https://www.plantuml.com/plantuml/png/hLNTSjii5BpdAVWotuKp4yUDyrEdCvt9cp08ZYrgu50un77gl7kbE6f3HgD6KUumx0AxQortYMIax2ohZGQkJ5GMMDeQ7sIKZblZWVO1E4QgaR7_Z5SsDyYQAAHlYqMKk_EDeJfNEK5Kw0aydIjqYssEI7jLBz9FApqjgYLSw-eMzEhc-dQzN5qilwpapUghlBowgYwlrklBPkfMwKN8piwjgHQw4krcxM_6I5OMPYedGWVnbFzYd2kqsVcPqMVyf38Ru-daZFyVjjyfPcX6tZ-FJXleV3x_Iv1QHqYfOH4yq4c1x31UEXW4X1ez29zT1N4G665Z4a44BIIrIECWaNI1xmpLlFt97z53F_lH1A5GzzxbwQqj0gEUQiwVlTumrptCZgF1cdk8U-60QjI3Tc3K7_KYoBq3J-yv9TKc1ECtuZrP4vAq8aIZMfjzTj0CXw0a7uHqc3qL-9vadjKA3IIDBN8f8nFzCVNQFY7RjCrZOkqaTl1FpxDrNCWrGMmj7VQ-WrUmnWWjVeptGeOGyLv-UWZbAUDsAB8vNl1ZHcA0A0bB1RsUX8WwAvfM4VVWDOug22K9DXZt3U7b1gARwbUaC7kA-x4LNOfDspn9QEVMAALeCGu_73cgMYOGsHjwN-iaQI6DmPl7uSTh1sPJ_xEyZZbFi73gEzfpcwxGYriTbwPyVShGy9_D6WyNWuX4aZgfez_oyg1bXtYMpygQuayudnyEl9jbx7K5bQiTfn-JjfcntfRaXoFYzI9ZBvz3Hp-wpbYpJJVrWc2i38iVIWO3dztiRHCqei62eeZUgER5_W4xiErqZGuQAvZKMbZOWDUpKtO7NcTdVmC0)
32+
![example link ](https://www.plantuml.com/plantuml/png/hLRjKYCh4FtFKynDtHzQPOpHwqTkrLRv9XG3dGmLFbXe4iUDxxxa0rTqYZN6Np9mJjDz3dRdCI3p6BKYdHJGSEcv0XAMqZZccMwKD82zWyPwx2mX_qZ3LKp83j65_oSJpzQN2ubTR6C0pwr1C7Z9hPuiexVOysuIVYfcS39hfpDnTpURjhUt_E7ceRrwk9f1shc_keUxwUtD3VkFp-RN4vVI6IlPJaHBjy6stuGWQnMSyHZGQl3dpI_IDDoggCsP51VDg9KBQN1qqVphbZ_GHqWhOtQhymGTZyT_24m83o4a5i8JZWfanXYceGfmdJL0JTGj-2hGmq8610-2CjmYfOQ0JBjcBR5hjf_DipKmp7wMZd8h1dDvUIyBjLwSAivhh7VC-G0pSmGekGBVKmtOML6LmthnLIqSwpKO_CmjePFEIREWd_4QBJ95dPSS4iv43MbPWo9xeapRQB303pYpgvOAG2PLuKjf6HssgIxxyTw6PJp6rbnYXd-tdpl5APiZ-AsaTUqpl8MvzL313sfFcNFUhjYtcr3UKezGzMQVuDU4j0uyGrjMCAY9yrP4ZgUrY1KOOOzg49mXBApl4-6G0SrRes62ZGPzVsoBBeiDotXQeJdNOogrolhwu8YUTom0ZKRYvxfEO0h2CNZvN1zUQv2Bxc_Dw-3pQHAUD4S7iiaDlSZgS7J2Vn-NM7ziIXgOLX416PQbrcTvvQBhZDYLJrOIrcVQ6knKJ_Tl8KIjcUzJ1CqGl6GgMIvsiGfbTgmKJIJKwCCSs_Mky2hQHq1mKCoubCyX7RIEvlQPpQZyWFYcgqdPiPiNEnvlDatU9UHjDywd9MSO4vQHaC61qMpsYBU7X1mwGwRl7o0XAKpUEY08ATzjioaOZ-bFrEet)
3333
```
3434
postgresql://sqlant_user:sqlant_pswd@localhost/sqlant_db -o mermaid
3535
```

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
db:
3-
image: postgres
3+
image: postgres:17.4
44
environment:
55
POSTGRES_USER: sql
66
POSTGRES_PASSWORD: sql

src/mermaid_generator.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ impl<'a> MermaidGenerator<'a> {
103103
let mut res: String = self
104104
.str_templates
105105
.render("column", &column)?
106-
.trim_end_matches(|c| c == ',')
106+
.trim_end_matches([','])
107107
.into();
108108
if column.is_nn {
109109
res += " \"NN\"";
@@ -136,7 +136,7 @@ impl<'a> MermaidGenerator<'a> {
136136
}
137137
}
138138

139-
impl<'a> ViewGenerator for MermaidGenerator<'a> {
139+
impl ViewGenerator for MermaidGenerator<'_> {
140140
fn generate(
141141
&self,
142142
mut sql_erd: SqlERData,

src/plantuml_generator.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::sync::Arc;
22

33
use super::sql_entities::{SqlERData, Table, TableColumn};
4-
use crate::{GeneratorConfigOptions, ViewGenerator};
4+
use crate::{sql_entities::View, GeneratorConfigOptions, ViewGenerator};
55
use serde::Serialize;
66
use tinytemplate::{format_unescaped, TinyTemplate};
77

@@ -14,11 +14,15 @@ static PUML_TEMPLATE: &str = "@startuml\n\n\
1414
skinparam linetype ortho\n\n\
1515
{puml_lib}\n\n\
1616
{{ for ent in entities}}{ent}\n{{ endfor }}\n\
17+
{{ for view in views}}{view}\n{{ endfor }}\n\
1718
{{ for fk in foreign_keys}}{fk}\n{{ endfor }}\n\
1819
{{ for e in enums}}{e}\n{{ endfor }}{legend}\n@enduml";
1920

2021
static ENTITY_TEMPLATE: &str = "table({name}) \\{\n{pks} ---\n{fks}{nns}{others}}\n";
2122

23+
static VIEW_TEMPLATE: &str =
24+
"view({name}{{ if materialized}}, $materialized=true{{ endif }}) \\{\n{columns}}\n";
25+
2226
static COLUMN_TEMPLATE: &str = " column({col.name}, \"{col.datatype}\"{{ if is_pk }}, $pk=true{{ endif }}{{ if is_fk }}, $fk=true{{ endif }}{{if is_nn}}, $nn=true{{ endif }})\n";
2327

2428
static REL_TEMPLATE: &str =
@@ -44,6 +48,13 @@ struct SColumn<'a> {
4448
is_nn_and_not_pk: bool,
4549
}
4650

51+
#[derive(Serialize)]
52+
struct SView {
53+
name: String,
54+
columns: String,
55+
materialized: bool,
56+
}
57+
4758
#[derive(Serialize)]
4859
struct SEntity {
4960
name: String,
@@ -59,7 +70,9 @@ struct SLegend(String);
5970
#[derive(Serialize)]
6071
struct SPuml {
6172
puml_lib: String,
73+
// entities can be renamed to "tables"
6274
entities: Vec<String>,
75+
views: Vec<String>,
6376
foreign_keys: Vec<String>,
6477
enums: Vec<String>,
6578
legend: Option<SLegend>,
@@ -85,6 +98,7 @@ impl<'a> PlantUmlDefaultGenerator<'a> {
8598
str_templates.add_template("puml", PUML_TEMPLATE)?;
8699
str_templates.add_template("column", COLUMN_TEMPLATE)?;
87100
str_templates.add_template("ent", ENTITY_TEMPLATE)?;
101+
str_templates.add_template("view", VIEW_TEMPLATE)?;
88102
str_templates.add_template("rel", REL_TEMPLATE)?;
89103
str_templates.add_template("enum", ENUM_TEMPLATE)?;
90104
str_templates.add_template("legend", PUML_LEGEND)?;
@@ -175,9 +189,41 @@ impl<'a> PlantUmlDefaultGenerator<'a> {
175189
},
176190
)?)
177191
}
192+
193+
fn view_render(&self, view: &View) -> Result<String, crate::SqlantError> {
194+
let columns_render = |columns: Vec<Arc<TableColumn>>| -> Result<String, _> {
195+
Ok::<std::string::String, crate::SqlantError>(columns.iter().try_fold(
196+
String::new(),
197+
|acc, col| {
198+
let r = self.str_templates.render(
199+
"column",
200+
&SColumn {
201+
col: col.as_ref(),
202+
is_fk: false,
203+
is_pk: false,
204+
is_nn: false,
205+
is_nn_and_not_pk: false,
206+
},
207+
);
208+
match r {
209+
Ok(r) => Ok(acc + &r),
210+
Err(e) => Err(e),
211+
}
212+
},
213+
)?)
214+
};
215+
Ok(self.str_templates.render(
216+
"view",
217+
&SView {
218+
columns: columns_render(view.columns.clone())?,
219+
name: view.name.clone(),
220+
materialized: view.materialized,
221+
},
222+
)?)
223+
}
178224
}
179225

180-
impl<'a> ViewGenerator for PlantUmlDefaultGenerator<'a> {
226+
impl ViewGenerator for PlantUmlDefaultGenerator<'_> {
181227
fn generate(
182228
&self,
183229
sql_erd: SqlERData,
@@ -188,6 +234,12 @@ impl<'a> ViewGenerator for PlantUmlDefaultGenerator<'a> {
188234
.iter()
189235
.map(|tbl| self.entity_render(tbl))
190236
.collect::<Result<Vec<String>, crate::SqlantError>>()?;
237+
let views: Vec<String> = sql_erd
238+
.views
239+
.iter()
240+
.map(|view| self.view_render(view))
241+
.collect::<Result<Vec<String>, crate::SqlantError>>()?;
242+
191243
let foreign_keys: Vec<String> = sql_erd
192244
.foreign_keys
193245
.iter()
@@ -241,11 +293,12 @@ impl<'a> ViewGenerator for PlantUmlDefaultGenerator<'a> {
241293
foreign_keys,
242294
enums,
243295
legend,
296+
views,
244297
},
245298
)?)
246299
}
247300
}
248-
static PUML_LIB_INCLUDE: &str = "!include https://raw.githubusercontent.com/kurotych/sqlant/b2e5db9ed8659f281208a687a344b34ff38129cd/puml-lib/db_ent.puml";
301+
static PUML_LIB_INCLUDE: &str = "!include https://raw.githubusercontent.com/kurotych/sqlant/9b19d6691b55c838b0809ed66707e61533a4c9f2/puml-lib/db_ent.puml";
249302

250303
// https://raw.githubusercontent.com/kurotych/sqlant/0497c6594364e406d77dfdc0999e0b5e596b7d73/puml-lib/db_ent.puml
251304
static PUML_LIB_INLINE: &str = r#"
@@ -269,6 +322,14 @@ static PUML_LIB_INLINE: &str = r#"
269322
!return 'entity "**' + $name + '**"' + " as " + $name
270323
!endfunction
271324
325+
!function view($name, $materialized=false)
326+
!if ($materialized == false)
327+
!return 'entity "**' + $name + ' **<color:SkyBlue>**(V)**</color>"' + " as " + $name
328+
!else
329+
!return 'entity "**' + $name + ' **<color:DarkBlue>**(MV)**</color>"' + " as " + $name
330+
!endif
331+
!endfunction
332+
272333
!procedure enum($name, $variants)
273334
!$list = %splitstr($variants, ",")
274335
@@ -286,6 +347,8 @@ static PUML_LIB_INLINE: &str = r#"
286347
|<color:#aaaaaa><&key></color>| Foreign Key |
287348
| &#8226; | Mandatory field (Not Null) |
288349
| <color:purple>**(E)**</color> | Enum |
350+
| <color:SkyBlue>**(V)**</color> | View |
351+
| <color:DarkBlue>**(MV)**</color> | Materialized View |
289352
endlegend
290353
!endprocedure
291354
"#;

src/psql_erd_loader.rs

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
use native_tls::TlsConnector;
22
use postgres_native_tls::MakeTlsConnector;
3-
use std::collections::{BTreeMap, BTreeSet};
4-
use std::sync::Arc;
5-
use tokio_postgres::Client;
3+
use std::{
4+
collections::{BTreeMap, BTreeSet},
5+
error::Error,
6+
sync::Arc,
7+
};
8+
use tokio_postgres::{
9+
types::{FromSql, Type},
10+
Client,
11+
};
612

13+
use crate::sql_entities::View;
714
use crate::{
815
sql_entities::{
916
ColumnConstraints, ForeignKey, SqlERData, SqlERDataLoader, SqlEnums, Table, TableColumn,
@@ -12,12 +19,19 @@ use crate::{
1219
};
1320

1421
static GET_TABLES_LIST_QUERY: &str = r#"
15-
SELECT trim(both '"' from table_name) as table_name
22+
SELECT trim(both '"' from table_name) as table_name, table_type
1623
FROM information_schema.tables
1724
WHERE table_schema = $1
1825
ORDER BY table_name;
1926
"#;
2027

28+
static GET_MATERIALIZED_VIEWS: &str = r#"
29+
SELECT trim(both '"' from matviewname) AS matview_name
30+
FROM pg_matviews
31+
WHERE schemaname = $1
32+
ORDER BY matviewname;
33+
"#;
34+
2135
/// https://www.postgresql.org/docs/current/catalog-pg-attribute.html
2236
static GET_COLUMNS_BASIC_INFO_QUERY: &str = r#"
2337
SELECT attname AS col_name,
@@ -72,7 +86,7 @@ WHERE pg_type.oid = $1
7286
ORDER BY pg_enum;
7387
"#;
7488

75-
/// https://www.postgresql.org/docs/current/view-pg-indexes.html
89+
// https://www.postgresql.org/docs/current/view-pg-indexes.html
7690
// If you'll need to add indexers support
7791
// static GET_INDEXES_QUERY: &'static str = r#"
7892
// SELECT
@@ -188,7 +202,6 @@ impl PostgreSqlERDLoader {
188202
.query(GET_COLUMNS_BASIC_INFO_QUERY, &[&table_names])
189203
.await?;
190204
for row in rows {
191-
// I don't know how to get rid this
192205
let col_num: i16 = row.get("col_num");
193206
let col_name: &str = row.get("col_name");
194207
let not_null: bool = row.get("not_null");
@@ -345,6 +358,27 @@ impl PostgreSqlERDLoader {
345358
}
346359
}
347360

361+
#[derive(Debug, PartialEq)]
362+
enum TableType {
363+
BaseTable,
364+
View,
365+
}
366+
367+
impl FromSql<'_> for TableType {
368+
fn from_sql(_ty: &Type, raw: &[u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
369+
let s = std::str::from_utf8(raw)?;
370+
match s {
371+
"BASE TABLE" => Ok(TableType::BaseTable),
372+
"VIEW" => Ok(TableType::View),
373+
other => Err(format!("Unknown table type: {}", other).into()),
374+
}
375+
}
376+
377+
fn accepts(ty: &Type) -> bool {
378+
*ty == Type::TEXT || *ty == Type::VARCHAR
379+
}
380+
}
381+
348382
#[async_trait::async_trait]
349383
impl SqlERDataLoader for PostgreSqlERDLoader {
350384
async fn load_erd_data(&mut self) -> Result<SqlERData, crate::SqlantError> {
@@ -362,14 +396,78 @@ impl SqlERDataLoader for PostgreSqlERDLoader {
362396
.client
363397
.query(GET_TABLES_LIST_QUERY, &[&self.schema_name])
364398
.await?;
365-
let table_names: Vec<String> = res.iter().map(|row| row.get("table_name")).collect();
366-
let (tables, enums) = self.load_tables(table_names).await?;
367-
let foreign_keys = self.get_fks(&tables)?;
399+
400+
// Collect table names and types as a vector of tuples
401+
let table_names_with_types: Vec<(String, TableType)> = res
402+
.iter()
403+
.map(|row| (row.get("table_name"), row.get("table_type")))
404+
.collect();
405+
406+
// Extract just the table names for loading
407+
let table_names: Vec<String> = table_names_with_types
408+
.iter()
409+
.map(|(name, _)| name.clone())
410+
.collect();
411+
412+
let (tables_and_views, enums) = self.load_tables(table_names).await?;
413+
let foreign_keys = self.get_fks(&tables_and_views)?;
414+
415+
let mat_views_name: Vec<String> = self
416+
.client
417+
.query(GET_MATERIALIZED_VIEWS, &[&self.schema_name])
418+
.await?
419+
.iter()
420+
.map(|row| row.get("matview_name"))
421+
.collect();
422+
let (mat_views, _) = self.load_tables(mat_views_name).await?;
423+
424+
// Collect table names and types as a vector of tuples
425+
let table_names_with_types: Vec<(String, TableType)> = res
426+
.iter()
427+
.map(|row| (row.get("table_name"), row.get("table_type")))
428+
.collect();
429+
430+
let mut views: Vec<Arc<View>> = mat_views
431+
.into_iter()
432+
.map(|v| {
433+
let v = Arc::try_unwrap(v).unwrap();
434+
View {
435+
materialized: true,
436+
name: v.name,
437+
columns: v.columns,
438+
}
439+
.into()
440+
})
441+
.collect();
442+
443+
let mut tables: Vec<Arc<Table>> = vec![];
444+
445+
for entity in tables_and_views.into_iter() {
446+
let (_, r#type) = table_names_with_types
447+
.iter()
448+
.find(|t| t.0 == entity.name)
449+
.unwrap();
450+
match r#type {
451+
TableType::BaseTable => tables.push(entity),
452+
TableType::View => {
453+
let Table { name, columns, .. } = Arc::try_unwrap(entity).unwrap();
454+
views.push(
455+
View {
456+
materialized: false,
457+
name,
458+
columns,
459+
}
460+
.into(),
461+
);
462+
}
463+
}
464+
}
368465

369466
Ok(SqlERData {
370467
tables,
371468
foreign_keys,
372469
enums,
470+
views,
373471
})
374472
}
375473
}

0 commit comments

Comments
 (0)