Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions broker/pgcql/pg_definition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package pgcql

import "github.com/indexdata/cql-go/cql"

type PgDefinition struct {
fields map[string]Field
}

func (pg *PgDefinition) AddField(name string, field Field) Definition {
if pg.fields == nil {
pg.fields = make(map[string]Field)
}
pg.fields[name] = field
return pg
}

func (pg *PgDefinition) GetFieldType(name string) Field {
if field, ok := pg.fields[name]; ok {
return field
}
return nil
}

func (pg *PgDefinition) Parse(q cql.Query, queryArgumentIndex int) (Query, error) {
query := &PgQuery{}
err := query.parse(q, queryArgumentIndex, pg)
return query, err
}
33 changes: 33 additions & 0 deletions broker/pgcql/pg_field_string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package pgcql

import (
"fmt"

"github.com/indexdata/cql-go/cql"
)

type FieldString struct {
column string
}

func (f *FieldString) GetColumn() string {
return f.column
}

func (f *FieldString) WithColumn(column string) Field {
f.column = column
return f
}

func (f *FieldString) Generate(sc cql.SearchClause, queryArgumentIndex int) (string, []interface{}, error) {
var operator string
switch sc.Relation {
case cql.EQ:
operator = "="
case cql.NE:
operator = "!="
default:
return "", nil, &PgError{message: "unsupported operator"}
}
return f.column + " " + operator + fmt.Sprintf(" $%d", queryArgumentIndex), []interface{}{sc.Term}, nil
}
86 changes: 86 additions & 0 deletions broker/pgcql/pg_query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package pgcql

import (
"fmt"

"github.com/indexdata/cql-go/cql"
)

type PgQuery struct {
def *PgDefinition
queryArgumentIndex int
arguments []interface{}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this any?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as I expect this could be ints or other types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to any

sql string
}

func (p *PgQuery) parse(q cql.Query, queryArgumentIndex int, def *PgDefinition) error {
p.def = def
p.arguments = make([]interface{}, 0)
p.queryArgumentIndex = queryArgumentIndex
if q.SortSpec != nil {
return &PgError{message: "sorting not supported"}
}
return p.parseClause(q.Clause, 0)
}

func (p *PgQuery) parseClause(sc cql.Clause, level int) error {
if sc.SearchClause != nil {
index := sc.SearchClause.Index
fieldType := p.def.GetFieldType(index)
if fieldType == nil {
return &PgError{message: fmt.Sprintf("unknown field %s", index)}
}
sql, args, err := fieldType.Generate(*sc.SearchClause, p.queryArgumentIndex)
if err != nil {
return err
}
p.sql += sql
if args != nil {
p.queryArgumentIndex += len(args)
p.arguments = append(p.arguments, args...)
}
return nil
} else if sc.BoolClause != nil {
if level > 0 {
p.sql += "("
}
err := p.parseClause(sc.BoolClause.Left, level+1)
if err != nil {
return err
}
if sc.BoolClause.Operator == cql.AND {
p.sql += " AND "
} else if sc.BoolClause.Operator == cql.OR {
p.sql += " OR "
} else if sc.BoolClause.Operator == cql.NOT {
p.sql += " AND NOT "
} else {
return &PgError{message: fmt.Sprintf("unsupported operator %s", sc.BoolClause.Operator)}
}
err = p.parseClause(sc.BoolClause.Right, level+1)
if err != nil {
return err
}
if level > 0 {
p.sql += ")"
}
return nil
}
return &PgError{message: "unsupported clause type"}
}

func (p *PgQuery) GetWhereClause() string {
return p.sql
}

func (p *PgQuery) GetQueryArguments() []interface{} {
return p.arguments
}

func (p *PgQuery) GetOrderByClause() string {
return ""
}

func (p *PgQuery) GetOrderByFields() string {
return ""
}
32 changes: 32 additions & 0 deletions broker/pgcql/pgcql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package pgcql

import (
"github.com/indexdata/cql-go/cql"
)

type PgError struct {
message string
}

func (e *PgError) Error() string {
return e.message
}

type Field interface {
GetColumn() string
WithColumn(column string) Field
Generate(sc cql.SearchClause, queryArgumentIndex int) (string, []interface{}, error)
}

type Definition interface {
AddField(name string, field Field) Definition
GetFieldType(name string) Field
Parse(q cql.Query, queryArgumentIndex int) (Query, error)
}

type Query interface {
GetWhereClause() string
GetQueryArguments() []interface{}
GetOrderByClause() string
GetOrderByFields() string
}
95 changes: 95 additions & 0 deletions broker/pgcql/pgcql_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package pgcql

import (
"reflect"
"strings"
"testing"

"github.com/indexdata/cql-go/cql"
"github.com/stretchr/testify/assert"
)

func TestBadSearchClause(t *testing.T) {
def := &PgDefinition{}

assert.Nil(t, def.GetFieldType("foo"))

q := cql.Query{}
_, err := def.Parse(q, 1)
assert.Error(t, err, "Expected error for empty query")
assert.Equal(t, "unsupported clause type", err.Error())
}

func TestParsing(t *testing.T) {
def := &PgDefinition{}
title := &FieldString{}
title.WithColumn("Title")
assert.Equal(t, title.GetColumn(), "Title", "GetColumn() should return the column name")

author := &FieldString{}
author.WithColumn("Author")

serverChoice := &FieldString{}
serverChoice.WithColumn("T")

def.AddField("title", title).AddField("author", author).AddField("cql.serverChoice", serverChoice)

for _, testcase := range []struct {
query string
expected string
expectedArgs []interface{}
}{
{"abc", "T = $1", []interface{}{"abc"}},
{"au=2", "-unknown field au", nil},
{"title>2", "-unsupported operator", nil},
{"title=2", "Title = $1", []interface{}{"2"}},
{"title<>2", "Title != $1", []interface{}{"2"}},
{"a or b and c", "(T = $1 OR T = $2) AND T = $3", []interface{}{"a", "b", "c"}},
{"title = abc", "Title = $1", []interface{}{"abc"}},
{"author = \"test\"", "Author = $1", []interface{}{"test"}},
{"title = a AND author = b c", "Title = $1 AND Author = $2", []interface{}{"a", "b c"}},
{"title = 'a' OR author = 'b'", "Title = $1 OR Author = $2", []interface{}{"'a'", "'b'"}},
{"title = a NOT author = b", "Title = $1 AND NOT Author = $2", []interface{}{"a", "b"}},
{"a prox b", "-unsupported operator prox", []interface{}{}},
{"a sortby title", "-sorting not supported", []interface{}{}},
{"au=2 or a", "-unknown field au", nil},
{"a or au=2", "-unknown field au", nil},
} {
var parser cql.Parser
q, err := parser.Parse(testcase.query)
if err != nil {
t.Errorf("%s: CQL parse error: %v", testcase.query, err)
continue
}
pgQuery, err := def.Parse(q, 1)

expectedError := strings.HasPrefix(testcase.expected, "-")

if err != nil {
if expectedError {
if strings.TrimPrefix(testcase.expected, "-") != err.Error() {
t.Errorf("%s: Expected error %s, got %s", testcase.query, strings.TrimPrefix(testcase.expected, "-"), err)
}
} else {
t.Errorf("%s: Failed to parse: %v", testcase.query, err)
}
continue
}
if expectedError {
t.Errorf("%s: Expected error, but got OK", testcase.query)
continue
}
if pgQuery.GetWhereClause() != testcase.expected {
t.Errorf("%s: Expected %s, got %s", testcase.query, testcase.expected, pgQuery.GetWhereClause())
}
if !reflect.DeepEqual(pgQuery.GetQueryArguments(), testcase.expectedArgs) {
t.Errorf("%s: Expected arguments %v, got %v", testcase.query, testcase.expectedArgs, pgQuery.GetQueryArguments())
}
if pgQuery.GetOrderByClause() != "" {
t.Errorf("%s: Expected empty order by clause, got %s", testcase.query, pgQuery.GetOrderByClause())
}
if pgQuery.GetOrderByFields() != "" {
t.Errorf("%s: Expected empty order by fields, got %s", testcase.query, pgQuery.GetOrderByFields())
}
}
}
Loading