Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relationship labels #50

Merged
merged 7 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .go-version
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This can be helpful for users of goenv

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1.19.6
7 changes: 7 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const (
UseAllSchemasKey = "useAllSchemas"
ShowSchemaPrefix = "showSchemaPrefix"
SchemaPrefixSeparator = "schemaPrefixSeparator"
RelationshipLabelsKey = "relationshipLabels"
)

type config struct{}
Expand All @@ -38,6 +39,7 @@ type MermerdConfig interface {
UseAllSchemas() bool
ShowSchemaPrefix() bool
SchemaPrefixSeparator() string
RelationshipLabels() []RelationshipLabel
}

func NewConfig() MermerdConfig {
Expand Down Expand Up @@ -72,6 +74,11 @@ func (c config) SelectedTables() []string {
return viper.GetStringSlice(SelectedTablesKey)
}

func (c config) RelationshipLabels() []RelationshipLabel {
labels := viper.GetStringSlice(RelationshipLabelsKey)
return parseLabels(labels)
}

func (c config) EncloseWithMermaidBackticks() bool {
return viper.GetBool(EncloseWithMermaidBackticksKey)
}
Expand Down
22 changes: 21 additions & 1 deletion config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package config

import (
"bytes"
"testing"

"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"testing"
)

func TestYamlConfig(t *testing.T) {
Expand Down Expand Up @@ -32,6 +33,10 @@ showDescriptions:
- enumValues
- columnComments
- notNull
relationshipLabels:
- "schema.table1 schema.table2 : is_a"
- "table-name another-table-name : has_many"
- "incorrect format"
useAllSchemas: true
showSchemaPrefix: true
schemaPrefixSeparator: "_"
Expand Down Expand Up @@ -63,4 +68,19 @@ connectionStringSuggestions:
assert.True(t, config.UseAllSchemas())
assert.True(t, config.ShowSchemaPrefix())
assert.Equal(t, "_", config.SchemaPrefixSeparator())
assert.ElementsMatch(t,
config.RelationshipLabels(),
[]RelationshipLabel{
RelationshipLabel{
PkName: "schema.table1",
FkName: "schema.table2",
Label: "is_a",
},
RelationshipLabel{
PkName: "table-name",
FkName: "another-table-name",
Label: "has_many",
},
},
)
}
52 changes: 52 additions & 0 deletions config/relationship_label.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package config

import (
"errors"
"regexp"
"strings"

"github.com/sirupsen/logrus"
)

type RelationshipLabel struct {
PkName string
FkName string
Label string
}

func parseLabels(labels []string) []RelationshipLabel {
var relationshipLabels []RelationshipLabel
for _, label := range labels {
parsed, err := parseLabel(label)
if err != nil {
logrus.Warnf("label '%s' is not in the correct format", label)
continue
}
relationshipLabels = append(relationshipLabels, parsed)
}
return relationshipLabels
}

func parseLabel(label string) (RelationshipLabel, error) {
label = strings.Trim(label, " \t")
matched, groups := match(label)
if !matched {
return RelationshipLabel{}, errors.New("invalid relationship label")
}

return RelationshipLabel{
PkName: string(groups[1]),
FkName: string(groups[2]),
Label: string(groups[3]),
}, nil
}

var labelRegex = regexp.MustCompile(`([\w\._-]+)[\s]+([\w\._-]+)[\s]+:[\s]+([\w._-]+)`)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will add comments here for the details of the regex


func match(label string) (bool, [][]byte) {
groups := labelRegex.FindSubmatch([]byte(label))
if groups == nil {
return false, [][]byte{}
}
return true, groups
}
3 changes: 2 additions & 1 deletion diagram/diagram.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,13 @@ func (d diagram) Create(result *database.Result) error {
}

var constraints []ErdConstraintData
relationshipLabelMap := BuildRelationshipLabelMap(d.config)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Building the map is O(n) and we then get (essentially) O(1) lookup times when finding a constraint later vs doing a O(n) linear search each time

for _, constraint := range allConstraints {
if shouldSkipConstraint(d.config, tableData, constraint) {
continue
}

constraints = append(constraints, getConstraintData(d.config, constraint))
constraints = append(constraints, getConstraintData(d.config, relationshipLabelMap, constraint))
}

diagramData := ErdDiagramData{
Expand Down
15 changes: 11 additions & 4 deletions diagram/diagram_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package diagram

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

"github.com/sirupsen/logrus"

"github.com/KarnerTh/mermerd/config"
"github.com/KarnerTh/mermerd/database"
)
Expand Down Expand Up @@ -90,15 +91,21 @@ func shouldSkipConstraint(config config.MermerdConfig, tables []ErdTableData, co
return !(tableNameInSlice(tables, constraint.PkTable) && tableNameInSlice(tables, constraint.FkTable))
}

func getConstraintData(config config.MermerdConfig, constraint database.ConstraintResult) ErdConstraintData {
func getConstraintData(config config.MermerdConfig, labelMap RelationshipLabelMap, constraint database.ConstraintResult) ErdConstraintData {
pkTableName := getTableName(config, database.TableDetail{Schema: constraint.PkSchema, Name: constraint.PkTable})
fkTableName := getTableName(config, database.TableDetail{Schema: constraint.FkSchema, Name: constraint.FkTable})

constraintLabel := constraint.ColumnName
if config.OmitConstraintLabels() {
constraintLabel = ""
}
if relationshipLabel, found := labelMap.LookupRelationshipLabel(pkTableName, fkTableName); found {
constraintLabel = relationshipLabel.Label
}

return ErdConstraintData{
PkTableName: getTableName(config, database.TableDetail{Schema: constraint.PkSchema, Name: constraint.PkTable}),
FkTableName: getTableName(config, database.TableDetail{Schema: constraint.FkSchema, Name: constraint.FkTable}),
PkTableName: pkTableName,
FkTableName: fkTableName,
Relation: getRelation(constraint),
ConstraintLabel: constraintLabel,
}
Expand Down
70 changes: 69 additions & 1 deletion diagram/diagram_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/stretchr/testify/assert"

"github.com/KarnerTh/mermerd/config"
"github.com/KarnerTh/mermerd/database"
"github.com/KarnerTh/mermerd/mocks"
)
Expand Down Expand Up @@ -284,20 +285,87 @@ func TestShouldSkipConstraint(t *testing.T) {
}

func TestGetConstraintData(t *testing.T) {
t.Run("The column name is used as the constraint label", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitConstraintLabels").Return(false).Once()
configMock.On("ShowSchemaPrefix").Return(false).Twice()
constraint := database.ConstraintResult{ColumnName: "Column1"}

// Act
result := getConstraintData(&configMock, &relationshipLabelMap{}, constraint)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, result.ConstraintLabel, "Column1")
})
t.Run("OmitConstraintLabels should remove the constraint label", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitConstraintLabels").Return(true).Once()
configMock.On("ShowSchemaPrefix").Return(false).Twice()

constraint := database.ConstraintResult{ColumnName: "Column1"}

// Act
result := getConstraintData(&configMock, constraint)
result := getConstraintData(&configMock, &relationshipLabelMap{}, constraint)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, result.ConstraintLabel, "")
})
t.Run("If a relationship label exists, it should be used", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitConstraintLabels").Return(true).Once()
configMock.On("ShowSchemaPrefix").Return(false).Twice()

labelsMap := &relationshipLabelMap{}
labelsMap.AddRelationshipLabel(config.RelationshipLabel{
PkName: "pk",
FkName: "fk",
Label: "relationship-label",
})

constraint := database.ConstraintResult{
PkTable: "pk",
FkTable: "fk",
ColumnName: "Column1",
}

// Act
result := getConstraintData(&configMock, labelsMap, constraint)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, result.ConstraintLabel, "relationship-label")
})
t.Run("If a relationship label exists, it should be used even if we don't omit constraint labels", func(t *testing.T) {
// Arrange
configMock := mocks.MermerdConfig{}
configMock.On("OmitConstraintLabels").Return(false).Once()
configMock.On("ShowSchemaPrefix").Return(false).Twice()

labelsMap := &relationshipLabelMap{}
labelsMap.AddRelationshipLabel(config.RelationshipLabel{
PkName: "pk",
FkName: "fk",
Label: "relationship-label",
})

constraint := database.ConstraintResult{
PkTable: "pk",
FkTable: "fk",
ColumnName: "Column1",
}

// Act
result := getConstraintData(&configMock, labelsMap, constraint)

// Assert
configMock.AssertExpectations(t)
assert.Equal(t, result.ConstraintLabel, "relationship-label")
})
}

func TestGetTableName(t *testing.T) {
Expand Down
42 changes: 42 additions & 0 deletions diagram/relationship_label_map.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package diagram

import (
"fmt"

"github.com/KarnerTh/mermerd/config"
)

type RelationshipLabelMap interface {
AddRelationshipLabel(label config.RelationshipLabel)
LookupRelationshipLabel(pkName, fkName string) (label config.RelationshipLabel, found bool)
}

type relationshipLabelMap struct {
mapping map[string]config.RelationshipLabel
}

func (r *relationshipLabelMap) AddRelationshipLabel(label config.RelationshipLabel) {
if r.mapping == nil {
r.mapping = make(map[string]config.RelationshipLabel)
}
key := r.buildMapKey(label.PkName, label.FkName)
r.mapping[key] = label
}

func (r *relationshipLabelMap) LookupRelationshipLabel(pkName, fkName string) (label config.RelationshipLabel, found bool) {
key := r.buildMapKey(pkName, fkName)
label, found = r.mapping[key]
return
}

func (r *relationshipLabelMap) buildMapKey(pkName, fkName string) string {
return fmt.Sprintf("%s-%s", pkName, fkName)
}

func BuildRelationshipLabelMap(c config.MermerdConfig) RelationshipLabelMap {
labelMap := &relationshipLabelMap{}
for _, label := range c.RelationshipLabels() {
labelMap.AddRelationshipLabel(label)
}
return labelMap
}
2 changes: 2 additions & 0 deletions exampleRunConfig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,7 @@ showDescriptions:
- enumValues
- columnComments
- notNull
relationshipLabels:
- "public_article public_article_comment : has_many"
showSchemaPrefix: true
schemaPrefixSeparator: "_"
21 changes: 20 additions & 1 deletion mocks/MermerdConfig.go

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

7 changes: 7 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ via `mermerd -h`
--showSchemaPrefix show schema prefix in table name
--useAllSchemas use all available schemas
--useAllTables use all available tables
--relationshipLabels strings use a different label besides the column name for specific table relationships; overrides `omitConstraintLabels` if specified
```

If the flag `--showAllConstraints` is provided, mermerd will print out all constraints of the selected tables, even when
Expand Down Expand Up @@ -152,6 +153,12 @@ showDescriptions:
- notNull
showSchemaPrefix: true
schemaPrefixSeparator: "_"

# Names must match the pattern <schema><schema_prefix><table>
relationshipLabels:
? - public_table
- public_another-table
: label
```

## Example usages
Expand Down