diff --git a/Makefile b/Makefile index 00a7510..9cf19b3 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,10 @@ test-all: test-unit: go test --short $(test_target) -cover -json | tparse -all +.PHONY: test-cleanup +test-cleanup: + go clean -testcache + .PHONY: publish-package publish-package: - GOPROXY=proxy.golang.org go list -m github.com/KarnerTh/mermerd@$(GIT_TAG) \ No newline at end of file + GOPROXY=proxy.golang.org go list -m github.com/KarnerTh/mermerd@$(GIT_TAG) diff --git a/changelog.md b/changelog.md index aced84a..b28e3c5 100644 --- a/changelog.md +++ b/changelog.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (after version 0.0.5). +## [0.5.0] - 2022-12-xx +### Added +- Support enum description ([Issue #15](https://github.com/KarnerTh/mermerd/issues/15)) + ## [0.4.1] - 2022-09-28 ### Fixed - Fix wrong column format for `is_primary` ([Issue #24](https://github.com/KarnerTh/mermerd/issues/24)) @@ -95,6 +99,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (after version 0.0 ### Added - Initial release of mermerd +[0.5.0]: https://github.com/KarnerTh/mermerd/releases/tag/v0.5.0 + [0.4.1]: https://github.com/KarnerTh/mermerd/releases/tag/v0.4.1 [0.4.0]: https://github.com/KarnerTh/mermerd/releases/tag/v0.4.0 diff --git a/cmd/root.go b/cmd/root.go index 45285a2..d60400a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -68,6 +68,7 @@ func init() { rootCmd.Flags().Bool(config.DebugKey, false, "show debug logs") rootCmd.Flags().Bool(config.OmitConstraintLabelsKey, false, "omit the constraint labels") rootCmd.Flags().Bool(config.OmitAttributeKeysKey, false, "omit the attribute keys (PK, FK)") + rootCmd.Flags().Bool(config.ShowEnumValuesKey, false, "show enum values in description column") rootCmd.Flags().BoolP(config.EncloseWithMermaidBackticksKey, "e", false, "enclose output with mermaid backticks (needed for e.g. in markdown viewer)") rootCmd.Flags().StringP(config.ConnectionStringKey, "c", "", "connection string that should be used") rootCmd.Flags().StringP(config.SchemaKey, "s", "", "schema that should be used") @@ -84,7 +85,7 @@ func init() { bindFlagToViper(config.SchemaKey) bindFlagToViper(config.OutputFileNameKey) bindFlagToViper(config.SelectedTablesKey) - + bindFlagToViper(config.ShowEnumValuesKey) } func bindFlagToViper(key string) { diff --git a/config/config.go b/config/config.go index ae83d16..b7a9bac 100644 --- a/config/config.go +++ b/config/config.go @@ -14,6 +14,7 @@ const ( DebugKey = "debug" OmitConstraintLabelsKey = "omitConstraintLabels" OmitAttributeKeysKey = "omitAttributeKeys" + ShowEnumValuesKey = "showEnumValues" ) type config struct{} @@ -30,6 +31,7 @@ type MermerdConfig interface { Debug() bool OmitConstraintLabels() bool OmitAttributeKeys() bool + ShowEnumValues() bool } func NewConfig() MermerdConfig { @@ -79,3 +81,7 @@ func (c config) OmitConstraintLabels() bool { func (c config) OmitAttributeKeys() bool { return viper.GetBool(OmitAttributeKeysKey) } + +func (c config) ShowEnumValues() bool { + return viper.GetBool(ShowEnumValuesKey) +} diff --git a/database/database_integration_test.go b/database/database_integration_test.go index 1b4612c..46ebda9 100644 --- a/database/database_integration_test.go +++ b/database/database_integration_test.go @@ -14,6 +14,17 @@ type columnTestResult struct { isForeign bool } +type connectionParameter struct { + connectionString string + schema string +} + +var ( + testConnectionPostgres connectionParameter = connectionParameter{connectionString: "postgresql://user:password@localhost:5432/mermerd_test", schema: "public"} + testConnectionMySql connectionParameter = connectionParameter{connectionString: "mysql://user:password@tcp(127.0.0.1:3306)/mermerd_test", schema: "mermerd_test"} + testConnectionMsSql connectionParameter = connectionParameter{connectionString: "sqlserver://sa:securePassword1!@localhost:1433?database=mermerd_test", schema: "dbo"} +) + func TestDatabaseIntegrations(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") @@ -27,18 +38,18 @@ func TestDatabaseIntegrations(t *testing.T) { }{ { dbType: Postgres, - connectionString: "postgresql://user:password@localhost:5432/mermerd_test", - schema: "public", + connectionString: testConnectionPostgres.connectionString, + schema: testConnectionPostgres.schema, }, { dbType: MySql, - connectionString: "mysql://user:password@tcp(127.0.0.1:3306)/mermerd_test", - schema: "mermerd_test", + connectionString: testConnectionMySql.connectionString, + schema: testConnectionMySql.schema, }, { dbType: MsSql, - connectionString: "sqlserver://sa:securePassword1!@localhost:1433?database=mermerd_test", - schema: "dbo", + connectionString: testConnectionMsSql.connectionString, + schema: testConnectionMsSql.schema, }, } @@ -95,6 +106,7 @@ func TestDatabaseIntegrations(t *testing.T) { "article_label", "test_1_a", "test_1_b", + "test_2_enum", } assert.Nil(t, err) assert.ElementsMatch(t, expectedResult, tables) diff --git a/database/mysql.go b/database/mysql.go index a6e7087..f1cdb91 100644 --- a/database/mysql.go +++ b/database/mysql.go @@ -91,7 +91,8 @@ func (c *mySqlConnector) GetColumns(tableName string) ([]ColumnResult, error) { left join information_schema.table_constraints tc on tc.constraint_name = cu.constraint_name where cu.column_name = c.column_name and cu.table_name = c.table_name - and tc.constraint_type = 'FOREIGN KEY') as is_foreign + and tc.constraint_type = 'FOREIGN KEY') as is_foreign, + case when c.data_type = 'enum' then REPLACE(REPLACE(REPLACE(REPLACE(c.column_type, 'enum', ''), '\'', ''), '(', ''), ')', '') else '' end as enum_values from information_schema.columns c where c.table_name = ? order by c.ordinal_position; @@ -103,7 +104,7 @@ func (c *mySqlConnector) GetColumns(tableName string) ([]ColumnResult, error) { var columns []ColumnResult for rows.Next() { var column ColumnResult - if err = rows.Scan(&column.Name, &column.DataType, &column.IsPrimary, &column.IsForeign); err != nil { + if err = rows.Scan(&column.Name, &column.DataType, &column.IsPrimary, &column.IsForeign, &column.EnumValues); err != nil { return nil, err } diff --git a/database/mysql_test.go b/database/mysql_test.go new file mode 100644 index 0000000..8ffce22 --- /dev/null +++ b/database/mysql_test.go @@ -0,0 +1,35 @@ +package database + +import ( + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestMysqlEnums(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + // Arrange + var enumValues string + + // Act + connector, _ := NewConnectorFactory().NewConnector(testConnectionMySql.connectionString) + if err := connector.Connect(); err != nil { + logrus.Error(err) + t.FailNow() + } + columns, err := connector.GetColumns("test_2_enum") + + // Assert + for _, column := range columns { + if column.Name == "fruit" { + enumValues = column.EnumValues + } + } + + assert.Nil(t, err) + assert.Equal(t, "apple,banana", enumValues) +} diff --git a/database/postgres.go b/database/postgres.go index 88fb885..6bc5f14 100644 --- a/database/postgres.go +++ b/database/postgres.go @@ -79,23 +79,35 @@ func (c *postgresConnector) GetTables(schemaName string) ([]string, error) { func (c *postgresConnector) GetColumns(tableName string) ([]ColumnResult, error) { rows, err := c.db.Query(` - select c.column_name, - c.data_type, - (select count(*) > 0 - from information_schema.key_column_usage cu - left join information_schema.table_constraints tc on tc.constraint_name = cu.constraint_name - where cu.column_name = c.column_name - and cu.table_name = c.table_name - and tc.constraint_type = 'PRIMARY KEY') as is_primary, - (select count(*) > 0 - from information_schema.key_column_usage cu - left join information_schema.table_constraints tc on tc.constraint_name = cu.constraint_name - where cu.column_name = c.column_name - and cu.table_name = c.table_name - and tc.constraint_type = 'FOREIGN KEY') as is_foreign - from information_schema.columns c - where c.table_name = $1 - order by c.ordinal_position; + select c.column_name, + (case + when c.data_type = 'USER-DEFINED' + then c.udt_name + else c.data_type + end) as data_type, + (select count(*) > 0 + from information_schema.key_column_usage cu + left join information_schema.table_constraints tc on tc.constraint_name = cu.constraint_name + where cu.column_name = c.column_name + and cu.table_name = c.table_name + and tc.constraint_type = 'PRIMARY KEY') as is_primary, + (select count(*) > 0 + from information_schema.key_column_usage cu + left join information_schema.table_constraints tc on tc.constraint_name = cu.constraint_name + where cu.column_name = c.column_name + and cu.table_name = c.table_name + and tc.constraint_type = 'FOREIGN KEY') as is_foreign, + coalesce(string_agg(enumlabel, ',' order by enumsortorder), '') as enum_values + from information_schema.columns c + left join pg_type typ on c.udt_name = typ.typname + left join pg_enum enu on typ.oid = enu.enumtypid + where c.table_name = $1 + group by c.column_name, + c.table_name, + c.data_type, + c.udt_name, + c.ordinal_position + order by c.ordinal_position; `, tableName) if err != nil { return nil, err @@ -104,7 +116,7 @@ func (c *postgresConnector) GetColumns(tableName string) ([]ColumnResult, error) var columns []ColumnResult for rows.Next() { var column ColumnResult - if err = rows.Scan(&column.Name, &column.DataType, &column.IsPrimary, &column.IsForeign); err != nil { + if err = rows.Scan(&column.Name, &column.DataType, &column.IsPrimary, &column.IsForeign, &column.EnumValues); err != nil { return nil, err } diff --git a/database/postgres_test.go b/database/postgres_test.go new file mode 100644 index 0000000..30f1d82 --- /dev/null +++ b/database/postgres_test.go @@ -0,0 +1,35 @@ +package database + +import ( + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestPostgresEnums(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + // Arrange + var enumValues string + + // Act + connector, _ := NewConnectorFactory().NewConnector(testConnectionPostgres.connectionString) + if err := connector.Connect(); err != nil { + logrus.Error(err) + t.FailNow() + } + columns, err := connector.GetColumns("test_2_enum") + + // Assert + for _, column := range columns { + if column.Name == "fruit" { + enumValues = column.EnumValues + } + } + + assert.Nil(t, err) + assert.Equal(t, "apple,banana", enumValues) +} diff --git a/database/result.go b/database/result.go index c693f12..6db1b7d 100644 --- a/database/result.go +++ b/database/result.go @@ -11,10 +11,11 @@ type TableResult struct { } type ColumnResult struct { - Name string - DataType string - IsPrimary bool - IsForeign bool + Name string + DataType string + IsPrimary bool + IsForeign bool + EnumValues string } type ConstraintResultList []ConstraintResult diff --git a/diagram/diagram.go b/diagram/diagram.go index 12c8ac2..a7ad97d 100644 --- a/diagram/diagram.go +++ b/diagram/diagram.go @@ -54,9 +54,15 @@ func (d diagram) Create(result *database.Result) error { attributeKey = none } + var enumValues string + if d.config.ShowEnumValues() { + enumValues = column.EnumValues + } + columnData[columnIndex] = ErdColumnData{ Name: column.Name, DataType: column.DataType, + EnumValues: enumValues, AttributeKey: attributeKey, } } diff --git a/diagram/diagram_data.go b/diagram/diagram_data.go index 96422af..847800c 100644 --- a/diagram/diagram_data.go +++ b/diagram/diagram_data.go @@ -29,6 +29,7 @@ type ErdTableData struct { type ErdColumnData struct { Name string DataType string + EnumValues string AttributeKey ErdAttributeKey } diff --git a/diagram/erd_template.gommd b/diagram/erd_template.gommd index 79499b2..0ca9ca7 100644 --- a/diagram/erd_template.gommd +++ b/diagram/erd_template.gommd @@ -3,7 +3,7 @@ erDiagram {{- range .Tables}} {{.Name}} { {{- range .Columns}} - {{.DataType}} {{.Name}} {{.AttributeKey}} + {{.DataType}} {{.Name}} {{.AttributeKey}} {{- if .EnumValues}}"{{.EnumValues}}"{{end -}} {{- end}} } {{end -}} diff --git a/exampleRunConfig.yaml b/exampleRunConfig.yaml index 2ac10e7..aff978e 100644 --- a/exampleRunConfig.yaml +++ b/exampleRunConfig.yaml @@ -15,3 +15,4 @@ encloseWithMermaidBackticks: false debug: false omitConstraintLabels: false omitAttributeKeys: false +showEnumValues: false diff --git a/mocks/MermerdConfig.go b/mocks/MermerdConfig.go index c222846..541560b 100644 --- a/mocks/MermerdConfig.go +++ b/mocks/MermerdConfig.go @@ -153,6 +153,20 @@ func (_m *MermerdConfig) ShowAllConstraints() bool { return r0 } +// ShowEnumValues provides a mock function with given fields: +func (_m *MermerdConfig) ShowEnumValues() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + // UseAllTables provides a mock function with given fields: func (_m *MermerdConfig) UseAllTables() bool { ret := _m.Called() diff --git a/readme.md b/readme.md index f101d95..5f20f3b 100644 --- a/readme.md +++ b/readme.md @@ -43,6 +43,7 @@ for your operating system. To be able to use it globally on your system, add the * Use it in CI/CD pipeline via a run configuration * Either generate plain mermaid syntax or enclose it with mermaid backticks to use directly in e.g. GitHub markdown * Show primary and foreign keys +* Show enum values of enum column ## Why would I need it / Why should I care? @@ -78,6 +79,7 @@ via `mermerd -h` -s, --schema string schema that should be used --selectedTables strings tables to include --showAllConstraints show all constraints, even though the table of the resulting constraint was not selected + --showEnumValues show enum values in description column --useAllTables use all available tables ``` @@ -98,6 +100,7 @@ outputFileName: "my-db.mmd" debug: false omitConstraintLabels: false omitAttributeKeys: false +showEnumValues: false # These connection strings are available as suggestions in the cli (use tab to access) connectionStringSuggestions: @@ -130,6 +133,7 @@ outputFileName: "my-db.mmd" debug: true omitConstraintLabels: true omitAttributeKeys: true +showEnumValues: true ``` ## Example usages diff --git a/test/db-table-setup.sql b/test/db-table-setup.sql index 57806c3..47183c0 100644 --- a/test/db-table-setup.sql +++ b/test/db-table-setup.sql @@ -46,17 +46,18 @@ alter table article_label add primary key (article_id, label_id); -- Test case for https://github.com/KarnerTh/mermerd/issues/8 -CREATE TABLE test_1_a +create table test_1_a ( id int, xid int, - PRIMARY KEY (id, xid) + primary key (id, xid) ); -CREATE TABLE test_1_b +create table test_1_b ( aid int, bid int, - PRIMARY KEY (aid, bid), - FOREIGN KEY (aid, bid) REFERENCES test_1_a (id, xid) -); \ No newline at end of file + primary key (aid, bid), + foreign key (aid, bid) references test_1_a (id, xid) +); + diff --git a/test/docker-compose.yaml b/test/docker-compose.yaml index 8d4e998..9540efe 100644 --- a/test/docker-compose.yaml +++ b/test/docker-compose.yaml @@ -10,6 +10,7 @@ services: - "5432:5432" volumes: - ./db-table-setup.sql:/docker-entrypoint-initdb.d/1.sql + - ./postgres/postgres-enum-setup.sql:/docker-entrypoint-initdb.d/2.sql mermerd-mysql-test-db: image: mysql:8.0 command: --default-authentication-plugin=mysql_native_password @@ -22,6 +23,7 @@ services: - "3306:3306" volumes: - ./db-table-setup.sql:/docker-entrypoint-initdb.d/1.sql + - ./mysql/mysql-enum-setup.sql:/docker-entrypoint-initdb.d/2.sql mermerd-mssql-test-db: image: mcr.microsoft.com/mssql/server:2019-latest environment: @@ -32,6 +34,7 @@ services: volumes: - ./db-table-setup.sql:/usr/src/app/db-table-setup.sql - ./mssql/mssql-setup.sql:/usr/src/app/mssql-setup.sql + - ./mssql/mssql-enum-setup.sql:/usr/src/app/mssql-enum-setup.sql - ./mssql/entrypoint.sh:/usr/src/app/entrypoint.sh working_dir: /usr/src/app - command: sh -c './entrypoint.sh & /opt/mssql/bin/sqlservr;' \ No newline at end of file + command: sh -c './entrypoint.sh & /opt/mssql/bin/sqlservr;' diff --git a/test/mssql/entrypoint.sh b/test/mssql/entrypoint.sh index 938117f..ea3d11b 100755 --- a/test/mssql/entrypoint.sh +++ b/test/mssql/entrypoint.sh @@ -11,5 +11,6 @@ echo importing data... # run the sql scripts to create the test database /opt/mssql-tools/bin/sqlcmd -S 0.0.0.0 -U sa -P $password -i ./mssql-setup.sql /opt/mssql-tools/bin/sqlcmd -S 0.0.0.0 -U sa -P $password -d mermerd_test -i ./db-table-setup.sql +/opt/mssql-tools/bin/sqlcmd -S 0.0.0.0 -U sa -P $password -d mermerd_test -i ./mssql-enum-setup.sql -echo importing done \ No newline at end of file +echo importing done diff --git a/test/mssql/mssql-enum-setup.sql b/test/mssql/mssql-enum-setup.sql new file mode 100644 index 0000000..38b2689 --- /dev/null +++ b/test/mssql/mssql-enum-setup.sql @@ -0,0 +1,7 @@ +-- Test case for https://github.com/KarnerTh/mermerd/issues/15 +-- due to the fact that mssql does not support enums, we use the closest possible solution +-- https://stackoverflow.com/a/1434338 +create table test_2_enum( + fruit varchar(10) not null check (fruit in('apple', 'banana')) +) + diff --git a/test/mysql/mysql-enum-setup.sql b/test/mysql/mysql-enum-setup.sql new file mode 100644 index 0000000..f959fc5 --- /dev/null +++ b/test/mysql/mysql-enum-setup.sql @@ -0,0 +1,4 @@ +-- Test case for https://github.com/KarnerTh/mermerd/issues/15 +create table test_2_enum( + fruit enum ('apple', 'banana') +) diff --git a/test/postgres/postgres-enum-setup.sql b/test/postgres/postgres-enum-setup.sql new file mode 100644 index 0000000..718bcf3 --- /dev/null +++ b/test/postgres/postgres-enum-setup.sql @@ -0,0 +1,6 @@ +create type FruitEnum as enum('apple', 'banana'); + +create table test_2_enum( + fruit FruitEnum +) +