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)
}
for i := range tables {
tbl := &tables[i]
setRelationships(tbl, tables)
}
return tables, nil
}
@ -102,3 +107,7 @@ func setForeignKeyNullability(t *Table) {
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
}
func (t testInterface) Columns(tableName string) ([]Column, error) {
return []Column{
Column{Name: "col1", Type: "character varying"},
Column{Name: "col2", Type: "character varying", Nullable: true},
}, nil
var testCols = []Column{
Column{Name: "col1", Type: "character varying"},
Column{Name: "col2", Type: "character varying", Nullable: true},
}
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) {
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) {
return []ForeignKey{
{
Name: "fkey1",
Column: "col1",
ForeignTable: "table3",
ForeignColumn: "col3",
},
{
Name: "fkey2",
Column: "col2",
ForeignTable: "table3",
ForeignColumn: "col3",
},
}, nil
return testFkeys, nil
}
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))
}
expectCols := []Column{
Column{Name: "col1", Type: "string"},
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 !reflect.DeepEqual(tables[0].Columns, testCols) {
t.Errorf("Did not get expected columns, got:\n%#v\n%#v", tables[0].Columns, testCols)
}
if !tables[0].IsJoinTable || !tables[1].IsJoinTable {
t.Errorf("Expected IsJoinTable to be true")
}
expectPkey := &PrimaryKey{Name: "pkey1", Columns: []string{"col1", "col2"}}
expectFkey := []ForeignKey{
{
Name: "fkey1",
Column: "col1",
ForeignTable: "table3",
ForeignColumn: "col3",
},
{
Name: "fkey2",
Column: "col2",
ForeignTable: "table3",
ForeignColumn: "col3",
Nullable: true,
},
if !reflect.DeepEqual(tables[0].PKey, testPkey) {
t.Errorf("Did not get expected PKey, got:\n#%v\n%#v", tables[0].PKey, testPkey)
}
if !reflect.DeepEqual(tables[0].FKeys, expectFkey) {
t.Errorf("Did not get expected Fkey, got:\n%#v\n%#v", 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, testFkeys)
}
if !reflect.DeepEqual(tables[0].PKey, expectPkey) {
t.Errorf("Did not get expected PKey, got:\n#%v\n%#v", tables[0].PKey, expectPkey)
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")
}
}
@ -161,3 +152,47 @@ func TestSetForeignKeyNullability(t *testing.T) {
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
// in the local table.
type ToManyRelationship struct {
Column string
ForeignTable string
ForeignColumn string
Column string
Nullable bool
ToJoinTable bool
JoinTable string
JoinLocalColumn string
JoinForeignColumn string
ForeignTable string
ForeignColumn string
ForeignColumnNullable bool
ToJoinTable bool
JoinTable string
JoinLocalColumn string
JoinLocalColumnNullable bool
JoinForeignColumn string
JoinForeignColumnNullable bool
}
// ToManyRelationships relationship lookups
// Input should be the sql name of a table like: videos
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
for _, t := range tables {
if t.Name == table {
if t.Name == table.Name {
continue
}
for _, f := range t.FKeys {
if f.ForeignTable != table {
if f.ForeignTable != table.Name {
continue
}
relationships = append(relationships, buildRelationship(table, f, t))
relationships = append(relationships, buildRelationship(table, f, t, tables))
}
}
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 {
col := localTable.GetColumn(foreignKey.ForeignColumn)
return ToManyRelationship{
Column: foreignKey.ForeignColumn,
ForeignTable: foreignTable.Name,
ForeignColumn: foreignKey.Column,
ToJoinTable: foreignTable.IsJoinTable,
Column: foreignKey.ForeignColumn,
Nullable: col.Nullable,
ForeignTable: foreignTable.Name,
ForeignColumn: foreignKey.Column,
ForeignColumnNullable: foreignKey.Nullable,
ToJoinTable: false,
}
}
col := foreignTable.GetColumn(foreignKey.Column)
relationship := ToManyRelationship{
Column: foreignKey.ForeignColumn,
Nullable: col.Nullable,
ToJoinTable: true,
JoinTable: foreignTable.Name,
}
for _, fk := range foreignTable.FKeys {
if fk.ForeignTable != localTable {
if fk.ForeignTable != localTable.Name {
relationship.JoinForeignColumn = fk.Column
relationship.JoinForeignColumnNullable = fk.Nullable
foreignTable := GetTable(tables, fk.ForeignTable)
foreignCol := foreignTable.GetColumn(fk.ForeignColumn)
relationship.ForeignTable = fk.ForeignTable
relationship.ForeignColumn = fk.ForeignColumn
relationship.ForeignColumnNullable = foreignCol.Nullable
} else {
relationship.JoinLocalColumn = fk.Column
relationship.JoinLocalColumnNullable = fk.Nullable
}
}

View file

@ -6,8 +6,15 @@ func TestToManyRelationships(t *testing.T) {
t.Parallel()
tables := []Table{
Table{Name: "users", Columns: []Column{{Name: "id"}}},
Table{Name: "contests", Columns: []Column{{Name: "id"}}},
Table{
Name: "videos",
Columns: []Column{
{Name: "id"},
{Name: "user_id"},
{Name: "contest_id"},
},
FKeys: []ForeignKey{
{Name: "videos_user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id"},
{Name: "videos_contest_id_fk", Column: "contest_id", ForeignTable: "contests", ForeignColumn: "id"},
@ -15,6 +22,10 @@ func TestToManyRelationships(t *testing.T) {
},
Table{
Name: "notifications",
Columns: []Column{
{Name: "user_id"},
{Name: "source_id"},
},
FKeys: []ForeignKey{
{Name: "notifications_user_id_fk", Column: "user_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{
Name: "users_video_tags",
IsJoinTable: true,
Columns: []Column{
{Name: "user_id"},
{Name: "video_id"},
},
FKeys: []ForeignKey{
{Name: "user_id_fk", Column: "user_id", ForeignTable: "users", 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" {
t.Error("wrong local column:", r.Column)
}
if r.Nullable {
t.Error("should not 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 not be nullable")
}
if r.ToJoinTable {
t.Error("not a join table")
}
@ -53,12 +74,18 @@ func TestToManyRelationships(t *testing.T) {
if r.Column != "id" {
t.Error("wrong local column:", r.Column)
}
if r.Nullable {
t.Error("should not 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 not be nullable")
}
if r.ToJoinTable {
t.Error("not a join table")
}
@ -67,12 +94,18 @@ func TestToManyRelationships(t *testing.T) {
if r.Column != "id" {
t.Error("wrong local column:", r.Column)
}
if r.Nullable {
t.Error("should not 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 not be nullable")
}
if r.ToJoinTable {
t.Error("not a join table")
}
@ -81,9 +114,15 @@ func TestToManyRelationships(t *testing.T) {
if r.Column != "id" {
t.Error("wrong local column:", r.Column)
}
if r.Nullable {
t.Error("should not be nullable")
}
if r.ForeignColumn != "id" {
t.Error("wrong foreign column:", r.Column)
}
if r.ForeignColumnNullable {
t.Error("should not be nullable")
}
if r.ForeignTable != "videos" {
t.Error("wrong foreign table:", r.ForeignTable)
}
@ -93,9 +132,159 @@ func TestToManyRelationships(t *testing.T) {
if r.JoinLocalColumn != "user_id" {
t.Error("wrong local join column:", r.JoinLocalColumn)
}
if r.JoinLocalColumnNullable {
t.Error("should not be nullable")
}
if r.JoinForeignColumn != "video_id" {
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 {
t.Error("expected a join table")
}

View file

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