diff --git a/README.md b/README.md index 579d550..fa3d3ed 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ users, err := models.Users(db, Load("FavoriteMovies")).All() if err != nil { return err } -fmt.Println(len(users.Loaded.FavoriteMovies)) +fmt.Println(len(users.R.FavoriteMovies)) ``` ## How to boil your database diff --git a/bdb/drivers/mock.go b/bdb/drivers/mock.go index b4c1943..f26b4c6 100644 --- a/bdb/drivers/mock.go +++ b/bdb/drivers/mock.go @@ -59,7 +59,7 @@ func (m *MockDriver) Columns(tableName string) ([]bdb.Column, error) { func (m *MockDriver) ForeignKeyInfo(tableName string) ([]bdb.ForeignKey, error) { return map[string][]bdb.ForeignKey{ "jets": { - {Table: "jets", Name: "jets_pilot_id_fk", Column: "pilot_id", ForeignTable: "pilots", ForeignColumn: "id"}, + {Table: "jets", Name: "jets_pilot_id_fk", Column: "pilot_id", ForeignTable: "pilots", ForeignColumn: "id", ForeignColumnUnique: true}, {Table: "jets", Name: "jets_airport_id_fk", Column: "airport_id", ForeignTable: "airports", ForeignColumn: "id"}, }, "licenses": { diff --git a/bdb/interface_test.go b/bdb/interface_test.go index be90f18..fd3e59f 100644 --- a/bdb/interface_test.go +++ b/bdb/interface_test.go @@ -1,98 +1,140 @@ package bdb import ( - "reflect" "testing" + + "github.com/vattle/sqlboiler/strmangle" ) -type testInterface struct{} +type mockDriver struct{} -func (t testInterface) TableNames(exclude []string) ([]string, error) { - return []string{"table1", "table2"}, nil +func (m mockDriver) TranslateColumnType(c Column) Column { return c } +func (m mockDriver) UseLastInsertID() bool { return false } +func (m mockDriver) Open() error { return nil } +func (m mockDriver) Close() {} + +func (m mockDriver) TableNames(exclude []string) ([]string, error) { + tables := []string{"pilots", "jets", "airports", "licenses", "hangars", "languages", "pilot_languages"} + return strmangle.SetComplement(tables, exclude), nil } -var testCols = []Column{ - {Name: "col1", Type: "character varying"}, - {Name: "col2", Type: "character varying", Nullable: true}, +// Columns returns a list of mock columns +func (m mockDriver) Columns(tableName string) ([]Column, error) { + return map[string][]Column{ + "pilots": { + {Name: "id", Type: "int", DBType: "integer"}, + {Name: "name", Type: "string", DBType: "character"}, + }, + "airports": { + {Name: "id", Type: "int", DBType: "integer"}, + {Name: "size", Type: "null.Int", DBType: "integer", Nullable: true}, + }, + "jets": { + {Name: "id", Type: "int", DBType: "integer"}, + {Name: "pilot_id", Type: "int", DBType: "integer", Nullable: true, Unique: true}, + {Name: "airport_id", Type: "int", DBType: "integer"}, + {Name: "name", Type: "string", DBType: "character", Nullable: false}, + {Name: "color", Type: "null.String", DBType: "character", Nullable: true}, + {Name: "uuid", Type: "string", DBType: "uuid", Nullable: true}, + {Name: "identifier", Type: "string", DBType: "uuid", Nullable: false}, + {Name: "cargo", Type: "[]byte", DBType: "bytea", Nullable: false}, + {Name: "manifest", Type: "[]byte", DBType: "bytea", Nullable: true, Unique: true}, + }, + "licenses": { + {Name: "id", Type: "int", DBType: "integer"}, + {Name: "pilot_id", Type: "int", DBType: "integer"}, + }, + "hangars": { + {Name: "id", Type: "int", DBType: "integer"}, + {Name: "name", Type: "string", DBType: "character", Nullable: true, Unique: true}, + {Name: "hangar_id", Type: "int", DBType: "integer", Nullable: true}, + }, + "languages": { + {Name: "id", Type: "int", DBType: "integer"}, + {Name: "language", Type: "string", DBType: "character", Nullable: false, Unique: true}, + }, + "pilot_languages": { + {Name: "pilot_id", Type: "int", DBType: "integer"}, + {Name: "language_id", Type: "int", DBType: "integer"}, + }, + }[tableName], nil } -func (t testInterface) Columns(tableName string) ([]Column, error) { - return testCols, nil +// ForeignKeyInfo returns a list of mock foreignkeys +func (m mockDriver) ForeignKeyInfo(tableName string) ([]ForeignKey, error) { + return map[string][]ForeignKey{ + "jets": { + {Table: "jets", Name: "jets_pilot_id_fk", Column: "pilot_id", ForeignTable: "pilots", ForeignColumn: "id", ForeignColumnUnique: true}, + {Table: "jets", Name: "jets_airport_id_fk", Column: "airport_id", ForeignTable: "airports", ForeignColumn: "id"}, + }, + "licenses": { + {Table: "licenses", Name: "licenses_pilot_id_fk", Column: "pilot_id", ForeignTable: "pilots", ForeignColumn: "id"}, + }, + "pilot_languages": { + {Table: "pilot_languages", Name: "pilot_id_fk", Column: "pilot_id", ForeignTable: "pilots", ForeignColumn: "id"}, + {Table: "pilot_languages", Name: "jet_id_fk", Column: "language_id", ForeignTable: "languages", ForeignColumn: "id"}, + }, + "hangars": { + {Table: "hangars", Name: "hangar_fk_id", Column: "hangar_id", ForeignTable: "hangars", ForeignColumn: "id"}, + }, + }[tableName], nil } -func (t testInterface) UseLastInsertID() bool { - return false +// PrimaryKeyInfo returns mock primary key info for the passed in table name +func (m mockDriver) PrimaryKeyInfo(tableName string) (*PrimaryKey, error) { + return map[string]*PrimaryKey{ + "pilots": {Name: "pilot_id_pkey", Columns: []string{"id"}}, + "airports": {Name: "airport_id_pkey", Columns: []string{"id"}}, + "jets": {Name: "jet_id_pkey", Columns: []string{"id"}}, + "licenses": {Name: "license_id_pkey", Columns: []string{"id"}}, + "hangars": {Name: "hangar_id_pkey", Columns: []string{"id"}}, + "languages": {Name: "language_id_pkey", Columns: []string{"id"}}, + "pilot_languages": {Name: "pilot_languages_pkey", Columns: []string{"pilot_id", "language_id"}}, + }[tableName], nil } -var testPkey = &PrimaryKey{Name: "pkey1", Columns: []string{"col1", "col2"}} - -func (t testInterface) PrimaryKeyInfo(tableName string) (*PrimaryKey, error) { - return testPkey, nil -} - -var testFkeys = []ForeignKey{ - { - Name: "fkey1", - Column: "col1", - ForeignTable: "table2", - ForeignColumn: "col2", - }, - { - Name: "fkey2", - Column: "col2", - ForeignTable: "table1", - ForeignColumn: "col1", - }, -} - -func (t testInterface) ForeignKeyInfo(tableName string) ([]ForeignKey, error) { - return testFkeys, nil -} - -func (t testInterface) TranslateColumnType(column Column) Column { - column.Type = "string" - return column -} - -func (t testInterface) Open() error { - return nil -} - -func (t testInterface) Close() {} - func TestTables(t *testing.T) { t.Parallel() - tables, err := Tables(testInterface{}) + tables, err := Tables(mockDriver{}) if err != nil { t.Error(err) } - if len(tables) != 2 { - t.Errorf("Expected len 2, got: %d\n", len(tables)) + if len(tables) != 7 { + t.Errorf("Expected len 7, got: %d\n", len(tables)) } - if !reflect.DeepEqual(tables[0].Columns, testCols) { - t.Errorf("Did not get expected columns, got:\n%#v\n%#v", tables[0].Columns, testCols) + pilots := GetTable(tables, "pilots") + if len(pilots.Columns) != 2 { + t.Error() + } + if pilots.ToManyRelationships[0].ForeignTable != "jets" { + t.Error("want a to many to jets") + } + if pilots.ToManyRelationships[1].ForeignTable != "licenses" { + t.Error("want a to many to languages") + } + if pilots.ToManyRelationships[2].ForeignTable != "languages" { + t.Error("want a to many to languages") } - if !tables[0].IsJoinTable || !tables[1].IsJoinTable { - t.Errorf("Expected IsJoinTable to be true") + jets := GetTable(tables, "jets") + if len(jets.ToManyRelationships) != 0 { + t.Error("want no to many relationships") } - if !reflect.DeepEqual(tables[0].PKey, testPkey) { - t.Errorf("Did not get expected PKey, got:\n#%v\n%#v", tables[0].PKey, testPkey) + languages := GetTable(tables, "pilot_languages") + if !languages.IsJoinTable { + t.Error("languages is a join table") } - if !reflect.DeepEqual(tables[0].FKeys, testFkeys) { - t.Errorf("Did not get expected Fkey, got:\n%#v\n%#v", tables[0].FKeys, testFkeys) + hangars := GetTable(tables, "hangars") + if len(hangars.ToManyRelationships) != 1 || hangars.ToManyRelationships[0].ForeignTable != "hangars" { + t.Error("want 1 to many relationships") } - - if len(tables[0].ToManyRelationships) != 1 { - t.Error("wanted a to many relationship") - } - if len(tables[1].ToManyRelationships) != 1 { - t.Error("wanted a to many relationship") + if len(hangars.FKeys) != 1 || hangars.FKeys[0].ForeignTable != "hangars" { + t.Error("want one hangar foreign key to itself") } } diff --git a/bdb/relationships.go b/bdb/relationships.go index 212a912..8bbcb84 100644 --- a/bdb/relationships.go +++ b/bdb/relationships.go @@ -38,10 +38,6 @@ func toManyRelationships(table Table, tables []Table) []ToManyRelationship { var relationships []ToManyRelationship for _, t := range tables { - if t.Name == table.Name { - continue - } - for _, f := range t.FKeys { if f.ForeignTable != table.Name { continue @@ -58,7 +54,7 @@ func buildRelationship(localTable Table, foreignKey ForeignKey, foreignTable Tab if !foreignTable.IsJoinTable { col := localTable.GetColumn(foreignKey.ForeignColumn) return ToManyRelationship{ - Table: foreignKey.Table, + Table: localTable.Name, Column: foreignKey.ForeignColumn, Nullable: col.Nullable, Unique: col.Unique, @@ -72,7 +68,7 @@ func buildRelationship(localTable Table, foreignKey ForeignKey, foreignTable Tab col := foreignTable.GetColumn(foreignKey.Column) relationship := ToManyRelationship{ - Table: foreignKey.Table, + Table: localTable.Name, Column: foreignKey.ForeignColumn, Nullable: col.Nullable, Unique: col.Unique, diff --git a/bdb/relationships_test.go b/bdb/relationships_test.go index 1eecb39..de994d4 100644 --- a/bdb/relationships_test.go +++ b/bdb/relationships_test.go @@ -77,6 +77,7 @@ func TestToManyRelationships(t *testing.T) { expected := []ToManyRelationship{ { + Table: "pilots", Column: "id", Nullable: false, Unique: false, @@ -89,6 +90,7 @@ func TestToManyRelationships(t *testing.T) { ToJoinTable: false, }, { + Table: "pilots", Column: "id", Nullable: false, Unique: false, @@ -101,6 +103,7 @@ func TestToManyRelationships(t *testing.T) { ToJoinTable: false, }, { + Table: "pilots", Column: "id", Nullable: false, Unique: false, @@ -209,6 +212,7 @@ func TestToManyRelationshipsNull(t *testing.T) { expected := []ToManyRelationship{ { + Table: "pilots", Column: "id", Nullable: true, Unique: true, @@ -221,6 +225,7 @@ func TestToManyRelationshipsNull(t *testing.T) { ToJoinTable: false, }, { + Table: "pilots", Column: "id", Nullable: true, Unique: true, @@ -233,6 +238,7 @@ func TestToManyRelationshipsNull(t *testing.T) { ToJoinTable: false, }, { + Table: "pilots", Column: "id", Nullable: true, Unique: true, diff --git a/boil/reflect.go b/boil/reflect.go index dbfb970..bfa5454 100644 --- a/boil/reflect.go +++ b/boil/reflect.go @@ -110,7 +110,7 @@ func (q *Query) BindFast(obj interface{}, titleCases map[string]string) error { // loadRelationships dynamically calls the template generated eager load // functions of the form: // -// func (t *TableLoaded) LoadRelationshipName(exec Executor, singular bool, obj interface{}) +// func (t *TableR) LoadRelationshipName(exec Executor, singular bool, obj interface{}) // // The arguments to this function are: // - t is not considered here, and is always passed nil. The function exists on a loaded @@ -125,11 +125,11 @@ func (q *Query) loadRelationships(obj interface{}, singular bool) error { typ = typ.Elem().Elem() } - rel, found := typ.FieldByName("Loaded") + rel, found := typ.FieldByName("R") // If the users object has no loaded struct, it must be // a custom object and we should not attempt to load any relationships. if !found { - return errors.New("load query mod was used but bound struct contained no Loaded field") + return errors.New("load query mod was used but bound struct contained no R field") } for _, relationship := range q.load { diff --git a/boil/reflect_test.go b/boil/reflect_test.go index 57e2de2..66495b8 100644 --- a/boil/reflect_test.go +++ b/boil/reflect_test.go @@ -166,9 +166,9 @@ func TestBindSingular(t *testing.T) { var loadFunctionCalled bool -type testLoadedStruct struct{} +type testRStruct struct{} -func (r *testLoadedStruct) LoadTestOne(exec Executor, singular bool, obj interface{}) error { +func (r *testRStruct) LoadTestOne(exec Executor, singular bool, obj interface{}) error { loadFunctionCalled = true return nil } @@ -179,7 +179,7 @@ func TestLoadRelationshipsSlice(t *testing.T) { testSlice := []*struct { ID int - Loaded *testLoadedStruct + R *testRStruct }{} q := Query{load: []string{"TestOne"}, executor: nil} @@ -198,7 +198,7 @@ func TestLoadRelationshipsSingular(t *testing.T) { testSingular := struct { ID int - Loaded *testLoadedStruct + R *testRStruct }{} q := Query{load: []string{"TestOne"}, executor: nil} diff --git a/templates/00_struct.tpl b/templates/00_struct.tpl index 50eabbf..b271e0e 100644 --- a/templates/00_struct.tpl +++ b/templates/00_struct.tpl @@ -11,15 +11,15 @@ type {{$modelName}} struct { {{end -}} {{- if .Table.IsJoinTable -}} {{- else}} - Loaded *{{$modelName}}Loaded `boil:"-" json:"-" toml:"-" yaml:"-"` + R *{{$modelName}}R `boil:"-" json:"-" toml:"-" yaml:"-"` {{end -}} } {{- $dot := . -}} {{- if .Table.IsJoinTable -}} {{- else}} -// {{$modelName}}Loaded are where relationships are eagerly loaded. -type {{$modelName}}Loaded struct { +// {{$modelName}}R is where relationships are stored. +type {{$modelName}}R struct { {{range .Table.FKeys -}} {{- $rel := textsFromForeignKey $dot.PkgName $dot.Tables $dot.Table . -}} {{- template "relationship_to_one_struct_helper" $rel}} diff --git a/templates/05_relationship_to_many.tpl b/templates/05_relationship_to_many.tpl index d8eae81..e886258 100644 --- a/templates/05_relationship_to_many.tpl +++ b/templates/05_relationship_to_many.tpl @@ -28,11 +28,11 @@ func ({{$rel.Function.Receiver}} *{{$rel.LocalTable.NameGo}}) {{$rel.Function.Na {{if .ToJoinTable -}} queryMods = append(queryMods, qm.InnerJoin(`"{{.JoinTable}}" as "{{id 1}}" on "{{id 0}}"."{{.ForeignColumn}}" = "{{id 1}}"."{{.JoinForeignColumn}}"`), - qm.Where(`"{{id 1}}"."{{.JoinLocalColumn}}"=$1`, {{.Column | titleCase | printf "%s.%s" $rel.Function.Receiver }}), + qm.Where(`"{{id 1}}"."{{.JoinLocalColumn}}"=$1`, {{$rel.Function.Receiver}}.{{$rel.LocalTable.ColumnNameGo}}), ) {{else -}} queryMods = append(queryMods, - qm.Where(`"{{id 0}}"."{{.ForeignColumn}}"=$1`, {{.Column | titleCase | printf "%s.%s" $rel.Function.Receiver }}), + qm.Where(`"{{id 0}}"."{{.ForeignColumn}}"=$1`, {{$rel.Function.Receiver}}.{{$rel.LocalTable.ColumnNameGo}}), ) {{end}} diff --git a/templates/06_relationship_to_one_eager.tpl b/templates/06_relationship_to_one_eager.tpl index baeecbc..7083366 100644 --- a/templates/06_relationship_to_one_eager.tpl +++ b/templates/06_relationship_to_one_eager.tpl @@ -3,7 +3,7 @@ {{- $slice := printf "%sSlice" .LocalTable.NameGo -}} // Load{{.Function.Name}} allows an eager lookup of values, cached into the // loaded structs of the objects. -func (r *{{.LocalTable.NameGo}}Loaded) Load{{.Function.Name}}(e boil.Executor, singular bool, {{$arg}} interface{}) error { +func (r *{{.LocalTable.NameGo}}R) Load{{.Function.Name}}(e boil.Executor, singular bool, {{$arg}} interface{}) error { var slice []*{{.LocalTable.NameGo}} var object *{{.LocalTable.NameGo}} @@ -45,20 +45,20 @@ func (r *{{.LocalTable.NameGo}}Loaded) Load{{.Function.Name}}(e boil.Executor, s } if singular && len(resultSlice) != 0 { - if object.Loaded == nil { - object.Loaded = &{{.LocalTable.NameGo}}Loaded{} + if object.R == nil { + object.R = &{{.LocalTable.NameGo}}R{} } - object.Loaded.{{.Function.Name}} = resultSlice[0] + object.R.{{.Function.Name}} = resultSlice[0] return nil } for _, foreign := range resultSlice { for _, local := range slice { if local.{{.Function.LocalAssignment}} == foreign.{{.Function.ForeignAssignment}} { - if local.Loaded == nil { - local.Loaded = &{{.LocalTable.NameGo}}Loaded{} + if local.R == nil { + local.R = &{{.LocalTable.NameGo}}R{} } - local.Loaded.{{.Function.Name}} = foreign + local.R.{{.Function.Name}} = foreign break } } diff --git a/templates/07_relationship_to_many_eager.tpl b/templates/07_relationship_to_many_eager.tpl index 986d995..0350990 100644 --- a/templates/07_relationship_to_many_eager.tpl +++ b/templates/07_relationship_to_many_eager.tpl @@ -10,7 +10,7 @@ {{- $slice := printf "%sSlice" $rel.LocalTable.NameGo -}} // Load{{$rel.Function.Name}} allows an eager lookup of values, cached into the // loaded structs of the objects. -func (r *{{$rel.LocalTable.NameGo}}Loaded) Load{{$rel.Function.Name}}(e boil.Executor, singular bool, {{$arg}} interface{}) error { +func (r *{{$rel.LocalTable.NameGo}}R) Load{{$rel.Function.Name}}(e boil.Executor, singular bool, {{$arg}} interface{}) error { var slice []*{{$rel.LocalTable.NameGo}} var object *{{$rel.LocalTable.NameGo}} @@ -82,10 +82,10 @@ func (r *{{$rel.LocalTable.NameGo}}Loaded) Load{{$rel.Function.Name}}(e boil.Exe {{end}} if singular { - if object.Loaded == nil { - object.Loaded = &{{$rel.LocalTable.NameGo}}Loaded{} + if object.R == nil { + object.R = &{{$rel.LocalTable.NameGo}}R{} } - object.Loaded.{{$rel.Function.Name}} = resultSlice + object.R.{{$rel.Function.Name}} = resultSlice return nil } @@ -94,10 +94,10 @@ func (r *{{$rel.LocalTable.NameGo}}Loaded) Load{{$rel.Function.Name}}(e boil.Exe localJoinCol := localJoinCols[i] for _, local := range slice { if local.{{$rel.Function.LocalAssignment}} == localJoinCol { - if local.Loaded == nil { - local.Loaded = &{{$rel.LocalTable.NameGo}}Loaded{} + if local.R == nil { + local.R = &{{$rel.LocalTable.NameGo}}R{} } - local.Loaded.{{$rel.Function.Name}} = append(local.Loaded.{{$rel.Function.Name}}, foreign) + local.R.{{$rel.Function.Name}} = append(local.R.{{$rel.Function.Name}}, foreign) break } } @@ -106,10 +106,10 @@ func (r *{{$rel.LocalTable.NameGo}}Loaded) Load{{$rel.Function.Name}}(e boil.Exe for _, foreign := range resultSlice { for _, local := range slice { if local.{{$rel.Function.LocalAssignment}} == foreign.{{$rel.Function.ForeignAssignment}} { - if local.Loaded == nil { - local.Loaded = &{{$rel.LocalTable.NameGo}}Loaded{} + if local.R == nil { + local.R = &{{$rel.LocalTable.NameGo}}R{} } - local.Loaded.{{$rel.Function.Name}} = append(local.Loaded.{{$rel.Function.Name}}, foreign) + local.R.{{$rel.Function.Name}} = append(local.R.{{$rel.Function.Name}}, foreign) break } } diff --git a/templates/relationship_to_many_setops.tpl b/templates/relationship_to_many_setops.tpl new file mode 100644 index 0000000..2ee47a9 --- /dev/null +++ b/templates/relationship_to_many_setops.tpl @@ -0,0 +1,216 @@ +{{- if .Table.IsJoinTable -}} +{{- else -}} + {{- $dot := . -}} + {{- $table := .Table -}} + {{- range .Table.ToManyRelationships -}} + {{- $varNameSingular := .ForeignTable | singular | camelCase -}} + {{- if (and .ForeignColumnUnique (not .ToJoinTable)) -}} +{{- template "relationship_to_one_setops_helper" (textsFromOneToOneRelationship $dot.PkgName $dot.Tables $table .) -}} + {{- else -}} + {{- $rel := textsFromRelationship $dot.Tables $table .}} + +// Add{{$rel.Function.Name}} adds the given related objects to the existing relationships +// of the {{$table.Name | singular}}, optionally inserting them as new records. +// Appends related to {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}}. +// Sets related.R.{{$rel.Function.ForeignName}} appropriately. +func ({{$rel.Function.Receiver}} *{{$rel.LocalTable.NameGo}}) Add{{$rel.Function.Name}}(exec boil.Executor, insert bool, related ...*{{$rel.ForeignTable.NameGo}}) error { + var err error + for _, rel := range related { + {{if not .ToJoinTable -}} + rel.{{$rel.Function.ForeignAssignment}} = {{$rel.Function.Receiver}}.{{$rel.Function.LocalAssignment}} + {{if .ForeignColumnNullable -}} + rel.{{$rel.ForeignTable.ColumnNameGo}}.Valid = true + {{end -}} + {{end -}} + if insert { + if err = rel.Insert(exec); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + }{{if not .ToJoinTable}} else { + if err = rel.Update(exec, "{{.ForeignColumn}}"); err != nil { + return errors.Wrap(err, "failed to update foreign table") + } + }{{end -}} + } + + {{if .ToJoinTable -}} + for _, rel := range related { + query := `insert into "{{.JoinTable}}" ({{.JoinLocalColumn}}, {{.JoinForeignColumn}}) values ($1, $2)` + values := []interface{}{{"{"}}{{$rel.Function.Receiver}}.{{$rel.LocalTable.ColumnNameGo}}, rel.{{$rel.ForeignTable.ColumnNameGo}}} + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, query) + fmt.Fprintln(boil.DebugWriter, values) + } + + _, err = exec.Exec(query, values...) + if err != nil { + return errors.Wrap(err, "failed to insert into join table") + } + } + {{end -}} + + if {{$rel.Function.Receiver}}.R == nil { + {{$rel.Function.Receiver}}.R = &{{$rel.LocalTable.NameGo}}R{ + {{$rel.Function.Name}}: related, + } + } else { + {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}} = append({{$rel.Function.Receiver}}.R.{{$rel.Function.Name}}, related...) + } + + {{if .ToJoinTable -}} + for _, rel := range related { + if rel.R == nil { + rel.R = &{{$rel.ForeignTable.NameGo}}R{ + {{$rel.Function.ForeignName}}: {{$rel.LocalTable.NameGo}}Slice{{"{"}}{{$rel.Function.Receiver}}{{"}"}}, + } + } else { + rel.R.{{$rel.Function.ForeignName}} = append(rel.R.{{$rel.Function.ForeignName}}, {{$rel.Function.Receiver}}) + } + } + {{else -}} + for _, rel := range related { + if rel.R == nil { + rel.R = &{{$rel.ForeignTable.NameGo}}R{ + {{$rel.Function.ForeignName}}: {{$rel.Function.Receiver}}, + } + } else { + rel.R.{{$rel.Function.ForeignName}} = {{$rel.Function.Receiver}} + } + } + {{end -}} + + return nil +} +{{- if .ForeignColumnNullable}} + +// Set{{$rel.Function.Name}} removes all previously related items of the +// {{$table.Name | singular}} replacing them completely with the passed +// in related items, optionally inserting them as new records. +// Sets {{$rel.Function.Receiver}}.R.{{$rel.Function.ForeignName}}'s {{$rel.Function.Name}} accordingly. +// Replaces {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}} with related. +// Sets related.R.{{$rel.Function.ForeignName}}'s {{$rel.Function.Name}} accordingly. +func ({{$rel.Function.Receiver}} *{{$rel.LocalTable.NameGo}}) Set{{$rel.Function.Name}}(exec boil.Executor, insert bool, related ...*{{$rel.ForeignTable.NameGo}}) error { + {{if .ToJoinTable -}} + query := `delete from "{{.JoinTable}}" where "{{.JoinLocalColumn}}" = $1` + values := []interface{}{{"{"}}{{$rel.Function.Receiver}}.{{$rel.LocalTable.ColumnNameGo}}} + {{else -}} + query := `update "{{.ForeignTable}}" set "{{.ForeignColumn}}" = null where "{{.ForeignColumn}}" = $1` + values := []interface{}{{"{"}}{{$rel.Function.Receiver}}.{{$rel.LocalTable.ColumnNameGo}}} + {{end -}} + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, query) + fmt.Fprintln(boil.DebugWriter, values) + } + + _, err := exec.Exec(query, values...) + if err != nil { + return errors.Wrap(err, "failed to remove relationships before set") + } + + {{if .ToJoinTable -}} + remove{{$rel.LocalTable.NameGo}}From{{$rel.ForeignTable.NameGo}}Slice({{$rel.Function.Receiver}}, related) + {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}} = nil + {{else -}} + if {{$rel.Function.Receiver}}.R != nil { + for _, rel := range {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}} { + rel.{{$rel.ForeignTable.ColumnNameGo}}.Valid = false + if rel.R == nil { + continue + } + + rel.R.{{$rel.Function.ForeignName}} = nil + } + + {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}} = nil + } + {{end -}} + + return {{$rel.Function.Receiver}}.Add{{$rel.Function.Name}}(exec, insert, related...) +} + +// Remove{{$rel.Function.Name}} relationships from objects passed in. +// Removes related items from R.{{$rel.Function.Name}} (uses pointer comparison, removal does not keep order) +// Sets related.R.{{$rel.Function.ForeignName}}. +func ({{$rel.Function.Receiver}} *{{$rel.LocalTable.NameGo}}) Remove{{$rel.Function.Name}}(exec boil.Executor, related ...*{{$rel.ForeignTable.NameGo}}) error { + var err error + {{if .ToJoinTable -}} + query := fmt.Sprintf( + `delete from "{{.JoinTable}}" where "{{.JoinLocalColumn}}" = $1 and "{{.JoinForeignColumn}}" in (%s)`, + strmangle.Placeholders(len(related), 1, 1), + ) + values := []interface{}{{"{"}}{{$rel.Function.Receiver}}.{{$rel.LocalTable.ColumnNameGo}}} + + if boil.DebugMode { + fmt.Fprintln(boil.DebugWriter, query) + fmt.Fprintln(boil.DebugWriter, values) + } + + _, err = exec.Exec(query, values...) + if err != nil { + return errors.Wrap(err, "failed to remove relationships before set") + } + {{else -}} + for _, rel := range related { + rel.{{$rel.ForeignTable.ColumnNameGo}}.Valid = false + {{if not .ToJoinTable -}} + if rel.R != nil { + rel.R.{{$rel.Function.ForeignName}} = nil + } + {{end -}} + if err = rel.Update(exec, "{{.ForeignColumn}}"); err != nil { + return err + } + } + {{end -}} + + {{if .ToJoinTable -}} + remove{{$rel.LocalTable.NameGo}}From{{$rel.ForeignTable.NameGo}}Slice({{$rel.Function.Receiver}}, related) + {{end -}} + if {{$rel.Function.Receiver}}.R == nil { + return nil + } + + for _, rel := range related { + for i, ri := range {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}} { + if rel != ri { + continue + } + + ln := len({{$rel.Function.Receiver}}.R.{{$rel.Function.Name}}) + if ln > 1 && i < ln-1 { + {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}}[i] = {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}}[ln-1] + } + {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}} = {{$rel.Function.Receiver}}.R.{{$rel.Function.Name}}[:ln-1] + break + } + } + + return nil +} + +{{if .ToJoinTable -}} +func remove{{$rel.LocalTable.NameGo}}From{{$rel.ForeignTable.NameGo}}Slice({{$rel.Function.Receiver}} *{{$rel.LocalTable.NameGo}}, related []*{{$rel.ForeignTable.NameGo}}) { + for _, rel := range related { + if rel.R == nil { + continue + } + for i, ri := range rel.R.{{$rel.Function.ForeignName}} { + if {{$rel.Function.Receiver}}.{{$rel.Function.LocalAssignment}} != ri.{{$rel.Function.LocalAssignment}} { + continue + } + + ln := len(rel.R.{{$rel.Function.ForeignName}}) + if ln > 1 && i < ln-1 { + rel.R.{{$rel.Function.ForeignName}}[i] = rel.R.{{$rel.Function.ForeignName}}[ln-1] + } + rel.R.{{$rel.Function.ForeignName}} = rel.R.{{$rel.Function.ForeignName}}[:ln-1] + break + } + } +} +{{end -}}{{- /* if join table */ -}} +{{- end -}}{{- /* if nullable foreign key */ -}} +{{- end -}}{{- /* if unique foreign key */ -}} +{{- end -}}{{- /* range relationships */ -}} +{{- end -}}{{- /* outer if join table */ -}} diff --git a/templates/relationship_to_one_setops.tpl b/templates/relationship_to_one_setops.tpl new file mode 100644 index 0000000..afd24aa --- /dev/null +++ b/templates/relationship_to_one_setops.tpl @@ -0,0 +1,103 @@ +{{- define "relationship_to_one_setops_helper" -}} +{{- $varNameSingular := .ForeignKey.ForeignTable | singular | camelCase}} + +// Set{{.Function.Name}} of the {{.ForeignKey.Table | singular}} to the related item. +// Sets {{.Function.Receiver}}.R.{{.Function.Name}} to related. +// Adds {{.Function.Receiver}} to related.R.{{.Function.ForeignName}}. +func ({{.Function.Receiver}} *{{.LocalTable.NameGo}}) Set{{.Function.Name}}(exec boil.Executor, insert bool, related *{{.ForeignTable.NameGo}}) error { + var err error + if insert { + if err = related.Insert(exec); err != nil { + return errors.Wrap(err, "failed to insert into foreign table") + } + } + + oldVal := {{.Function.Receiver}}.{{.LocalTable.ColumnNameGo}} + {{.Function.Receiver}}.{{.Function.LocalAssignment}} = related.{{.Function.ForeignAssignment}} + if err = {{.Function.Receiver}}.Update(exec, "{{.ForeignKey.Column}}"); err != nil { + {{.Function.Receiver}}.{{.LocalTable.ColumnNameGo}} = oldVal + return errors.Wrap(err, "failed to update local table") + } + + if {{.Function.Receiver}}.R == nil { + {{.Function.Receiver}}.R = &{{.LocalTable.NameGo}}R{ + {{.Function.Name}}: related, + } + } else { + {{.Function.Receiver}}.R.{{.Function.Name}} = related + } + + {{if (or .ForeignKey.Unique .Function.OneToOne) -}} + if related.R == nil { + related.R = &{{.ForeignTable.NameGo}}R{ + {{.Function.ForeignName}}: {{.Function.Receiver}}, + } + } else { + related.R.{{.Function.ForeignName}} = {{.Function.Receiver}} + } + {{else -}} + if related.R == nil { + related.R = &{{.ForeignTable.NameGo}}R{ + {{.Function.ForeignName}}: {{.LocalTable.NameGo}}Slice{{"{"}}{{.Function.Receiver}}{{"}"}}, + } + } else { + related.R.{{.Function.ForeignName}} = append(related.R.{{.Function.ForeignName}}, {{.Function.Receiver}}) + } + {{end -}} + + {{if .ForeignKey.Nullable}} + {{.Function.Receiver}}.{{.LocalTable.ColumnNameGo}}.Valid = true + {{end -}} + return nil +} +{{- if .ForeignKey.Nullable}} + +// Remove{{.Function.Name}} relationship. +// Sets {{.Function.Receiver}}.R.{{.Function.Name}} to nil. +// Removes {{.Function.Receiver}} from all passed in related items' relationships struct (Optional). +func ({{.Function.Receiver}} *{{.LocalTable.NameGo}}) Remove{{.Function.Name}}(exec boil.Executor, related ...*{{.ForeignTable.NameGo}}) error { + var err error + + {{.Function.Receiver}}.{{.LocalTable.ColumnNameGo}}.Valid = false + if err = {{.Function.Receiver}}.Update(exec, "{{.ForeignKey.Column}}"); err != nil { + {{.Function.Receiver}}.{{.LocalTable.ColumnNameGo}}.Valid = true + return errors.Wrap(err, "failed to update local table") + } + + for _, rel := range related { + if rel.R == nil { + continue + } + + {{if .ForeignKey.Unique -}} + rel.R.{{.Function.ForeignName}} = nil + {{else -}} + for i, ri := range rel.R.{{.Function.ForeignName}} { + if {{.Function.Receiver}}.{{.Function.LocalAssignment}} != ri.{{.Function.LocalAssignment}} { + continue + } + + ln := len(rel.R.{{.Function.ForeignName}}) + if ln > 1 && i < ln-1 { + rel.R.{{.Function.ForeignName}}[i], rel.R.{{.Function.ForeignName}}[ln-1] = + rel.R.{{.Function.ForeignName}}[ln-1], rel.R.{{.Function.ForeignName}}[i] + } + rel.R.{{.Function.ForeignName}} = rel.R.{{.Function.ForeignName}}[:ln-1] + break + } + {{end -}} + } + + {{.Function.Receiver}}.R.{{.Function.Name}} = nil + return nil +} +{{end -}} +{{- end -}} +{{- if .Table.IsJoinTable -}} +{{- else -}} + {{- $dot := . -}} + {{- range .Table.FKeys -}} + {{- $rel := textsFromForeignKey $dot.PkgName $dot.Tables $dot.Table . -}} +{{- template "relationship_to_one_setops_helper" $rel -}} +{{- end -}} +{{- end -}} diff --git a/templates_test/relationship_to_many.tpl b/templates_test/relationship_to_many.tpl index 8466fc7..83164e8 100644 --- a/templates_test/relationship_to_many.tpl +++ b/templates_test/relationship_to_many.tpl @@ -41,11 +41,11 @@ func test{{$rel.LocalTable.NameGo}}ToMany{{$rel.Function.Name}}(t *testing.T) { } {{if .ToJoinTable -}} - _, err = tx.Exec(`insert into {{.JoinTable}} ({{.JoinLocalColumn}}, {{.JoinForeignColumn}}) values ($1, $2)`, a.{{.Column | titleCase}}, b.{{.ForeignColumn | titleCase}}) + _, err = tx.Exec(`insert into "{{.JoinTable}}" ({{.JoinLocalColumn}}, {{.JoinForeignColumn}}) values ($1, $2)`, a.{{$rel.LocalTable.ColumnNameGo}}, b.{{$rel.ForeignTable.ColumnNameGo}}) if err != nil { t.Fatal(err) } - _, err = tx.Exec(`insert into {{.JoinTable}} ({{.JoinLocalColumn}}, {{.JoinForeignColumn}}) values ($1, $2)`, a.{{.Column | titleCase}}, c.{{.ForeignColumn | titleCase}}) + _, err = tx.Exec(`insert into "{{.JoinTable}}" ({{.JoinLocalColumn}}, {{.JoinForeignColumn}}) values ($1, $2)`, a.{{$rel.LocalTable.ColumnNameGo}}, c.{{$rel.ForeignTable.ColumnNameGo}}) if err != nil { t.Fatal(err) } @@ -75,18 +75,18 @@ func test{{$rel.LocalTable.NameGo}}ToMany{{$rel.Function.Name}}(t *testing.T) { } slice := {{$rel.LocalTable.NameGo}}Slice{&a} - if err = a.Loaded.Load{{$rel.Function.Name}}(tx, false, &slice); err != nil { + if err = a.R.Load{{$rel.Function.Name}}(tx, false, &slice); err != nil { t.Fatal(err) } - if got := len(a.Loaded.{{$rel.Function.Name}}); got != 2 { + if got := len(a.R.{{$rel.Function.Name}}); got != 2 { t.Error("number of eager loaded records wrong, got:", got) } - a.Loaded.{{$rel.Function.Name}} = nil - if err = a.Loaded.Load{{$rel.Function.Name}}(tx, true, &a); err != nil { + a.R.{{$rel.Function.Name}} = nil + if err = a.R.Load{{$rel.Function.Name}}(tx, true, &a); err != nil { t.Fatal(err) } - if got := len(a.Loaded.{{$rel.Function.Name}}); got != 2 { + if got := len(a.R.{{$rel.Function.Name}}); got != 2 { t.Error("number of eager loaded records wrong, got:", got) } diff --git a/templates_test/relationship_to_many_setops.tpl b/templates_test/relationship_to_many_setops.tpl new file mode 100644 index 0000000..b6fd0f7 --- /dev/null +++ b/templates_test/relationship_to_many_setops.tpl @@ -0,0 +1,308 @@ +{{- if .Table.IsJoinTable -}} +{{- else -}} + {{- $dot := . -}} + {{- $table := .Table -}} + {{- range .Table.ToManyRelationships -}} + {{- if (and .ForeignColumnUnique (not .ToJoinTable)) -}} +{{- template "relationship_to_one_setops_test_helper" (textsFromOneToOneRelationship $dot.PkgName $dot.Tables $table .) -}} + {{- else -}} + {{- $varNameSingular := .Table | singular | camelCase -}} + {{- $foreignVarNameSingular := .ForeignTable | singular | camelCase -}} + {{- $rel := textsFromRelationship $dot.Tables $table .}} + +func test{{$rel.LocalTable.NameGo}}ToManyAddOp{{$rel.Function.Name}}(t *testing.T) { + var err error + + tx := MustTx(boil.Begin()) + defer tx.Rollback() + + var a {{$rel.LocalTable.NameGo}} + var b, c, d, e {{$rel.ForeignTable.NameGo}} + + seed := randomize.NewSeed() + if err = randomize.Struct(seed, &a, {{$varNameSingular}}DBTypes, false, {{$varNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + foreigners := []*{{$rel.ForeignTable.NameGo}}{&b, &c, &d, &e} + for _, x := range foreigners { + if err = randomize.Struct(seed, x, {{$foreignVarNameSingular}}DBTypes, false, {{$foreignVarNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + } + + if err := a.Insert(tx); err != nil { + t.Fatal(err) + } + if err = b.Insert(tx); err != nil { + t.Fatal(err) + } + if err = c.Insert(tx); err != nil { + t.Fatal(err) + } + + foreignersSplitByInsertion := [][]*{{$rel.ForeignTable.NameGo}}{ + {&b, &c}, + {&d, &e}, + } + + for i, x := range foreignersSplitByInsertion { + err = a.Add{{$rel.Function.Name}}(tx, i != 0, x...) + if err != nil { + t.Fatal(err) + } + + first := x[0] + second := x[1] + {{- if .ToJoinTable}} + + if first.R.{{$rel.Function.ForeignName}}[0] != &a { + t.Error("relationship was not added properly to the slice") + } + if second.R.{{$rel.Function.ForeignName}}[0] != &a { + t.Error("relationship was not added properly to the slice") + } + {{- else}} + + if a.{{$rel.Function.LocalAssignment}} != first.{{$rel.Function.ForeignAssignment}} { + t.Error("foreign key was wrong value", a.{{$rel.Function.LocalAssignment}}, first.{{$rel.Function.ForeignAssignment}}) + } + if a.{{$rel.Function.LocalAssignment}} != second.{{$rel.Function.ForeignAssignment}} { + t.Error("foreign key was wrong value", a.{{$rel.Function.LocalAssignment}}, second.{{$rel.Function.ForeignAssignment}}) + } + + if first.R.{{$rel.Function.ForeignName}} != &a { + t.Error("relationship was not added properly to the foreign slice") + } + if second.R.{{$rel.Function.ForeignName}} != &a { + t.Error("relationship was not added properly to the foreign slice") + } + {{- end}} + + if a.R.{{$rel.Function.Name}}[i*2] != first { + t.Error("relationship struct slice not set to correct value") + } + if a.R.{{$rel.Function.Name}}[i*2+1] != second { + t.Error("relationship struct slice not set to correct value") + } + + count, err := a.{{$rel.Function.Name}}(tx).Count() + if err != nil { + t.Fatal(err) + } + if want := int64((i+1)*2); count != want { + t.Error("want", want, "got", count) + } + } +} +{{if .ForeignColumnNullable}} + +func test{{$rel.LocalTable.NameGo}}ToManySetOp{{$rel.Function.Name}}(t *testing.T) { + var err error + + tx := MustTx(boil.Begin()) + defer tx.Rollback() + + var a {{$rel.LocalTable.NameGo}} + var b, c, d, e {{$rel.ForeignTable.NameGo}} + + seed := randomize.NewSeed() + if err = randomize.Struct(seed, &a, {{$varNameSingular}}DBTypes, false, {{$varNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + foreigners := []*{{$rel.ForeignTable.NameGo}}{&b, &c, &d, &e} + for _, x := range foreigners { + if err = randomize.Struct(seed, x, {{$foreignVarNameSingular}}DBTypes, false, {{$foreignVarNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + } + + if err = a.Insert(tx); err != nil { + t.Fatal(err) + } + if err = b.Insert(tx); err != nil { + t.Fatal(err) + } + if err = c.Insert(tx); err != nil { + t.Fatal(err) + } + + err = a.Set{{$rel.Function.Name}}(tx, false, &b, &c) + if err != nil { + t.Fatal(err) + } + + count, err := a.{{$rel.Function.Name}}(tx).Count() + if err != nil { + t.Fatal(err) + } + if count != 2 { + t.Error("count was wrong:", count) + } + + err = a.Set{{$rel.Function.Name}}(tx, true, &d, &e) + if err != nil { + t.Fatal(err) + } + + count, err = a.{{$rel.Function.Name}}(tx).Count() + if err != nil { + t.Fatal(err) + } + if count != 2 { + t.Error("count was wrong:", count) + } + + {{- if .ToJoinTable}} + + if len(b.R.{{$rel.Function.ForeignName}}) != 0 { + t.Error("relationship was not removed properly from the slice") + } + if len(c.R.{{$rel.Function.ForeignName}}) != 0 { + t.Error("relationship was not removed properly from the slice") + } + if d.R.{{$rel.Function.ForeignName}}[0] != &a { + t.Error("relationship was not added properly to the slice") + } + if e.R.{{$rel.Function.ForeignName}}[0] != &a { + t.Error("relationship was not added properly to the slice") + } + {{- else}} + + if b.{{$rel.ForeignTable.ColumnNameGo}}.Valid { + t.Error("want b's foreign key value to be nil") + } + if c.{{$rel.ForeignTable.ColumnNameGo}}.Valid { + t.Error("want c's foreign key value to be nil") + } + if a.{{$rel.Function.LocalAssignment}} != d.{{$rel.Function.ForeignAssignment}} { + t.Error("foreign key was wrong value", a.{{$rel.Function.LocalAssignment}}, d.{{$rel.Function.ForeignAssignment}}) + } + if a.{{$rel.Function.LocalAssignment}} != e.{{$rel.Function.ForeignAssignment}} { + t.Error("foreign key was wrong value", a.{{$rel.Function.LocalAssignment}}, e.{{$rel.Function.ForeignAssignment}}) + } + + if b.R.{{$rel.Function.ForeignName}} != nil { + t.Error("relationship was not removed properly from the foreign struct") + } + if c.R.{{$rel.Function.ForeignName}} != nil { + t.Error("relationship was not removed properly from the foreign struct") + } + if d.R.{{$rel.Function.ForeignName}} != &a { + t.Error("relationship was not added properly to the foreign struct") + } + if e.R.{{$rel.Function.ForeignName}} != &a { + t.Error("relationship was not added properly to the foreign struct") + } + {{- end}} + + if a.R.{{$rel.Function.Name}}[0] != &d { + t.Error("relationship struct slice not set to correct value") + } + if a.R.{{$rel.Function.Name}}[1] != &e { + t.Error("relationship struct slice not set to correct value") + } +} + +func test{{$rel.LocalTable.NameGo}}ToManyRemoveOp{{$rel.Function.Name}}(t *testing.T) { + var err error + + tx := MustTx(boil.Begin()) + defer tx.Rollback() + + var a {{$rel.LocalTable.NameGo}} + var b, c, d, e {{$rel.ForeignTable.NameGo}} + + seed := randomize.NewSeed() + if err = randomize.Struct(seed, &a, {{$varNameSingular}}DBTypes, false, {{$varNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + foreigners := []*{{$rel.ForeignTable.NameGo}}{&b, &c, &d, &e} + for _, x := range foreigners { + if err = randomize.Struct(seed, x, {{$foreignVarNameSingular}}DBTypes, false, {{$foreignVarNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + } + + if err := a.Insert(tx); err != nil { + t.Fatal(err) + } + + err = a.Add{{$rel.Function.Name}}(tx, true, foreigners...) + if err != nil { + t.Fatal(err) + } + + count, err := a.{{$rel.Function.Name}}(tx).Count() + if err != nil { + t.Fatal(err) + } + if count != 4 { + t.Error("count was wrong:", count) + } + + err = a.Remove{{$rel.Function.Name}}(tx, foreigners[:2]...) + if err != nil { + t.Fatal(err) + } + + count, err = a.{{$rel.Function.Name}}(tx).Count() + if err != nil { + t.Fatal(err) + } + if count != 2 { + t.Error("count was wrong:", count) + } + + {{- if .ToJoinTable}} + + if len(b.R.{{$rel.Function.ForeignName}}) != 0 { + t.Error("relationship was not removed properly from the slice") + } + if len(c.R.{{$rel.Function.ForeignName}}) != 0 { + t.Error("relationship was not removed properly from the slice") + } + if d.R.{{$rel.Function.ForeignName}}[0] != &a { + t.Error("relationship was not added properly to the foreign struct") + } + if e.R.{{$rel.Function.ForeignName}}[0] != &a { + t.Error("relationship was not added properly to the foreign struct") + } + {{- else}} + + if b.{{$rel.ForeignTable.ColumnNameGo}}.Valid { + t.Error("want b's foreign key value to be nil") + } + if c.{{$rel.ForeignTable.ColumnNameGo}}.Valid { + t.Error("want c's foreign key value to be nil") + } + + if b.R.{{$rel.Function.ForeignName}} != nil { + t.Error("relationship was not removed properly from the foreign struct") + } + if c.R.{{$rel.Function.ForeignName}} != nil { + t.Error("relationship was not removed properly from the foreign struct") + } + if d.R.{{$rel.Function.ForeignName}} != &a { + t.Error("relationship to a should have been preserved") + } + if e.R.{{$rel.Function.ForeignName}} != &a { + t.Error("relationship to a should have been preserved") + } + {{- end}} + + if len(a.R.{{$rel.Function.Name}}) != 2 { + t.Error("should have preserved two relationships") + } + + // Removal doesn't do a stable deletion for performance so we have to flip the order + if a.R.{{$rel.Function.Name}}[1] != &d { + t.Error("relationship to d should have been preserved") + } + if a.R.{{$rel.Function.Name}}[0] != &e { + t.Error("relationship to e should have been preserved") + } +} +{{end -}} +{{- end -}}{{- /* if unique foreign key */ -}} +{{- end -}}{{- /* range relationships */ -}} +{{- end -}}{{- /* outer if join table */ -}} diff --git a/templates_test/relationship_to_one.tpl b/templates_test/relationship_to_one.tpl index b33890d..c19b431 100644 --- a/templates_test/relationship_to_one.tpl +++ b/templates_test/relationship_to_one.tpl @@ -12,7 +12,7 @@ func test{{.LocalTable.NameGo}}ToOne{{.ForeignTable.NameGo}}_{{.Function.Name}}( foreign.{{.ForeignKey.ForeignColumn | titleCase}}.Valid = true {{end}} - {{if not .Function.ReverseInserts -}} + {{if not .Function.OneToOne -}} if err := foreign.Insert(tx); err != nil { t.Fatal(err) } @@ -42,18 +42,18 @@ func test{{.LocalTable.NameGo}}ToOne{{.ForeignTable.NameGo}}_{{.Function.Name}}( } slice := {{.LocalTable.NameGo}}Slice{&local} - if err = local.Loaded.Load{{.Function.Name}}(tx, false, &slice); err != nil { + if err = local.R.Load{{.Function.Name}}(tx, false, &slice); err != nil { t.Fatal(err) } - if local.Loaded.{{.Function.Name}} == nil { + if local.R.{{.Function.Name}} == nil { t.Error("struct should have been eager loaded") } - local.Loaded.{{.Function.Name}} = nil - if err = local.Loaded.Load{{.Function.Name}}(tx, true, &local); err != nil { + local.R.{{.Function.Name}} = nil + if err = local.R.Load{{.Function.Name}}(tx, true, &local); err != nil { t.Fatal(err) } - if local.Loaded.{{.Function.Name}} == nil { + if local.R.{{.Function.Name}} == nil { t.Error("struct should have been eager loaded") } } diff --git a/templates_test/relationship_to_one_setops.tpl b/templates_test/relationship_to_one_setops.tpl new file mode 100644 index 0000000..ab27db5 --- /dev/null +++ b/templates_test/relationship_to_one_setops.tpl @@ -0,0 +1,133 @@ +{{- define "relationship_to_one_setops_test_helper" -}} +{{- $varNameSingular := .ForeignKey.Table | singular | camelCase -}} +{{- $foreignVarNameSingular := .ForeignKey.ForeignTable | singular | camelCase -}} +func test{{.LocalTable.NameGo}}ToOneSetOp{{.ForeignTable.NameGo}}_{{.Function.Name}}(t *testing.T) { + var err error + + tx := MustTx(boil.Begin()) + defer tx.Rollback() + + var a {{.LocalTable.NameGo}} + var b, c {{.ForeignTable.NameGo}} + + seed := randomize.NewSeed() + if err = randomize.Struct(seed, &a, {{$varNameSingular}}DBTypes, false, {{$varNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + if err = randomize.Struct(seed, &b, {{$foreignVarNameSingular}}DBTypes, false, {{$foreignVarNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + if err = randomize.Struct(seed, &c, {{$foreignVarNameSingular}}DBTypes, false, {{$foreignVarNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + + if err := a.Insert(tx); err != nil { + t.Fatal(err) + } + if err = b.Insert(tx); err != nil { + t.Fatal(err) + } + + for i, x := range []*{{.ForeignTable.NameGo}}{&b, &c} { + err = a.Set{{.Function.Name}}(tx, i != 0, x) + if err != nil { + t.Fatal(err) + } + + if a.{{.Function.LocalAssignment}} != x.{{.Function.ForeignAssignment}} { + t.Error("foreign key was wrong value", a.{{.Function.LocalAssignment}}) + } + if a.R.{{.Function.Name}} != x { + t.Error("relationship struct not set to correct value") + } + + zero := reflect.Zero(reflect.TypeOf(a.{{.Function.LocalAssignment}})) + reflect.Indirect(reflect.ValueOf(&a.{{.Function.LocalAssignment}})).Set(zero) + + if err = a.Reload(tx); err != nil { + t.Fatal("failed to reload", err) + } + + if a.{{.Function.LocalAssignment}} != x.{{.Function.ForeignAssignment}} { + t.Error("foreign key was wrong value", a.{{.Function.LocalAssignment}}, x.{{.Function.ForeignAssignment}}) + } + + {{if .ForeignKey.Unique -}} + if x.R.{{.Function.ForeignName}} != &a { + t.Error("failed to append to foreign relationship struct") + } + {{else -}} + if x.R.{{.Function.ForeignName}}[0] != &a { + t.Error("failed to append to foreign relationship struct") + } + {{end -}} + } +} +{{- if .ForeignKey.Nullable}} + +func test{{.LocalTable.NameGo}}ToOneRemoveOp{{.ForeignTable.NameGo}}_{{.Function.Name}}(t *testing.T) { + var err error + + tx := MustTx(boil.Begin()) + defer tx.Rollback() + + var a {{.LocalTable.NameGo}} + var b {{.ForeignTable.NameGo}} + + seed := randomize.NewSeed() + if err = randomize.Struct(seed, &a, {{$varNameSingular}}DBTypes, false, {{$varNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + if err = randomize.Struct(seed, &b, {{$foreignVarNameSingular}}DBTypes, false, {{$foreignVarNameSingular}}PrimaryKeyColumns...); err != nil { + t.Fatal(err) + } + + if err = a.Insert(tx); err != nil { + t.Fatal(err) + } + + if err = a.Set{{.Function.Name}}(tx, true, &b); err != nil { + t.Fatal(err) + } + + if err = a.Remove{{.Function.Name}}(tx, &b); err != nil { + t.Error("failed to remove relationship") + } + + count, err := a.{{.Function.Name}}(tx).Count() + if err != nil { + t.Error(err) + } + if count != 0 { + t.Error("want no relationships remaining") + } + + if a.R.{{.Function.Name}} != nil { + t.Error("R struct entry should be nil") + } + + if a.{{.LocalTable.ColumnNameGo}}.Valid { + t.Error("R struct entry should be nil") + } + + {{if .ForeignKey.Unique -}} + if b.R.{{.Function.ForeignName}} != nil { + t.Error("failed to remove a from b's relationships") + } + {{else -}} + if len(b.R.{{.Function.ForeignName}}) != 0 { + t.Error("failed to remove a from b's relationships") + } + {{end -}} +} +{{end -}} +{{- end -}} +{{- if .Table.IsJoinTable -}} +{{- else -}} + {{- $dot := . -}} + {{- range .Table.FKeys -}} + {{- $rel := textsFromForeignKey $dot.PkgName $dot.Tables $dot.Table .}} + +{{template "relationship_to_one_setops_test_helper" $rel -}} +{{- end -}} +{{- end -}} diff --git a/templates_test/singleton/boil_suites_test.tpl b/templates_test/singleton/boil_suites_test.tpl index 0ac193a..fc7b3c7 100644 --- a/templates_test/singleton/boil_suites_test.tpl +++ b/templates_test/singleton/boil_suites_test.tpl @@ -138,8 +138,23 @@ func TestInsert(t *testing.T) { {{- end -}} } +// TestToOne tests cannot be run in parallel +// or deadlocks can occur. +func TestToOne(t *testing.T) { + {{- $dot := . -}} +{{- range $index, $table := .Tables}} + {{- if $table.IsJoinTable -}} + {{- else -}} + {{- range $table.FKeys -}} + {{- $rel := textsFromForeignKey $dot.PkgName $dot.Tables $table . -}} + t.Run("{{$rel.LocalTable.NameGo}}To{{$rel.ForeignTable.NameGo}}_{{$rel.Function.Name}}", test{{$rel.LocalTable.NameGo}}ToOne{{$rel.ForeignTable.NameGo}}_{{$rel.Function.Name}}) + {{end -}}{{- /* fkey range */ -}} + {{- end -}}{{- /* if join table */ -}} +{{- end -}}{{- /* tables range */ -}} +} + // TestToMany tests cannot be run in parallel -// or postgres deadlocks will occur. +// or deadlocks can occur. func TestToMany(t *testing.T) { {{- $dot := .}} {{- range $index, $table := .Tables}} @@ -159,21 +174,105 @@ func TestToMany(t *testing.T) { {{- end -}}{{- /* outer tables range */ -}} } -// TestToOne tests cannot be run in parallel -// or postgres deadlocks will occur. -func TestToOne(t *testing.T) { +// TestToOneSet tests cannot be run in parallel +// or deadlocks can occur. +func TestToOneSet(t *testing.T) { {{- $dot := . -}} {{- range $index, $table := .Tables}} {{- if $table.IsJoinTable -}} {{- else -}} {{- range $table.FKeys -}} {{- $rel := textsFromForeignKey $dot.PkgName $dot.Tables $table . -}} - t.Run("{{$rel.LocalTable.NameGo}}To{{$rel.ForeignTable.NameGo}}_{{$rel.Function.Name}}", test{{$rel.LocalTable.NameGo}}ToOne{{$rel.ForeignTable.NameGo}}_{{$rel.Function.Name}}) + t.Run("{{$rel.LocalTable.NameGo}}To{{$rel.ForeignTable.NameGo}}_{{$rel.Function.Name}}", test{{$rel.LocalTable.NameGo}}ToOneSetOp{{$rel.ForeignTable.NameGo}}_{{$rel.Function.Name}}) {{end -}}{{- /* fkey range */ -}} {{- end -}}{{- /* if join table */ -}} {{- end -}}{{- /* tables range */ -}} } +// TestToOneRemove tests cannot be run in parallel +// or deadlocks can occur. +func TestToOneRemove(t *testing.T) { + {{- $dot := . -}} +{{- range $index, $table := .Tables}} + {{- if $table.IsJoinTable -}} + {{- else -}} + {{- range $table.FKeys -}} + {{- $rel := textsFromForeignKey $dot.PkgName $dot.Tables $table . -}} + {{- if $rel.ForeignKey.Nullable -}} + t.Run("{{$rel.LocalTable.NameGo}}To{{$rel.ForeignTable.NameGo}}_{{$rel.Function.Name}}", test{{$rel.LocalTable.NameGo}}ToOneRemoveOp{{$rel.ForeignTable.NameGo}}_{{$rel.Function.Name}}) + {{end -}}{{- /* if foreign key nullable */ -}} + {{- end -}}{{- /* fkey range */ -}} + {{- end -}}{{- /* if join table */ -}} +{{- end -}}{{- /* tables range */ -}} +} + +// TestToManyAdd tests cannot be run in parallel +// or deadlocks can occur. +func TestToManyAdd(t *testing.T) { + {{- $dot := .}} + {{- range $index, $table := .Tables}} + {{- $tableName := $table.Name | plural | titleCase -}} + {{- if $table.IsJoinTable -}} + {{- else -}} + {{- range $table.ToManyRelationships -}} + {{- $rel := textsFromRelationship $dot.Tables $table . -}} + {{- if (and .ForeignColumnUnique (not .ToJoinTable)) -}} + {{- else -}} + t.Run("{{$rel.LocalTable.NameGo}}ToMany{{$rel.Function.Name}}", test{{$rel.LocalTable.NameGo}}ToManyAddOp{{$rel.Function.Name}}) + {{end -}}{{- /* if unique */ -}} + {{- end -}}{{- /* range */ -}} + {{- end -}}{{- /* outer if join table */ -}} + {{- end -}}{{- /* outer tables range */ -}} +} + +// TestToManySet tests cannot be run in parallel +// or deadlocks can occur. +func TestToManySet(t *testing.T) { + {{- $dot := .}} + {{- range $index, $table := .Tables}} + {{- $tableName := $table.Name | plural | titleCase -}} + {{- if $table.IsJoinTable -}} + {{- else -}} + {{- range $table.ToManyRelationships -}} + {{- if not .ForeignColumnNullable -}} + {{- else -}} + {{- $rel := textsFromRelationship $dot.Tables $table . -}} + {{- if (and .ForeignColumnUnique (not .ToJoinTable)) -}} + {{- $oneToOne := textsFromOneToOneRelationship $dot.PkgName $dot.Tables $table . -}} + t.Run("{{$oneToOne.LocalTable.NameGo}}OneToOne{{$oneToOne.ForeignTable.NameGo}}_{{$oneToOne.Function.Name}}", test{{$oneToOne.LocalTable.NameGo}}ToOneSetOp{{$oneToOne.ForeignTable.NameGo}}_{{$oneToOne.Function.Name}}) + {{else -}} + t.Run("{{$rel.LocalTable.NameGo}}ToMany{{$rel.Function.Name}}", test{{$rel.LocalTable.NameGo}}ToManySetOp{{$rel.Function.Name}}) + {{end -}}{{- /* if unique */ -}} + {{- end -}}{{- /* if foreign column nullable */ -}} + {{- end -}}{{- /* range */ -}} + {{- end -}}{{- /* outer if join table */ -}} + {{- end -}}{{- /* outer tables range */ -}} +} + +// TestToManyRemove tests cannot be run in parallel +// or deadlocks can occur. +func TestToManyRemove(t *testing.T) { + {{- $dot := .}} + {{- range $index, $table := .Tables}} + {{- $tableName := $table.Name | plural | titleCase -}} + {{- if $table.IsJoinTable -}} + {{- else -}} + {{- range $table.ToManyRelationships -}} + {{- if not .ForeignColumnNullable -}} + {{- else -}} + {{- $rel := textsFromRelationship $dot.Tables $table . -}} + {{- if (and .ForeignColumnUnique (not .ToJoinTable)) -}} + {{- $oneToOne := textsFromOneToOneRelationship $dot.PkgName $dot.Tables $table . -}} + t.Run("{{$oneToOne.LocalTable.NameGo}}OneToOne{{$oneToOne.ForeignTable.NameGo}}_{{$oneToOne.Function.Name}}", test{{$oneToOne.LocalTable.NameGo}}ToOneRemoveOp{{$oneToOne.ForeignTable.NameGo}}_{{$oneToOne.Function.Name}}) + {{else -}} + t.Run("{{$rel.LocalTable.NameGo}}ToMany{{$rel.Function.Name}}", test{{$rel.LocalTable.NameGo}}ToManyRemoveOp{{$rel.Function.Name}}) + {{end -}}{{- /* if unique */ -}} + {{- end -}}{{- /* if foreign column nullable */ -}} + {{- end -}}{{- /* range */ -}} + {{- end -}}{{- /* outer if join table */ -}} + {{- end -}}{{- /* outer tables range */ -}} +} + func TestReload(t *testing.T) { {{- range $index, $table := .Tables}} {{- if $table.IsJoinTable -}} diff --git a/text_helpers.go b/text_helpers.go index 29a991b..0b0b11b 100644 --- a/text_helpers.go +++ b/text_helpers.go @@ -28,10 +28,11 @@ type RelationshipToOneTexts struct { Function struct { PackageName string Name string + ForeignName string - Varname string - Receiver string - ReverseInserts bool + Varname string + Receiver string + OneToOne bool LocalAssignment string ForeignAssignment string @@ -54,6 +55,11 @@ func textsFromForeignKey(packageName string, tables []bdb.Table, table bdb.Table r.Function.PackageName = packageName r.Function.Name = strmangle.TitleCase(strmangle.Singular(strings.TrimSuffix(fkey.Column, "_id"))) + plurality := strmangle.Plural + if fkey.Unique { + plurality = strmangle.Singular + } + r.Function.ForeignName = mkFunctionName(strmangle.Singular(fkey.ForeignTable), strmangle.TitleCase(plurality(fkey.Table)), fkey.Column, false) r.Function.Varname = strmangle.CamelCase(strmangle.Singular(fkey.ForeignTable)) r.Function.Receiver = strings.ToLower(table.Name[:1]) @@ -91,7 +97,8 @@ func textsFromOneToOneRelationship(packageName string, tables []bdb.Table, table rel := textsFromForeignKey(packageName, tables, table, fkey) rel.Function.Name = strmangle.TitleCase(strmangle.Singular(toMany.ForeignTable)) - rel.Function.ReverseInserts = true + rel.Function.ForeignName = mkFunctionName(strmangle.Singular(toMany.Table), strmangle.TitleCase(strmangle.Singular(toMany.Table)), toMany.ForeignColumn, false) + rel.Function.OneToOne = true return rel } @@ -100,6 +107,7 @@ type RelationshipToManyTexts struct { LocalTable struct { NameGo string NameSingular string + ColumnNameGo string } ForeignTable struct { @@ -107,12 +115,14 @@ type RelationshipToManyTexts struct { NameSingular string NamePluralGo string NameHumanReadable string + ColumnNameGo string Slice string } Function struct { - Name string - Receiver string + Name string + ForeignName string + Receiver string LocalAssignment string ForeignAssignment string @@ -125,23 +135,24 @@ func textsFromRelationship(tables []bdb.Table, table bdb.Table, rel bdb.ToManyRe r := RelationshipToManyTexts{} r.LocalTable.NameSingular = strmangle.Singular(table.Name) r.LocalTable.NameGo = strmangle.TitleCase(r.LocalTable.NameSingular) + r.LocalTable.ColumnNameGo = strmangle.TitleCase(rel.Column) r.ForeignTable.NameSingular = strmangle.Singular(rel.ForeignTable) r.ForeignTable.NamePluralGo = strmangle.TitleCase(strmangle.Plural(rel.ForeignTable)) r.ForeignTable.NameGo = strmangle.TitleCase(r.ForeignTable.NameSingular) + r.ForeignTable.ColumnNameGo = strmangle.TitleCase(rel.ForeignColumn) r.ForeignTable.Slice = fmt.Sprintf("%sSlice", strmangle.TitleCase(r.ForeignTable.NameSingular)) r.ForeignTable.NameHumanReadable = strings.Replace(rel.ForeignTable, "_", " ", -1) r.Function.Receiver = strings.ToLower(table.Name[:1]) - - // Check to see if the foreign key name is the same as the local table name. - // Simple case: yes - we can name the function the same as the plural table name - // Not simple case: We have to name the function based off the foreign key and - if colName := strings.TrimSuffix(rel.ForeignColumn, "_id"); rel.ToJoinTable || r.LocalTable.NameSingular == colName { - r.Function.Name = r.ForeignTable.NamePluralGo - } else { - r.Function.Name = strmangle.TitleCase(colName) + r.ForeignTable.NamePluralGo + r.Function.Name = mkFunctionName(r.LocalTable.NameSingular, r.ForeignTable.NamePluralGo, rel.ForeignColumn, rel.ToJoinTable) + plurality := strmangle.Singular + foreignNamingColumn := rel.ForeignColumn + if rel.ToJoinTable { + plurality = strmangle.Plural + foreignNamingColumn = rel.JoinLocalColumn } + r.Function.ForeignName = strmangle.TitleCase(plurality(strings.TrimSuffix(foreignNamingColumn, "_id"))) if rel.Nullable { col := table.GetColumn(rel.Column) @@ -160,3 +171,15 @@ func textsFromRelationship(tables []bdb.Table, table bdb.Table, rel bdb.ToManyRe return r } + +// mkFunctionName checks to see if the foreign key name is the same as the local table name (minus _id suffix) +// Simple case: yes - we can name the function the same as the plural table name +// Not simple case: We have to name the function based off the foreign key and the foreign table name +func mkFunctionName(fkeyTableSingular, foreignTablePluralGo, fkeyColumn string, toJoinTable bool) string { + colName := strings.TrimSuffix(fkeyColumn, "_id") + if toJoinTable || fkeyTableSingular == colName { + return foreignTablePluralGo + } + + return strmangle.TitleCase(colName) + foreignTablePluralGo +} diff --git a/text_helpers_test.go b/text_helpers_test.go index aa574ad..65ab7ef 100644 --- a/text_helpers_test.go +++ b/text_helpers_test.go @@ -34,9 +34,10 @@ func TestTextsFromForeignKey(t *testing.T) { expect.Function.PackageName = "models" expect.Function.Name = "Pilot" + expect.Function.ForeignName = "Jet" expect.Function.Varname = "pilot" expect.Function.Receiver = "j" - expect.Function.ReverseInserts = false + expect.Function.OneToOne = false expect.Function.LocalAssignment = "PilotID.Int" expect.Function.ForeignAssignment = "ID" @@ -44,6 +45,37 @@ func TestTextsFromForeignKey(t *testing.T) { if !reflect.DeepEqual(expect, texts) { t.Errorf("Want:\n%s\nGot:\n%s\n", spew.Sdump(expect), spew.Sdump(texts)) } + + texts = textsFromForeignKey("models", tables, jets, jets.FKeys[1]) + expect = RelationshipToOneTexts{} + expect.ForeignKey = jets.FKeys[1] + + expect.LocalTable.NameGo = "Jet" + expect.LocalTable.ColumnNameGo = "AirportID" + + expect.ForeignTable.Name = "airports" + expect.ForeignTable.NameGo = "Airport" + expect.ForeignTable.NamePluralGo = "Airports" + expect.ForeignTable.ColumnName = "id" + expect.ForeignTable.ColumnNameGo = "ID" + + expect.Function.PackageName = "models" + expect.Function.Name = "Airport" + expect.Function.ForeignName = "Jets" + expect.Function.Varname = "airport" + expect.Function.Receiver = "j" + expect.Function.OneToOne = false + + expect.Function.LocalAssignment = "AirportID" + expect.Function.ForeignAssignment = "ID" + + if !reflect.DeepEqual(expect, texts) { + t.Errorf("Want:\n%s\nGot:\n%s\n", spew.Sdump(expect), spew.Sdump(texts)) + } + + if !reflect.DeepEqual(expect, texts) { + t.Errorf("Want:\n%s\nGot:\n%s\n", spew.Sdump(expect), spew.Sdump(texts)) + } } func TestTextsFromOneToOneRelationship(t *testing.T) { @@ -59,7 +91,7 @@ func TestTextsFromOneToOneRelationship(t *testing.T) { expect := RelationshipToOneTexts{} expect.ForeignKey = bdb.ForeignKey{ - Table: "jets", + Table: "pilots", Name: "none", Column: "id", Nullable: false, @@ -82,9 +114,10 @@ func TestTextsFromOneToOneRelationship(t *testing.T) { expect.Function.PackageName = "models" expect.Function.Name = "Jet" + expect.Function.ForeignName = "Pilot" expect.Function.Varname = "jet" expect.Function.Receiver = "p" - expect.Function.ReverseInserts = true + expect.Function.OneToOne = true expect.Function.LocalAssignment = "ID" expect.Function.ForeignAssignment = "PilotID.Int" @@ -107,14 +140,17 @@ func TestTextsFromRelationship(t *testing.T) { expect := RelationshipToManyTexts{} expect.LocalTable.NameGo = "Pilot" expect.LocalTable.NameSingular = "pilot" + expect.LocalTable.ColumnNameGo = "ID" expect.ForeignTable.NameGo = "Jet" expect.ForeignTable.NameSingular = "jet" expect.ForeignTable.NamePluralGo = "Jets" expect.ForeignTable.NameHumanReadable = "jets" + expect.ForeignTable.ColumnNameGo = "PilotID" expect.ForeignTable.Slice = "JetSlice" expect.Function.Name = "Jets" + expect.Function.ForeignName = "Pilot" expect.Function.Receiver = "p" expect.Function.LocalAssignment = "ID" expect.Function.ForeignAssignment = "PilotID.Int" @@ -127,14 +163,17 @@ func TestTextsFromRelationship(t *testing.T) { expect = RelationshipToManyTexts{} expect.LocalTable.NameGo = "Pilot" expect.LocalTable.NameSingular = "pilot" + expect.LocalTable.ColumnNameGo = "ID" expect.ForeignTable.NameGo = "License" expect.ForeignTable.NameSingular = "license" expect.ForeignTable.NamePluralGo = "Licenses" expect.ForeignTable.NameHumanReadable = "licenses" + expect.ForeignTable.ColumnNameGo = "PilotID" expect.ForeignTable.Slice = "LicenseSlice" expect.Function.Name = "Licenses" + expect.Function.ForeignName = "Pilot" expect.Function.Receiver = "p" expect.Function.LocalAssignment = "ID" expect.Function.ForeignAssignment = "PilotID" @@ -147,14 +186,17 @@ func TestTextsFromRelationship(t *testing.T) { expect = RelationshipToManyTexts{} expect.LocalTable.NameGo = "Pilot" expect.LocalTable.NameSingular = "pilot" + expect.LocalTable.ColumnNameGo = "ID" expect.ForeignTable.NameGo = "Language" expect.ForeignTable.NameSingular = "language" expect.ForeignTable.NamePluralGo = "Languages" expect.ForeignTable.NameHumanReadable = "languages" + expect.ForeignTable.ColumnNameGo = "ID" expect.ForeignTable.Slice = "LanguageSlice" expect.Function.Name = "Languages" + expect.Function.ForeignName = "Pilots" expect.Function.Receiver = "p" expect.Function.LocalAssignment = "ID" expect.Function.ForeignAssignment = "ID"