Skip to content

Commit

Permalink
Merge pull request #1282 from chrisngyn/feature/define-relationships-…
Browse files Browse the repository at this point in the history
…in-config

Allow define foreign key relationships in config file
  • Loading branch information
stephenafamo authored Jul 23, 2023
2 parents 67897db + 1fbc1c1 commit 78d521d
Show file tree
Hide file tree
Showing 10 changed files with 461 additions and 34 deletions.
123 changes: 102 additions & 21 deletions boilingcore/config.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package boilingcore

import (
"fmt"
"io/fs"
"path/filepath"
"strings"
"text/template"

"github.com/friendsofgo/errors"
"github.com/spf13/cast"

"github.com/volatiletech/sqlboiler/v4/drivers"
Expand Down Expand Up @@ -46,10 +48,11 @@ type Config struct {
DefaultTemplates fs.FS `toml:"-" json:"-"`
CustomTemplateFuncs template.FuncMap `toml:"-" json:"-"`

Aliases Aliases `toml:"aliases,omitempty" json:"aliases,omitempty"`
TypeReplaces []TypeReplace `toml:"type_replaces,omitempty" json:"type_replaces,omitempty"`
AutoColumns AutoColumns `toml:"auto_columns,omitempty" json:"auto_columns,omitempty"`
Inflections Inflections `toml:"inflections,omitempty" json:"inflections,omitempty"`
Aliases Aliases `toml:"aliases,omitempty" json:"aliases,omitempty"`
TypeReplaces []TypeReplace `toml:"type_replaces,omitempty" json:"type_replaces,omitempty"`
AutoColumns AutoColumns `toml:"auto_columns,omitempty" json:"auto_columns,omitempty"`
Inflections Inflections `toml:"inflections,omitempty" json:"inflections,omitempty"`
ForeignKeys []drivers.ForeignKey `toml:"foreign_keys,omitempty" json:"foreign_keys,omitempty" `

Version string `toml:"version" json:"version"`
}
Expand Down Expand Up @@ -91,27 +94,27 @@ func (c *Config) OutputDirDepth() int {
//
// It also supports two different syntaxes, because of viper:
//
// [aliases.tables.table_name]
// fields... = "values"
// [aliases.tables.columns]
// colname = "alias"
// [aliases.tables.relationships.fkey_name]
// local = "x"
// foreign = "y"
// [aliases.tables.table_name]
// fields... = "values"
// [aliases.tables.columns]
// colname = "alias"
// [aliases.tables.relationships.fkey_name]
// local = "x"
// foreign = "y"
//
// Or alternatively (when toml key names or viper's
// lowercasing of key names gets in the way):
//
// [[aliases.tables]]
// name = "table_name"
// fields... = "values"
// [[aliases.tables.columns]]
// name = "colname"
// alias = "alias"
// [[aliases.tables.relationships]]
// name = "fkey_name"
// local = "x"
// foreign = "y"
// [[aliases.tables]]
// name = "table_name"
// fields... = "values"
// [[aliases.tables.columns]]
// name = "colname"
// alias = "alias"
// [[aliases.tables.relationships]]
// name = "fkey_name"
// local = "x"
// foreign = "y"
func ConvertAliases(i interface{}) (a Aliases) {
if i == nil {
return a
Expand Down Expand Up @@ -283,3 +286,81 @@ func columnFromInterface(i interface{}) (col drivers.Column) {

return col
}

// ConvertForeignKeys is necessary because viper
//
// It also supports two different syntaxes, because of viper:
//
// [foreign_keys.fk_1]
// table = "table_name"
// column = "column_name"
// foreign_table = "foreign_table_name"
// foreign_column = "foreign_column_name"
//
// Or alternatively (when toml key names or viper's
// lowercasing of key names gets in the way):
//
// [[foreign_keys]]
// name = "fk_1"
// table = "table_name"
// column = "column_name"
// foreign_table = "foreign_table_name"
// foreign_column = "foreign_column_name"
func ConvertForeignKeys(i interface{}) (fks []drivers.ForeignKey) {
if i == nil {
return nil
}

iterateMapOrSlice(i, func(name string, obj interface{}) {
t := cast.ToStringMap(obj)

fk := drivers.ForeignKey{
Table: cast.ToString(t["table"]),
Name: name,
Column: cast.ToString(t["column"]),
ForeignTable: cast.ToString(t["foreign_table"]),
ForeignColumn: cast.ToString(t["foreign_column"]),
}
if err := validateForeignKey(fk); err != nil {
panic(errors.Errorf("invalid foreign key %s: %s", name, err))
}
fks = append(fks, fk)
})

if err := validateDuplicateForeignKeys(fks); err != nil {
panic(errors.Errorf("invalid foreign keys: %s", err))
}

return fks
}

func validateForeignKey(fk drivers.ForeignKey) error {
if fk.Name == "" {
return errors.New("foreign key must have a name")
}
if fk.Table == "" {
return errors.New("foreign key must have a table")
}
if fk.Column == "" {
return errors.New("foreign key must have a column")
}
if fk.ForeignTable == "" {
return errors.New("foreign key must have a foreign table")
}
if fk.ForeignColumn == "" {
return errors.New("foreign key must have a foreign column")
}
return nil
}

func validateDuplicateForeignKeys(fks []drivers.ForeignKey) error {
fkMap := make(map[string]drivers.ForeignKey)
for _, fk := range fks {
key := fmt.Sprintf("%s.%s", fk.Table, fk.Column)
if _, ok := fkMap[key]; ok {
return errors.Errorf("duplicate foreign key name: %s", fk.Name)
}
fkMap[key] = fk
}
return nil
}
63 changes: 63 additions & 0 deletions boilingcore/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,66 @@ func TestConvertTypeReplace(t *testing.T) {
t.Error("tables in types.match wrong:", got)
}
}

func TestConvertForeignKeys(t *testing.T) {
t.Parallel()

var intf interface{} = map[string]interface{}{
"fk_1": map[string]interface{}{
"table": "table_name",
"column": "column_name",
"foreign_table": "foreign_table_name",
"foreign_column": "foreign_column_name",
},
}

fks := ConvertForeignKeys(intf)
if len(fks) != 1 {
t.Error("should have one entry")
}

fk := fks[0]
expectedFK := drivers.ForeignKey{
Name: "fk_1",
Table: "table_name",
Column: "column_name",
ForeignTable: "foreign_table_name",
ForeignColumn: "foreign_column_name",
}

if fk != expectedFK {
t.Error("value was wrong:", fk)
}
}

func TestConvertForeignKeysAltSyntax(t *testing.T) {
t.Parallel()

var intf interface{} = []interface{}{
map[string]interface{}{
"name": "fk_1",
"table": "table_name",
"column": "column_name",
"foreign_table": "foreign_table_name",
"foreign_column": "foreign_column_name",
},
}

fks := ConvertForeignKeys(intf)
if len(fks) != 1 {
t.Error("should have one entry")
}

fk := fks[0]
expectedFK := drivers.ForeignKey{
Name: "fk_1",
Table: "table_name",
Column: "column_name",
ForeignTable: "foreign_table_name",
ForeignColumn: "foreign_column_name",
}

if fk != expectedFK {
t.Error("value was wrong:", fk)
}
}
62 changes: 62 additions & 0 deletions drivers/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,41 @@ func (c Config) StringSlice(key string) ([]string, bool) {
return slice, true
}

func (c Config) MustForeignKeys(key string) []ForeignKey {
rawValue, ok := c[key]
if !ok {
return nil
}

switch v := rawValue.(type) {
case nil:
return nil
case []ForeignKey:
return v
case []interface{}: // in case binary, config is pass to driver in json format, so this key will be []interface{}
fks := make([]ForeignKey, 0, len(v))
for _, item := range v {
fk, ok := item.(map[string]interface{})
if !ok {
panic(errors.Errorf("found item of foreign keys, but it was not a map[string]interface{} (%T)", v))
}

configFK := Config(fk)

fks = append(fks, ForeignKey{
Name: configFK.MustString("name"),
Table: configFK.MustString("table"),
Column: configFK.MustString("column"),
ForeignTable: configFK.MustString("foreign_table"),
ForeignColumn: configFK.MustString("foreign_column"),
})
}
return fks
default:
panic(errors.Errorf("found key %s in config, but it was invalid (%T)", key, v))
}
}

// DefaultEnv grabs a value from the environment or a default.
// This is shared by drivers to get config for testing.
func DefaultEnv(key, def string) string {
Expand Down Expand Up @@ -211,3 +246,30 @@ func ColumnsFromList(list []string, tablename string) []string {

return columns
}

// CombineConfigAndDBForeignKeys takes foreign keys from both config and db, filter by tableName and
// deduplicate by column name. If a foreign key is found in both config and db, the one in config will be used.
func CombineConfigAndDBForeignKeys(configForeignKeys []ForeignKey, tableName string, dbForeignKeys []ForeignKey) []ForeignKey {
combinedForeignKeys := make([]ForeignKey, 0, len(configForeignKeys)+len(dbForeignKeys))
appearedColumns := make(map[string]bool)

for _, fk := range configForeignKeys {
// need check table name here cause configForeignKeys contains all foreign keys of all tables
if fk.Table != tableName {
continue
}

combinedForeignKeys = append(combinedForeignKeys, fk)
appearedColumns[fk.Column] = true
}

for _, fk := range dbForeignKeys {
// no need check table here, because dbForeignKeys are already filtered by table name
if appearedColumns[fk.Column] {
continue
}
combinedForeignKeys = append(combinedForeignKeys, fk)
}

return combinedForeignKeys
}
Loading

0 comments on commit 78d521d

Please sign in to comment.