Futher reduce allocations on the compiler hot path

This commit is contained in:
Vikram Rangnekar
2019-06-14 00:32:15 -04:00
parent 9aa4928d7b
commit 9af320f396
17 changed files with 434 additions and 306 deletions

7
psql/bench.2 Normal file
View File

@ -0,0 +1,7 @@
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/psql
BenchmarkCompile-8 50000 26899 ns/op 4984 B/op 136 allocs/op
BenchmarkCompileParallel-8 200000 8128 ns/op 5046 B/op 136 allocs/op
PASS
ok github.com/dosco/super-graph/psql 3.455s

7
psql/bench.3 Normal file
View File

@ -0,0 +1,7 @@
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/psql
BenchmarkCompile-8 50000 25670 ns/op 4533 B/op 134 allocs/op
BenchmarkCompileParallel-8 200000 7533 ns/op 4590 B/op 134 allocs/op
PASS
ok github.com/dosco/super-graph/psql 3.149s

7
psql/bench.4 Normal file
View File

@ -0,0 +1,7 @@
goos: darwin
goarch: amd64
pkg: github.com/dosco/super-graph/psql
BenchmarkCompile-8 100000 16476 ns/op 3282 B/op 66 allocs/op
BenchmarkCompileParallel-8 300000 4639 ns/op 3324 B/op 66 allocs/op
PASS
ok github.com/dosco/super-graph/psql 3.274s

View File

@ -8,6 +8,7 @@ import (
"math"
"strings"
"github.com/cespare/xxhash/v2"
"github.com/dosco/super-graph/qcode"
"github.com/dosco/super-graph/util"
)
@ -18,31 +19,30 @@ const (
)
type Config struct {
Schema *DBSchema
Vars map[string]string
TableMap map[string]string
Schema *DBSchema
Vars map[string]string
}
type Compiler struct {
schema *DBSchema
vars map[string]string
tmap map[string]string
}
func NewCompiler(conf Config) *Compiler {
return &Compiler{conf.Schema, conf.Vars, conf.TableMap}
return &Compiler{conf.Schema, conf.Vars}
}
func (c *Compiler) AddRelationship(key uint64, val *DBRel) {
c.schema.RelMap[key] = val
func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error {
return c.schema.SetRel(child, parent, rel)
}
func (c *Compiler) IDColumn(table string) string {
t, ok := c.schema.Tables[table]
if !ok {
return empty
func (c *Compiler) IDColumn(table string) (string, error) {
t, err := c.schema.GetTable(table)
if err != nil {
return empty, err
}
return t.PrimaryCol
return t.PrimaryCol, nil
}
type compilerContext struct {
@ -78,7 +78,6 @@ func (co *Compiler) Compile(qc *qcode.QCode, w *bytes.Buffer) (uint32, error) {
c.w.WriteString(`) FROM (`)
var ignored uint32
var err error
for {
if st.Len() == 0 {
@ -90,12 +89,17 @@ func (co *Compiler) Compile(qc *qcode.QCode, w *bytes.Buffer) (uint32, error) {
if id < closeBlock {
sel := &c.s[id]
ti, err := c.schema.GetTable(sel.Table)
if err != nil {
return 0, err
}
if sel.ID != 0 {
if err = c.renderJoin(sel); err != nil {
return 0, err
}
}
skipped, err := c.renderSelect(sel)
skipped, err := c.renderSelect(sel, ti)
if err != nil {
return 0, err
}
@ -113,7 +117,16 @@ func (co *Compiler) Compile(qc *qcode.QCode, w *bytes.Buffer) (uint32, error) {
} else {
sel := &c.s[(id - closeBlock)]
err = c.renderSelectClose(sel)
ti, err := c.schema.GetTable(sel.Table)
if err != nil {
return 0, err
}
err = c.renderSelectClose(sel, ti)
if err != nil {
return 0, err
}
if sel.ID != 0 {
if err = c.renderJoinClose(sel); err != nil {
@ -130,16 +143,7 @@ func (co *Compiler) Compile(qc *qcode.QCode, w *bytes.Buffer) (uint32, error) {
return ignored, nil
}
func (c *compilerContext) getTable(sel *qcode.Select) (
*DBTableInfo, error) {
if tn, ok := c.tmap[sel.Table]; ok {
return c.schema.GetTable(tn)
}
return c.schema.GetTable(sel.Table)
}
func (c *compilerContext) processChildren(sel *qcode.Select) (uint32, []*qcode.Column) {
func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (uint32, []*qcode.Column) {
var skipped uint32
cols := make([]*qcode.Column, 0, len(sel.Cols))
@ -152,8 +156,8 @@ func (c *compilerContext) processChildren(sel *qcode.Select) (uint32, []*qcode.C
for _, id := range sel.Children {
child := &c.s[id]
rel, ok := c.schema.RelMap[child.RelID]
if !ok {
rel, err := c.schema.GetRel(child.Table, ti.Name)
if err != nil {
skipped |= (1 << uint(id))
continue
}
@ -183,12 +187,12 @@ func (c *compilerContext) processChildren(sel *qcode.Select) (uint32, []*qcode.C
return skipped, cols
}
func (c *compilerContext) renderSelect(sel *qcode.Select) (uint32, error) {
skipped, childCols := c.processChildren(sel)
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint32, error) {
skipped, childCols := c.processChildren(sel, ti)
hasOrder := len(sel.OrderBy) != 0
// SELECT
if sel.AsList {
if ti.Singular == false {
//fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, c.sel.Table)
c.w.WriteString(`SELECT coalesce(json_agg("`)
c.w.WriteString(sel.Table)
@ -246,7 +250,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select) (uint32, error) {
// END-SELECT
// FROM (SELECT .... )
err = c.renderBaseSelect(sel, childCols, skipped)
err = c.renderBaseSelect(sel, ti, childCols, skipped)
if err != nil {
return skipped, err
}
@ -255,7 +259,7 @@ func (c *compilerContext) renderSelect(sel *qcode.Select) (uint32, error) {
return skipped, nil
}
func (c *compilerContext) renderSelectClose(sel *qcode.Select) error {
func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo) error {
hasOrder := len(sel.OrderBy) != 0
if hasOrder {
@ -270,6 +274,10 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select) error {
c.w.WriteString(` LIMIT ('`)
c.w.WriteString(sel.Paging.Limit)
c.w.WriteString(`') :: integer`)
} else if ti.Singular {
c.w.WriteString(` LIMIT ('1') :: integer`)
} else {
c.w.WriteString(` LIMIT ('20') :: integer`)
}
@ -281,7 +289,7 @@ func (c *compilerContext) renderSelectClose(sel *qcode.Select) error {
c.w.WriteString(`') :: integer`)
}
if sel.AsList {
if ti.Singular == false {
//fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Table, c.sel.ID)
c.w.WriteString(`)`)
aliasWithID(c.w, sel.Table, sel.ID)
@ -304,16 +312,21 @@ func (c *compilerContext) renderJoinClose(sel *qcode.Select) error {
}
func (c *compilerContext) renderJoinTable(sel *qcode.Select) {
rel, ok := c.schema.RelMap[sel.RelID]
if !ok {
panic(errors.New("no relationship found"))
parent := &c.s[sel.ParentID]
rel, err := c.schema.GetRel(sel.Table, parent.Table)
if err != nil {
panic(err)
}
if rel.Type != RelOneToManyThrough {
return
}
parent := &c.s[sel.ParentID]
pt, err := c.schema.GetTable(parent.Table)
if err != nil {
return
}
//fmt.Fprintf(w, ` LEFT OUTER JOIN "%s" ON (("%s"."%s") = ("%s_%d"."%s"))`,
//rel.Through, rel.Through, rel.ColT, c.parent.Table, c.parent.ID, rel.Col1)
@ -322,7 +335,7 @@ func (c *compilerContext) renderJoinTable(sel *qcode.Select) {
c.w.WriteString(`" ON ((`)
colWithTable(c.w, rel.Through, rel.ColT)
c.w.WriteString(`) = (`)
colWithTableID(c.w, parent.Table, parent.ID, rel.Col1)
colWithTableID(c.w, pt.Name, parent.ID, rel.Col1)
c.w.WriteString(`))`)
}
@ -343,8 +356,8 @@ func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select) {
for _, id := range sel.Children {
child := &c.s[id]
rel, ok := c.schema.RelMap[child.RelID]
if !ok || rel.Type != RelRemote {
rel, err := c.schema.GetRel(child.Table, sel.Table)
if err != nil || rel.Type != RelRemote {
continue
}
if i != 0 || len(sel.Cols) != 0 {
@ -380,15 +393,10 @@ func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, skipped uint32)
return nil
}
func (c *compilerContext) renderBaseSelect(sel *qcode.Select,
func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
childCols []*qcode.Column, skipped uint32) error {
var groupBy []int
ti, err := c.getTable(sel)
if err != nil {
return err
}
isRoot := sel.ID == 0
isFil := sel.Where != nil
isSearch := sel.Args["search"] != nil
@ -473,19 +481,30 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select,
}
c.w.WriteString(` FROM `)
if tn, ok := c.tmap[sel.Table]; ok {
if c.schema.IsAlias(sel.Table) || ti.Singular {
//fmt.Fprintf(w, ` FROM "%s" AS "%s"`, tn, c.sel.Table)
colWithAlias(c.w, tn, sel.Table)
tableWithAlias(c.w, ti.Name, sel.Table)
} else {
//fmt.Fprintf(w, ` FROM "%s"`, c.sel.Table)
c.w.WriteString(`"`)
c.w.WriteString(sel.Table)
c.w.WriteString(ti.Name)
c.w.WriteString(`"`)
}
// if tn, ok := c.tmap[sel.Table]; ok {
// //fmt.Fprintf(w, ` FROM "%s" AS "%s"`, tn, c.sel.Table)
// tableWithAlias(c.w, ti.Name, sel.Table)
// } else {
// //fmt.Fprintf(w, ` FROM "%s"`, c.sel.Table)
// c.w.WriteString(`"`)
// c.w.WriteString(sel.Table)
// c.w.WriteString(`"`)
// }
if isRoot && isFil {
c.w.WriteString(` WHERE (`)
if err := c.renderWhere(sel); err != nil {
if err := c.renderWhere(sel, ti); err != nil {
return err
}
c.w.WriteString(`)`)
@ -499,7 +518,7 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select,
if isFil {
c.w.WriteString(` AND `)
if err := c.renderWhere(sel); err != nil {
if err := c.renderWhere(sel, ti); err != nil {
return err
}
}
@ -525,6 +544,10 @@ func (c *compilerContext) renderBaseSelect(sel *qcode.Select,
c.w.WriteString(` LIMIT ('`)
c.w.WriteString(sel.Paging.Limit)
c.w.WriteString(`') :: integer`)
} else if ti.Singular {
c.w.WriteString(` LIMIT ('1') :: integer`)
} else {
c.w.WriteString(` LIMIT ('20') :: integer`)
}
@ -562,13 +585,13 @@ func (c *compilerContext) renderOrderByColumns(sel *qcode.Select) {
}
func (c *compilerContext) renderRelationship(sel *qcode.Select) {
rel, ok := c.schema.RelMap[sel.RelID]
if !ok {
panic(errors.New("no relationship found"))
}
parent := c.s[sel.ParentID]
rel, err := c.schema.GetRel(sel.Table, parent.Table)
if err != nil {
panic(err)
}
switch rel.Type {
case RelBelongTo:
//fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`,
@ -599,18 +622,13 @@ func (c *compilerContext) renderRelationship(sel *qcode.Select) {
}
}
func (c *compilerContext) renderWhere(sel *qcode.Select) error {
func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error {
st := util.NewStack()
if sel.Where != nil {
st.Push(sel.Where)
}
ti, err := c.getTable(sel)
if err != nil {
return err
}
for {
if st.Len() == 0 {
break
@ -909,6 +927,14 @@ func colWithAlias(w *bytes.Buffer, col, alias string) {
w.WriteString(`"`)
}
func tableWithAlias(w *bytes.Buffer, table, alias string) {
w.WriteString(`"`)
w.WriteString(table)
w.WriteString(`" AS "`)
w.WriteString(alias)
w.WriteString(`"`)
}
func colWithTable(w *bytes.Buffer, table, col string) {
w.WriteString(`"`)
w.WriteString(table)
@ -987,3 +1013,11 @@ func int2string(w *bytes.Buffer, val int32) {
w.WriteByte(charset[d])
}
}
func relID(h *xxhash.Digest, child, parent string) uint64 {
h.WriteString(child)
h.WriteString(parent)
v := h.Sum64()
h.Reset()
return v
}

View File

@ -100,12 +100,17 @@ func TestMain(m *testing.M) {
}
schema := &DBSchema{
Tables: make(map[string]*DBTableInfo),
RelMap: make(map[uint64]*DBRel),
t: make(map[string]*DBTableInfo),
rm: make(map[string]map[string]*DBRel),
al: make(map[string]struct{}),
}
aliases := map[string][]string{
"users": []string{"mes"},
}
for i, t := range tables {
schema.updateSchema(t, columns[i])
schema.updateSchema(t, columns[i], aliases)
}
vars := NewVariables(map[string]string{
@ -115,9 +120,6 @@ func TestMain(m *testing.M) {
pcompile = NewCompiler(Config{
Schema: schema,
Vars: vars,
TableMap: map[string]string{
"mes": "users",
},
})
os.Exit(m.Run())
@ -260,7 +262,7 @@ func fetchByID(t *testing.T) {
}
}`
sql := `SELECT json_object_agg('product', products) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "products" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > (0)) AND (("products"."price") < (8)) AND (("id") = (15))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "done_1337";`
sql := `SELECT json_object_agg('product', product) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "product_0"."id" AS "id", "product_0"."name" AS "name") AS "sel_0")) AS "product" FROM (SELECT "product"."id", "product"."name" FROM "products" AS "product" WHERE ((("product"."price") > (0)) AND (("product"."price") < (8)) AND (("id") = (15))) LIMIT ('1') :: integer) AS "product_0" LIMIT ('1') :: integer) AS "done_1337";`
resSQL, err := compileGQLToPSQL(gql)
if err != nil {
@ -432,7 +434,7 @@ func queryWithVariables(t *testing.T) {
}
}`
sql := `SELECT json_object_agg('product', products) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."name" AS "name") AS "sel_0")) AS "products" FROM (SELECT "products"."id", "products"."name" FROM "products" WHERE ((("products"."price") > (0)) AND (("products"."price") < (8)) AND (("products"."price") = ('{{PRODUCT_PRICE}}')) AND (("id") = ('{{PRODUCT_ID}}'))) LIMIT ('1') :: integer) AS "products_0" LIMIT ('1') :: integer) AS "done_1337";`
sql := `SELECT json_object_agg('product', product) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "product_0"."id" AS "id", "product_0"."name" AS "name") AS "sel_0")) AS "product" FROM (SELECT "product"."id", "product"."name" FROM "products" AS "product" WHERE ((("product"."price") > (0)) AND (("product"."price") < (8)) AND (("product"."price") = ('{{PRODUCT_PRICE}}')) AND (("id") = ('{{PRODUCT_ID}}'))) LIMIT ('1') :: integer) AS "product_0" LIMIT ('1') :: integer) AS "done_1337";`
resSQL, err := compileGQLToPSQL(gql)
if err != nil {
@ -451,7 +453,7 @@ func syntheticTables(t *testing.T) {
}
}`
sql := `SELECT json_object_agg('me', mes) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "mes_0"."email" AS "email") AS "sel_0")) AS "mes" FROM (SELECT "mes"."email" FROM "users" AS "mes" WHERE ((("mes"."id") = ('{{user_id}}'))) LIMIT ('1') :: integer) AS "mes_0" LIMIT ('1') :: integer) AS "done_1337";`
sql := `SELECT json_object_agg('me', me) FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "me_0"."email" AS "email") AS "sel_0")) AS "me" FROM (SELECT "me"."email" FROM "users" AS "me" WHERE ((("me"."id") = ('{{user_id}}'))) LIMIT ('1') :: integer) AS "me_0" LIMIT ('1') :: integer) AS "done_1337";`
resSQL, err := compileGQLToPSQL(gql)
if err != nil {

View File

@ -4,166 +4,10 @@ import (
"fmt"
"strings"
"github.com/cespare/xxhash/v2"
"github.com/go-pg/pg"
"github.com/gobuffalo/flect"
)
type DBSchema struct {
Tables map[string]*DBTableInfo
RelMap map[uint64]*DBRel
}
type DBTableInfo struct {
Name string
PrimaryCol string
TSVCol string
Columns map[string]*DBColumn
}
type RelType int
const (
RelBelongTo RelType = iota + 1
RelOneToMany
RelOneToManyThrough
RelRemote
)
type DBRel struct {
Type RelType
Through string
ColT string
Col1 string
Col2 string
}
func NewDBSchema(db *pg.DB) (*DBSchema, error) {
schema := &DBSchema{
Tables: make(map[string]*DBTableInfo),
RelMap: make(map[uint64]*DBRel),
}
tables, err := GetTables(db)
if err != nil {
return nil, err
}
for _, t := range tables {
cols, err := GetColumns(db, "public", t.Name)
if err != nil {
return nil, err
}
schema.updateSchema(t, cols)
}
return schema, nil
}
func (s *DBSchema) updateSchema(t *DBTable, cols []*DBColumn) {
// Current table
ti := &DBTableInfo{
Name: t.Name,
Columns: make(map[string]*DBColumn, len(cols)),
}
// Foreign key columns in current table
var jcols []*DBColumn
colByID := make(map[int]*DBColumn)
for i := range cols {
c := cols[i]
ti.Columns[strings.ToLower(c.Name)] = cols[i]
colByID[c.ID] = cols[i]
}
ct := strings.ToLower(t.Name)
s.Tables[ct] = ti
h := xxhash.New()
for _, c := range cols {
switch {
case c.Type == "tsvector":
s.Tables[ct].TSVCol = c.Name
case c.PrimaryKey:
s.Tables[ct].PrimaryCol = c.Name
case len(c.FKeyTable) != 0:
if len(c.FKeyColID) == 0 {
continue
}
// Foreign key column name
ft := strings.ToLower(c.FKeyTable)
fc, ok := colByID[c.FKeyColID[0]]
if !ok {
continue
}
// Belongs-to relation between current table and the
// table in the foreign key
rel1 := &DBRel{RelBelongTo, "", "", c.Name, fc.Name}
s.RelMap[relID(h, ct, ft)] = rel1
// One-to-many relation between the foreign key table and the
// the current table
rel2 := &DBRel{RelOneToMany, "", "", fc.Name, c.Name}
s.RelMap[relID(h, ft, ct)] = rel2
jcols = append(jcols, c)
}
}
// If table contains multiple foreign key columns it's a possible
// join table for many-to-many relationships or multiple one-to-many
// relations
// Below one-to-many relations use the current table as the
// join table aka through table.
if len(jcols) > 1 {
for i := range jcols {
for n := range jcols {
if n != i {
s.updateSchemaOTMT(h, ct, jcols[i], jcols[n], colByID)
}
}
}
}
}
func (s *DBSchema) updateSchemaOTMT(
h *xxhash.Digest,
ct string,
col1, col2 *DBColumn,
colByID map[int]*DBColumn) {
t1 := strings.ToLower(col1.FKeyTable)
t2 := strings.ToLower(col2.FKeyTable)
fc1, ok := colByID[col1.FKeyColID[0]]
if !ok {
return
}
fc2, ok := colByID[col2.FKeyColID[0]]
if !ok {
return
}
// One-to-many-through relation between 1nd foreign key table and the
// 2nd foreign key table
//rel1 := &DBRel{RelOneToManyThrough, ct, fc1.Name, col1.Name}
rel1 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name, col1.Name}
s.RelMap[relID(h, t1, t2)] = rel1
// One-to-many-through relation between 2nd foreign key table and the
// 1nd foreign key table
//rel2 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name}
rel2 := &DBRel{RelOneToManyThrough, ct, col1.Name, fc1.Name, col2.Name}
s.RelMap[relID(h, t2, t1)] = rel2
}
type DBTable struct {
Name string `sql:"name"`
Type string `sql:"type"`
@ -255,18 +99,231 @@ WHERE c.relkind = 'r'::char
return t, nil
}
type DBSchema struct {
t map[string]*DBTableInfo
rm map[string]map[string]*DBRel
al map[string]struct{}
}
type DBTableInfo struct {
Name string
Singular bool
PrimaryCol string
TSVCol string
Columns map[string]*DBColumn
}
type RelType int
const (
RelBelongTo RelType = iota + 1
RelOneToMany
RelOneToManyThrough
RelRemote
)
type DBRel struct {
Type RelType
Through string
ColT string
Col1 string
Col2 string
}
func NewDBSchema(db *pg.DB, aliases map[string][]string) (*DBSchema, error) {
schema := &DBSchema{
t: make(map[string]*DBTableInfo),
rm: make(map[string]map[string]*DBRel),
al: make(map[string]struct{}),
}
tables, err := GetTables(db)
if err != nil {
return nil, err
}
for _, t := range tables {
cols, err := GetColumns(db, "public", t.Name)
if err != nil {
return nil, err
}
schema.updateSchema(t, cols, aliases)
}
return schema, nil
}
func (s *DBSchema) updateSchema(
t *DBTable,
cols []*DBColumn,
aliases map[string][]string) {
// Foreign key columns in current table
colByID := make(map[int]*DBColumn)
columns := make(map[string]*DBColumn, len(cols))
for i := range cols {
c := cols[i]
columns[strings.ToLower(c.Name)] = cols[i]
colByID[c.ID] = cols[i]
}
singular := strings.ToLower(flect.Singularize(t.Name))
s.t[singular] = &DBTableInfo{
Name: t.Name,
Singular: true,
Columns: columns,
}
plural := strings.ToLower(flect.Pluralize(t.Name))
s.t[plural] = &DBTableInfo{
Name: t.Name,
Singular: false,
Columns: columns,
}
ct := strings.ToLower(t.Name)
if al, ok := aliases[ct]; ok {
for i := range al {
k1 := flect.Singularize(al[i])
s.t[k1] = s.t[singular]
k2 := flect.Pluralize(al[i])
s.t[k2] = s.t[plural]
s.al[k1] = struct{}{}
s.al[k2] = struct{}{}
}
}
jcols := make([]*DBColumn, 0, len(cols))
for _, c := range cols {
switch {
case c.Type == "tsvector":
s.t[singular].TSVCol = c.Name
s.t[plural].TSVCol = c.Name
case c.PrimaryKey:
s.t[singular].PrimaryCol = c.Name
s.t[plural].PrimaryCol = c.Name
case len(c.FKeyTable) != 0:
if len(c.FKeyColID) == 0 {
continue
}
// Foreign key column name
ft := strings.ToLower(c.FKeyTable)
fc, ok := colByID[c.FKeyColID[0]]
if !ok {
continue
}
// Belongs-to relation between current table and the
// table in the foreign key
rel1 := &DBRel{RelBelongTo, "", "", c.Name, fc.Name}
s.SetRel(ct, ft, rel1)
// One-to-many relation between the foreign key table and the
// the current table
rel2 := &DBRel{RelOneToMany, "", "", fc.Name, c.Name}
s.SetRel(ft, ct, rel2)
jcols = append(jcols, c)
}
}
// If table contains multiple foreign key columns it's a possible
// join table for many-to-many relationships or multiple one-to-many
// relations
// Below one-to-many relations use the current table as the
// join table aka through table.
if len(jcols) > 1 {
for i := range jcols {
for n := range jcols {
if n != i {
s.updateSchemaOTMT(ct, jcols[i], jcols[n], colByID)
}
}
}
}
}
func (s *DBSchema) updateSchemaOTMT(
ct string,
col1, col2 *DBColumn,
colByID map[int]*DBColumn) {
t1 := strings.ToLower(col1.FKeyTable)
t2 := strings.ToLower(col2.FKeyTable)
fc1, ok := colByID[col1.FKeyColID[0]]
if !ok {
return
}
fc2, ok := colByID[col2.FKeyColID[0]]
if !ok {
return
}
// One-to-many-through relation between 1nd foreign key table and the
// 2nd foreign key table
//rel1 := &DBRel{RelOneToManyThrough, ct, fc1.Name, col1.Name}
rel1 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name, col1.Name}
s.SetRel(t1, t2, rel1)
// One-to-many-through relation between 2nd foreign key table and the
// 1nd foreign key table
//rel2 := &DBRel{RelOneToManyThrough, ct, col2.Name, fc2.Name}
rel2 := &DBRel{RelOneToManyThrough, ct, col1.Name, fc1.Name, col2.Name}
s.SetRel(t2, t1, rel2)
}
func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) {
t, ok := s.Tables[table]
t, ok := s.t[table]
if !ok {
return nil, fmt.Errorf("unknown table '%s'", table)
}
return t, nil
}
func relID(h *xxhash.Digest, child, parent string) uint64 {
h.WriteString(child)
h.WriteString(parent)
v := h.Sum64()
h.Reset()
return v
func (s *DBSchema) SetRel(child, parent string, rel *DBRel) error {
sc := strings.ToLower(flect.Singularize(child))
pc := strings.ToLower(flect.Pluralize(child))
if _, ok := s.rm[sc]; !ok {
s.rm[sc] = make(map[string]*DBRel)
}
if _, ok := s.rm[pc]; !ok {
s.rm[pc] = make(map[string]*DBRel)
}
sp := strings.ToLower(flect.Singularize(parent))
pp := strings.ToLower(flect.Pluralize(parent))
s.rm[sc][sp] = rel
s.rm[sc][pp] = rel
s.rm[pc][sp] = rel
s.rm[pc][pp] = rel
return nil
}
func (s *DBSchema) GetRel(child, parent string) (*DBRel, error) {
rel, ok := s.rm[child][parent]
if !ok {
return nil, fmt.Errorf("unknown relationship '%s' -> '%s'",
child, parent)
}
return rel, nil
}
func (s *DBSchema) IsAlias(name string) bool {
_, ok := s.al[name]
return ok
}