2019-03-24 14:57:29 +01:00
|
|
|
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 {
|
2019-04-09 03:24:29 +02:00
|
|
|
Tables map[string]*DBTableInfo
|
2019-03-24 14:57:29 +01:00
|
|
|
RelMap map[TTKey]*DBRel
|
|
|
|
}
|
|
|
|
|
2019-04-05 07:44:30 +02:00
|
|
|
type DBTableInfo struct {
|
2019-04-09 03:24:29 +02:00
|
|
|
Name string
|
2019-04-05 07:44:30 +02:00
|
|
|
PrimaryCol string
|
|
|
|
TSVCol string
|
2019-04-09 03:24:29 +02:00
|
|
|
Columns map[string]*DBColumn
|
2019-04-05 07:44:30 +02:00
|
|
|
}
|
|
|
|
|
2019-03-24 14:57:29 +01:00
|
|
|
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) {
|
2019-04-09 03:24:29 +02:00
|
|
|
schema := &DBSchema{
|
|
|
|
Tables: make(map[string]*DBTableInfo),
|
|
|
|
RelMap: make(map[TTKey]*DBRel),
|
|
|
|
}
|
2019-03-24 14:57:29 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-04-09 03:24:29 +02:00
|
|
|
schema.updateSchema(t, cols)
|
2019-03-24 23:16:03 +01:00
|
|
|
}
|
2019-03-24 14:57:29 +01:00
|
|
|
|
2019-03-24 23:16:03 +01:00
|
|
|
return schema, nil
|
|
|
|
}
|
2019-03-24 14:57:29 +01:00
|
|
|
|
2019-04-09 03:24:29 +02:00
|
|
|
func (s *DBSchema) updateSchema(t *DBTable, cols []*DBColumn) {
|
2019-03-24 23:16:03 +01:00
|
|
|
// Current table
|
2019-04-09 03:24:29 +02:00
|
|
|
ti := &DBTableInfo{
|
|
|
|
Name: t.Name,
|
|
|
|
Columns: make(map[string]*DBColumn, len(cols)),
|
|
|
|
}
|
2019-03-24 14:57:29 +01:00
|
|
|
|
2019-03-24 23:16:03 +01:00
|
|
|
// Foreign key columns in current table
|
|
|
|
var jcols []*DBColumn
|
2019-04-09 03:24:29 +02:00
|
|
|
colByID := make(map[int]*DBColumn)
|
2019-03-24 14:57:29 +01:00
|
|
|
|
2019-04-09 03:24:29 +02:00
|
|
|
for i := range cols {
|
|
|
|
c := cols[i]
|
|
|
|
ti.Columns[strings.ToLower(c.Name)] = cols[i]
|
|
|
|
colByID[c.ID] = cols[i]
|
2019-03-24 23:16:03 +01:00
|
|
|
}
|
2019-03-24 14:57:29 +01:00
|
|
|
|
2019-04-09 03:24:29 +02:00
|
|
|
ct := strings.ToLower(t.Name)
|
|
|
|
s.Tables[ct] = ti
|
|
|
|
|
2019-03-24 23:16:03 +01:00
|
|
|
for _, c := range cols {
|
|
|
|
switch {
|
2019-04-05 07:44:30 +02:00
|
|
|
case c.Type == "tsvector":
|
2019-04-09 03:24:29 +02:00
|
|
|
s.Tables[ct].TSVCol = c.Name
|
2019-04-05 07:44:30 +02:00
|
|
|
|
2019-03-24 23:16:03 +01:00
|
|
|
case c.PrimaryKey:
|
2019-04-09 03:24:29 +02:00
|
|
|
s.Tables[ct].PrimaryCol = c.Name
|
2019-03-24 14:57:29 +01:00
|
|
|
|
2019-03-24 23:16:03 +01:00
|
|
|
case len(c.FKeyTable) != 0:
|
|
|
|
if len(c.FKeyColID) == 0 {
|
2019-03-24 14:57:29 +01:00
|
|
|
continue
|
|
|
|
}
|
2019-03-24 23:16:03 +01:00
|
|
|
|
|
|
|
// Foreign key column name
|
|
|
|
ft := strings.ToLower(c.FKeyTable)
|
2019-04-09 03:24:29 +02:00
|
|
|
fc, ok := colByID[c.FKeyColID[0]]
|
2019-03-24 14:57:29 +01:00
|
|
|
if !ok {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2019-03-24 23:16:03 +01:00
|
|
|
// Belongs-to relation between current table and the
|
|
|
|
// table in the foreign key
|
|
|
|
rel1 := &DBRel{RelBelongTo, "", "", c.Name, fc.Name}
|
2019-04-09 03:24:29 +02:00
|
|
|
s.RelMap[TTKey{ct, ft}] = rel1
|
2019-03-24 23:16:03 +01:00
|
|
|
|
|
|
|
// One-to-many relation between the foreign key table and the
|
|
|
|
// the current table
|
|
|
|
rel2 := &DBRel{RelOneToMany, "", "", fc.Name, c.Name}
|
2019-04-09 03:24:29 +02:00
|
|
|
s.RelMap[TTKey{ft, ct}] = rel2
|
2019-03-24 23:16:03 +01:00
|
|
|
|
|
|
|
jcols = append(jcols, c)
|
2019-03-24 14:57:29 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-24 23:16:03 +01:00
|
|
|
// 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 {
|
2019-04-04 06:53:24 +02:00
|
|
|
for i := range jcols {
|
|
|
|
for n := range jcols {
|
|
|
|
if n != i {
|
2019-04-09 03:24:29 +02:00
|
|
|
s.updateSchemaOTMT(ct, jcols[i], jcols[n], colByID)
|
2019-04-04 06:53:24 +02:00
|
|
|
}
|
|
|
|
}
|
2019-03-24 23:16:03 +01:00
|
|
|
}
|
2019-04-04 06:53:24 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-09 03:24:29 +02:00
|
|
|
func (s *DBSchema) updateSchemaOTMT(
|
|
|
|
ct string, col1, col2 *DBColumn, colByID map[int]*DBColumn) {
|
|
|
|
|
2019-04-04 06:53:24 +02:00
|
|
|
t1 := strings.ToLower(col1.FKeyTable)
|
|
|
|
t2 := strings.ToLower(col2.FKeyTable)
|
2019-03-24 23:16:03 +01:00
|
|
|
|
2019-04-09 03:24:29 +02:00
|
|
|
fc1, ok := colByID[col1.FKeyColID[0]]
|
2019-04-04 06:53:24 +02:00
|
|
|
if !ok {
|
|
|
|
return
|
2019-03-24 23:16:03 +01:00
|
|
|
}
|
2019-04-09 03:24:29 +02:00
|
|
|
fc2, ok := colByID[col2.FKeyColID[0]]
|
2019-04-04 06:53:24 +02:00
|
|
|
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}
|
2019-04-09 03:24:29 +02:00
|
|
|
s.RelMap[TTKey{t1, t2}] = rel1
|
2019-04-04 06:53:24 +02:00
|
|
|
|
|
|
|
// 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}
|
2019-04-09 03:24:29 +02:00
|
|
|
s.RelMap[TTKey{t2, t1}] = rel2
|
2019-03-24 14:57:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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 {
|
2019-04-09 03:24:29 +02:00
|
|
|
return nil, fmt.Errorf("error fetching columns: %s", err)
|
2019-03-24 14:57:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var t []*DBColumn
|
|
|
|
_, err = stmt.Query(&t, schema, table)
|
|
|
|
if err != nil {
|
2019-04-09 03:24:29 +02:00
|
|
|
return nil, fmt.Errorf("error fetching columns: %s", err)
|
2019-03-24 14:57:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return t, nil
|
|
|
|
}
|
2019-04-05 07:44:30 +02:00
|
|
|
|
|
|
|
func (s *DBSchema) GetTable(table string) (*DBTableInfo, error) {
|
|
|
|
t, ok := s.Tables[table]
|
|
|
|
if !ok {
|
2019-04-09 03:24:29 +02:00
|
|
|
return nil, fmt.Errorf("unknown table '%s'", table)
|
2019-04-05 07:44:30 +02:00
|
|
|
}
|
|
|
|
return t, nil
|
|
|
|
}
|