snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
105
server/store/adapters/rdbms/ql/converter.go
Normal file
105
server/store/adapters/rdbms/ql/converter.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package ql
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
"github.com/doug-martin/goqu/v9/exp"
|
||||
)
|
||||
|
||||
type (
|
||||
refHandler func(*ql.ASTNode, ...exp.Expression) (exp.Expression, error)
|
||||
symHandler func(*ql.ASTNode) (exp.Expression, error)
|
||||
valHandler func(*ql.ASTNode) (exp.Expression, error)
|
||||
|
||||
// converts query and AST from the parsed query into goqu expression
|
||||
// that can be used in the SQL
|
||||
converter struct {
|
||||
parser *ql.Parser
|
||||
refHandler refHandler
|
||||
symHandler symHandler
|
||||
valHandler valHandler
|
||||
}
|
||||
|
||||
op func(*converter)
|
||||
)
|
||||
|
||||
// Converter initializes new converter
|
||||
func Converter(oo ...op) *converter {
|
||||
c := &converter{
|
||||
parser: ql.NewParser(),
|
||||
refHandler: DefaultRefHandler,
|
||||
symHandler: DefaultSymbolHandler,
|
||||
valHandler: DefaultValueHandler,
|
||||
}
|
||||
|
||||
for _, o := range oo {
|
||||
o(c)
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// RefHandler sets custom ref handler
|
||||
func RefHandler(h refHandler) op {
|
||||
return func(c *converter) {
|
||||
c.refHandler = h
|
||||
}
|
||||
}
|
||||
|
||||
// SymHandler sets custom symbol handler
|
||||
func SymHandler(h symHandler) op {
|
||||
return func(c *converter) {
|
||||
c.symHandler = h
|
||||
}
|
||||
}
|
||||
|
||||
// ValHandler sets custom value handler
|
||||
func ValHandler(h valHandler) op {
|
||||
return func(c *converter) {
|
||||
c.valHandler = h
|
||||
}
|
||||
}
|
||||
|
||||
func (c *converter) Parse(q string) (exp.Expression, error) {
|
||||
n, err := c.parser.Parse(q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c.Convert(n)
|
||||
}
|
||||
|
||||
// Convert recursively walks through AST and converts the entire tree into expression
|
||||
// that can be used in goqu SQL builder
|
||||
func (c *converter) Convert(n *ql.ASTNode) (_ exp.Expression, err error) {
|
||||
switch {
|
||||
case n.Symbol != "":
|
||||
return c.symHandler(n)
|
||||
case n.Value != nil:
|
||||
return c.valHandler(n)
|
||||
}
|
||||
|
||||
args := make([]exp.Expression, len(n.Args))
|
||||
for i, a := range n.Args {
|
||||
args[i], err = c.Convert(a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return c.refHandler(n, args...)
|
||||
}
|
||||
|
||||
// DefaultRefHandler converts ref from the AST node using ref2exp
|
||||
func DefaultRefHandler(n *ql.ASTNode, args ...exp.Expression) (exp.Expression, error) {
|
||||
return ref2exp.RefHandler(n, args...)
|
||||
}
|
||||
|
||||
// DefaultSymbolHandler parses symbol from the AST node into an identifier
|
||||
func DefaultSymbolHandler(n *ql.ASTNode) (exp.Expression, error) {
|
||||
return exp.ParseIdentifier(n.Symbol), nil
|
||||
}
|
||||
|
||||
// DefaultValueHandler converts node into placeholder and a new attribute
|
||||
func DefaultValueHandler(n *ql.ASTNode) (exp.Expression, error) {
|
||||
return exp.NewLiteralExpression("?", n.Value.V.Get()), nil
|
||||
}
|
||||
100
server/store/adapters/rdbms/ql/converter_test.go
Normal file
100
server/store/adapters/rdbms/ql/converter_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package ql
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConverter(t *testing.T) {
|
||||
const WHERE = " WHERE "
|
||||
var (
|
||||
base = goqu.Dialect("sqlite").Select().Prepared(true)
|
||||
conv = Converter()
|
||||
|
||||
cases = []struct {
|
||||
qry string
|
||||
sql string
|
||||
args []any
|
||||
}{
|
||||
{
|
||||
qry: `foo = 1`,
|
||||
sql: `("foo" = ?)`,
|
||||
args: []any{int64(1)},
|
||||
},
|
||||
{
|
||||
qry: `foo = 1 AND bar = baz`,
|
||||
sql: `(("foo" = ?) AND ("bar" = "baz"))`,
|
||||
args: []any{int64(1)},
|
||||
},
|
||||
{
|
||||
qry: `foo = 1 OR (bar = 'baz')`,
|
||||
sql: `(("foo" = ?) OR ("bar" = ?))`,
|
||||
args: []any{int64(1), "baz"},
|
||||
},
|
||||
{
|
||||
qry: `foo is not null`,
|
||||
sql: `("foo" IS NOT NULL)`,
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
qry: `!foo`,
|
||||
sql: `(NOT "foo")`,
|
||||
args: []any{},
|
||||
},
|
||||
{
|
||||
qry: `one + 2 / 3 * 4 < 10`,
|
||||
sql: `("one" + ? / ? * ? < ?)`,
|
||||
args: []any{int64(2), int64(3), int64(4), int64(10)},
|
||||
},
|
||||
{
|
||||
qry: `concat('foo', 'bar')`,
|
||||
sql: `CONCAT(?, ?)`,
|
||||
args: []any{"foo", "bar"},
|
||||
},
|
||||
{
|
||||
qry: `quarter('2022-07-21')`,
|
||||
sql: `QUARTER(?)`,
|
||||
args: []any{"2022-07-21"},
|
||||
},
|
||||
{
|
||||
qry: `year('2022-07-21')`,
|
||||
sql: `YEAR(?)`,
|
||||
args: []any{"2022-07-21"},
|
||||
},
|
||||
{
|
||||
qry: `month('2022-07-21')`,
|
||||
sql: `MONTH(?)`,
|
||||
args: []any{"2022-07-21"},
|
||||
},
|
||||
{
|
||||
qry: `date('2022-07-21')`,
|
||||
sql: `DAY(?)`,
|
||||
args: []any{"2022-07-21"},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.qry, func(t *testing.T) {
|
||||
req := require.New(t)
|
||||
|
||||
t.Log(c.qry)
|
||||
ee, err := conv.Parse(c.qry)
|
||||
req.NoError(err)
|
||||
|
||||
sql, args, err := base.Where(ee).ToSQL()
|
||||
req.NoError(err)
|
||||
|
||||
p := strings.Index(sql, WHERE)
|
||||
req.Positive(p)
|
||||
|
||||
sql = sql[p+len(WHERE):]
|
||||
|
||||
req.Equal(c.sql, sql)
|
||||
req.Equal(c.args, args)
|
||||
})
|
||||
}
|
||||
}
|
||||
11
server/store/adapters/rdbms/ql/ql.go
Normal file
11
server/store/adapters/rdbms/ql/ql.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package ql
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
)
|
||||
|
||||
// Aliasing types from the pkg/ql to simplify imports
|
||||
|
||||
type (
|
||||
ASTNode = ql.ASTNode
|
||||
)
|
||||
335
server/store/adapters/rdbms/ql/ref.go
Normal file
335
server/store/adapters/rdbms/ql/ref.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package ql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
"github.com/doug-martin/goqu/v9"
|
||||
"github.com/doug-martin/goqu/v9/exp"
|
||||
)
|
||||
|
||||
type (
|
||||
ExprHandlerMap map[string]*ExprHandler
|
||||
|
||||
ExprHandler struct {
|
||||
Handler func(...exp.Expression) exp.Expression
|
||||
HandlerE func(...exp.Expression) (exp.Expression, error)
|
||||
RangeHandler func(exp.Expression, exp.RangeVal) exp.Expression
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ref2exp = ExprHandlerMap{
|
||||
// keywords
|
||||
"null": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewLiteralExpression("NULL")
|
||||
},
|
||||
},
|
||||
"nnull": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewLiteralExpression("NOT NULL")
|
||||
},
|
||||
},
|
||||
|
||||
// operators
|
||||
"not": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewLiteralExpression("(NOT ?)", args[0])
|
||||
},
|
||||
},
|
||||
|
||||
// - bool
|
||||
"and": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewExpressionList(exp.AndType, args...)
|
||||
},
|
||||
},
|
||||
"or": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewExpressionList(exp.OrType, args...)
|
||||
},
|
||||
},
|
||||
|
||||
// - comp.
|
||||
"eq": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.EqOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
"ne": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.NeqOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
"lt": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.LtOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
"le": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.LteOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
"gt": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.GtOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
"ge": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.GteOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
|
||||
"in": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.InOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
"nin": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.NotInOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
|
||||
// - math
|
||||
"add": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewLiteralExpression("? + ?", args[0], args[1])
|
||||
},
|
||||
},
|
||||
"sub": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewLiteralExpression("? - ?", args[0], args[1])
|
||||
},
|
||||
},
|
||||
"mult": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
// Handling COUNT(*) scenario:
|
||||
// ql parser interprets * as a multiplier
|
||||
//
|
||||
// in situations where there are 0 arguments we'll just
|
||||
// return star
|
||||
if len(args) == 0 {
|
||||
return exp.Star()
|
||||
}
|
||||
|
||||
return exp.NewLiteralExpression("? * ?", args[0], args[1])
|
||||
},
|
||||
},
|
||||
"div": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewLiteralExpression("? / ?", args[0], args[1])
|
||||
},
|
||||
},
|
||||
|
||||
// - strings
|
||||
"concat": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
aa := make([]any, len(args))
|
||||
for a := range args {
|
||||
aa[a] = args[a]
|
||||
}
|
||||
|
||||
return exp.NewSQLFunctionExpression("CONCAT", aa...)
|
||||
},
|
||||
},
|
||||
|
||||
"interval": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
|
||||
// The problem here is similar to the one with PgSQL... but more complicated...
|
||||
// The interval duration identifiers are keywords and can't be bound via value placeholders.
|
||||
// This forces us to interpolate the thing when building the literal expression below.
|
||||
//
|
||||
// Since goqu doesn't let me get the raw value of the expression... I was forced
|
||||
// to do this calamity
|
||||
_, aa, err := goqu.Select(args[0]).ToSQL()
|
||||
if err != nil {
|
||||
// This error should never occur
|
||||
panic(err)
|
||||
}
|
||||
intv := aa[0].(string)
|
||||
|
||||
return exp.NewLiteralExpression(fmt.Sprintf("INTERVAL ? %s", intv), args[1])
|
||||
},
|
||||
},
|
||||
|
||||
"date_add": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("DATE_ADD", args[0], args[1])
|
||||
},
|
||||
},
|
||||
|
||||
"date_sub": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("DATE_SUB", args[0], args[1])
|
||||
},
|
||||
},
|
||||
|
||||
// @todo better negation?
|
||||
"like": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.ILikeOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
"nlike": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.NotILikeOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
|
||||
// range operation
|
||||
"between": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewRangeExpression(exp.BetweenOp, args[0], exp.NewRangeVal(args[1], args[2]))
|
||||
},
|
||||
},
|
||||
"nbetween": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewRangeExpression(exp.NotBetweenOp, args[0], exp.NewRangeVal(args[1], args[2]))
|
||||
},
|
||||
},
|
||||
|
||||
"is": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.IsOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
|
||||
"nis": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewBooleanExpression(exp.IsNotOp, args[0], args[1])
|
||||
},
|
||||
},
|
||||
|
||||
"group": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewLiteralExpression("(?)", args[0])
|
||||
},
|
||||
},
|
||||
|
||||
// - filtering
|
||||
"date_format": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("DATE_FORMAT", args[0], args[1])
|
||||
},
|
||||
},
|
||||
"now": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("NOW")
|
||||
},
|
||||
},
|
||||
"quarter": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("QUARTER", args[0])
|
||||
},
|
||||
},
|
||||
"year": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("YEAR", args[0])
|
||||
},
|
||||
},
|
||||
"month": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("MONTH", args[0])
|
||||
},
|
||||
},
|
||||
"day": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("DAY", args[0])
|
||||
},
|
||||
},
|
||||
"week": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("WEEK", args[0])
|
||||
},
|
||||
},
|
||||
"date": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("DATE", args[0])
|
||||
},
|
||||
},
|
||||
"timestamp": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewLiteralExpression("TIMESTAMP(?)", args[0])
|
||||
},
|
||||
},
|
||||
"time": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewLiteralExpression("TIME(?)", args[0])
|
||||
},
|
||||
},
|
||||
"count": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
var arg exp.Expression = exp.NewLiteralExpression("*")
|
||||
if len(args) > 0 {
|
||||
arg = args[0]
|
||||
}
|
||||
|
||||
return exp.NewSQLFunctionExpression("COUNT", arg)
|
||||
},
|
||||
},
|
||||
"sum": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("SUM", args[0])
|
||||
},
|
||||
},
|
||||
"avg": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("AVG", args[0])
|
||||
},
|
||||
},
|
||||
"min": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("MIN", args[0])
|
||||
},
|
||||
},
|
||||
"max": {
|
||||
Handler: func(args ...exp.Expression) exp.Expression {
|
||||
return exp.NewSQLFunctionExpression("MAX", args[0])
|
||||
},
|
||||
},
|
||||
|
||||
"std": {
|
||||
HandlerE: func(args ...exp.Expression) (exp.Expression, error) {
|
||||
return nil, fmt.Errorf("STD function not supported on this database")
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func (ee ExprHandlerMap) ExprHandlers() (out ExprHandlerMap) {
|
||||
// Set the default values; make sure to copy the global map
|
||||
out = make(map[string]*ExprHandler, len(ref2exp))
|
||||
for name, expr := range ref2exp {
|
||||
out[name] = expr
|
||||
}
|
||||
|
||||
// Overwrite with customs
|
||||
for name, expr := range ee {
|
||||
out[name] = expr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (ee ExprHandlerMap) RefHandler(n *ql.ASTNode, args ...exp.Expression) (exp.Expression, error) {
|
||||
r := strings.ToLower(n.Ref)
|
||||
|
||||
if ee[r] == nil {
|
||||
return nil, fmt.Errorf("unknown ref %q", r)
|
||||
}
|
||||
|
||||
if ee[r].Handler != nil && ee[r].HandlerE != nil {
|
||||
panic("invalid ql ref definition: both Handler and HandlerE are set")
|
||||
}
|
||||
|
||||
if ee[r].Handler != nil {
|
||||
return ee[r].Handler(args...), nil
|
||||
}
|
||||
|
||||
return ee[r].HandlerE(args...)
|
||||
}
|
||||
Reference in New Issue
Block a user