1067 lines
24 KiB
Go
1067 lines
24 KiB
Go
package psql
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"strings"
|
|
|
|
"github.com/cespare/xxhash/v2"
|
|
"github.com/dosco/super-graph/qcode"
|
|
"github.com/dosco/super-graph/util"
|
|
)
|
|
|
|
const (
|
|
empty = ""
|
|
closeBlock = 500
|
|
)
|
|
|
|
type Variables map[string]json.RawMessage
|
|
|
|
type Config struct {
|
|
Schema *DBSchema
|
|
Vars map[string]string
|
|
}
|
|
|
|
type Compiler struct {
|
|
schema *DBSchema
|
|
vars map[string]string
|
|
}
|
|
|
|
func NewCompiler(conf Config) *Compiler {
|
|
return &Compiler{conf.Schema, conf.Vars}
|
|
}
|
|
|
|
func (c *Compiler) AddRelationship(child, parent string, rel *DBRel) error {
|
|
return c.schema.SetRel(child, parent, rel)
|
|
}
|
|
|
|
func (c *Compiler) IDColumn(table string) (string, error) {
|
|
t, err := c.schema.GetTable(table)
|
|
if err != nil {
|
|
return empty, err
|
|
}
|
|
|
|
return t.PrimaryCol, nil
|
|
}
|
|
|
|
type compilerContext struct {
|
|
w *bytes.Buffer
|
|
s []qcode.Select
|
|
*Compiler
|
|
}
|
|
|
|
func (co *Compiler) CompileEx(qc *qcode.QCode, vars Variables) (uint32, []byte, error) {
|
|
w := &bytes.Buffer{}
|
|
skipped, err := co.Compile(qc, w, vars)
|
|
return skipped, w.Bytes(), err
|
|
}
|
|
|
|
func (co *Compiler) Compile(qc *qcode.QCode, w *bytes.Buffer, vars Variables) (uint32, error) {
|
|
switch qc.Type {
|
|
case qcode.QTQuery:
|
|
return co.compileQuery(qc, w)
|
|
case qcode.QTMutation:
|
|
return co.compileMutation(qc, w, vars)
|
|
}
|
|
|
|
return 0, errors.New("unknown operation")
|
|
}
|
|
|
|
func (co *Compiler) compileQuery(qc *qcode.QCode, w *bytes.Buffer) (uint32, error) {
|
|
if len(qc.Selects) == 0 {
|
|
return 0, errors.New("empty query")
|
|
}
|
|
|
|
c := &compilerContext{w, qc.Selects, co}
|
|
root := &qc.Selects[0]
|
|
|
|
ti, err := c.schema.GetTable(root.Table)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
st := NewStack()
|
|
st.Push(root.ID + closeBlock)
|
|
st.Push(root.ID)
|
|
|
|
//fmt.Fprintf(w, `SELECT json_object_agg('%s', %s) FROM (`,
|
|
//root.FieldName, root.Table)
|
|
c.w.WriteString(`SELECT json_object_agg('`)
|
|
c.w.WriteString(root.FieldName)
|
|
c.w.WriteString(`', `)
|
|
|
|
if ti.Singular == false {
|
|
c.w.WriteString(root.Table)
|
|
} else {
|
|
c.w.WriteString("sel_json_")
|
|
int2string(c.w, root.ID)
|
|
}
|
|
c.w.WriteString(`) FROM (`)
|
|
|
|
var ignored uint32
|
|
|
|
for {
|
|
if st.Len() == 0 {
|
|
break
|
|
}
|
|
|
|
id := st.Pop()
|
|
|
|
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, ti)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
ignored |= skipped
|
|
|
|
for _, cid := range sel.Children {
|
|
if hasBit(skipped, uint32(cid)) {
|
|
continue
|
|
}
|
|
child := &c.s[cid]
|
|
|
|
st.Push(child.ID + closeBlock)
|
|
st.Push(child.ID)
|
|
}
|
|
|
|
} else {
|
|
sel := &c.s[(id - closeBlock)]
|
|
|
|
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 {
|
|
return 0, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
c.w.WriteString(`)`)
|
|
alias(c.w, `done_1337`)
|
|
c.w.WriteString(`;`)
|
|
|
|
return ignored, nil
|
|
}
|
|
|
|
func (c *compilerContext) processChildren(sel *qcode.Select, ti *DBTableInfo) (uint32, []*qcode.Column) {
|
|
var skipped uint32
|
|
|
|
cols := make([]*qcode.Column, 0, len(sel.Cols))
|
|
colmap := make(map[string]struct{}, len(sel.Cols))
|
|
|
|
for i := range sel.Cols {
|
|
colmap[sel.Cols[i].Name] = struct{}{}
|
|
}
|
|
|
|
for _, id := range sel.Children {
|
|
child := &c.s[id]
|
|
|
|
rel, err := c.schema.GetRel(child.Table, ti.Name)
|
|
if err != nil {
|
|
skipped |= (1 << uint(id))
|
|
continue
|
|
}
|
|
|
|
switch rel.Type {
|
|
case RelOneToMany:
|
|
fallthrough
|
|
case RelBelongTo:
|
|
if _, ok := colmap[rel.Col2]; !ok {
|
|
cols = append(cols, &qcode.Column{ti.Name, rel.Col2, rel.Col2})
|
|
}
|
|
case RelOneToManyThrough:
|
|
if _, ok := colmap[rel.Col1]; !ok {
|
|
cols = append(cols, &qcode.Column{ti.Name, rel.Col1, rel.Col1})
|
|
}
|
|
case RelRemote:
|
|
if _, ok := colmap[rel.Col1]; !ok {
|
|
cols = append(cols, &qcode.Column{ti.Name, rel.Col1, rel.Col2})
|
|
}
|
|
skipped |= (1 << uint(id))
|
|
|
|
default:
|
|
skipped |= (1 << uint(id))
|
|
}
|
|
}
|
|
|
|
return skipped, cols
|
|
}
|
|
|
|
func (c *compilerContext) renderSelect(sel *qcode.Select, ti *DBTableInfo) (uint32, error) {
|
|
skipped, childCols := c.processChildren(sel, ti)
|
|
hasOrder := len(sel.OrderBy) != 0
|
|
|
|
// SELECT
|
|
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_json_")
|
|
int2string(c.w, sel.ID)
|
|
c.w.WriteString(`"`)
|
|
|
|
if hasOrder {
|
|
err := c.renderOrderBy(sel, ti)
|
|
if err != nil {
|
|
return skipped, err
|
|
}
|
|
}
|
|
|
|
//fmt.Fprintf(w, `), '[]') AS "%s" FROM (`, c.sel.Table)
|
|
c.w.WriteString(`), '[]')`)
|
|
alias(c.w, sel.Table)
|
|
c.w.WriteString(` FROM (`)
|
|
}
|
|
|
|
// ROW-TO-JSON
|
|
c.w.WriteString(`SELECT `)
|
|
|
|
if len(sel.DistinctOn) != 0 {
|
|
c.renderDistinctOn(sel, ti)
|
|
}
|
|
|
|
c.w.WriteString(`row_to_json((`)
|
|
|
|
//fmt.Fprintf(w, `SELECT "sel_%d" FROM (SELECT `, c.sel.ID)
|
|
c.w.WriteString(`SELECT "sel_`)
|
|
int2string(c.w, sel.ID)
|
|
c.w.WriteString(`" FROM (SELECT `)
|
|
|
|
// Combined column names
|
|
c.renderColumns(sel, ti)
|
|
|
|
c.renderRemoteRelColumns(sel, ti)
|
|
|
|
err := c.renderJoinedColumns(sel, ti, skipped)
|
|
if err != nil {
|
|
return skipped, err
|
|
}
|
|
|
|
//fmt.Fprintf(w, `) AS "sel_%d"`, c.sel.ID)
|
|
c.w.WriteString(`)`)
|
|
aliasWithID(c.w, "sel", sel.ID)
|
|
|
|
//fmt.Fprintf(w, `)) AS "%s"`, c.sel.Table)
|
|
c.w.WriteString(`))`)
|
|
aliasWithID(c.w, "sel_json", sel.ID)
|
|
// END-ROW-TO-JSON
|
|
|
|
if hasOrder {
|
|
c.renderOrderByColumns(sel, ti)
|
|
}
|
|
// END-SELECT
|
|
|
|
// FROM (SELECT .... )
|
|
err = c.renderBaseSelect(sel, ti, childCols, skipped)
|
|
if err != nil {
|
|
return skipped, err
|
|
}
|
|
// END-FROM
|
|
|
|
return skipped, nil
|
|
}
|
|
|
|
func (c *compilerContext) renderSelectClose(sel *qcode.Select, ti *DBTableInfo) error {
|
|
hasOrder := len(sel.OrderBy) != 0
|
|
|
|
if hasOrder {
|
|
err := c.renderOrderBy(sel, ti)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if sel.Action == 0 {
|
|
if len(sel.Paging.Limit) != 0 {
|
|
//fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit)
|
|
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`)
|
|
}
|
|
}
|
|
|
|
if len(sel.Paging.Offset) != 0 {
|
|
//fmt.Fprintf(w, ` OFFSET ('%s') :: integer`, c.sel.Paging.Offset)
|
|
c.w.WriteString(`OFFSET ('`)
|
|
c.w.WriteString(sel.Paging.Offset)
|
|
c.w.WriteString(`') :: integer`)
|
|
}
|
|
|
|
if ti.Singular == false {
|
|
//fmt.Fprintf(w, `) AS "sel_json_agg_%d"`, c.sel.ID)
|
|
c.w.WriteString(`)`)
|
|
aliasWithID(c.w, "sel_json_agg", sel.ID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *compilerContext) renderJoin(sel *qcode.Select) error {
|
|
c.w.WriteString(` LEFT OUTER JOIN LATERAL (`)
|
|
return nil
|
|
}
|
|
|
|
func (c *compilerContext) renderJoinClose(sel *qcode.Select) error {
|
|
//fmt.Fprintf(w, `) AS "%s_%d_join" ON ('true')`, c.sel.Table, c.sel.ID)
|
|
c.w.WriteString(`)`)
|
|
aliasWithIDSuffix(c.w, sel.Table, sel.ID, "_join")
|
|
c.w.WriteString(` ON ('true')`)
|
|
return nil
|
|
}
|
|
|
|
func (c *compilerContext) renderJoinTable(sel *qcode.Select) error {
|
|
parent := &c.s[sel.ParentID]
|
|
|
|
rel, err := c.schema.GetRel(sel.Table, parent.Table)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if rel.Type != RelOneToManyThrough {
|
|
return err
|
|
}
|
|
|
|
pt, err := c.schema.GetTable(parent.Table)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
//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)
|
|
c.w.WriteString(` LEFT OUTER JOIN "`)
|
|
c.w.WriteString(rel.Through)
|
|
c.w.WriteString(`" ON ((`)
|
|
colWithTable(c.w, rel.Through, rel.ColT)
|
|
c.w.WriteString(`) = (`)
|
|
colWithTableID(c.w, pt.Name, parent.ID, rel.Col1)
|
|
c.w.WriteString(`))`)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *compilerContext) renderColumns(sel *qcode.Select, ti *DBTableInfo) {
|
|
for i, col := range sel.Cols {
|
|
if i != 0 {
|
|
io.WriteString(c.w, ", ")
|
|
}
|
|
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`,
|
|
//c.sel.Table, c.sel.ID, col.Name, col.FieldName)
|
|
colWithTableIDAlias(c.w, ti.Name, sel.ID, col.Name, col.FieldName)
|
|
}
|
|
}
|
|
|
|
func (c *compilerContext) renderRemoteRelColumns(sel *qcode.Select, ti *DBTableInfo) {
|
|
i := 0
|
|
|
|
for _, id := range sel.Children {
|
|
child := &c.s[id]
|
|
|
|
rel, err := c.schema.GetRel(child.Table, sel.Table)
|
|
if err != nil || rel.Type != RelRemote {
|
|
continue
|
|
}
|
|
if i != 0 || len(sel.Cols) != 0 {
|
|
io.WriteString(c.w, ", ")
|
|
}
|
|
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s"`,
|
|
//c.sel.Table, c.sel.ID, rel.Col1, rel.Col2)
|
|
colWithTableID(c.w, ti.Name, sel.ID, rel.Col1)
|
|
alias(c.w, rel.Col2)
|
|
i++
|
|
}
|
|
}
|
|
|
|
func (c *compilerContext) renderJoinedColumns(sel *qcode.Select, ti *DBTableInfo, skipped uint32) error {
|
|
colsRendered := len(sel.Cols) != 0
|
|
|
|
for _, id := range sel.Children {
|
|
skipThis := hasBit(skipped, uint32(id))
|
|
|
|
if colsRendered && !skipThis {
|
|
io.WriteString(c.w, ", ")
|
|
}
|
|
if skipThis {
|
|
continue
|
|
}
|
|
childSel := &c.s[id]
|
|
|
|
//fmt.Fprintf(w, `"%s_%d_join"."%s" AS "%s"`,
|
|
//s.Table, s.ID, s.Table, s.FieldName)
|
|
colWithTableIDSuffixAlias(c.w, childSel.Table, childSel.ID,
|
|
"_join", childSel.Table, childSel.FieldName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *compilerContext) renderBaseSelect(sel *qcode.Select, ti *DBTableInfo,
|
|
childCols []*qcode.Column, skipped uint32) error {
|
|
var groupBy []int
|
|
|
|
isRoot := sel.ID == 0
|
|
isFil := sel.Where != nil
|
|
isSearch := sel.Args["search"] != nil
|
|
isAgg := false
|
|
|
|
c.w.WriteString(` FROM (SELECT `)
|
|
|
|
for i, col := range sel.Cols {
|
|
cn := col.Name
|
|
|
|
_, isRealCol := ti.Columns[cn]
|
|
|
|
if !isRealCol {
|
|
if isSearch {
|
|
switch {
|
|
case cn == "search_rank":
|
|
cn = ti.TSVCol
|
|
arg := sel.Args["search"]
|
|
|
|
//fmt.Fprintf(w, `ts_rank("%s"."%s", to_tsquery('%s')) AS %s`,
|
|
//c.sel.Table, cn, arg.Val, col.Name)
|
|
c.w.WriteString(`ts_rank(`)
|
|
colWithTable(c.w, ti.Name, cn)
|
|
c.w.WriteString(`, to_tsquery('`)
|
|
c.w.WriteString(arg.Val)
|
|
c.w.WriteString(`')`)
|
|
alias(c.w, col.Name)
|
|
|
|
case strings.HasPrefix(cn, "search_headline_"):
|
|
cn = cn[16:]
|
|
arg := sel.Args["search"]
|
|
|
|
//fmt.Fprintf(w, `ts_headline("%s"."%s", to_tsquery('%s')) AS %s`,
|
|
//c.sel.Table, cn, arg.Val, col.Name)
|
|
c.w.WriteString(`ts_headlinek(`)
|
|
colWithTable(c.w, ti.Name, cn)
|
|
c.w.WriteString(`, to_tsquery('`)
|
|
c.w.WriteString(arg.Val)
|
|
c.w.WriteString(`')`)
|
|
alias(c.w, col.Name)
|
|
}
|
|
} else {
|
|
pl := funcPrefixLen(cn)
|
|
if pl == 0 {
|
|
//fmt.Fprintf(w, `'%s not defined' AS %s`, cn, col.Name)
|
|
c.w.WriteString(`'`)
|
|
c.w.WriteString(cn)
|
|
c.w.WriteString(` not defined'`)
|
|
alias(c.w, col.Name)
|
|
} else {
|
|
isAgg = true
|
|
fn := cn[0 : pl-1]
|
|
cn := cn[pl:]
|
|
//fmt.Fprintf(w, `%s("%s"."%s") AS %s`, fn, c.sel.Table, cn, col.Name)
|
|
c.w.WriteString(fn)
|
|
c.w.WriteString(`(`)
|
|
colWithTable(c.w, ti.Name, cn)
|
|
c.w.WriteString(`)`)
|
|
alias(c.w, col.Name)
|
|
}
|
|
}
|
|
} else {
|
|
groupBy = append(groupBy, i)
|
|
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Table, cn)
|
|
colWithTable(c.w, ti.Name, cn)
|
|
}
|
|
|
|
if i < len(sel.Cols)-1 || len(childCols) != 0 {
|
|
//io.WriteString(w, ", ")
|
|
c.w.WriteString(`, `)
|
|
}
|
|
}
|
|
|
|
for i, col := range childCols {
|
|
if i != 0 {
|
|
//io.WriteString(w, ", ")
|
|
c.w.WriteString(`, `)
|
|
}
|
|
|
|
//fmt.Fprintf(w, `"%s"."%s"`, col.Table, col.Name)
|
|
colWithTable(c.w, col.Table, col.Name)
|
|
}
|
|
|
|
c.w.WriteString(` FROM `)
|
|
|
|
//fmt.Fprintf(w, ` FROM "%s"`, c.sel.Table)
|
|
c.w.WriteString(`"`)
|
|
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, ti); err != nil {
|
|
return err
|
|
}
|
|
c.w.WriteString(`)`)
|
|
}
|
|
|
|
if !isRoot {
|
|
if err := c.renderJoinTable(sel); err != nil {
|
|
return err
|
|
}
|
|
|
|
c.w.WriteString(` WHERE (`)
|
|
|
|
if err := c.renderRelationship(sel, ti); err != nil {
|
|
return err
|
|
}
|
|
|
|
if isFil {
|
|
c.w.WriteString(` AND `)
|
|
if err := c.renderWhere(sel, ti); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
c.w.WriteString(`)`)
|
|
}
|
|
|
|
if isAgg {
|
|
if len(groupBy) != 0 {
|
|
c.w.WriteString(` GROUP BY `)
|
|
|
|
for i, id := range groupBy {
|
|
if i != 0 {
|
|
c.w.WriteString(`, `)
|
|
}
|
|
//fmt.Fprintf(w, `"%s"."%s"`, c.sel.Table, c.sel.Cols[id].Name)
|
|
colWithTable(c.w, ti.Name, sel.Cols[id].Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
if sel.Action == 0 {
|
|
if len(sel.Paging.Limit) != 0 {
|
|
//fmt.Fprintf(w, ` LIMIT ('%s') :: integer`, c.sel.Paging.Limit)
|
|
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`)
|
|
}
|
|
}
|
|
|
|
if len(sel.Paging.Offset) != 0 {
|
|
//fmt.Fprintf(w, ` OFFSET ('%s') :: integer`, c.sel.Paging.Offset)
|
|
c.w.WriteString(` OFFSET ('`)
|
|
c.w.WriteString(sel.Paging.Offset)
|
|
c.w.WriteString(`') :: integer`)
|
|
}
|
|
|
|
//fmt.Fprintf(w, `) AS "%s_%d"`, c.sel.Table, c.sel.ID)
|
|
c.w.WriteString(`)`)
|
|
aliasWithID(c.w, ti.Name, sel.ID)
|
|
return nil
|
|
}
|
|
|
|
func (c *compilerContext) renderOrderByColumns(sel *qcode.Select, ti *DBTableInfo) {
|
|
colsRendered := len(sel.Cols) != 0
|
|
|
|
for i := range sel.OrderBy {
|
|
if colsRendered {
|
|
//io.WriteString(w, ", ")
|
|
c.w.WriteString(`, `)
|
|
}
|
|
|
|
col := sel.OrderBy[i].Col
|
|
//fmt.Fprintf(w, `"%s_%d"."%s" AS "%s_%d_%s_ob"`,
|
|
//c.sel.Table, c.sel.ID, c,
|
|
//c.sel.Table, c.sel.ID, c)
|
|
colWithTableID(c.w, ti.Name, sel.ID, col)
|
|
c.w.WriteString(` AS `)
|
|
tableIDColSuffix(c.w, sel.Table, sel.ID, col, "_ob")
|
|
}
|
|
}
|
|
|
|
func (c *compilerContext) renderRelationship(sel *qcode.Select, ti *DBTableInfo) error {
|
|
parent := c.s[sel.ParentID]
|
|
|
|
rel, err := c.schema.GetRel(sel.Table, parent.Table)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
switch rel.Type {
|
|
case RelBelongTo:
|
|
//fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`,
|
|
//c.sel.Table, rel.Col1, c.parent.Table, c.parent.ID, rel.Col2)
|
|
c.w.WriteString(`((`)
|
|
colWithTable(c.w, ti.Name, rel.Col1)
|
|
c.w.WriteString(`) = (`)
|
|
colWithTableID(c.w, parent.Table, parent.ID, rel.Col2)
|
|
c.w.WriteString(`))`)
|
|
|
|
case RelOneToMany:
|
|
//fmt.Fprintf(w, `(("%s"."%s") = ("%s_%d"."%s"))`,
|
|
//c.sel.Table, rel.Col1, c.parent.Table, c.parent.ID, rel.Col2)
|
|
c.w.WriteString(`((`)
|
|
colWithTable(c.w, ti.Name, rel.Col1)
|
|
c.w.WriteString(`) = (`)
|
|
colWithTableID(c.w, parent.Table, parent.ID, rel.Col2)
|
|
c.w.WriteString(`))`)
|
|
|
|
case RelOneToManyThrough:
|
|
//fmt.Fprintf(w, `(("%s"."%s") = ("%s"."%s"))`,
|
|
//c.sel.Table, rel.Col1, rel.Through, rel.Col2)
|
|
c.w.WriteString(`((`)
|
|
colWithTable(c.w, ti.Name, rel.Col1)
|
|
c.w.WriteString(`) = (`)
|
|
colWithTable(c.w, rel.Through, rel.Col2)
|
|
c.w.WriteString(`))`)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *compilerContext) renderWhere(sel *qcode.Select, ti *DBTableInfo) error {
|
|
st := util.NewStack()
|
|
|
|
if sel.Where != nil {
|
|
st.Push(sel.Where)
|
|
}
|
|
|
|
for {
|
|
if st.Len() == 0 {
|
|
break
|
|
}
|
|
|
|
intf := st.Pop()
|
|
|
|
switch val := intf.(type) {
|
|
case qcode.ExpOp:
|
|
switch val {
|
|
case qcode.OpAnd:
|
|
c.w.WriteString(` AND `)
|
|
case qcode.OpOr:
|
|
c.w.WriteString(` OR `)
|
|
case qcode.OpNot:
|
|
c.w.WriteString(`NOT `)
|
|
default:
|
|
return fmt.Errorf("11: unexpected value %v (%t)", intf, intf)
|
|
}
|
|
|
|
case *qcode.Exp:
|
|
switch val.Op {
|
|
case qcode.OpAnd, qcode.OpOr:
|
|
for i := len(val.Children) - 1; i >= 0; i-- {
|
|
st.Push(val.Children[i])
|
|
if i > 0 {
|
|
st.Push(val.Op)
|
|
}
|
|
}
|
|
qcode.FreeExp(val)
|
|
|
|
case qcode.OpNot:
|
|
st.Push(val.Children[0])
|
|
st.Push(qcode.OpNot)
|
|
qcode.FreeExp(val)
|
|
|
|
default:
|
|
if val.NestedCol {
|
|
//fmt.Fprintf(w, `(("%s") `, val.Col)
|
|
c.w.WriteString(`(("`)
|
|
c.w.WriteString(val.Col)
|
|
c.w.WriteString(`") `)
|
|
|
|
} else if len(val.Col) != 0 {
|
|
//fmt.Fprintf(w, `(("%s"."%s") `, c.sel.Table, val.Col)
|
|
c.w.WriteString(`((`)
|
|
colWithTable(c.w, ti.Name, val.Col)
|
|
c.w.WriteString(`) `)
|
|
}
|
|
valExists := true
|
|
|
|
switch val.Op {
|
|
case qcode.OpEquals:
|
|
c.w.WriteString(`=`)
|
|
case qcode.OpNotEquals:
|
|
c.w.WriteString(`!=`)
|
|
case qcode.OpGreaterOrEquals:
|
|
c.w.WriteString(`>=`)
|
|
case qcode.OpLesserOrEquals:
|
|
c.w.WriteString(`<=`)
|
|
case qcode.OpGreaterThan:
|
|
c.w.WriteString(`>`)
|
|
case qcode.OpLesserThan:
|
|
c.w.WriteString(`<`)
|
|
case qcode.OpIn:
|
|
c.w.WriteString(`IN`)
|
|
case qcode.OpNotIn:
|
|
c.w.WriteString(`NOT IN`)
|
|
case qcode.OpLike:
|
|
c.w.WriteString(`LIKE`)
|
|
case qcode.OpNotLike:
|
|
c.w.WriteString(`NOT LIKE`)
|
|
case qcode.OpILike:
|
|
c.w.WriteString(`ILIKE`)
|
|
case qcode.OpNotILike:
|
|
c.w.WriteString(`NOT ILIKE`)
|
|
case qcode.OpSimilar:
|
|
c.w.WriteString(`SIMILAR TO`)
|
|
case qcode.OpNotSimilar:
|
|
c.w.WriteString(`NOT SIMILAR TO`)
|
|
case qcode.OpContains:
|
|
c.w.WriteString(`@>`)
|
|
case qcode.OpContainedIn:
|
|
c.w.WriteString(`<@`)
|
|
case qcode.OpHasKey:
|
|
c.w.WriteString(`?`)
|
|
case qcode.OpHasKeyAny:
|
|
c.w.WriteString(`?|`)
|
|
case qcode.OpHasKeyAll:
|
|
c.w.WriteString(`?&`)
|
|
case qcode.OpIsNull:
|
|
if strings.EqualFold(val.Val, "true") {
|
|
c.w.WriteString(`IS NULL)`)
|
|
} else {
|
|
c.w.WriteString(`IS NOT NULL)`)
|
|
}
|
|
valExists = false
|
|
case qcode.OpEqID:
|
|
if len(ti.PrimaryCol) == 0 {
|
|
return fmt.Errorf("no primary key column defined for %s", ti.Name)
|
|
}
|
|
//fmt.Fprintf(w, `(("%s") =`, c.ti.PrimaryCol)
|
|
c.w.WriteString(`((`)
|
|
colWithTable(c.w, ti.Name, ti.PrimaryCol)
|
|
//c.w.WriteString(ti.PrimaryCol)
|
|
c.w.WriteString(`) =`)
|
|
case qcode.OpTsQuery:
|
|
if len(ti.TSVCol) == 0 {
|
|
return fmt.Errorf("no tsv column defined for %s", ti.Name)
|
|
}
|
|
//fmt.Fprintf(w, `(("%s") @@ to_tsquery('%s'))`, c.ti.TSVCol, val.Val)
|
|
c.w.WriteString(`(("`)
|
|
c.w.WriteString(ti.TSVCol)
|
|
c.w.WriteString(`") @@ to_tsquery('`)
|
|
c.w.WriteString(val.Val)
|
|
c.w.WriteString(`'))`)
|
|
valExists = false
|
|
|
|
default:
|
|
return fmt.Errorf("[Where] unexpected op code %d", val.Op)
|
|
}
|
|
|
|
if valExists {
|
|
if val.Type == qcode.ValList {
|
|
c.renderList(val)
|
|
} else {
|
|
c.renderVal(val, c.vars)
|
|
}
|
|
c.w.WriteString(`)`)
|
|
}
|
|
|
|
qcode.FreeExp(val)
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("12: unexpected value %v (%t)", intf, intf)
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (c *compilerContext) renderOrderBy(sel *qcode.Select, ti *DBTableInfo) error {
|
|
c.w.WriteString(` ORDER BY `)
|
|
for i := range sel.OrderBy {
|
|
if i != 0 {
|
|
c.w.WriteString(`, `)
|
|
}
|
|
ob := sel.OrderBy[i]
|
|
|
|
switch ob.Order {
|
|
case qcode.OrderAsc:
|
|
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC`, sel.Table, sel.ID, ob.Col)
|
|
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
|
c.w.WriteString(` ASC`)
|
|
case qcode.OrderDesc:
|
|
//fmt.Fprintf(w, `"%s_%d.ob.%s" DESC`, sel.Table, sel.ID, ob.Col)
|
|
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
|
c.w.WriteString(` DESC`)
|
|
case qcode.OrderAscNullsFirst:
|
|
//fmt.Fprintf(w, `"%s_%d.ob.%s" ASC NULLS FIRST`, sel.Table, sel.ID, ob.Col)
|
|
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
|
c.w.WriteString(` ASC NULLS FIRST`)
|
|
case qcode.OrderDescNullsFirst:
|
|
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS FIRST`, sel.Table, sel.ID, ob.Col)
|
|
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
|
c.w.WriteString(` DESC NULLLS FIRST`)
|
|
case qcode.OrderAscNullsLast:
|
|
//fmt.Fprintf(w, `"%s_%d.ob.%s ASC NULLS LAST`, sel.Table, sel.ID, ob.Col)
|
|
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
|
c.w.WriteString(` ASC NULLS LAST`)
|
|
case qcode.OrderDescNullsLast:
|
|
//fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS LAST`, sel.Table, sel.ID, ob.Col)
|
|
tableIDColSuffix(c.w, ti.Name, sel.ID, ob.Col, "_ob")
|
|
c.w.WriteString(` DESC NULLS LAST`)
|
|
default:
|
|
return fmt.Errorf("13: unexpected value %v", ob.Order)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *compilerContext) renderDistinctOn(sel *qcode.Select, ti *DBTableInfo) {
|
|
io.WriteString(c.w, `DISTINCT ON (`)
|
|
for i := range sel.DistinctOn {
|
|
if i != 0 {
|
|
c.w.WriteString(`, `)
|
|
}
|
|
//fmt.Fprintf(w, `"%s_%d.ob.%s"`, c.sel.Table, c.sel.ID, c.sel.DistinctOn[i])
|
|
tableIDColSuffix(c.w, ti.Name, sel.ID, sel.DistinctOn[i], "_ob")
|
|
}
|
|
c.w.WriteString(`) `)
|
|
}
|
|
|
|
func (c *compilerContext) renderList(ex *qcode.Exp) {
|
|
io.WriteString(c.w, ` (`)
|
|
for i := range ex.ListVal {
|
|
if i != 0 {
|
|
c.w.WriteString(`, `)
|
|
}
|
|
switch ex.ListType {
|
|
case qcode.ValBool, qcode.ValInt, qcode.ValFloat:
|
|
c.w.WriteString(ex.ListVal[i])
|
|
case qcode.ValStr:
|
|
c.w.WriteString(`'`)
|
|
c.w.WriteString(ex.ListVal[i])
|
|
c.w.WriteString(`'`)
|
|
}
|
|
}
|
|
c.w.WriteString(`)`)
|
|
}
|
|
|
|
func (c *compilerContext) renderVal(ex *qcode.Exp, vars map[string]string) {
|
|
io.WriteString(c.w, ` `)
|
|
|
|
switch ex.Type {
|
|
case qcode.ValBool, qcode.ValInt, qcode.ValFloat:
|
|
if len(ex.Val) != 0 {
|
|
c.w.WriteString(ex.Val)
|
|
} else {
|
|
c.w.WriteString(`''`)
|
|
}
|
|
|
|
case qcode.ValStr:
|
|
c.w.WriteString(`'`)
|
|
c.w.WriteString(ex.Val)
|
|
c.w.WriteString(`'`)
|
|
|
|
case qcode.ValVar:
|
|
if val, ok := vars[ex.Val]; ok {
|
|
c.w.WriteString(val)
|
|
} else {
|
|
//fmt.Fprintf(w, `'{{%s}}'`, ex.Val)
|
|
c.w.WriteString(`{{`)
|
|
c.w.WriteString(ex.Val)
|
|
c.w.WriteString(`}}`)
|
|
}
|
|
}
|
|
//c.w.WriteString(`)`)
|
|
}
|
|
|
|
func funcPrefixLen(fn string) int {
|
|
switch {
|
|
case strings.HasPrefix(fn, "avg_"):
|
|
return 4
|
|
case strings.HasPrefix(fn, "count_"):
|
|
return 6
|
|
case strings.HasPrefix(fn, "max_"):
|
|
return 4
|
|
case strings.HasPrefix(fn, "min_"):
|
|
return 4
|
|
case strings.HasPrefix(fn, "sum_"):
|
|
return 4
|
|
case strings.HasPrefix(fn, "stddev_"):
|
|
return 7
|
|
case strings.HasPrefix(fn, "stddev_pop_"):
|
|
return 11
|
|
case strings.HasPrefix(fn, "stddev_samp_"):
|
|
return 12
|
|
case strings.HasPrefix(fn, "variance_"):
|
|
return 9
|
|
case strings.HasPrefix(fn, "var_pop_"):
|
|
return 8
|
|
case strings.HasPrefix(fn, "var_samp_"):
|
|
return 9
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func hasBit(n uint32, pos uint32) bool {
|
|
val := n & (1 << pos)
|
|
return (val > 0)
|
|
}
|
|
|
|
func alias(w *bytes.Buffer, alias string) {
|
|
w.WriteString(` AS "`)
|
|
w.WriteString(alias)
|
|
w.WriteString(`"`)
|
|
}
|
|
|
|
func aliasWithID(w *bytes.Buffer, alias string, id int32) {
|
|
w.WriteString(` AS "`)
|
|
w.WriteString(alias)
|
|
w.WriteString(`_`)
|
|
int2string(w, id)
|
|
w.WriteString(`"`)
|
|
}
|
|
|
|
func aliasWithIDSuffix(w *bytes.Buffer, alias string, id int32, suffix string) {
|
|
w.WriteString(` AS "`)
|
|
w.WriteString(alias)
|
|
w.WriteString(`_`)
|
|
int2string(w, id)
|
|
w.WriteString(suffix)
|
|
w.WriteString(`"`)
|
|
}
|
|
|
|
func colWithAlias(w *bytes.Buffer, col, alias string) {
|
|
w.WriteString(`"`)
|
|
w.WriteString(col)
|
|
w.WriteString(`" AS "`)
|
|
w.WriteString(alias)
|
|
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)
|
|
w.WriteString(`"."`)
|
|
w.WriteString(col)
|
|
w.WriteString(`"`)
|
|
}
|
|
|
|
func colWithTableID(w *bytes.Buffer, table string, id int32, col string) {
|
|
w.WriteString(`"`)
|
|
w.WriteString(table)
|
|
w.WriteString(`_`)
|
|
int2string(w, id)
|
|
w.WriteString(`"."`)
|
|
w.WriteString(col)
|
|
w.WriteString(`"`)
|
|
}
|
|
|
|
func colWithTableIDAlias(w *bytes.Buffer, table string, id int32, col, alias string) {
|
|
w.WriteString(`"`)
|
|
w.WriteString(table)
|
|
w.WriteString(`_`)
|
|
int2string(w, id)
|
|
w.WriteString(`"."`)
|
|
w.WriteString(col)
|
|
w.WriteString(`" AS "`)
|
|
w.WriteString(alias)
|
|
w.WriteString(`"`)
|
|
}
|
|
|
|
func colWithTableIDSuffixAlias(w *bytes.Buffer, table string, id int32,
|
|
suffix, col, alias string) {
|
|
w.WriteString(`"`)
|
|
w.WriteString(table)
|
|
w.WriteString(`_`)
|
|
int2string(w, id)
|
|
w.WriteString(suffix)
|
|
w.WriteString(`"."`)
|
|
w.WriteString(col)
|
|
w.WriteString(`" AS "`)
|
|
w.WriteString(alias)
|
|
w.WriteString(`"`)
|
|
}
|
|
|
|
func tableIDColSuffix(w *bytes.Buffer, table string, id int32, col, suffix string) {
|
|
w.WriteString(`"`)
|
|
w.WriteString(table)
|
|
w.WriteString(`_`)
|
|
int2string(w, id)
|
|
w.WriteString(`_`)
|
|
w.WriteString(col)
|
|
w.WriteString(suffix)
|
|
w.WriteString(`"`)
|
|
}
|
|
|
|
const charset = "0123456789"
|
|
|
|
func int2string(w *bytes.Buffer, val int32) {
|
|
if val < 10 {
|
|
w.WriteByte(charset[val])
|
|
return
|
|
}
|
|
|
|
temp := int32(0)
|
|
val2 := val
|
|
for val2 > 0 {
|
|
temp *= 10
|
|
temp += val2 % 10
|
|
val2 = int32(math.Floor(float64(val2 / 10)))
|
|
}
|
|
|
|
val3 := temp
|
|
for val3 > 0 {
|
|
d := val3 % 10
|
|
val3 /= 10
|
|
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
|
|
}
|