Skip to content

Commit

Permalink
Multi schema support (#27)
Browse files Browse the repository at this point in the history
* WIP: added support for multiple schemas; added support for mysql

* add test setup for postgres and mssql; fix tests

* check for schema name in mysql

* fix tests

* simplify tests with schema

* add support for postgres and mssql; add tests

* cleanup

* add useAllSchemas flag

* update changelog
  • Loading branch information
KarnerTh committed Dec 28, 2022
1 parent 924a43d commit 8c52c4c
Show file tree
Hide file tree
Showing 31 changed files with 449 additions and 172 deletions.
64 changes: 46 additions & 18 deletions analyzer/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package analyzer

import (
"errors"
"fmt"

"github.com/sirupsen/logrus"

Expand All @@ -19,11 +20,10 @@ type analyzer struct {

type Analyzer interface {
Analyze() (*database.Result, error)

GetConnectionString() (string, error)
GetSchema(db database.Connector) (string, error)
GetTables(db database.Connector, selectedSchema string) ([]string, error)
GetColumnsAndConstraints(db database.Connector, selectedTables []string) ([]database.TableResult, error)
GetSchemas(db database.Connector) ([]string, error)
GetTables(db database.Connector, selectedSchemas []string) ([]database.TableDetail, error)
GetColumnsAndConstraints(db database.Connector, selectedTables []database.TableDetail) ([]database.TableResult, error)
}

func NewAnalyzer(config config.MermerdConfig, connectorFactory database.ConnectorFactory, questioner Questioner) Analyzer {
Expand All @@ -49,12 +49,12 @@ func (a analyzer) Analyze() (*database.Result, error) {
defer db.Close()
a.loadingSpinner.Stop()

selectedSchema, err := a.GetSchema(db)
selectedSchemas, err := a.GetSchemas(db)
if err != nil {
return nil, err
}

selectedTables, err := a.GetTables(db, selectedSchema)
selectedTables, err := a.GetTables(db, selectedSchemas)
if err != nil {
return nil, err
}
Expand All @@ -75,8 +75,8 @@ func (a analyzer) GetConnectionString() (string, error) {
return a.questioner.AskConnectionQuestion(a.config.ConnectionStringSuggestions())
}

func (a analyzer) GetSchema(db database.Connector) (string, error) {
if selectedSchema := a.config.Schema(); selectedSchema != "" {
func (a analyzer) GetSchemas(db database.Connector) ([]string, error) {
if selectedSchema := a.config.Schemas(); len(selectedSchema) > 0 {
return selectedSchema, nil
}

Expand All @@ -85,44 +85,72 @@ func (a analyzer) GetSchema(db database.Connector) (string, error) {
a.loadingSpinner.Stop()
if err != nil {
logrus.Error("Getting schemas failed", " | ", err)
return "", err
return []string{}, err
}

logrus.WithField("count", len(schemas)).Info("Got schemas")
if a.config.UseAllSchemas() {
return schemas, nil
}

switch len(schemas) {
case 0:
return "", errors.New("no schemas available")
return []string{}, errors.New("no schemas available")
case 1:
return schemas[0], nil
return schemas, nil
default:
return a.questioner.AskSchemaQuestion(schemas)
}
}

func (a analyzer) GetTables(db database.Connector, selectedSchema string) ([]string, error) {
func (a analyzer) GetTables(db database.Connector, selectedSchemas []string) ([]database.TableDetail, error) {
if selectedTables := a.config.SelectedTables(); len(selectedTables) > 0 {
return selectedTables, nil
return util.Map2(selectedTables, func(value string) database.TableDetail {
res, err := database.ParseTableName(value, selectedSchemas)
if err != nil {
logrus.Error("Could not parse table name", value)
}

return res
}), nil
}

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

if len(tables) == 0 {
logrus.Error("No tables found")
}

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

if a.config.UseAllTables() {
return tables, nil
} else {
return a.questioner.AskTableQuestion(tables)
}

tableNames := util.Map2(tables, func(table database.TableDetail) string {
return fmt.Sprintf("%s.%s", table.Schema, table.Name)
})
surveyResult, err := a.questioner.AskTableQuestion(tableNames)
if err != nil {
return []database.TableDetail{}, err
}
return util.Map2(surveyResult, func(value string) database.TableDetail {
res, err := database.ParseTableName(value, selectedSchemas)
if err != nil {
logrus.Error("Could not parse table name", value)
}

return res
}), nil
}

func (a analyzer) GetColumnsAndConstraints(db database.Connector, selectedTables []string) ([]database.TableResult, error) {
func (a analyzer) GetColumnsAndConstraints(db database.Connector, selectedTables []database.TableDetail) ([]database.TableResult, error) {
var tableResults []database.TableResult
a.loadingSpinner.Start("Getting columns and constraints")
for _, table := range selectedTables {
Expand All @@ -138,7 +166,7 @@ func (a analyzer) GetColumnsAndConstraints(db database.Connector, selectedTables
return nil, err
}

tableResults = append(tableResults, database.TableResult{TableName: table, Columns: columns, Constraints: constraints})
tableResults = append(tableResults, database.TableResult{Table: table, Columns: columns, Constraints: constraints})
}
a.loadingSpinner.Stop()
columnCount, constraintCount := getTableResultStats(tableResults)
Expand Down
79 changes: 52 additions & 27 deletions analyzer/analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,27 +53,46 @@ func TestAnalyzer_GetSchema(t *testing.T) {
// Arrange
analyzer, configMock, _, _ := getAnalyzerWithMocks()
connectorMock := mocks.Connector{}
configMock.On("Schema").Return("configuredSchema").Once()
configMock.On("Schemas").Return([]string{"configuredSchema"}).Once()

// Act
result, err := analyzer.GetSchema(&connectorMock)
result, err := analyzer.GetSchemas(&connectorMock)

// Assert
configMock.AssertExpectations(t)
connectorMock.AssertExpectations(t)
assert.Nil(t, err)
assert.Equal(t, "configuredSchema", result)
assert.ElementsMatch(t, []string{"configuredSchema"}, result)
})

t.Run("Use all available schema", func(t *testing.T) {
// Arrange
analyzer, configMock, _, _ := getAnalyzerWithMocks()
connectorMock := mocks.Connector{}
configMock.On("UseAllSchemas").Return(true).Once()
configMock.On("Schemas").Return([]string{}).Once()
connectorMock.On("GetSchemas").Return([]string{"schema1", "schema2"}, nil).Once()

// Act
result, err := analyzer.GetSchemas(&connectorMock)

// Assert
configMock.AssertExpectations(t)
connectorMock.AssertExpectations(t)
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"schema1", "schema2"}, result)
})

t.Run("No schema available return error", func(t *testing.T) {
// Arrange
analyzer, configMock, _, _ := getAnalyzerWithMocks()
connectorMock := mocks.Connector{}
configMock.On("Schema").Return("").Once()
configMock.On("Schemas").Return([]string{}).Once()
configMock.On("UseAllSchemas").Return(false).Once()
connectorMock.On("GetSchemas").Return([]string{}, nil).Once()

// Act
result, err := analyzer.GetSchema(&connectorMock)
result, err := analyzer.GetSchemas(&connectorMock)

// Assert
configMock.AssertExpectations(t)
Expand All @@ -86,36 +105,38 @@ func TestAnalyzer_GetSchema(t *testing.T) {
// Arrange
analyzer, configMock, _, _ := getAnalyzerWithMocks()
connectorMock := mocks.Connector{}
configMock.On("Schema").Return("").Once()
configMock.On("Schemas").Return([]string{}).Once()
configMock.On("UseAllSchemas").Return(false).Once()
connectorMock.On("GetSchemas").Return([]string{"onlyItem"}, nil).Once()

// Act
result, err := analyzer.GetSchema(&connectorMock)
result, err := analyzer.GetSchemas(&connectorMock)

// Assert
configMock.AssertExpectations(t)
connectorMock.AssertExpectations(t)
assert.Nil(t, err)
assert.Equal(t, "onlyItem", result)
assert.ElementsMatch(t, []string{"onlyItem"}, result)
})

t.Run("Use value from questioner", func(t *testing.T) {
// Arrange
analyzer, configMock, _, questionerMock := getAnalyzerWithMocks()
connectorMock := mocks.Connector{}
configMock.On("Schema").Return("").Once()
configMock.On("Schemas").Return([]string{}).Once()
configMock.On("UseAllSchemas").Return(false).Once()
connectorMock.On("GetSchemas").Return([]string{"first", "second"}, nil).Once()
questionerMock.On("AskSchemaQuestion", []string{"first", "second"}).Return("first", nil).Once()
questionerMock.On("AskSchemaQuestion", []string{"first", "second"}).Return([]string{"first"}, nil).Once()

// Act
result, err := analyzer.GetSchema(&connectorMock)
result, err := analyzer.GetSchemas(&connectorMock)

// Assert
configMock.AssertExpectations(t)
connectorMock.AssertExpectations(t)
questionerMock.AssertExpectations(t)
assert.Nil(t, err)
assert.Equal(t, "first", result)
assert.ElementsMatch(t, []string{"first"}, result)
})
}

Expand All @@ -127,51 +148,55 @@ func TestAnalyzer_GetTables(t *testing.T) {
configMock.On("SelectedTables").Return([]string{"configuredTable"}).Once()

// Act
result, err := analyzer.GetTables(&connectorMock, "validSchema")
result, err := analyzer.GetTables(&connectorMock, []string{"validSchema"})

// Assert
configMock.AssertExpectations(t)
connectorMock.AssertExpectations(t)
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"configuredTable"}, result)
assert.Len(t, result, 1)
assert.Equal(t, "configuredTable", result[0].Name)
})

t.Run("Use all available tables", func(t *testing.T) {
// Arrange
analyzer, configMock, _, _ := getAnalyzerWithMocks()
connectorMock := mocks.Connector{}
configMock.On("SelectedTables").Return([]string{}).Once()
connectorMock.On("GetTables", "validSchema").Return([]string{"tableA", "tableB"}, nil).Once()
connectorMock.On("GetTables", []string{"validSchema"}).Return([]database.TableDetail{{Schema: "validSchema", Name: "tableA"}, {Schema: "validSchema", Name: "tableB"}}, nil).Once()
configMock.On("UseAllTables").Return(true).Once()

// Act
result, err := analyzer.GetTables(&connectorMock, "validSchema")
result, err := analyzer.GetTables(&connectorMock, []string{"validSchema"})

// Assert
configMock.AssertExpectations(t)
connectorMock.AssertExpectations(t)
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"tableA", "tableB"}, result)
assert.Len(t, result, 2)
assert.Equal(t, "tableA", result[0].Name)
assert.Equal(t, "tableB", result[1].Name)
})

t.Run("Use value from questioner", func(t *testing.T) {
// Arrange
analyzer, configMock, _, questionerMock := getAnalyzerWithMocks()
connectorMock := mocks.Connector{}
configMock.On("SelectedTables").Return([]string{}).Once()
connectorMock.On("GetTables", "validSchema").Return([]string{"tableA", "tableB"}, nil).Once()
connectorMock.On("GetTables", []string{"validSchema"}).Return([]database.TableDetail{{Schema: "validSchema", Name: "tableA"}, {Schema: "validSchema", Name: "tableB"}}, nil).Once()
configMock.On("UseAllTables").Return(false).Once()
questionerMock.On("AskTableQuestion", []string{"tableA", "tableB"}).Return([]string{"tableA"}, nil).Once()
questionerMock.On("AskTableQuestion", []string{"validSchema.tableA", "validSchema.tableB"}).Return([]string{"validSchema.tableA"}, nil).Once()

// Act
result, err := analyzer.GetTables(&connectorMock, "validSchema")
result, err := analyzer.GetTables(&connectorMock, []string{"validSchema"})

// Assert
configMock.AssertExpectations(t)
connectorMock.AssertExpectations(t)
questionerMock.AssertExpectations(t)
assert.Nil(t, err)
assert.ElementsMatch(t, []string{"tableA"}, result)
assert.Len(t, result, 1)
assert.Equal(t, "tableA", result[0].Name)
})
}

Expand All @@ -184,9 +209,9 @@ func TestAnalyzer_Analyze(t *testing.T) {
connectionFactoryMock.On("NewConnector", "validConnectionString").Return(&connectorMock, nil).Once()
connectorMock.On("Connect").Return(nil).Once()
connectorMock.On("Close").Return().Once()
configMock.On("Schema").Return("validSchema").Once()
configMock.On("SelectedTables").Return([]string{"tableA", "tableB"}).Once()
connectorMock.On("GetColumns", "tableA").Return([]database.ColumnResult{
configMock.On("Schemas").Return([]string{"validSchema"}).Once()
configMock.On("SelectedTables").Return([]string{"validSchema.tableA", "validSchema.tableB"}).Once()
connectorMock.On("GetColumns", database.TableDetail{Schema: "validSchema", Name: "tableA"}).Return([]database.ColumnResult{
{
Name: "fieldA",
DataType: "int",
Expand All @@ -196,7 +221,7 @@ func TestAnalyzer_Analyze(t *testing.T) {
DataType: "string",
},
}, nil).Once()
connectorMock.On("GetColumns", "tableB").Return([]database.ColumnResult{
connectorMock.On("GetColumns", database.TableDetail{Schema: "validSchema", Name: "tableB"}).Return([]database.ColumnResult{
{
Name: "fieldC",
DataType: "int",
Expand All @@ -206,14 +231,14 @@ func TestAnalyzer_Analyze(t *testing.T) {
DataType: "string",
},
}, nil).Once()
connectorMock.On("GetConstraints", "tableA").Return([]database.ConstraintResult{{
connectorMock.On("GetConstraints", database.TableDetail{Schema: "validSchema", Name: "tableA"}).Return([]database.ConstraintResult{{
FkTable: "tableA",
PkTable: "tableB",
ConstraintName: "testConstraint",
IsPrimary: false,
HasMultiplePK: false,
}}, nil).Once()
connectorMock.On("GetConstraints", "tableB").Return([]database.ConstraintResult{{
connectorMock.On("GetConstraints", database.TableDetail{Schema: "validSchema", Name: "tableB"}).Return([]database.ConstraintResult{{
FkTable: "tableA",
PkTable: "tableB",
ConstraintName: "testConstraint",
Expand Down
12 changes: 6 additions & 6 deletions analyzer/questioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ type questioner struct{}

type Questioner interface {
AskConnectionQuestion(suggestions []string) (string, error)
AskSchemaQuestion(schemas []string) (string, error)
AskSchemaQuestion(schemas []string) ([]string, error)
AskTableQuestion(tables []string) ([]string, error)
}

Expand All @@ -35,14 +35,14 @@ func (q questioner) AskConnectionQuestion(suggestions []string) (string, error)
return os.ExpandEnv(result), nil
}

func (q questioner) AskSchemaQuestion(schemas []string) (string, error) {
var result string
question := &survey.Select{
Message: "Choose a schema:",
func (q questioner) AskSchemaQuestion(schemas []string) ([]string, error) {
var result []string
question := &survey.MultiSelect{
Message: "Choose schemas:",
Options: schemas,
}

err := survey.AskOne(question, &result)
err := survey.AskOne(question, &result, survey.WithValidator(survey.MinItems(1)))
return result, err
}

Expand Down
3 changes: 2 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +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
## [0.5.0] - 2022-12-28
### Added
- Support enum description ([Issue #15](https://github.com/KarnerTh/mermerd/issues/15))
- Support multiple schemas ([Issue #23](https://github.com/KarnerTh/mermerd/issues/23))

## [0.4.1] - 2022-09-28
### Fixed
Expand Down
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ 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.UseAllSchemasKey, false, "use all available schemas")
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)")
Expand All @@ -77,6 +78,7 @@ func init() {

bindFlagToViper(config.ShowAllConstraintsKey)
bindFlagToViper(config.UseAllTablesKey)
bindFlagToViper(config.UseAllSchemasKey)
bindFlagToViper(config.DebugKey)
bindFlagToViper(config.OmitConstraintLabelsKey)
bindFlagToViper(config.OmitAttributeKeysKey)
Expand Down
Loading

0 comments on commit 8c52c4c

Please sign in to comment.