Skip to content

Commit

Permalink
Sub query returns multiple entries (#9)
Browse files Browse the repository at this point in the history
* added debug flag and log messages

* fix subquery issue for postgres; show column name in constraint

* add test case for multiple pks

* added new tables to get columns integration test

* fix subquery for mysql

* add flag for omitting the constraint labels

* cleanup; updated changelog for new version
  • Loading branch information
KarnerTh authored Jul 1, 2022
1 parent 7dcfbc9 commit 4f50b47
Show file tree
Hide file tree
Showing 18 changed files with 203 additions and 45 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
GIT_TAG := $(shell git describe --tags `git rev-list --tags --max-count=1`)
GIT_TAG := $(shell git describe --tags --abbrev=0)
test_target := "./..."

.PHONY: test-coverage
Expand Down
25 changes: 22 additions & 3 deletions analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package analyzer

import (
"errors"

"github.com/sirupsen/logrus"

"github.com/KarnerTh/mermerd/config"
"github.com/KarnerTh/mermerd/database"
"github.com/KarnerTh/mermerd/util"
Expand Down Expand Up @@ -81,11 +82,13 @@ func (a analyzer) GetSchema(db database.Connector) (string, error) {
a.loadingSpinner.Start("Getting schemas")
schemas, err := db.GetSchemas()
a.loadingSpinner.Stop()

if err != nil {
logrus.Error("Getting schemas failed", " | ", err)
return "", err
}

logrus.WithField("count", len(schemas)).Info("Got schemas")

switch len(schemas) {
case 0:
return "", errors.New("no schemas available")
Expand All @@ -103,10 +106,13 @@ func (a analyzer) GetTables(db database.Connector, selectedSchema string) ([]str

a.loadingSpinner.Start("Getting tables")
tables, err := db.GetTables(selectedSchema)
a.loadingSpinner.Stop()
if err != nil {
logrus.Error("Getting tables failed", " | ", err)
return nil, err
}
a.loadingSpinner.Stop()

logrus.WithField("count", len(tables)).Info("Got tables")

if a.config.UseAllTables() {
return tables, nil
Expand All @@ -121,16 +127,29 @@ func (a analyzer) GetColumnsAndConstraints(db database.Connector, selectedTables
for _, table := range selectedTables {
columns, err := db.GetColumns(table)
if err != nil {
logrus.Error("Getting columns failed", " | ", err)
return nil, err
}

constraints, err := db.GetConstraints(table)
if err != nil {
logrus.Error("Getting constraints failed", " | ", err)
return nil, err
}

tableResults = append(tableResults, database.TableResult{TableName: table, Columns: columns, Constraints: constraints})
}
a.loadingSpinner.Stop()
columnCount, constraintCount := getTableResultStats(tableResults)
logrus.WithFields(logrus.Fields{"columns": columnCount, "constraints": constraintCount}).Info("Got columns and constraints constraints")
return tableResults, nil
}

func getTableResultStats(tableResults []database.TableResult) (columnCount int, constraintCount int) {
for _, tableResult := range tableResults {
columnCount += len(tableResult.Columns)
constraintCount += len(tableResult.Constraints)
}

return columnCount, constraintCount
}
13 changes: 13 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ 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.2.0] - 2022-06-01
### Added
- A `--debug` flag/config to show debug information
- A `--omitConstraintLabels` flag/config to toggle the new constraint labels

### Changed
- The column name is now displayed as the constraint label (can be switched off)

### Fixed
- Sub query for constraints returned multiple items ([Issue #8](https://github.com/KarnerTh/mermerd/issues/8))

## [0.1.0] - 2022-04-15
### Added
- Mermerd is available via the go tools
Expand Down Expand Up @@ -55,6 +66,8 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (after version 0.0
### Added
- Initial release of mermerd

[0.2.0]: https://github.com/KarnerTh/mermerd/releases/tag/v0.2.0

[0.1.0]: https://github.com/KarnerTh/mermerd/releases/tag/v0.1.0

[0.0.5]: https://github.com/KarnerTh/mermerd/releases/tag/v0.0.5
Expand Down
12 changes: 10 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package cmd

import (
"fmt"
"io/ioutil"
"os"

"github.com/fatih/color"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"

Expand All @@ -29,16 +31,18 @@ var rootCmd = &cobra.Command{
analyzer := analyzer.NewAnalyzer(config, connectorFactory, questioner)
diagram := diagram.NewDiagram(config)

if !config.Debug() {
logrus.SetOutput(ioutil.Discard)
}

result, err := analyzer.Analyze()
if err != nil {
fmt.Println(err.Error())
util.ShowError()
os.Exit(1)
}

err = diagram.Create(result)
if err != nil {
fmt.Println(err.Error())
util.ShowError()
os.Exit(1)
}
Expand All @@ -59,13 +63,17 @@ func init() {
rootCmd.Flags().StringVar(&runConfig, "runConfig", "", "run configuration (replaces global configuration)")
rootCmd.Flags().Bool(config.ShowAllConstraintsKey, false, "show all constraints, even though the table of the resulting constraint was not selected")
rootCmd.Flags().Bool(config.UseAllTablesKey, false, "use all available tables")
rootCmd.Flags().Bool(config.DebugKey, false, "show debug logs")
rootCmd.Flags().Bool(config.OmitConstraintLabelsKey, false, "omit the constraint labels")
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")

bindFlagToViper(config.ShowAllConstraintsKey)
bindFlagToViper(config.UseAllTablesKey)
bindFlagToViper(config.DebugKey)
bindFlagToViper(config.OmitConstraintLabelsKey)
bindFlagToViper(config.EncloseWithMermaidBackticksKey)
bindFlagToViper(config.ConnectionStringKey)
bindFlagToViper(config.SchemaKey)
Expand Down
12 changes: 12 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ const (
ConnectionStringSuggestionsKey = "connectionStringSuggestions"
OutputFileNameKey = "outputFileName"
EncloseWithMermaidBackticksKey = "encloseWithMermaidBackticks"
DebugKey = "debug"
OmitConstraintLabelsKey = "omitConstraintLabels"
)

type config struct{}
Expand All @@ -24,6 +26,8 @@ type MermerdConfig interface {
ConnectionStringSuggestions() []string
SelectedTables() []string
EncloseWithMermaidBackticks() bool
Debug() bool
OmitConstraintLabels() bool
}

func NewConfig() MermerdConfig {
Expand Down Expand Up @@ -61,3 +65,11 @@ func (c config) SelectedTables() []string {
func (c config) EncloseWithMermaidBackticks() bool {
return viper.GetBool(EncloseWithMermaidBackticksKey)
}

func (c config) Debug() bool {
return viper.GetBool(DebugKey)
}

func (c config) OmitConstraintLabels() bool {
return viper.GetBool(OmitConstraintLabelsKey)
}
33 changes: 32 additions & 1 deletion database/database_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,15 @@ func TestDatabaseIntegrations(t *testing.T) {
tables, err := connector.GetTables(schema)

// Assert
expectedResult := []string{"article", "article_detail", "article_comment", "label", "article_label"}
expectedResult := []string{
"article",
"article_detail",
"article_comment",
"label",
"article_label",
"test_1_a",
"test_1_b",
}
assert.Nil(t, err)
assert.ElementsMatch(t, expectedResult, tables)
})
Expand All @@ -87,6 +95,8 @@ func TestDatabaseIntegrations(t *testing.T) {
{tableName: "article_comment", expectedColumns: []string{"id", "article_id", "comment"}},
{tableName: "label", expectedColumns: []string{"id", "label"}},
{tableName: "article_label", expectedColumns: []string{"article_id", "label_id"}},
{tableName: "test_1_a", expectedColumns: []string{"id", "xid"}},
{tableName: "test_1_b", expectedColumns: []string{"aid", "bid"}},
}

for index, testCase := range testCases {
Expand Down Expand Up @@ -156,12 +166,33 @@ func TestDatabaseIntegrations(t *testing.T) {
for _, item := range constraintResults {
if item.FkTable == fkTableName {
constraint = &item
break
}
}
assert.NotNil(t, constraint)
assert.True(t, constraint.IsPrimary)
assert.True(t, constraint.HasMultiplePK)
})

// Multiple primary keys (https://github.com/KarnerTh/mermerd/issues/8)
t.Run("Test 1 (Issue #8)", func(t *testing.T) {
// Arrange
pkTableName := "test_1_b"

// Act
constraintResults, err := connector.GetConstraints(pkTableName)

// Assert
assert.Nil(t, err)
assert.NotNil(t, constraintResults)
assert.Len(t, constraintResults, 2)
assert.True(t, constraintResults[0].IsPrimary)
assert.True(t, constraintResults[0].HasMultiplePK)
assert.Equal(t, constraintResults[0].ColumnName, "aid")
assert.True(t, constraintResults[1].IsPrimary)
assert.True(t, constraintResults[1].HasMultiplePK)
assert.Equal(t, constraintResults[1].ColumnName, "bid")
})
})
})
}
Expand Down
8 changes: 6 additions & 2 deletions database/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,14 @@ func (c mySqlConnector) GetConstraints(tableName string) ([]ConstraintResult, er
select c.TABLE_NAME,
c.REFERENCED_TABLE_NAME,
c.CONSTRAINT_NAME,
kcu.COLUMN_NAME,
(
select kc2.CONSTRAINT_NAME is not null "isPrimary"
from information_schema.KEY_COLUMN_USAGE kc
left join information_schema.KEY_COLUMN_USAGE kc2
ON kc.COLUMN_NAME = kc2.COLUMN_NAME AND kc2.CONSTRAINT_NAME = 'PRIMARY' AND
kc2.TABLE_NAME = kc.TABLE_NAME
where kc.CONSTRAINT_NAME = c.CONSTRAINT_NAME
where kc.CONSTRAINT_NAME = c.CONSTRAINT_NAME and kc.COLUMN_NAME = kcu.COLUMN_NAME
) "isPrimary",
(
select COUNT(*) > 1
Expand All @@ -124,6 +125,7 @@ func (c mySqlConnector) GetConstraints(tableName string) ([]ConstraintResult, er
and kc.CONSTRAINT_NAME = 'PRIMARY'
) "hasMultiplePk"
from information_schema.REFERENTIAL_CONSTRAINTS c
inner join information_schema.KEY_COLUMN_USAGE kcu on c.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
where c.TABLE_NAME = ? or c.REFERENCED_TABLE_NAME = ?
`, tableName, tableName)
if err != nil {
Expand All @@ -133,9 +135,11 @@ func (c mySqlConnector) GetConstraints(tableName string) ([]ConstraintResult, er
var constraints []ConstraintResult
for rows.Next() {
var constraint ConstraintResult
err = rows.Scan(&constraint.FkTable,
err = rows.Scan(
&constraint.FkTable,
&constraint.PkTable,
&constraint.ConstraintName,
&constraint.ColumnName,
&constraint.IsPrimary,
&constraint.HasMultiplePK,
)
Expand Down
54 changes: 28 additions & 26 deletions database/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,32 +106,33 @@ func (c postgresConnector) GetColumns(tableName string) ([]ColumnResult, error)

func (c postgresConnector) GetConstraints(tableName string) ([]ConstraintResult, error) {
rows, err := c.db.Query(`
select distinct fk.table_name,
pk.table_name,
c.constraint_name,
coalesce(
(select tc.constraint_type is not null "isPrimary"
from information_schema.key_column_usage kc
inner join information_schema.key_column_usage kc2
ON kc2.column_name = kc.column_name and kc2.table_name = kc.table_name
inner join information_schema.table_constraints tc
on kc2.constraint_name = tc.constraint_name and
tc.constraint_type = 'PRIMARY KEY'
where kc.constraint_name = c.constraint_name)
, false) "isPrimary",
(
select COUNT(*) > 1 "hasMultiplePk"
from information_schema.table_constraints tc
-- one constraint can have multiple columns
inner join information_schema.key_column_usage kc
on kc.constraint_name = tc.constraint_name
where tc.table_name = fk.table_name
and tc.constraint_type = 'PRIMARY KEY'
)
from information_schema.referential_constraints c
inner join information_schema.table_constraints fk on c.constraint_name = fk.constraint_name
inner join information_schema.table_constraints pk on c.unique_constraint_name = pk.constraint_name
where fk.table_name = $1 or pk.table_name = $1
select fk.table_name,
pk.table_name,
c.constraint_name,
kcu.column_name,
coalesce(
(select tc.constraint_type is not null "isPrimary"
from information_schema.key_column_usage kc
inner join information_schema.key_column_usage kc2
ON kc2.column_name = kc.column_name and kc2.table_name = kc.table_name
inner join information_schema.table_constraints tc
on kc2.constraint_name = tc.constraint_name and
tc.constraint_type = 'PRIMARY KEY'
where kc.constraint_name = c.constraint_name
and kc.column_name = kcu.column_name)
, false) "isPrimary",
(select COUNT(*) > 1 "hasMultiplePk"
from information_schema.table_constraints tc
-- one constraint can have multiple columns
inner join information_schema.key_column_usage kc
on kc.constraint_name = tc.constraint_name
where tc.table_name = fk.table_name
and tc.constraint_type = 'PRIMARY KEY')
from information_schema.referential_constraints c
inner join information_schema.table_constraints fk on c.constraint_name = fk.constraint_name
inner join information_schema.table_constraints pk on c.unique_constraint_name = pk.constraint_name
inner join information_schema.key_column_usage kcu on c.constraint_name = kcu.constraint_name
where fk.table_name = $1 or pk.table_name = $1;
`, tableName)
if err != nil {
return nil, err
Expand All @@ -144,6 +145,7 @@ func (c postgresConnector) GetConstraints(tableName string) ([]ConstraintResult,
&constraint.FkTable,
&constraint.PkTable,
&constraint.ConstraintName,
&constraint.ColumnName,
&constraint.IsPrimary,
&constraint.HasMultiplePK,
)
Expand Down
1 change: 1 addition & 0 deletions database/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type ConstraintResult struct {
FkTable string
PkTable string
ConstraintName string
ColumnName string
IsPrimary bool
HasMultiplePK bool
}
Expand Down
Loading

0 comments on commit 4f50b47

Please sign in to comment.