First commit
This commit is contained in:
537
psql/psql.go
Normal file
537
psql/psql.go
Normal file
@ -0,0 +1,537 @@
|
||||
package psql
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/dosco/super-graph/qcode"
|
||||
"github.com/dosco/super-graph/util"
|
||||
)
|
||||
|
||||
type Variables map[string]string
|
||||
|
||||
type Compiler struct {
|
||||
schema *DBSchema
|
||||
vars Variables
|
||||
}
|
||||
|
||||
func NewCompiler(schema *DBSchema, vars Variables) *Compiler {
|
||||
return &Compiler{schema, vars}
|
||||
}
|
||||
|
||||
func (c *Compiler) Compile(w io.Writer, qc *qcode.QCode) error {
|
||||
st := util.NewStack()
|
||||
|
||||
st.Push(&selectBlockClose{nil, qc.Query.Select})
|
||||
st.Push(&selectBlock{nil, qc.Query.Select, c})
|
||||
|
||||
fmt.Fprintf(w, `SELECT json_object_agg('%s', %s) FROM (`,
|
||||
qc.Query.Select.FieldName, qc.Query.Select.Table)
|
||||
|
||||
for {
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
intf := st.Pop()
|
||||
|
||||
switch v := intf.(type) {
|
||||
case *selectBlock:
|
||||
childCols, childIDs := c.relationshipColumns(v.sel)
|
||||
v.render(w, c.schema, childCols, childIDs)
|
||||
|
||||
for i := range childIDs {
|
||||
sub := v.sel.Joins[childIDs[i]]
|
||||
st.Push(&joinClose{sub})
|
||||
st.Push(&selectBlockClose{v.sel, sub})
|
||||
st.Push(&selectBlock{v.sel, sub, c})
|
||||
st.Push(&joinOpen{sub})
|
||||
}
|
||||
case *selectBlockClose:
|
||||
v.render(w)
|
||||
|
||||
case *joinOpen:
|
||||
v.render(w)
|
||||
|
||||
case *joinClose:
|
||||
v.render(w)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(w, `) AS "done_1337";`)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Compiler) relationshipColumns(parent *qcode.Select) (
|
||||
cols []*qcode.Column, childIDs []int) {
|
||||
|
||||
colmap := make(map[string]struct{}, len(parent.Cols))
|
||||
for i := range parent.Cols {
|
||||
colmap[parent.Cols[i].Name] = struct{}{}
|
||||
}
|
||||
|
||||
for i, sub := range parent.Joins {
|
||||
k := TTKey{sub.Table, parent.Table}
|
||||
|
||||
rel, ok := c.schema.RelMap[k]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if rel.Type == RelBelongTo || rel.Type == RelOneToMany {
|
||||
if _, ok := colmap[rel.Col2]; !ok {
|
||||
cols = append(cols, &qcode.Column{parent.Table, rel.Col2, rel.Col2})
|
||||
}
|
||||
childIDs = append(childIDs, i)
|
||||
}
|
||||
|
||||
if rel.Type == RelOneToManyThrough {
|
||||
if _, ok := colmap[rel.Col1]; !ok {
|
||||
cols = append(cols, &qcode.Column{parent.Table, rel.Col1, rel.Col1})
|
||||
}
|
||||
childIDs = append(childIDs, i)
|
||||
}
|
||||
}
|
||||
|
||||
return cols, childIDs
|
||||
}
|
||||
|
||||
type selectBlock struct {
|
||||
parent *qcode.Select
|
||||
sel *qcode.Select
|
||||
*Compiler
|
||||
}
|
||||
|
||||
func (v *selectBlock) render(w io.Writer,
|
||||
schema *DBSchema, childCols []*qcode.Column, childIDs []int) error {
|
||||
|
||||
isNotRoot := (v.parent != nil)
|
||||
hasFilters := (v.sel.Where != nil)
|
||||
hasOrder := len(v.sel.OrderBy) != 0
|
||||
|
||||
// SELECT
|
||||
if v.sel.AsList {
|
||||
fmt.Fprintf(w, `SELECT coalesce(json_agg("%s"`, v.sel.Table)
|
||||
|
||||
if hasOrder {
|
||||
err := renderOrderBy(w, v.sel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, v.sel.Table)
|
||||
}
|
||||
|
||||
// ROW-TO-JSON
|
||||
io.WriteString(w, `SELECT `)
|
||||
|
||||
if len(v.sel.DistinctOn) != 0 {
|
||||
v.renderDistinctOn(w)
|
||||
}
|
||||
|
||||
io.WriteString(w, `row_to_json((`)
|
||||
|
||||
fmt.Fprintf(w, `SELECT "sel_%d" FROM (SELECT `, v.sel.ID)
|
||||
|
||||
// Combined column names
|
||||
v.renderColumns(w)
|
||||
|
||||
err := v.renderJoinedColumns(w, childIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, `) AS "sel_%d"`, v.sel.ID)
|
||||
|
||||
fmt.Fprintf(w, `)) AS "%s"`, v.sel.Table)
|
||||
// END-ROW-TO-JSON
|
||||
|
||||
if hasOrder {
|
||||
v.renderOrderByColumns(w)
|
||||
}
|
||||
// END-SELECT
|
||||
|
||||
// FROM
|
||||
io.WriteString(w, " FROM (SELECT ")
|
||||
|
||||
// Local column names
|
||||
v.renderLocalColumns(w, append(v.sel.Cols, childCols...))
|
||||
|
||||
fmt.Fprintf(w, ` FROM "%s"`, v.sel.Table)
|
||||
|
||||
if isNotRoot || hasFilters {
|
||||
if isNotRoot {
|
||||
v.renderJoinTable(w, schema, childIDs)
|
||||
}
|
||||
|
||||
io.WriteString(w, ` WHERE (`)
|
||||
|
||||
if isNotRoot {
|
||||
v.renderRelationship(w, schema)
|
||||
}
|
||||
|
||||
if hasFilters {
|
||||
err := v.renderWhere(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(w, ` ) `)
|
||||
}
|
||||
|
||||
if len(v.sel.Paging.Limit) != 0 {
|
||||
fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, v.sel.Paging.Limit)
|
||||
} else {
|
||||
io.WriteString(w, ` LIMIT ('20') :: integer`)
|
||||
}
|
||||
|
||||
if len(v.sel.Paging.Offset) != 0 {
|
||||
fmt.Fprintf(w, ` OFFSET ('%s') :: integer`, v.sel.Paging.Offset)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, `) AS "%s_%d"`, v.sel.Table, v.sel.ID)
|
||||
// END-FROM
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type selectBlockClose struct {
|
||||
parent *qcode.Select
|
||||
sel *qcode.Select
|
||||
}
|
||||
|
||||
func (v *selectBlockClose) render(w io.Writer) error {
|
||||
hasOrder := len(v.sel.OrderBy) != 0
|
||||
|
||||
if hasOrder {
|
||||
err := renderOrderBy(w, v.sel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.sel.Paging.Limit) != 0 {
|
||||
fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, v.sel.Paging.Limit)
|
||||
} else {
|
||||
io.WriteString(w, ` LIMIT ('20') :: integer`)
|
||||
}
|
||||
|
||||
if len(v.sel.Paging.Offset) != 0 {
|
||||
fmt.Fprintf(w, ` OFFSET ('%s') :: integer`, v.sel.Paging.Offset)
|
||||
}
|
||||
|
||||
if v.sel.AsList {
|
||||
fmt.Fprintf(w, `) AS "%s_%d"`, v.sel.Table, v.sel.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type joinOpen struct {
|
||||
sel *qcode.Select
|
||||
}
|
||||
|
||||
func (v joinOpen) render(w io.Writer) error {
|
||||
io.WriteString(w, ` LEFT OUTER JOIN LATERAL (`)
|
||||
return nil
|
||||
}
|
||||
|
||||
type joinClose struct {
|
||||
sel *qcode.Select
|
||||
}
|
||||
|
||||
func (v *joinClose) render(w io.Writer) error {
|
||||
fmt.Fprintf(w, `) AS "%s_%d.join" ON ('true') `, v.sel.Table, v.sel.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *selectBlock) renderJoinTable(w io.Writer, schema *DBSchema, childIDs []int) {
|
||||
k := TTKey{v.sel.Table, v.parent.Table}
|
||||
rel, ok := schema.RelMap[k]
|
||||
if !ok {
|
||||
panic(errors.New("no relationship found"))
|
||||
}
|
||||
|
||||
if rel.Type != RelOneToManyThrough {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, ` LEFT OUTER JOIN "%s" ON (("%s"."%s") = ("%s_%d"."%s"))`,
|
||||
rel.Through, rel.Through, rel.ColT, v.parent.Table, v.parent.ID, rel.Col1)
|
||||
|
||||
}
|
||||
|
||||
func (v *selectBlock) renderColumns(w io.Writer) {
|
||||
for i, col := range v.sel.Cols {
|
||||
fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`,
|
||||
v.sel.Table, v.sel.ID, col.Name, col.FieldName)
|
||||
|
||||
if i < len(v.sel.Cols)-1 {
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *selectBlock) renderJoinedColumns(w io.Writer, childIDs []int) error {
|
||||
if len(v.sel.Cols) != 0 && len(childIDs) != 0 {
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
|
||||
for i := range childIDs {
|
||||
s := v.sel.Joins[childIDs[i]]
|
||||
|
||||
fmt.Fprintf(w, `"%s_%d.join"."%s" AS "%s"`,
|
||||
s.Table, s.ID, s.Table, s.FieldName)
|
||||
|
||||
if i < len(childIDs)-1 {
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *selectBlock) renderLocalColumns(w io.Writer, columns []*qcode.Column) {
|
||||
for i, col := range columns {
|
||||
if len(col.Table) != 0 {
|
||||
fmt.Fprintf(w, `"%s"."%s"`, col.Table, col.Name)
|
||||
} else {
|
||||
fmt.Fprintf(w, `"%s"."%s"`, v.sel.Table, col.Name)
|
||||
}
|
||||
|
||||
if i < len(columns)-1 {
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *selectBlock) renderOrderByColumns(w io.Writer) {
|
||||
if len(v.sel.Cols) != 0 {
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
|
||||
for i := range v.sel.OrderBy {
|
||||
c := v.sel.OrderBy[i].Col
|
||||
fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d.ob.%s"`,
|
||||
v.sel.Table, v.sel.ID, c,
|
||||
v.sel.Table, v.sel.ID, c)
|
||||
|
||||
if i < len(v.sel.OrderBy)-1 {
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *selectBlock) renderRelationship(w io.Writer, schema *DBSchema) {
|
||||
hasFilters := (v.sel.Where != nil)
|
||||
|
||||
k := TTKey{v.sel.Table, v.parent.Table}
|
||||
rel, ok := schema.RelMap[k]
|
||||
if !ok {
|
||||
panic(errors.New("no relationship found"))
|
||||
}
|
||||
|
||||
switch rel.Type {
|
||||
case RelBelongTo:
|
||||
fmt.Fprintf(w, ` (("%s"."%s") = ("%s_%d"."%s"))`,
|
||||
v.sel.Table, rel.Col1, v.parent.Table, v.parent.ID, rel.Col2)
|
||||
|
||||
case RelOneToMany:
|
||||
fmt.Fprintf(w, ` (("%s"."%s") = ("%s_%d"."%s"))`,
|
||||
v.sel.Table, rel.Col1, v.parent.Table, v.parent.ID, rel.Col2)
|
||||
|
||||
case RelOneToManyThrough:
|
||||
fmt.Fprintf(w, ` (("%s"."%s") = ("%s"."%s"))`,
|
||||
v.sel.Table, rel.Col1, rel.Through, rel.Col2)
|
||||
|
||||
}
|
||||
|
||||
if hasFilters {
|
||||
io.WriteString(w, ` AND `)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *selectBlock) renderWhere(w io.Writer) error {
|
||||
st := util.NewStack()
|
||||
|
||||
if v.sel.Where == nil {
|
||||
return nil
|
||||
}
|
||||
st.Push(v.sel.Where)
|
||||
|
||||
for {
|
||||
if st.Len() == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
intf := st.Pop()
|
||||
|
||||
switch val := intf.(type) {
|
||||
case qcode.ExpOp:
|
||||
switch val {
|
||||
case qcode.OpAnd:
|
||||
io.WriteString(w, ` AND `)
|
||||
case qcode.OpOr:
|
||||
io.WriteString(w, ` OR `)
|
||||
case qcode.OpNot:
|
||||
io.WriteString(w, ` NOT `)
|
||||
default:
|
||||
return fmt.Errorf("[Where] unexpected value encountered %v", intf)
|
||||
}
|
||||
case *qcode.Exp:
|
||||
switch val.Op {
|
||||
case qcode.OpAnd:
|
||||
st.Push(val.Children[1])
|
||||
st.Push(qcode.OpAnd)
|
||||
st.Push(val.Children[0])
|
||||
continue
|
||||
case qcode.OpOr:
|
||||
st.Push(val.Children[1])
|
||||
st.Push(qcode.OpOr)
|
||||
st.Push(val.Children[0])
|
||||
continue
|
||||
case qcode.OpNot:
|
||||
st.Push(qcode.OpNot)
|
||||
st.Push(val.Children[0])
|
||||
continue
|
||||
}
|
||||
|
||||
if val.NestedCol {
|
||||
fmt.Fprintf(w, `(("%s") `, val.Col)
|
||||
} else {
|
||||
fmt.Fprintf(w, `(("%s"."%s") `, v.sel.Table, val.Col)
|
||||
}
|
||||
|
||||
switch val.Op {
|
||||
case qcode.OpEquals:
|
||||
io.WriteString(w, `=`)
|
||||
case qcode.OpNotEquals:
|
||||
io.WriteString(w, `!=`)
|
||||
case qcode.OpGreaterOrEquals:
|
||||
io.WriteString(w, `>=`)
|
||||
case qcode.OpLesserOrEquals:
|
||||
io.WriteString(w, `<=`)
|
||||
case qcode.OpGreaterThan:
|
||||
io.WriteString(w, `>`)
|
||||
case qcode.OpLesserThan:
|
||||
io.WriteString(w, `<`)
|
||||
case qcode.OpIn:
|
||||
io.WriteString(w, `IN`)
|
||||
case qcode.OpNotIn:
|
||||
io.WriteString(w, `NOT IN`)
|
||||
case qcode.OpLike:
|
||||
io.WriteString(w, `LIKE`)
|
||||
case qcode.OpNotLike:
|
||||
io.WriteString(w, `NOT LIKE`)
|
||||
case qcode.OpILike:
|
||||
io.WriteString(w, `ILIKE`)
|
||||
case qcode.OpNotILike:
|
||||
io.WriteString(w, `NOT ILIKE`)
|
||||
case qcode.OpSimilar:
|
||||
io.WriteString(w, `SIMILAR TO`)
|
||||
case qcode.OpNotSimilar:
|
||||
io.WriteString(w, `NOT SIMILAR TO`)
|
||||
case qcode.OpContains:
|
||||
io.WriteString(w, `CONTAINS`)
|
||||
case qcode.OpContainedIn:
|
||||
io.WriteString(w, `CONTAINED IN`)
|
||||
case qcode.OpHasKey:
|
||||
io.WriteString(w, `HAS KEY`)
|
||||
default:
|
||||
return fmt.Errorf("[Where] unexpected op code %d", val.Op)
|
||||
}
|
||||
|
||||
if val.Type == qcode.ValList {
|
||||
renderList(w, val)
|
||||
} else {
|
||||
renderVal(w, val, v.vars)
|
||||
}
|
||||
|
||||
io.WriteString(w, `)`)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("[Where] unexpected value encountered %v", intf)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func renderOrderBy(w io.Writer, sel *qcode.Select) error {
|
||||
io.WriteString(w, ` ORDER BY `)
|
||||
for i := range sel.OrderBy {
|
||||
ob := sel.OrderBy[i]
|
||||
|
||||
switch ob.Order {
|
||||
case qcode.OrderAsc:
|
||||
fmt.Fprintf(w, `"%s_%d.ob.%s" ASC`, sel.Table, sel.ID, ob.Col)
|
||||
case qcode.OrderDesc:
|
||||
fmt.Fprintf(w, `"%s_%d.ob.%s" DESC`, sel.Table, sel.ID, ob.Col)
|
||||
case qcode.OrderAscNullsFirst:
|
||||
fmt.Fprintf(w, `"%s_%d.ob.%s" ASC NULLS FIRST`, sel.Table, sel.ID, ob.Col)
|
||||
case qcode.OrderDescNullsFirst:
|
||||
fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS FIRST`, sel.Table, sel.ID, ob.Col)
|
||||
case qcode.OrderAscNullsLast:
|
||||
fmt.Fprintf(w, `"%s_%d.ob.%s ASC NULLS LAST`, sel.Table, sel.ID, ob.Col)
|
||||
case qcode.OrderDescNullsLast:
|
||||
fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS LAST`, sel.Table, sel.ID, ob.Col)
|
||||
default:
|
||||
return fmt.Errorf("[qcode.Order By] unexpected value encountered %v", ob.Order)
|
||||
}
|
||||
if i < len(sel.OrderBy)-1 {
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v selectBlock) renderDistinctOn(w io.Writer) {
|
||||
io.WriteString(w, ` DISTINCT ON (`)
|
||||
for i := range v.sel.DistinctOn {
|
||||
fmt.Fprintf(w, `"%s"`, v.sel.DistinctOn[i])
|
||||
|
||||
if i < len(v.sel.DistinctOn)-1 {
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
io.WriteString(w, `) `)
|
||||
}
|
||||
|
||||
func renderList(w io.Writer, ex *qcode.Exp) {
|
||||
io.WriteString(w, ` (`)
|
||||
for i := range ex.ListVal {
|
||||
switch ex.ListType {
|
||||
case qcode.ValBool, qcode.ValInt, qcode.ValFloat:
|
||||
io.WriteString(w, ex.ListVal[i])
|
||||
case qcode.ValStr:
|
||||
fmt.Fprintf(w, `'%s'`, ex.ListVal[i])
|
||||
}
|
||||
|
||||
if i < len(ex.ListVal)-1 {
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
io.WriteString(w, `)`)
|
||||
}
|
||||
|
||||
func renderVal(w io.Writer, ex *qcode.Exp, vars Variables) {
|
||||
io.WriteString(w, ` (`)
|
||||
switch ex.Type {
|
||||
case qcode.ValBool, qcode.ValInt, qcode.ValFloat:
|
||||
io.WriteString(w, ex.Val)
|
||||
case qcode.ValStr:
|
||||
fmt.Fprintf(w, `'%s'`, ex.Val)
|
||||
case qcode.ValVar:
|
||||
if val, ok := vars[ex.Val]; ok {
|
||||
io.WriteString(w, val)
|
||||
} else {
|
||||
fmt.Fprintf(w, `'{{%s}}'`, ex.Val)
|
||||
}
|
||||
}
|
||||
io.WriteString(w, `)`)
|
||||
}
|
9
psql/psql_test.go
Normal file
9
psql/psql_test.go
Normal file
@ -0,0 +1,9 @@
|
||||
package psql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompileToPSQL(t *testing.T) {
|
||||
|
||||
}
|
229
psql/tables.go
Normal file
229
psql/tables.go
Normal file
@ -0,0 +1,229 @@
|
||||
package psql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-pg/pg"
|
||||
)
|
||||
|
||||
type TCKey struct {
|
||||
Table, Column string
|
||||
}
|
||||
|
||||
type TTKey struct {
|
||||
Table1, Table2 string
|
||||
}
|
||||
|
||||
type DBSchema struct {
|
||||
ColMap map[TCKey]*DBColumn
|
||||
ColIDMap map[int]*DBColumn
|
||||
PCols map[string]*DBColumn
|
||||
|
||||
RelMap map[TTKey]*DBRel
|
||||
}
|
||||
|
||||
type RelType int
|
||||
|
||||
const (
|
||||
RelBelongTo RelType = iota + 1
|
||||
RelOneToMany
|
||||
RelOneToManyThrough
|
||||
)
|
||||
|
||||
type DBRel struct {
|
||||
Type RelType
|
||||
Through string
|
||||
ColT string
|
||||
Col1 string
|
||||
Col2 string
|
||||
}
|
||||
|
||||
func NewDBSchema(db *pg.DB) (*DBSchema, error) {
|
||||
schema := &DBSchema{
|
||||
ColMap: make(map[TCKey]*DBColumn),
|
||||
ColIDMap: make(map[int]*DBColumn),
|
||||
PCols: make(map[string]*DBColumn),
|
||||
RelMap: make(map[TTKey]*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
|
||||
}
|
||||
// Current table
|
||||
ct := strings.ToLower(t.Name)
|
||||
|
||||
// Foreign key columns in current table
|
||||
var jcols []*DBColumn
|
||||
|
||||
for _, c := range cols {
|
||||
schema.ColMap[TCKey{ct, strings.ToLower(c.Name)}] = c
|
||||
schema.ColIDMap[c.ID] = c
|
||||
}
|
||||
|
||||
for _, c := range cols {
|
||||
switch {
|
||||
case c.PrimaryKey:
|
||||
schema.PCols[ct] = c
|
||||
|
||||
case len(c.FKeyTable) != 0:
|
||||
if len(c.FKeyColID) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Foreign key column name
|
||||
ft := strings.ToLower(c.FKeyTable)
|
||||
fc, ok := schema.ColIDMap[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}
|
||||
schema.RelMap[TTKey{ct, ft}] = rel1
|
||||
|
||||
// One-to-many relation between the foreign key table and the
|
||||
// the current table
|
||||
rel2 := &DBRel{RelOneToMany, "", "", fc.Name, c.Name}
|
||||
schema.RelMap[TTKey{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 {
|
||||
col1, col2 := jcols[0], jcols[1]
|
||||
|
||||
t1 := strings.ToLower(col1.FKeyTable)
|
||||
t2 := strings.ToLower(col2.FKeyTable)
|
||||
|
||||
fc1, ok := schema.ColIDMap[col1.FKeyColID[0]]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fc2, ok := schema.ColIDMap[col2.FKeyColID[0]]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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}
|
||||
schema.RelMap[TTKey{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}
|
||||
schema.RelMap[TTKey{t2, t1}] = rel2
|
||||
}
|
||||
}
|
||||
|
||||
return schema, nil
|
||||
}
|
||||
|
||||
type DBTable struct {
|
||||
Name string `sql:"name"`
|
||||
Type string `sql:"type"`
|
||||
}
|
||||
|
||||
func GetTables(db *pg.DB) ([]*DBTable, error) {
|
||||
sqlStmt := `
|
||||
SELECT
|
||||
c.relname as "name",
|
||||
CASE c.relkind WHEN 'r' THEN 'table'
|
||||
WHEN 'v' THEN 'view'
|
||||
WHEN 'm' THEN 'materialized view'
|
||||
WHEN 'f' THEN 'foreign table' END as "type"
|
||||
FROM pg_catalog.pg_class c
|
||||
LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
WHERE c.relkind IN ('r','v','m','f','')
|
||||
AND n.nspname <> 'pg_catalog'
|
||||
AND n.nspname <> 'information_schema'
|
||||
AND n.nspname !~ '^pg_toast'
|
||||
AND pg_catalog.pg_table_is_visible(c.oid);
|
||||
`
|
||||
|
||||
var t []*DBTable
|
||||
_, err := db.Query(&t, sqlStmt)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fetching tables: %s", err)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
type DBColumn struct {
|
||||
ID int `sql:"id"`
|
||||
Name string `sql:"name"`
|
||||
Type string `sql:"type"`
|
||||
NotNull bool `sql:"notnull"`
|
||||
PrimaryKey bool `sql:"primarykey"`
|
||||
Uniquekey bool `sql:"uniquekey"`
|
||||
FKeyTable string `sql:"foreignkey"`
|
||||
FKeyColID []int `sql:"foreignkey_fieldnum,array"`
|
||||
}
|
||||
|
||||
func GetColumns(db *pg.DB, schema, table string) ([]*DBColumn, error) {
|
||||
sqlStmt := `
|
||||
SELECT
|
||||
f.attnum AS id,
|
||||
f.attname AS name,
|
||||
f.attnotnull AS notnull,
|
||||
pg_catalog.format_type(f.atttypid,f.atttypmod) AS type,
|
||||
CASE
|
||||
WHEN p.contype = 'p' THEN 't'
|
||||
ELSE 'f'
|
||||
END AS primarykey,
|
||||
CASE
|
||||
WHEN p.contype = 'u' THEN 't'
|
||||
ELSE 'f'
|
||||
END AS uniquekey,
|
||||
CASE
|
||||
WHEN p.contype = 'f' THEN g.relname
|
||||
END AS foreignkey,
|
||||
CASE
|
||||
WHEN p.contype = 'f' THEN p.confkey
|
||||
END AS foreignkey_fieldnum
|
||||
FROM pg_attribute f
|
||||
JOIN pg_class c ON c.oid = f.attrelid
|
||||
JOIN pg_type t ON t.oid = f.atttypid
|
||||
LEFT JOIN pg_attrdef d ON d.adrelid = c.oid AND d.adnum = f.attnum
|
||||
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
LEFT JOIN pg_constraint p ON p.conrelid = c.oid AND f.attnum = ANY (p.conkey)
|
||||
LEFT JOIN pg_class AS g ON p.confrelid = g.oid
|
||||
WHERE c.relkind = 'r'::char
|
||||
AND n.nspname = $1 -- Replace with Schema name
|
||||
AND c.relname = $2 -- Replace with table name
|
||||
AND f.attnum > 0 ORDER BY id;
|
||||
`
|
||||
|
||||
stmt, err := db.Prepare(sqlStmt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fetching columns: %s", err)
|
||||
}
|
||||
|
||||
var t []*DBColumn
|
||||
_, err = stmt.Query(&t, schema, table)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error fetching columns: %s", err)
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
Reference in New Issue
Block a user