Skip to content

Commit

Permalink
Support column comments (#32)
Browse files Browse the repository at this point in the history
* support column comments for mysql

* support column comments for postgres

* support column comments for mssql

* add tests for column comment support

* add support for combining enumValues and columnComments

* fix typo
  • Loading branch information
lnschroeder authored Apr 6, 2023
1 parent 02a6f59 commit 0a1eaf0
Show file tree
Hide file tree
Showing 14 changed files with 112 additions and 41 deletions.
4 changes: 2 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,13 @@ 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().Bool(config.ShowSchemaPrefix, false, "show schema prefix in table name")
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")
rootCmd.Flags().StringP(config.OutputFileNameKey, "o", "result.mmd", "output file name")
rootCmd.Flags().String(config.SchemaPrefixSeparator, ".", "the separator that should be used between schema and table name")
rootCmd.Flags().StringSlice(config.ShowDescriptionsKey, []string{""}, "show 'enumValues' and/or 'columnComments' in the description column")
rootCmd.Flags().StringSlice(config.SelectedTablesKey, []string{""}, "tables to include")

bindFlagToViper(config.ShowAllConstraintsKey)
Expand All @@ -89,7 +89,7 @@ func init() {
bindFlagToViper(config.SchemaKey)
bindFlagToViper(config.OutputFileNameKey)
bindFlagToViper(config.SelectedTablesKey)
bindFlagToViper(config.ShowEnumValuesKey)
bindFlagToViper(config.ShowDescriptionsKey)
bindFlagToViper(config.ShowSchemaPrefix)
bindFlagToViper(config.SchemaPrefixSeparator)
}
Expand Down
8 changes: 4 additions & 4 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const (
DebugKey = "debug"
OmitConstraintLabelsKey = "omitConstraintLabels"
OmitAttributeKeysKey = "omitAttributeKeys"
ShowEnumValuesKey = "showEnumValues"
ShowDescriptionsKey = "showDescriptions"
UseAllSchemasKey = "useAllSchemas"
ShowSchemaPrefix = "showSchemaPrefix"
SchemaPrefixSeparator = "schemaPrefixSeparator"
Expand All @@ -34,7 +34,7 @@ type MermerdConfig interface {
Debug() bool
OmitConstraintLabels() bool
OmitAttributeKeys() bool
ShowEnumValues() bool
ShowDescriptions() []string
UseAllSchemas() bool
ShowSchemaPrefix() bool
SchemaPrefixSeparator() string
Expand Down Expand Up @@ -88,8 +88,8 @@ func (c config) OmitAttributeKeys() bool {
return viper.GetBool(OmitAttributeKeysKey)
}

func (c config) ShowEnumValues() bool {
return viper.GetBool(ShowEnumValuesKey)
func (c config) ShowDescriptions() []string {
return viper.GetStringSlice(ShowDescriptionsKey)
}

func (c config) UseAllSchemas() bool {
Expand Down
6 changes: 4 additions & 2 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ encloseWithMermaidBackticks: false
debug: true
omitConstraintLabels: true
omitAttributeKeys: true
showEnumValues: true
showDescriptions:
- enumValues
- columnComments
useAllSchemas: true
showSchemaPrefix: true
schemaPrefixSeparator: "_"
Expand Down Expand Up @@ -56,7 +58,7 @@ connectionStringSuggestions:
assert.True(t, config.Debug())
assert.True(t, config.OmitConstraintLabels())
assert.True(t, config.OmitAttributeKeys())
assert.True(t, config.ShowEnumValues())
assert.ElementsMatch(t, []string{"enumValues", "columnComments"}, config.ShowDescriptions())
assert.True(t, config.UseAllSchemas())
assert.True(t, config.ShowSchemaPrefix())
assert.Equal(t, "_", config.SchemaPrefixSeparator())
Expand Down
8 changes: 6 additions & 2 deletions database/mssql.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,11 @@ func (c *mssqlConnector) GetColumns(tableName TableDetail) ([]ColumnResult, erro
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,
(select ISNULL(ep.value, '') from sys.tables t
inner join sys.columns col on col.object_id = t.object_id and col.name = c.column_name
left join sys.extended_properties ep on ep.major_id = t.object_id and ep.minor_id = col.column_id
where t.name = c.table_name and SCHEMA_NAME(t.schema_id) = c.TABLE_SCHEMA) as comment
from information_schema.columns c
where c.table_name = @p1 and c.TABLE_SCHEMA = @p2
order by c.ordinal_position;
Expand All @@ -113,7 +117,7 @@ func (c *mssqlConnector) GetColumns(tableName TableDetail) ([]ColumnResult, erro
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.Comment); err != nil {
return nil, err
}

Expand Down
5 changes: 3 additions & 2 deletions database/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ func (c *mySqlConnector) GetColumns(tableName TableDetail) ([]ColumnResult, erro
where cu.column_name = c.column_name
and cu.table_name = c.table_name
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
case when c.data_type = 'enum' then REPLACE(REPLACE(REPLACE(REPLACE(c.column_type, 'enum', ''), '\'', ''), '(', ''), ')', '') else '' end as enum_values,
c.column_comment as comment
from information_schema.columns c
where c.table_name = ? and c.TABLE_SCHEMA = ?
order by c.ordinal_position;
Expand All @@ -111,7 +112,7 @@ func (c *mySqlConnector) GetColumns(tableName TableDetail) ([]ColumnResult, erro
var columns []ColumnResult
for rows.Next() {
var column ColumnResult
if err = rows.Scan(&column.Name, &column.DataType, &column.IsPrimary, &column.IsForeign, &column.EnumValues); err != nil {
if err = rows.Scan(&column.Name, &column.DataType, &column.IsPrimary, &column.IsForeign, &column.EnumValues, &column.Comment); err != nil {
return nil, err
}

Expand Down
11 changes: 8 additions & 3 deletions database/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,21 @@ func (c *postgresConnector) GetColumns(tableName TableDetail) ([]ColumnResult, e
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
coalesce(string_agg(enumlabel, ',' order by enumsortorder), '') as enum_values,
coalesce(pd.description, '') as comment
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
left join pg_class cls on c.table_name = cls.relname
left join pg_namespace ns on cls.relnamespace = ns.oid
left join pg_description pd on cls.oid = pd.objoid and c.ordinal_position = pd.objsubid
where c.table_name = $1 and c.table_schema = $2
group by c.column_name,
c.table_name,
c.data_type,
c.udt_name,
c.ordinal_position
c.ordinal_position,
pd.description
order by c.ordinal_position;
`, tableName.Name, tableName.Schema)
if err != nil {
Expand All @@ -120,7 +125,7 @@ func (c *postgresConnector) GetColumns(tableName TableDetail) ([]ColumnResult, e
var columns []ColumnResult
for rows.Next() {
var column ColumnResult
if err = rows.Scan(&column.Name, &column.DataType, &column.IsPrimary, &column.IsForeign, &column.EnumValues); err != nil {
if err = rows.Scan(&column.Name, &column.DataType, &column.IsPrimary, &column.IsForeign, &column.EnumValues, &column.Comment); err != nil {
return nil, err
}

Expand Down
1 change: 1 addition & 0 deletions database/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type ColumnResult struct {
IsPrimary bool
IsForeign bool
EnumValues string
Comment string
}

type ConstraintResultList []ConstraintResult
Expand Down
2 changes: 1 addition & 1 deletion diagram/diagram_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type ErdTableData struct {
type ErdColumnData struct {
Name string
DataType string
EnumValues string
Description string
AttributeKey ErdAttributeKey
}

Expand Down
26 changes: 20 additions & 6 deletions diagram/diagram_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package diagram

import (
"fmt"
"github.com/sirupsen/logrus"
"strings"

"github.com/KarnerTh/mermerd/config"
"github.com/KarnerTh/mermerd/database"
Expand Down Expand Up @@ -43,19 +45,31 @@ func getColumnData(config config.MermerdConfig, column database.ColumnResult) Er
attributeKey = none
}

var enumValues string
if config.ShowEnumValues() {
enumValues = column.EnumValues
}

return ErdColumnData{
Name: column.Name,
DataType: column.DataType,
EnumValues: enumValues,
Description: getDescription(config.ShowDescriptions(), column),
AttributeKey: attributeKey,
}
}

func getDescription(options []string, column database.ColumnResult) string {
var description []string
for _, option := range options {
switch option {
case "enumValues":
if column.EnumValues != "" {
description = append(description, "<"+column.EnumValues+">")
}
case "columnComments":
description = append(description, column.Comment)
default:
logrus.Errorf("Could not parse option %q", option)
}
}
return strings.TrimSpace(strings.Join(description, " "))
}

func shouldSkipConstraint(config config.MermerdConfig, tables []ErdTableData, constraint database.ConstraintResult) bool {
if config.ShowAllConstraints() {
return false
Expand Down
52 changes: 43 additions & 9 deletions diagram/diagram_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,73 +128,107 @@ func TestTableNameInSlice(t *testing.T) {
func TestGetColumnData(t *testing.T) {
columnName := "testColumn"
enumValues := "a,b"
comment := "comment"
column := database.ColumnResult{
Name: columnName,
IsPrimary: true,
EnumValues: enumValues,
Comment: comment,
}

t.Run("Get all fields", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitAttributeKeys").Return(false).Once()
configMock.On("ShowEnumValues").Return(true).Once()
configMock.On("ShowDescriptions").Return([]string{"enumValues", "columnComments"}).Once()

// Act
result := getColumnData(&configMock, column)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, columnName, result.Name)
assert.Equal(t, enumValues, result.EnumValues)
assert.Equal(t, "<"+enumValues+"> "+comment, result.Description)
assert.Equal(t, primaryKey, result.AttributeKey)
})

t.Run("Get all fields except enum values", func(t *testing.T) {
t.Run("Get all fields with enum values", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitAttributeKeys").Return(false).Once()
configMock.On("ShowEnumValues").Return(false).Once()
configMock.On("ShowDescriptions").Return([]string{"enumValues"}).Once()

// Act
result := getColumnData(&configMock, column)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, columnName, result.Name)
assert.Equal(t, "", result.EnumValues)
assert.Equal(t, "<"+enumValues+">", result.Description)
assert.Equal(t, primaryKey, result.AttributeKey)
})

t.Run("Get all fields with column comments", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitAttributeKeys").Return(false).Once()
configMock.On("ShowDescriptions").Return([]string{"columnComments"}).Once()

// Act
result := getColumnData(&configMock, column)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, columnName, result.Name)
assert.Equal(t, comment, result.Description)
assert.Equal(t, primaryKey, result.AttributeKey)
})

t.Run("Get all fields except description", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitAttributeKeys").Return(false).Once()
configMock.On("ShowDescriptions").Return([]string{""}).Once()

// Act
result := getColumnData(&configMock, column)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, columnName, result.Name)
assert.Equal(t, "", result.Description)
assert.Equal(t, primaryKey, result.AttributeKey)
})

t.Run("Get all fields except attribute key", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitAttributeKeys").Return(true).Once()
configMock.On("ShowEnumValues").Return(true).Once()
configMock.On("ShowDescriptions").Return([]string{"enumValues", "columnComments"}).Once()

// Act
result := getColumnData(&configMock, column)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, columnName, result.Name)
assert.Equal(t, enumValues, result.EnumValues)
assert.Equal(t, "<"+enumValues+"> "+comment, result.Description)
assert.Equal(t, none, result.AttributeKey)
})

t.Run("Get only minimal fields", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitAttributeKeys").Return(true).Once()
configMock.On("ShowEnumValues").Return(false).Once()
configMock.On("ShowDescriptions").Return([]string{""}).Once()

// Act
result := getColumnData(&configMock, column)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, columnName, result.Name)
assert.Equal(t, "", result.EnumValues)
assert.Equal(t, "", result.Description)
assert.Equal(t, none, result.AttributeKey)
})
}
Expand Down
2 changes: 1 addition & 1 deletion diagram/erd_template.gommd
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ erDiagram
{{- range .Tables}}
{{.Name}} {
{{- range .Columns}}
{{.DataType}} {{.Name}} {{.AttributeKey}} {{- if .EnumValues}}"{{.EnumValues}}"{{end -}}
{{.DataType}} {{.Name}} {{.AttributeKey}} {{- if .Description}}"{{.Description}}"{{end -}}
{{- end}}
}
{{end -}}
Expand Down
4 changes: 3 additions & 1 deletion exampleRunConfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ encloseWithMermaidBackticks: false
debug: false
omitConstraintLabels: false
omitAttributeKeys: false
showEnumValues: false
showDescriptions:
- enumValues
- columnComments
showSchemaPrefix: true
schemaPrefixSeparator: "_"
12 changes: 7 additions & 5 deletions mocks/MermerdConfig.go

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

Loading

0 comments on commit 0a1eaf0

Please sign in to comment.