Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
- run: ./target/release/sqlant $TEST_DATABASE_URL -s test_schema -o mermaid > input.mmd
- run: mmdc -i input.mmd -o output.png

resource_class: small
resource_class: medium

workflows:
default:
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sqlant"
version = "0.5.0"
version = "0.6.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/kurotych/sqlant"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ sqlant postgresql://sqlant_user:sqlant_pswd@localhost/sqlant_db --legend -e
docker run --network host kurotych/sqlant postgresql://sqlant_user:sqlant_pswd@localhost/sqlant_db
```

![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)
![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)
```
postgresql://sqlant_user:sqlant_pswd@localhost/sqlant_db -o mermaid
```
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
db:
image: postgres
image: postgres:17.4
environment:
POSTGRES_USER: sql
POSTGRES_PASSWORD: sql
Expand Down
4 changes: 2 additions & 2 deletions src/mermaid_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ impl<'a> MermaidGenerator<'a> {
let mut res: String = self
.str_templates
.render("column", &column)?
.trim_end_matches(|c| c == ',')
.trim_end_matches([','])
.into();
if column.is_nn {
res += " \"NN\"";
Expand Down Expand Up @@ -136,7 +136,7 @@ impl<'a> MermaidGenerator<'a> {
}
}

impl<'a> ViewGenerator for MermaidGenerator<'a> {
impl ViewGenerator for MermaidGenerator<'_> {
fn generate(
&self,
mut sql_erd: SqlERData,
Expand Down
69 changes: 66 additions & 3 deletions src/plantuml_generator.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::sync::Arc;

use super::sql_entities::{SqlERData, Table, TableColumn};
use crate::{GeneratorConfigOptions, ViewGenerator};
use crate::{sql_entities::View, GeneratorConfigOptions, ViewGenerator};
use serde::Serialize;
use tinytemplate::{format_unescaped, TinyTemplate};

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

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

static VIEW_TEMPLATE: &str =
"view({name}{{ if materialized}}, $materialized=true{{ endif }}) \\{\n{columns}}\n";

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";

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

#[derive(Serialize)]
struct SView {
name: String,
columns: String,
materialized: bool,
}

#[derive(Serialize)]
struct SEntity {
name: String,
Expand All @@ -59,7 +70,9 @@ struct SLegend(String);
#[derive(Serialize)]
struct SPuml {
puml_lib: String,
// entities can be renamed to "tables"
Copy link

Copilot AI May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] This placeholder comment appears to be a leftover note. Consider removing or clarifying it to keep comments directly relevant.

Suggested change
// entities can be renamed to "tables"

Copilot uses AI. Check for mistakes.
entities: Vec<String>,
views: Vec<String>,
foreign_keys: Vec<String>,
enums: Vec<String>,
legend: Option<SLegend>,
Expand All @@ -85,6 +98,7 @@ impl<'a> PlantUmlDefaultGenerator<'a> {
str_templates.add_template("puml", PUML_TEMPLATE)?;
str_templates.add_template("column", COLUMN_TEMPLATE)?;
str_templates.add_template("ent", ENTITY_TEMPLATE)?;
str_templates.add_template("view", VIEW_TEMPLATE)?;
str_templates.add_template("rel", REL_TEMPLATE)?;
str_templates.add_template("enum", ENUM_TEMPLATE)?;
str_templates.add_template("legend", PUML_LEGEND)?;
Expand Down Expand Up @@ -175,9 +189,41 @@ impl<'a> PlantUmlDefaultGenerator<'a> {
},
)?)
}

fn view_render(&self, view: &View) -> Result<String, crate::SqlantError> {
let columns_render = |columns: Vec<Arc<TableColumn>>| -> Result<String, _> {
Ok::<std::string::String, crate::SqlantError>(columns.iter().try_fold(
String::new(),
|acc, col| {
let r = self.str_templates.render(
"column",
&SColumn {
col: col.as_ref(),
is_fk: false,
is_pk: false,
is_nn: false,
is_nn_and_not_pk: false,
},
);
match r {
Ok(r) => Ok(acc + &r),
Err(e) => Err(e),
}
},
)?)
};
Ok(self.str_templates.render(
"view",
&SView {
columns: columns_render(view.columns.clone())?,
name: view.name.clone(),
materialized: view.materialized,
},
)?)
}
}

impl<'a> ViewGenerator for PlantUmlDefaultGenerator<'a> {
impl ViewGenerator for PlantUmlDefaultGenerator<'_> {
fn generate(
&self,
sql_erd: SqlERData,
Expand All @@ -188,6 +234,12 @@ impl<'a> ViewGenerator for PlantUmlDefaultGenerator<'a> {
.iter()
.map(|tbl| self.entity_render(tbl))
.collect::<Result<Vec<String>, crate::SqlantError>>()?;
let views: Vec<String> = sql_erd
.views
.iter()
.map(|view| self.view_render(view))
.collect::<Result<Vec<String>, crate::SqlantError>>()?;

let foreign_keys: Vec<String> = sql_erd
.foreign_keys
.iter()
Expand Down Expand Up @@ -241,11 +293,12 @@ impl<'a> ViewGenerator for PlantUmlDefaultGenerator<'a> {
foreign_keys,
enums,
legend,
views,
},
)?)
}
}
static PUML_LIB_INCLUDE: &str = "!include https://raw.githubusercontent.com/kurotych/sqlant/b2e5db9ed8659f281208a687a344b34ff38129cd/puml-lib/db_ent.puml";
static PUML_LIB_INCLUDE: &str = "!include https://raw.githubusercontent.com/kurotych/sqlant/9b19d6691b55c838b0809ed66707e61533a4c9f2/puml-lib/db_ent.puml";

// https://raw.githubusercontent.com/kurotych/sqlant/0497c6594364e406d77dfdc0999e0b5e596b7d73/puml-lib/db_ent.puml
static PUML_LIB_INLINE: &str = r#"
Expand All @@ -269,6 +322,14 @@ static PUML_LIB_INLINE: &str = r#"
!return 'entity "**' + $name + '**"' + " as " + $name
!endfunction

!function view($name, $materialized=false)
!if ($materialized == false)
!return 'entity "**' + $name + ' **<color:SkyBlue>**(V)**</color>"' + " as " + $name
!else
!return 'entity "**' + $name + ' **<color:DarkBlue>**(MV)**</color>"' + " as " + $name
!endif
!endfunction

!procedure enum($name, $variants)
!$list = %splitstr($variants, ",")

Expand All @@ -286,6 +347,8 @@ static PUML_LIB_INLINE: &str = r#"
|<color:#aaaaaa><&key></color>| Foreign Key |
| &#8226; | Mandatory field (Not Null) |
| <color:purple>**(E)**</color> | Enum |
| <color:SkyBlue>**(V)**</color> | View |
| <color:DarkBlue>**(MV)**</color> | Materialized View |
endlegend
!endprocedure
"#;
116 changes: 107 additions & 9 deletions src/psql_erd_loader.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
use native_tls::TlsConnector;
use postgres_native_tls::MakeTlsConnector;
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
use tokio_postgres::Client;
use std::{
collections::{BTreeMap, BTreeSet},
error::Error,
sync::Arc,
};
use tokio_postgres::{
types::{FromSql, Type},
Client,
};

use crate::sql_entities::View;
use crate::{
sql_entities::{
ColumnConstraints, ForeignKey, SqlERData, SqlERDataLoader, SqlEnums, Table, TableColumn,
Expand All @@ -12,12 +19,19 @@ use crate::{
};

static GET_TABLES_LIST_QUERY: &str = r#"
SELECT trim(both '"' from table_name) as table_name
SELECT trim(both '"' from table_name) as table_name, table_type
FROM information_schema.tables
WHERE table_schema = $1
ORDER BY table_name;
"#;

static GET_MATERIALIZED_VIEWS: &str = r#"
SELECT trim(both '"' from matviewname) AS matview_name
FROM pg_matviews
WHERE schemaname = $1
ORDER BY matviewname;
"#;

/// https://www.postgresql.org/docs/current/catalog-pg-attribute.html
static GET_COLUMNS_BASIC_INFO_QUERY: &str = r#"
SELECT attname AS col_name,
Expand Down Expand Up @@ -72,7 +86,7 @@ WHERE pg_type.oid = $1
ORDER BY pg_enum;
"#;

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

#[derive(Debug, PartialEq)]
enum TableType {
BaseTable,
View,
}

impl FromSql<'_> for TableType {
fn from_sql(_ty: &Type, raw: &[u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
let s = std::str::from_utf8(raw)?;
match s {
"BASE TABLE" => Ok(TableType::BaseTable),
"VIEW" => Ok(TableType::View),
other => Err(format!("Unknown table type: {}", other).into()),
}
}

fn accepts(ty: &Type) -> bool {
*ty == Type::TEXT || *ty == Type::VARCHAR
}
}

#[async_trait::async_trait]
impl SqlERDataLoader for PostgreSqlERDLoader {
async fn load_erd_data(&mut self) -> Result<SqlERData, crate::SqlantError> {
Expand All @@ -362,14 +396,78 @@ impl SqlERDataLoader for PostgreSqlERDLoader {
.client
.query(GET_TABLES_LIST_QUERY, &[&self.schema_name])
.await?;
let table_names: Vec<String> = res.iter().map(|row| row.get("table_name")).collect();
let (tables, enums) = self.load_tables(table_names).await?;
let foreign_keys = self.get_fks(&tables)?;

// Collect table names and types as a vector of tuples
let table_names_with_types: Vec<(String, TableType)> = res
.iter()
.map(|row| (row.get("table_name"), row.get("table_type")))
.collect();

// Extract just the table names for loading
let table_names: Vec<String> = table_names_with_types
.iter()
.map(|(name, _)| name.clone())
.collect();

let (tables_and_views, enums) = self.load_tables(table_names).await?;
let foreign_keys = self.get_fks(&tables_and_views)?;

let mat_views_name: Vec<String> = self
.client
.query(GET_MATERIALIZED_VIEWS, &[&self.schema_name])
.await?
.iter()
.map(|row| row.get("matview_name"))
.collect();
let (mat_views, _) = self.load_tables(mat_views_name).await?;

// Collect table names and types as a vector of tuples
let table_names_with_types: Vec<(String, TableType)> = res
.iter()
.map(|row| (row.get("table_name"), row.get("table_type")))
.collect();

Comment on lines +424 to +429
Copy link

Copilot AI May 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block re-collects table_names_with_types but it was already built earlier—consider reusing the initial vector instead of duplicating the query for clarity and reduced overhead.

Suggested change
// Collect table names and types as a vector of tuples
let table_names_with_types: Vec<(String, TableType)> = res
.iter()
.map(|row| (row.get("table_name"), row.get("table_type")))
.collect();
// Reuse the previously collected table_names_with_types vector

Copilot uses AI. Check for mistakes.
let mut views: Vec<Arc<View>> = mat_views
.into_iter()
.map(|v| {
let v = Arc::try_unwrap(v).unwrap();
View {
materialized: true,
name: v.name,
columns: v.columns,
}
.into()
})
.collect();

let mut tables: Vec<Arc<Table>> = vec![];

for entity in tables_and_views.into_iter() {
let (_, r#type) = table_names_with_types
.iter()
.find(|t| t.0 == entity.name)
.unwrap();
match r#type {
TableType::BaseTable => tables.push(entity),
TableType::View => {
let Table { name, columns, .. } = Arc::try_unwrap(entity).unwrap();
views.push(
View {
materialized: false,
name,
columns,
}
.into(),
);
}
}
}

Ok(SqlERData {
tables,
foreign_keys,
enums,
views,
})
}
}
Loading