Add support for HAVING with aggregate queries
This commit is contained in:
55
psql/psql.go
55
psql/psql.go
@ -263,8 +263,9 @@ func (v *selectBlock) renderJoinedColumns(w io.Writer, childIDs []int) error {
|
||||
func (v *selectBlock) renderBaseSelect(w io.Writer, schema *DBSchema, childCols []*qcode.Column, childIDs []int) error {
|
||||
var groupBy []int
|
||||
|
||||
isNotRoot := (v.parent != nil)
|
||||
hasFilters := (v.sel.Where != nil)
|
||||
isNotRoot := v.parent != nil
|
||||
isFil := v.sel.Where != nil
|
||||
isAgg := false
|
||||
|
||||
io.WriteString(w, " FROM (SELECT ")
|
||||
|
||||
@ -277,6 +278,7 @@ func (v *selectBlock) renderBaseSelect(w io.Writer, schema *DBSchema, childCols
|
||||
if pl == 0 {
|
||||
continue
|
||||
}
|
||||
isAgg = true
|
||||
fn = cn[0 : pl-1]
|
||||
cn = cn[pl:]
|
||||
}
|
||||
@ -303,28 +305,31 @@ func (v *selectBlock) renderBaseSelect(w io.Writer, schema *DBSchema, childCols
|
||||
|
||||
fmt.Fprintf(w, ` FROM "%s"`, v.sel.Table)
|
||||
|
||||
if isNotRoot || hasFilters {
|
||||
if isNotRoot {
|
||||
v.renderJoinTable(w, schema, childIDs)
|
||||
}
|
||||
if isNotRoot {
|
||||
v.renderJoinTable(w, schema, childIDs)
|
||||
}
|
||||
|
||||
switch {
|
||||
case isNotRoot:
|
||||
io.WriteString(w, ` WHERE (`)
|
||||
|
||||
if isNotRoot {
|
||||
v.renderRelationship(w, schema)
|
||||
}
|
||||
|
||||
if hasFilters {
|
||||
err := v.renderWhere(w)
|
||||
if err != nil {
|
||||
v.renderRelationship(w, schema)
|
||||
if isFil {
|
||||
io.WriteString(w, ` AND `)
|
||||
if err := v.renderWhere(w); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
io.WriteString(w, `)`)
|
||||
|
||||
case isFil && !isAgg:
|
||||
io.WriteString(w, ` WHERE (`)
|
||||
if err := v.renderWhere(w); err != nil {
|
||||
return err
|
||||
}
|
||||
io.WriteString(w, `)`)
|
||||
}
|
||||
|
||||
if len(groupBy) != 0 {
|
||||
if isAgg && len(groupBy) != 0 {
|
||||
fmt.Fprintf(w, ` GROUP BY `)
|
||||
|
||||
for i, id := range groupBy {
|
||||
@ -334,6 +339,14 @@ func (v *selectBlock) renderBaseSelect(w io.Writer, schema *DBSchema, childCols
|
||||
io.WriteString(w, ", ")
|
||||
}
|
||||
}
|
||||
|
||||
if isFil {
|
||||
io.WriteString(w, ` HAVING (`)
|
||||
if err := v.renderWhere(w); err != nil {
|
||||
return err
|
||||
}
|
||||
io.WriteString(w, `)`)
|
||||
}
|
||||
}
|
||||
|
||||
if len(v.sel.Paging.Limit) != 0 {
|
||||
@ -368,8 +381,6 @@ func (v *selectBlock) renderOrderByColumns(w io.Writer) {
|
||||
}
|
||||
|
||||
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 {
|
||||
@ -388,21 +399,15 @@ func (v *selectBlock) renderRelationship(w io.Writer, schema *DBSchema) {
|
||||
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
|
||||
if v.sel.Where != nil {
|
||||
st.Push(v.sel.Where)
|
||||
}
|
||||
st.Push(v.sel.Where)
|
||||
|
||||
for {
|
||||
if st.Len() == 0 {
|
||||
|
@ -110,7 +110,7 @@ func compileGQLToPSQL(gql string) (string, error) {
|
||||
return sqlStmt.String(), nil
|
||||
}
|
||||
|
||||
func TestCompileGQLWithComplexArgs(t *testing.T) {
|
||||
func withComplexArgs(t *testing.T) {
|
||||
gql := `query {
|
||||
products(
|
||||
# returns only 30 items
|
||||
@ -145,7 +145,7 @@ func TestCompileGQLWithComplexArgs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGQLWithWhereMultiOr(t *testing.T) {
|
||||
func withWhereMultiOr(t *testing.T) {
|
||||
gql := `query {
|
||||
products(
|
||||
where: {
|
||||
@ -173,7 +173,7 @@ func TestCompileGQLWithWhereMultiOr(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGQLWithWhereIsNull(t *testing.T) {
|
||||
func withWhereIsNull(t *testing.T) {
|
||||
gql := `query {
|
||||
products(
|
||||
where: {
|
||||
@ -199,7 +199,7 @@ func TestCompileGQLWithWhereIsNull(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGQLWithWhereAndList(t *testing.T) {
|
||||
func withWhereAndList(t *testing.T) {
|
||||
gql := `query {
|
||||
products(
|
||||
where: {
|
||||
@ -225,7 +225,7 @@ func TestCompileGQLWithWhereAndList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGQLOneToMany(t *testing.T) {
|
||||
func oneToMany(t *testing.T) {
|
||||
gql := `query {
|
||||
users {
|
||||
email
|
||||
@ -248,7 +248,7 @@ func TestCompileGQLOneToMany(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGQLBelongTo(t *testing.T) {
|
||||
func belongsTo(t *testing.T) {
|
||||
gql := `query {
|
||||
products {
|
||||
name
|
||||
@ -271,7 +271,7 @@ func TestCompileGQLBelongTo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGQLManyToMany(t *testing.T) {
|
||||
func manyToMany(t *testing.T) {
|
||||
gql := `query {
|
||||
products {
|
||||
name
|
||||
@ -294,7 +294,7 @@ func TestCompileGQLManyToMany(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGQLManyToManyReverse(t *testing.T) {
|
||||
func manyToManyReverse(t *testing.T) {
|
||||
gql := `query {
|
||||
customers {
|
||||
email
|
||||
@ -317,7 +317,7 @@ func TestCompileGQLManyToManyReverse(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGQLAggFunction(t *testing.T) {
|
||||
func aggFunction(t *testing.T) {
|
||||
gql := `query {
|
||||
products {
|
||||
name
|
||||
@ -337,6 +337,39 @@ func TestCompileGQLAggFunction(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func aggFunctionWithFilter(t *testing.T) {
|
||||
gql := `query {
|
||||
products(where: { id: { gt: 10 } }) {
|
||||
id
|
||||
max_price
|
||||
}
|
||||
}`
|
||||
|
||||
sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."id" AS "id", "products_0"."max_price" AS "max_price") AS "sel_0")) AS "products" FROM (SELECT "products"."id", max("products"."price") AS max_price FROM "products" GROUP BY "products"."id" HAVING ((("products"."id") > (10))) LIMIT ('20') :: integer) AS "products_0" LIMIT ('20') :: integer) AS "products_0") AS "done_1337";`
|
||||
|
||||
resSQL, err := compileGQLToPSQL(gql)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if resSQL != sql {
|
||||
t.Fatal(errNotExpected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompileGQL(t *testing.T) {
|
||||
t.Run("withComplexArgs", withComplexArgs)
|
||||
t.Run("withWhereAndList", withWhereAndList)
|
||||
t.Run("withWhereIsNull", withWhereIsNull)
|
||||
t.Run("withWhereMultiOr", withWhereMultiOr)
|
||||
t.Run("belongsTo", belongsTo)
|
||||
t.Run("oneToMany", oneToMany)
|
||||
t.Run("manyToMany", manyToMany)
|
||||
t.Run("manyToManyReverse", manyToManyReverse)
|
||||
t.Run("aggFunction", aggFunction)
|
||||
t.Run("aggFunctionWithFilter", aggFunctionWithFilter)
|
||||
}
|
||||
|
||||
func BenchmarkCompileGQLToSQL(b *testing.B) {
|
||||
gql := `query {
|
||||
products(
|
||||
|
Reference in New Issue
Block a user