From b7a04e849c2c0facc3b9440632dcaaa0ba030744 Mon Sep 17 00:00:00 2001 From: Patrick O'brien Date: Sat, 16 Jul 2016 05:09:32 +1000 Subject: [PATCH] Added unique constraint to table state --- bdb/column.go | 1 + bdb/drivers/postgres.go | 20 ++- bdb/interface.go | 6 +- bdb/interface_test.go | 26 ++- bdb/keys.go | 2 + bdb/relationships.go | 20 ++- bdb/relationships_test.go | 346 ++++++++++++++++---------------------- 7 files changed, 202 insertions(+), 219 deletions(-) diff --git a/bdb/column.go b/bdb/column.go index 9c8ae0a..41b41f2 100644 --- a/bdb/column.go +++ b/bdb/column.go @@ -16,6 +16,7 @@ type Column struct { DBType string Default string Nullable bool + Unique bool } // ColumnNames of the columns. diff --git a/bdb/drivers/postgres.go b/bdb/drivers/postgres.go index a892b89..fd052a4 100644 --- a/bdb/drivers/postgres.go +++ b/bdb/drivers/postgres.go @@ -81,9 +81,15 @@ func (p *PostgresDriver) Columns(tableName string) ([]bdb.Column, error) { var columns []bdb.Column rows, err := p.dbConn.Query(` - select column_name, data_type, column_default, is_nullable - from information_schema.columns - where table_name=$1 and table_schema = 'public' + select column_name, data_type, column_default, is_nullable, + ( + select cast(count(*) as bit) as is_unique + from information_schema.constraint_column_usage as ccu + inner join information_schema.table_constraints tc on ccu.constraint_name = tc.constraint_name + where ccu.column_name = c.column_name and tc.constraint_type = 'UNIQUE' + ) as is_unique + from information_schema.columns as c + where table_name=$1 and table_schema = 'public'; `, tableName) if err != nil { @@ -92,9 +98,10 @@ func (p *PostgresDriver) Columns(tableName string) ([]bdb.Column, error) { defer rows.Close() for rows.Next() { - var colName, colType, colDefault, Nullable string + var colName, colType, colDefault, nullable string + var unique bool var defaultPtr *string - if err := rows.Scan(&colName, &colType, &defaultPtr, &Nullable); err != nil { + if err := rows.Scan(&colName, &colType, &defaultPtr, &nullable, &unique); err != nil { return nil, fmt.Errorf("unable to scan for table %s: %s", tableName, err) } @@ -108,7 +115,8 @@ func (p *PostgresDriver) Columns(tableName string) ([]bdb.Column, error) { Name: colName, DBType: colType, Default: colDefault, - Nullable: Nullable == "YES", + Nullable: nullable == "YES", + Unique: unique, } columns = append(columns, column) } diff --git a/bdb/interface.go b/bdb/interface.go index 3ab738d..1e42778 100644 --- a/bdb/interface.go +++ b/bdb/interface.go @@ -59,7 +59,7 @@ func Tables(db Interface, names ...string) ([]Table, error) { // Relationships have a dependency on foreign key nullability. for i := range tables { tbl := &tables[i] - setForeignKeyNullability(tbl, tables) + setForeignKeyConstraints(tbl, tables) } for i := range tables { tbl := &tables[i] @@ -93,14 +93,16 @@ func setIsJoinTable(t *Table) { t.IsJoinTable = true } -func setForeignKeyNullability(t *Table, tables []Table) { +func setForeignKeyConstraints(t *Table, tables []Table) { for i, fkey := range t.FKeys { localColumn := t.GetColumn(fkey.Column) foreignTable := GetTable(tables, fkey.ForeignTable) foreignColumn := foreignTable.GetColumn(fkey.ForeignColumn) t.FKeys[i].Nullable = localColumn.Nullable + t.FKeys[i].Unique = localColumn.Unique t.FKeys[i].ForeignColumnNullable = foreignColumn.Nullable + t.FKeys[i].ForeignColumnUnique = foreignColumn.Unique } } diff --git a/bdb/interface_test.go b/bdb/interface_test.go index d8fae8b..3dbe4ce 100644 --- a/bdb/interface_test.go +++ b/bdb/interface_test.go @@ -125,22 +125,22 @@ func TestSetIsJoinTable(t *testing.T) { } } -func TestSetForeignKeyNullability(t *testing.T) { +func TestSetForeignKeyConstraints(t *testing.T) { t.Parallel() tables := []Table{ Table{ Name: "one", Columns: []Column{ - Column{Name: "id1", Type: "string", Nullable: false}, - Column{Name: "id2", Type: "string", Nullable: true}, + Column{Name: "id1", Type: "string", Nullable: false, Unique: false}, + Column{Name: "id2", Type: "string", Nullable: true, Unique: true}, }, }, Table{ Name: "other", Columns: []Column{ - Column{Name: "one_id_1", Type: "string", Nullable: false}, - Column{Name: "one_id_2", Type: "string", Nullable: true}, + Column{Name: "one_id_1", Type: "string", Nullable: false, Unique: false}, + Column{Name: "one_id_2", Type: "string", Nullable: true, Unique: true}, }, FKeys: []ForeignKey{ {Column: "one_id_1", ForeignTable: "one", ForeignColumn: "id1"}, @@ -149,23 +149,35 @@ func TestSetForeignKeyNullability(t *testing.T) { }, } - setForeignKeyNullability(&tables[0], tables) - setForeignKeyNullability(&tables[1], tables) + setForeignKeyConstraints(&tables[0], tables) + setForeignKeyConstraints(&tables[1], tables) first := tables[1].FKeys[0] second := tables[1].FKeys[1] if first.Nullable { t.Error("should not be nullable") } + if first.Unique { + t.Error("should not be unique") + } if first.ForeignColumnNullable { t.Error("should be nullable") } + if first.ForeignColumnUnique { + t.Error("should be unique") + } if !second.Nullable { t.Error("should be nullable") } + if !second.Unique { + t.Error("should be unique") + } if !second.ForeignColumnNullable { t.Error("should be nullable") } + if !second.ForeignColumnUnique { + t.Error("should be unique") + } } func TestSetRelationships(t *testing.T) { diff --git a/bdb/keys.go b/bdb/keys.go index d3c672a..b1744e2 100644 --- a/bdb/keys.go +++ b/bdb/keys.go @@ -19,10 +19,12 @@ type ForeignKey struct { Name string Column string Nullable bool + Unique bool ForeignTable string ForeignColumn string ForeignColumnNullable bool + ForeignColumnUnique bool } // SQLColumnDef formats a column name and type like an SQL column definition. diff --git a/bdb/relationships.go b/bdb/relationships.go index 7f24c52..69abb7d 100644 --- a/bdb/relationships.go +++ b/bdb/relationships.go @@ -6,17 +6,23 @@ package bdb type ToManyRelationship struct { Column string Nullable bool + Unique bool ForeignTable string ForeignColumn string ForeignColumnNullable bool + ForeignColumnUnique bool + + ToJoinTable bool + JoinTable string + + JoinLocalColumn string + JoinLocalColumnNullable bool + JoinLocalColumnUnique bool - ToJoinTable bool - JoinTable string - JoinLocalColumn string - JoinLocalColumnNullable bool JoinForeignColumn string JoinForeignColumnNullable bool + JoinForeignColumnUnique bool } // ToManyRelationships relationship lookups @@ -53,9 +59,11 @@ func buildRelationship(localTable Table, foreignKey ForeignKey, foreignTable Tab return ToManyRelationship{ Column: foreignKey.ForeignColumn, Nullable: col.Nullable, + Unique: col.Unique, ForeignTable: foreignTable.Name, ForeignColumn: foreignKey.Column, ForeignColumnNullable: foreignKey.Nullable, + ForeignColumnUnique: foreignKey.Unique, ToJoinTable: false, } } @@ -64,6 +72,7 @@ func buildRelationship(localTable Table, foreignKey ForeignKey, foreignTable Tab relationship := ToManyRelationship{ Column: foreignKey.ForeignColumn, Nullable: col.Nullable, + Unique: col.Unique, ToJoinTable: true, JoinTable: foreignTable.Name, } @@ -72,15 +81,18 @@ func buildRelationship(localTable Table, foreignKey ForeignKey, foreignTable Tab if fk.ForeignTable != localTable.Name { relationship.JoinForeignColumn = fk.Column relationship.JoinForeignColumnNullable = fk.Nullable + relationship.JoinForeignColumnUnique = fk.Unique foreignTable := GetTable(tables, fk.ForeignTable) foreignCol := foreignTable.GetColumn(fk.ForeignColumn) relationship.ForeignTable = fk.ForeignTable relationship.ForeignColumn = fk.ForeignColumn relationship.ForeignColumnNullable = foreignCol.Nullable + relationship.ForeignColumnUnique = foreignCol.Unique } else { relationship.JoinLocalColumn = fk.Column relationship.JoinLocalColumnNullable = fk.Nullable + relationship.JoinLocalColumnUnique = fk.Unique } } diff --git a/bdb/relationships_test.go b/bdb/relationships_test.go index e500266..2be1824 100644 --- a/bdb/relationships_test.go +++ b/bdb/relationships_test.go @@ -1,6 +1,9 @@ package bdb -import "testing" +import ( + "reflect" + "testing" +) func TestToManyRelationships(t *testing.T) { t.Parallel() @@ -46,103 +49,75 @@ func TestToManyRelationships(t *testing.T) { } relationships := ToManyRelationships("users", tables) + + expected := []ToManyRelationship{ + ToManyRelationship{ + Column: "id", + Nullable: false, + Unique: false, + + ForeignTable: "videos", + ForeignColumn: "user_id", + ForeignColumnNullable: false, + ForeignColumnUnique: false, + + ToJoinTable: false, + }, + ToManyRelationship{ + Column: "id", + Nullable: false, + Unique: false, + + ForeignTable: "notifications", + ForeignColumn: "user_id", + ForeignColumnNullable: false, + ForeignColumnUnique: false, + + ToJoinTable: false, + }, + ToManyRelationship{ + Column: "id", + Nullable: false, + Unique: false, + + ForeignTable: "notifications", + ForeignColumn: "source_id", + ForeignColumnNullable: false, + ForeignColumnUnique: false, + + ToJoinTable: false, + }, + ToManyRelationship{ + Column: "id", + Nullable: false, + Unique: false, + + ForeignTable: "videos", + ForeignColumn: "id", + ForeignColumnNullable: false, + ForeignColumnUnique: false, + + ToJoinTable: true, + JoinTable: "users_video_tags", + + JoinLocalColumn: "user_id", + JoinLocalColumnNullable: false, + JoinLocalColumnUnique: false, + + JoinForeignColumn: "video_id", + JoinForeignColumnNullable: false, + JoinForeignColumnUnique: false, + }, + } + 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 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") - } - - r = relationships[1] - 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") - } - - r = relationships[2] - 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") - } - - r = relationships[3] - 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) - } - 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 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") + for i, v := range relationships { + if !reflect.DeepEqual(v, expected[i]) { + t.Errorf("[%d] Mismatch between relationships:\n\n%#v\n\n%#v\n\n", i, v, expected[i]) + } } } @@ -150,41 +125,41 @@ 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: "users", Columns: []Column{{Name: "id", Nullable: true, Unique: true}}}, + Table{Name: "contests", Columns: []Column{{Name: "id", Nullable: true, Unique: true}}}, Table{ Name: "videos", Columns: []Column{ - {Name: "id", Nullable: true}, - {Name: "user_id", Nullable: true}, - {Name: "contest_id", Nullable: true}, + {Name: "id", Nullable: true, Unique: true}, + {Name: "user_id", Nullable: true, Unique: true}, + {Name: "contest_id", Nullable: true, Unique: 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}, + {Name: "videos_user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id", Nullable: true, Unique: true}, + {Name: "videos_contest_id_fk", Column: "contest_id", ForeignTable: "contests", ForeignColumn: "id", Nullable: true, Unique: true}, }, }, Table{ Name: "notifications", Columns: []Column{ - {Name: "user_id", Nullable: true}, - {Name: "source_id", Nullable: true}, + {Name: "user_id", Nullable: true, Unique: true}, + {Name: "source_id", Nullable: true, Unique: 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}, + {Name: "notifications_user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id", Nullable: true, Unique: true}, + {Name: "notifications_source_id_fk", Column: "source_id", ForeignTable: "users", ForeignColumn: "id", Nullable: true, Unique: true}, }, }, Table{ Name: "users_video_tags", IsJoinTable: true, Columns: []Column{ - {Name: "user_id", Nullable: true}, - {Name: "video_id", Nullable: true}, + {Name: "user_id", Nullable: true, Unique: true}, + {Name: "video_id", Nullable: true, Unique: 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}, + {Name: "user_id_fk", Column: "user_id", ForeignTable: "users", ForeignColumn: "id", Nullable: true, Unique: true}, + {Name: "video_id_fk", Column: "video_id", ForeignTable: "videos", ForeignColumn: "id", Nullable: true, Unique: true}, }, }, } @@ -194,98 +169,69 @@ func TestToManyRelationshipsNull(t *testing.T) { 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") + expected := []ToManyRelationship{ + ToManyRelationship{ + Column: "id", + Nullable: true, + Unique: true, + + ForeignTable: "videos", + ForeignColumn: "user_id", + ForeignColumnNullable: true, + ForeignColumnUnique: true, + + ToJoinTable: false, + }, + ToManyRelationship{ + Column: "id", + Nullable: true, + Unique: true, + + ForeignTable: "notifications", + ForeignColumn: "user_id", + ForeignColumnNullable: true, + ForeignColumnUnique: true, + + ToJoinTable: false, + }, + ToManyRelationship{ + Column: "id", + Nullable: true, + Unique: true, + + ForeignTable: "notifications", + ForeignColumn: "source_id", + ForeignColumnNullable: true, + ForeignColumnUnique: true, + + ToJoinTable: false, + }, + ToManyRelationship{ + Column: "id", + Nullable: true, + Unique: true, + + ForeignTable: "videos", + ForeignColumn: "id", + ForeignColumnNullable: true, + ForeignColumnUnique: true, + + ToJoinTable: true, + JoinTable: "users_video_tags", + + JoinLocalColumn: "user_id", + JoinLocalColumnNullable: true, + JoinLocalColumnUnique: true, + + JoinForeignColumn: "video_id", + JoinForeignColumnNullable: true, + JoinForeignColumnUnique: true, + }, } - 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") + for i, v := range relationships { + if !reflect.DeepEqual(v, expected[i]) { + t.Errorf("[%d] Mismatch between relationships null:\n\n%#v\n\n%#v\n\n", i, v, expected[i]) + } } }