Add to_many relationships as a first class citizen

- to_many relationships are now cached on the table data structure
- to_many relationships now know if any columns involved are nullable
This commit is contained in:
Aaron L 2016-07-11 23:49:42 -07:00
parent bac9bb30ce
commit fb802ad687
5 changed files with 319 additions and 62 deletions

View file

@ -57,6 +57,11 @@ func Tables(db Interface, names ...string) ([]Table, error) {
tables = append(tables, t) tables = append(tables, t)
} }
for i := range tables {
tbl := &tables[i]
setRelationships(tbl, tables)
}
return tables, nil return tables, nil
} }
@ -102,3 +107,7 @@ func setForeignKeyNullability(t *Table) {
t.FKeys[i].Nullable = t.Columns[found].Nullable t.FKeys[i].Nullable = t.Columns[found].Nullable
} }
} }
func setRelationships(t *Table, tables []Table) {
t.ToManyRelationships = toManyRelationships(*t, tables)
}

View file

@ -11,32 +11,38 @@ func (t testInterface) TableNames() ([]string, error) {
return []string{"table1", "table2"}, nil return []string{"table1", "table2"}, nil
} }
func (t testInterface) Columns(tableName string) ([]Column, error) { var testCols = []Column{
return []Column{ Column{Name: "col1", Type: "character varying"},
Column{Name: "col1", Type: "character varying"}, Column{Name: "col2", Type: "character varying", Nullable: true},
Column{Name: "col2", Type: "character varying", Nullable: true},
}, nil
} }
func (t testInterface) Columns(tableName string) ([]Column, error) {
return testCols, nil
}
var testPkey = &PrimaryKey{Name: "pkey1", Columns: []string{"col1", "col2"}}
func (t testInterface) PrimaryKeyInfo(tableName string) (*PrimaryKey, error) { func (t testInterface) PrimaryKeyInfo(tableName string) (*PrimaryKey, error) {
return &PrimaryKey{Name: "pkey1", Columns: []string{"col1", "col2"}}, nil 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) { func (t testInterface) ForeignKeyInfo(tableName string) ([]ForeignKey, error) {
return []ForeignKey{ return testFkeys, nil
{
Name: "fkey1",
Column: "col1",
ForeignTable: "table3",
ForeignColumn: "col3",
},
{
Name: "fkey2",
Column: "col2",
ForeignTable: "table3",
ForeignColumn: "col3",
},
}, nil
} }
func (t testInterface) TranslateColumnType(column Column) Column { func (t testInterface) TranslateColumnType(column Column) Column {
@ -62,42 +68,27 @@ func TestTables(t *testing.T) {
t.Errorf("Expected len 2, got: %d\n", len(tables)) t.Errorf("Expected len 2, got: %d\n", len(tables))
} }
expectCols := []Column{ if !reflect.DeepEqual(tables[0].Columns, testCols) {
Column{Name: "col1", Type: "string"}, t.Errorf("Did not get expected columns, got:\n%#v\n%#v", tables[0].Columns, testCols)
Column{Name: "col2", Type: "string", Nullable: true},
}
if !reflect.DeepEqual(tables[0].Columns, expectCols) {
t.Errorf("Did not get expected columns, got:\n%#v\n%#v", tables[0].Columns, expectCols)
} }
if !tables[0].IsJoinTable || !tables[1].IsJoinTable { if !tables[0].IsJoinTable || !tables[1].IsJoinTable {
t.Errorf("Expected IsJoinTable to be true") t.Errorf("Expected IsJoinTable to be true")
} }
expectPkey := &PrimaryKey{Name: "pkey1", Columns: []string{"col1", "col2"}} if !reflect.DeepEqual(tables[0].PKey, testPkey) {
expectFkey := []ForeignKey{ t.Errorf("Did not get expected PKey, got:\n#%v\n%#v", tables[0].PKey, testPkey)
{
Name: "fkey1",
Column: "col1",
ForeignTable: "table3",
ForeignColumn: "col3",
},
{
Name: "fkey2",
Column: "col2",
ForeignTable: "table3",
ForeignColumn: "col3",
Nullable: true,
},
} }
if !reflect.DeepEqual(tables[0].FKeys, expectFkey) { if !reflect.DeepEqual(tables[0].FKeys, testFkeys) {
t.Errorf("Did not get expected Fkey, got:\n%#v\n%#v", tables[0].FKeys, expectFkey) t.Errorf("Did not get expected Fkey, got:\n%#v\n%#v", tables[0].FKeys, testFkeys)
} }
if !reflect.DeepEqual(tables[0].PKey, expectPkey) { if len(tables[0].ToManyRelationships) != 1 {
t.Errorf("Did not get expected PKey, got:\n#%v\n%#v", tables[0].PKey, expectPkey) t.Error("wanted a to many relationship")
}
if len(tables[1].ToManyRelationships) != 1 {
t.Error("wanted a to many relationship")
} }
} }
@ -161,3 +152,47 @@ func TestSetForeignKeyNullability(t *testing.T) {
t.Error("should be nullable") t.Error("should be nullable")
} }
} }
func TestSetRelationships(t *testing.T) {
t.Parallel()
tables := []Table{
Table{
Name: "one",
Columns: []Column{
Column{Name: "id", Type: "string"},
},
},
Table{
Name: "other",
Columns: []Column{
Column{Name: "other_id", Type: "string"},
},
FKeys: []ForeignKey{{Column: "other_id", ForeignTable: "one", ForeignColumn: "id", Nullable: true}},
},
}
setRelationships(&tables[0], tables)
setRelationships(&tables[1], tables)
if got := len(tables[0].ToManyRelationships); got != 1 {
t.Error("should have a relationship:", got)
}
if got := len(tables[1].ToManyRelationships); got != 0 {
t.Error("should have no to many relationships:", got)
}
rel := tables[0].ToManyRelationships[0]
if rel.Column != "id" {
t.Error("wrong column:", rel.Column)
}
if rel.ForeignTable != "other" {
t.Error("wrong table:", rel.ForeignTable)
}
if rel.ForeignColumn != "other_id" {
t.Error("wrong column:", rel.ForeignColumn)
}
if rel.ToJoinTable {
t.Error("should not be a join table")
}
}

View file

@ -4,61 +4,83 @@ package bdb
// local table has no id, and the foreign table has an id that matches a column // local table has no id, and the foreign table has an id that matches a column
// in the local table. // in the local table.
type ToManyRelationship struct { type ToManyRelationship struct {
Column string Column string
ForeignTable string Nullable bool
ForeignColumn string
ToJoinTable bool ForeignTable string
JoinTable string ForeignColumn string
JoinLocalColumn string ForeignColumnNullable bool
JoinForeignColumn string
ToJoinTable bool
JoinTable string
JoinLocalColumn string
JoinLocalColumnNullable bool
JoinForeignColumn string
JoinForeignColumnNullable bool
} }
// ToManyRelationships relationship lookups // ToManyRelationships relationship lookups
// Input should be the sql name of a table like: videos // Input should be the sql name of a table like: videos
func ToManyRelationships(table string, tables []Table) []ToManyRelationship { func ToManyRelationships(table string, tables []Table) []ToManyRelationship {
localTable := GetTable(tables, table)
return toManyRelationships(localTable, tables)
}
func toManyRelationships(table Table, tables []Table) []ToManyRelationship {
var relationships []ToManyRelationship var relationships []ToManyRelationship
for _, t := range tables { for _, t := range tables {
if t.Name == table { if t.Name == table.Name {
continue continue
} }
for _, f := range t.FKeys { for _, f := range t.FKeys {
if f.ForeignTable != table { if f.ForeignTable != table.Name {
continue continue
} }
relationships = append(relationships, buildRelationship(table, f, t)) relationships = append(relationships, buildRelationship(table, f, t, tables))
} }
} }
return relationships return relationships
} }
func buildRelationship(localTable string, foreignKey ForeignKey, foreignTable Table) ToManyRelationship { func buildRelationship(localTable Table, foreignKey ForeignKey, foreignTable Table, tables []Table) ToManyRelationship {
if !foreignTable.IsJoinTable { if !foreignTable.IsJoinTable {
col := localTable.GetColumn(foreignKey.ForeignColumn)
return ToManyRelationship{ return ToManyRelationship{
Column: foreignKey.ForeignColumn, Column: foreignKey.ForeignColumn,
ForeignTable: foreignTable.Name, Nullable: col.Nullable,
ForeignColumn: foreignKey.Column, ForeignTable: foreignTable.Name,
ToJoinTable: foreignTable.IsJoinTable, ForeignColumn: foreignKey.Column,
ForeignColumnNullable: foreignKey.Nullable,
ToJoinTable: false,
} }
} }
col := foreignTable.GetColumn(foreignKey.Column)
relationship := ToManyRelationship{ relationship := ToManyRelationship{
Column: foreignKey.ForeignColumn, Column: foreignKey.ForeignColumn,
Nullable: col.Nullable,
ToJoinTable: true, ToJoinTable: true,
JoinTable: foreignTable.Name, JoinTable: foreignTable.Name,
} }
for _, fk := range foreignTable.FKeys { for _, fk := range foreignTable.FKeys {
if fk.ForeignTable != localTable { if fk.ForeignTable != localTable.Name {
relationship.JoinForeignColumn = fk.Column relationship.JoinForeignColumn = fk.Column
relationship.JoinForeignColumnNullable = fk.Nullable
foreignTable := GetTable(tables, fk.ForeignTable)
foreignCol := foreignTable.GetColumn(fk.ForeignColumn)
relationship.ForeignTable = fk.ForeignTable relationship.ForeignTable = fk.ForeignTable
relationship.ForeignColumn = fk.ForeignColumn relationship.ForeignColumn = fk.ForeignColumn
relationship.ForeignColumnNullable = foreignCol.Nullable
} else { } else {
relationship.JoinLocalColumn = fk.Column relationship.JoinLocalColumn = fk.Column
relationship.JoinLocalColumnNullable = fk.Nullable
} }
} }

View file

@ -6,8 +6,15 @@ func TestToManyRelationships(t *testing.T) {
t.Parallel() t.Parallel()
tables := []Table{ tables := []Table{
Table{Name: "users", Columns: []Column{{Name: "id"}}},
Table{Name: "contests", Columns: []Column{{Name: "id"}}},
Table{ Table{
Name: "videos", Name: "videos",
Columns: []Column{
{Name: "id"},
{Name: "user_id"},
{Name: "contest_id"},
},
FKeys: []ForeignKey{ FKeys: []ForeignKey{
{Name: "videos_user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id"}, {Name: "videos_user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id"},
{Name: "videos_contest_id_fk", Column: "contest_id", ForeignTable: "contests", ForeignColumn: "id"}, {Name: "videos_contest_id_fk", Column: "contest_id", ForeignTable: "contests", ForeignColumn: "id"},
@ -15,6 +22,10 @@ func TestToManyRelationships(t *testing.T) {
}, },
Table{ Table{
Name: "notifications", Name: "notifications",
Columns: []Column{
{Name: "user_id"},
{Name: "source_id"},
},
FKeys: []ForeignKey{ FKeys: []ForeignKey{
{Name: "notifications_user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id"}, {Name: "notifications_user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id"},
{Name: "notifications_source_id_fk", Column: "source_id", ForeignTable: "users", ForeignColumn: "id"}, {Name: "notifications_source_id_fk", Column: "source_id", ForeignTable: "users", ForeignColumn: "id"},
@ -23,6 +34,10 @@ func TestToManyRelationships(t *testing.T) {
Table{ Table{
Name: "users_video_tags", Name: "users_video_tags",
IsJoinTable: true, IsJoinTable: true,
Columns: []Column{
{Name: "user_id"},
{Name: "video_id"},
},
FKeys: []ForeignKey{ FKeys: []ForeignKey{
{Name: "user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id"}, {Name: "user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id"},
{Name: "video_id_fk", Column: "video_id", ForeignTable: "videos", ForeignColumn: "id"}, {Name: "video_id_fk", Column: "video_id", ForeignTable: "videos", ForeignColumn: "id"},
@ -39,12 +54,18 @@ func TestToManyRelationships(t *testing.T) {
if r.Column != "id" { if r.Column != "id" {
t.Error("wrong local column:", r.Column) t.Error("wrong local column:", r.Column)
} }
if r.Nullable {
t.Error("should not be nullable")
}
if r.ForeignTable != "videos" { if r.ForeignTable != "videos" {
t.Error("wrong foreign table:", r.ForeignTable) t.Error("wrong foreign table:", r.ForeignTable)
} }
if r.ForeignColumn != "user_id" { if r.ForeignColumn != "user_id" {
t.Error("wrong foreign column:", r.ForeignColumn) t.Error("wrong foreign column:", r.ForeignColumn)
} }
if r.ForeignColumnNullable {
t.Error("should not be nullable")
}
if r.ToJoinTable { if r.ToJoinTable {
t.Error("not a join table") t.Error("not a join table")
} }
@ -53,12 +74,18 @@ func TestToManyRelationships(t *testing.T) {
if r.Column != "id" { if r.Column != "id" {
t.Error("wrong local column:", r.Column) t.Error("wrong local column:", r.Column)
} }
if r.Nullable {
t.Error("should not be nullable")
}
if r.ForeignTable != "notifications" { if r.ForeignTable != "notifications" {
t.Error("wrong foreign table:", r.ForeignTable) t.Error("wrong foreign table:", r.ForeignTable)
} }
if r.ForeignColumn != "user_id" { if r.ForeignColumn != "user_id" {
t.Error("wrong foreign column:", r.ForeignColumn) t.Error("wrong foreign column:", r.ForeignColumn)
} }
if r.ForeignColumnNullable {
t.Error("should not be nullable")
}
if r.ToJoinTable { if r.ToJoinTable {
t.Error("not a join table") t.Error("not a join table")
} }
@ -67,12 +94,18 @@ func TestToManyRelationships(t *testing.T) {
if r.Column != "id" { if r.Column != "id" {
t.Error("wrong local column:", r.Column) t.Error("wrong local column:", r.Column)
} }
if r.Nullable {
t.Error("should not be nullable")
}
if r.ForeignTable != "notifications" { if r.ForeignTable != "notifications" {
t.Error("wrong foreign table:", r.ForeignTable) t.Error("wrong foreign table:", r.ForeignTable)
} }
if r.ForeignColumn != "source_id" { if r.ForeignColumn != "source_id" {
t.Error("wrong foreign column:", r.ForeignColumn) t.Error("wrong foreign column:", r.ForeignColumn)
} }
if r.ForeignColumnNullable {
t.Error("should not be nullable")
}
if r.ToJoinTable { if r.ToJoinTable {
t.Error("not a join table") t.Error("not a join table")
} }
@ -81,9 +114,15 @@ func TestToManyRelationships(t *testing.T) {
if r.Column != "id" { if r.Column != "id" {
t.Error("wrong local column:", r.Column) t.Error("wrong local column:", r.Column)
} }
if r.Nullable {
t.Error("should not be nullable")
}
if r.ForeignColumn != "id" { if r.ForeignColumn != "id" {
t.Error("wrong foreign column:", r.Column) t.Error("wrong foreign column:", r.Column)
} }
if r.ForeignColumnNullable {
t.Error("should not be nullable")
}
if r.ForeignTable != "videos" { if r.ForeignTable != "videos" {
t.Error("wrong foreign table:", r.ForeignTable) t.Error("wrong foreign table:", r.ForeignTable)
} }
@ -93,9 +132,159 @@ func TestToManyRelationships(t *testing.T) {
if r.JoinLocalColumn != "user_id" { if r.JoinLocalColumn != "user_id" {
t.Error("wrong local join column:", r.JoinLocalColumn) t.Error("wrong local join column:", r.JoinLocalColumn)
} }
if r.JoinLocalColumnNullable {
t.Error("should not be nullable")
}
if r.JoinForeignColumn != "video_id" { if r.JoinForeignColumn != "video_id" {
t.Error("wrong foreign join column:", r.JoinForeignColumn) t.Error("wrong foreign join column:", r.JoinForeignColumn)
} }
if r.JoinForeignColumnNullable {
t.Error("should not be nullable")
}
if !r.ToJoinTable {
t.Error("expected a join table")
}
}
func TestToManyRelationshipsNull(t *testing.T) {
t.Parallel()
tables := []Table{
Table{Name: "users", Columns: []Column{{Name: "id", Nullable: true}}},
Table{Name: "contests", Columns: []Column{{Name: "id", Nullable: true}}},
Table{
Name: "videos",
Columns: []Column{
{Name: "id", Nullable: true},
{Name: "user_id", Nullable: true},
{Name: "contest_id", Nullable: true},
},
FKeys: []ForeignKey{
{Name: "videos_user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id", Nullable: true},
{Name: "videos_contest_id_fk", Column: "contest_id", ForeignTable: "contests", ForeignColumn: "id", Nullable: true},
},
},
Table{
Name: "notifications",
Columns: []Column{
{Name: "user_id", Nullable: true},
{Name: "source_id", Nullable: true},
},
FKeys: []ForeignKey{
{Name: "notifications_user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id", Nullable: true},
{Name: "notifications_source_id_fk", Column: "source_id", ForeignTable: "users", ForeignColumn: "id", Nullable: true},
},
},
Table{
Name: "users_video_tags",
IsJoinTable: true,
Columns: []Column{
{Name: "user_id", Nullable: true},
{Name: "video_id", Nullable: true},
},
FKeys: []ForeignKey{
{Name: "user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id", Nullable: true},
{Name: "video_id_fk", Column: "video_id", ForeignTable: "videos", ForeignColumn: "id", Nullable: true},
},
},
}
relationships := ToManyRelationships("users", tables)
if len(relationships) != 4 {
t.Error("wrong # of relationships:", len(relationships))
}
r := relationships[0]
if r.Column != "id" {
t.Error("wrong local column:", r.Column)
}
if !r.Nullable {
t.Error("should be nullable")
}
if r.ForeignTable != "videos" {
t.Error("wrong foreign table:", r.ForeignTable)
}
if r.ForeignColumn != "user_id" {
t.Error("wrong foreign column:", r.ForeignColumn)
}
if !r.ForeignColumnNullable {
t.Error("should be nullable")
}
if r.ToJoinTable {
t.Error("not a join table")
}
r = relationships[1]
if r.Column != "id" {
t.Error("wrong local column:", r.Column)
}
if !r.Nullable {
t.Error("should be nullable")
}
if r.ForeignTable != "notifications" {
t.Error("wrong foreign table:", r.ForeignTable)
}
if r.ForeignColumn != "user_id" {
t.Error("wrong foreign column:", r.ForeignColumn)
}
if !r.ForeignColumnNullable {
t.Error("should be nullable")
}
if r.ToJoinTable {
t.Error("not a join table")
}
r = relationships[2]
if r.Column != "id" {
t.Error("wrong local column:", r.Column)
}
if !r.Nullable {
t.Error("should be nullable")
}
if r.ForeignTable != "notifications" {
t.Error("wrong foreign table:", r.ForeignTable)
}
if r.ForeignColumn != "source_id" {
t.Error("wrong foreign column:", r.ForeignColumn)
}
if !r.ForeignColumnNullable {
t.Error("should be nullable")
}
if r.ToJoinTable {
t.Error("not a join table")
}
r = relationships[3]
if r.Column != "id" {
t.Error("wrong local column:", r.Column)
}
if !r.Nullable {
t.Error("should be nullable")
}
if r.ForeignColumn != "id" {
t.Error("wrong foreign column:", r.Column)
}
if !r.ForeignColumnNullable {
t.Error("should be nullable")
}
if r.ForeignTable != "videos" {
t.Error("wrong foreign table:", r.ForeignTable)
}
if r.JoinTable != "users_video_tags" {
t.Error("wrong join table:", r.ForeignTable)
}
if r.JoinLocalColumn != "user_id" {
t.Error("wrong local join column:", r.JoinLocalColumn)
}
if !r.JoinLocalColumnNullable {
t.Error("should be nullable")
}
if r.JoinForeignColumn != "video_id" {
t.Error("wrong foreign join column:", r.JoinForeignColumn)
}
if !r.JoinForeignColumnNullable {
t.Error("should be nullable")
}
if !r.ToJoinTable { if !r.ToJoinTable {
t.Error("expected a join table") t.Error("expected a join table")
} }

View file

@ -11,6 +11,8 @@ type Table struct {
FKeys []ForeignKey FKeys []ForeignKey
IsJoinTable bool IsJoinTable bool
ToManyRelationships []ToManyRelationship
} }
// GetTable by name. Panics if not found (for use in templates mostly). // GetTable by name. Panics if not found (for use in templates mostly).