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:
359
server/pkg/dal/aggregator.go
Normal file
359
server/pkg/dal/aggregator.go
Normal file
@@ -0,0 +1,359 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
"github.com/modern-go/reflect2"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type (
|
||||
// aggregator performs value aggregation primarily used for the aggregate step
|
||||
// @todo consider moving into a separate package
|
||||
//
|
||||
// The aggregator performs the requested value expressions as well as
|
||||
// aggregation operations over the evaluated values.
|
||||
//
|
||||
// The aggregator computes the values on the fly (for the ones it can).
|
||||
aggregator struct {
|
||||
// aggregates holds output aggregation values
|
||||
// @note we'll use float64 for all values, but it might make more sense to split it up
|
||||
// in the future. For now, it'll be ok.
|
||||
aggregates []float64
|
||||
|
||||
// counts holds the number of values for each aggregate including multi value fields.
|
||||
// Counts are currently only used for average.
|
||||
counts []int
|
||||
|
||||
// def provides a list of aggregates.
|
||||
//
|
||||
// Each index corresponds to the aggregates and counts slices
|
||||
def []aggregateDef
|
||||
// scanned indicates whether the aggregator has been scanned since
|
||||
// we need to block writes after the first scan.
|
||||
scanned bool
|
||||
}
|
||||
|
||||
// aggregateDef is a wrapper for the provided value aggregation
|
||||
aggregateDef struct {
|
||||
outIdent string
|
||||
|
||||
aggOp string
|
||||
|
||||
inIdent string
|
||||
eval evaluator
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
// aggregateFunctionIndex specifies all of the registered aggregate functions
|
||||
//
|
||||
// @todo consider making this expandable via some registry/plugin/...
|
||||
aggregateFunctionIndex = map[string]bool{
|
||||
"count": true,
|
||||
"sum": true,
|
||||
"min": true,
|
||||
"max": true,
|
||||
"avg": true,
|
||||
}
|
||||
)
|
||||
|
||||
// Aggregator initializes a new aggregator for the given set of mappings
|
||||
//
|
||||
// The aggregator is not routine safe; consider defining multiple aggregators
|
||||
// and then combining them together.
|
||||
func Aggregator() *aggregator {
|
||||
return &aggregator{
|
||||
aggregates: make([]float64, 0, 16),
|
||||
counts: make([]int, 0, 16),
|
||||
}
|
||||
}
|
||||
|
||||
// AddAggregateE adds a new aggregate from a raw expression
|
||||
func (a *aggregator) AddAggregateE(ident, expr string) (err error) {
|
||||
// @note the converter is reused so we can safely do this
|
||||
n, err := newConverterGval().Parse(expr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return a.AddAggregate(ident, n)
|
||||
}
|
||||
|
||||
// AddAggregate adds a new aggregate from an already parsed expression
|
||||
func (a *aggregator) AddAggregate(ident string, expr *ql.ASTNode) (err error) {
|
||||
def := aggregateDef{
|
||||
outIdent: ident,
|
||||
}
|
||||
|
||||
inIdent, expr, err := unpackMappingSource(expr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Take it from the source
|
||||
if inIdent != "" {
|
||||
def.inIdent = inIdent
|
||||
}
|
||||
|
||||
// Take it from the expression
|
||||
// - agg. op.
|
||||
def.aggOp, expr, err = unpackExpressionNode(expr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Prepare a runner in case we're not simply copying values
|
||||
if inIdent == "" {
|
||||
// - make evaluator
|
||||
if expr != nil {
|
||||
def.eval, err = newRunnerGvalParsed(expr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a.aggregates = append(a.aggregates, 0)
|
||||
a.counts = append(a.counts, 0)
|
||||
a.def = append(a.def, def)
|
||||
return
|
||||
}
|
||||
|
||||
// Aggregate aggregates the given value
|
||||
func (a *aggregator) Aggregate(ctx context.Context, v ValueGetter) (err error) {
|
||||
if a.scanned {
|
||||
// If we attempt to add data to the aggregator, the previous data might
|
||||
// no longer be valid.
|
||||
return fmt.Errorf("cannot call Aggregate on an already scanned aggregator")
|
||||
}
|
||||
|
||||
// @todo consider throwing all of the aggregates into a routine.
|
||||
// I think (for now) performance gains will be negligible.
|
||||
for i, attr := range a.def {
|
||||
err = a.aggregate(ctx, attr, i, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Scan scans the aggregated values into the setter
|
||||
func (a *aggregator) Scan(s ValueSetter) (err error) {
|
||||
// On first scan, complete partial aggregates
|
||||
if !a.scanned {
|
||||
a.completePartials()
|
||||
}
|
||||
|
||||
a.scanned = true
|
||||
|
||||
// Set the values
|
||||
for i, attr := range a.def {
|
||||
// @note each aggregated value can be at most one so no need for multi-value
|
||||
// suport here.
|
||||
err = s.SetValue(attr.outIdent, 0, a.aggregates[i])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// aggregate applies the provided value into the requested aggregate
|
||||
func (a *aggregator) aggregate(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
|
||||
switch attr.aggOp {
|
||||
case "count":
|
||||
return a.count(ctx, attr, i, v)
|
||||
|
||||
case "sum":
|
||||
return a.sum(ctx, attr, i, v)
|
||||
|
||||
case "min":
|
||||
return a.min(ctx, attr, i, v)
|
||||
|
||||
case "max":
|
||||
return a.max(ctx, attr, i, v)
|
||||
|
||||
case "avg":
|
||||
return a.avg(ctx, attr, i, v)
|
||||
}
|
||||
|
||||
return fmt.Errorf("unsupported aggregate function: %s", attr.aggOp)
|
||||
}
|
||||
|
||||
// walkValues traverses the available values for the specified attribute
|
||||
func (a *aggregator) walkValues(ctx context.Context, r ValueGetter, cc map[string]uint, attr aggregateDef, run func(v any, isNil bool)) (err error) {
|
||||
var out any
|
||||
if attr.inIdent == "" {
|
||||
if attr.eval != nil {
|
||||
out, err = attr.eval.Eval(ctx, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
out = r
|
||||
}
|
||||
|
||||
run(out, reflect2.IsNil(out))
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := uint(0); i < cc[attr.inIdent]; i++ {
|
||||
v, err := r.GetValue(attr.inIdent, i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
run(v, reflect2.IsNil(v))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Aggregate methods
|
||||
|
||||
func (a *aggregator) count(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
|
||||
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
|
||||
if isNil {
|
||||
return
|
||||
}
|
||||
|
||||
a.aggregates[i]++
|
||||
a.counts[i]++
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aggregator) sum(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
|
||||
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
|
||||
if isNil {
|
||||
return
|
||||
}
|
||||
a.aggregates[i] += cast.ToFloat64(v)
|
||||
a.counts[i]++
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aggregator) min(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
|
||||
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
|
||||
if isNil {
|
||||
return
|
||||
}
|
||||
|
||||
if a.counts[i] == 0 {
|
||||
a.aggregates[i] = cast.ToFloat64(v)
|
||||
} else {
|
||||
a.aggregates[i] = math.Min(a.aggregates[i], cast.ToFloat64(v))
|
||||
}
|
||||
a.counts[i]++
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aggregator) max(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
|
||||
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
|
||||
if isNil {
|
||||
return
|
||||
}
|
||||
|
||||
if a.counts[i] == 0 {
|
||||
a.aggregates[i] = cast.ToFloat64(v)
|
||||
} else {
|
||||
a.aggregates[i] = math.Max(a.aggregates[i], cast.ToFloat64(v))
|
||||
}
|
||||
a.counts[i]++
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aggregator) avg(ctx context.Context, attr aggregateDef, i int, v ValueGetter) (err error) {
|
||||
err = a.walkValues(ctx, v, v.CountValues(), attr, func(v any, isNil bool) {
|
||||
if isNil {
|
||||
return
|
||||
}
|
||||
|
||||
a.aggregates[i] += cast.ToFloat64(v)
|
||||
|
||||
a.counts[i]++
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a *aggregator) completePartials() {
|
||||
a.completeAverage()
|
||||
}
|
||||
|
||||
func (a *aggregator) completeAverage() {
|
||||
for i, attr := range a.def {
|
||||
if attr.aggOp == "avg" {
|
||||
if a.counts[i] == 0 {
|
||||
return
|
||||
}
|
||||
a.aggregates[i] = a.aggregates[i] / float64(a.counts[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
||||
func unpackMappingSource(n *ql.ASTNode) (ident string, expr *ql.ASTNode, err error) {
|
||||
// Check if first arg of agg. fnc. is an attr.
|
||||
if len(n.Args) == 1 && n.Args[0].Symbol != "" {
|
||||
return n.Args[0].Symbol, n, nil
|
||||
}
|
||||
|
||||
expr = n
|
||||
return
|
||||
}
|
||||
|
||||
func unpackExpressionNode(n *ql.ASTNode) (aggOp string, expr *ql.ASTNode, err error) {
|
||||
if n.Ref != "" {
|
||||
aggOp = strings.ToLower(n.Ref)
|
||||
}
|
||||
if !aggregateFunctionIndex[aggOp] {
|
||||
err = fmt.Errorf("root expression must be an aggregate function")
|
||||
return
|
||||
}
|
||||
|
||||
if len(n.Args) > 0 {
|
||||
expr = n.Args[0]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// reset is a benchmarking utility to reset the aggregator
|
||||
//
|
||||
// Don't use it in production code.
|
||||
func (a *aggregator) reset() {
|
||||
for i := 0; i < len(a.aggregates); i++ {
|
||||
a.aggregates[i] = 0
|
||||
a.counts[i] = 0
|
||||
}
|
||||
a.scanned = false
|
||||
}
|
||||
191
server/pkg/dal/aggregator_test.go
Normal file
191
server/pkg/dal/aggregator_test.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAggregator(t *testing.T) {
|
||||
tcc := []struct {
|
||||
name string
|
||||
|
||||
rows []simpleRow
|
||||
attrubutes []simpleAttribute
|
||||
out simpleRow
|
||||
}{
|
||||
// Plain operations
|
||||
{
|
||||
name: "simple count",
|
||||
rows: []simpleRow{
|
||||
{"v": 1},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"count": float64(4)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "count", expr: "count(v)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple count nulls",
|
||||
rows: []simpleRow{
|
||||
{"v": nil},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"count": float64(3)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "count", expr: "count(v)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple sum",
|
||||
rows: []simpleRow{
|
||||
{"v": 1},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"sum": float64(52.5)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "sum", expr: "sum(v)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple sum nulls",
|
||||
rows: []simpleRow{
|
||||
{"v": nil},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"sum": float64(51.5)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "sum", expr: "sum(v)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple min",
|
||||
rows: []simpleRow{
|
||||
{"v": 1},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"min": float64(1)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "min", expr: "min(v)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple min nulls",
|
||||
rows: []simpleRow{
|
||||
{"v": nil},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"min": float64(5)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "min", expr: "min(v)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple max",
|
||||
rows: []simpleRow{
|
||||
{"v": 1},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"max": float64(35)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "max", expr: "max(v)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple max nulls",
|
||||
rows: []simpleRow{
|
||||
{"v": 1},
|
||||
{"v": 5},
|
||||
{"v": nil},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"max": float64(11.5)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "max", expr: "max(v)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple avg",
|
||||
rows: []simpleRow{
|
||||
{"v": 1},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"avg": float64(13.125)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "avg", expr: "avg(v)"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "simple avg nulls",
|
||||
rows: []simpleRow{
|
||||
{"v": nil},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": nil},
|
||||
},
|
||||
out: simpleRow{"avg": float64(20)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "avg", expr: "avg(v)"},
|
||||
},
|
||||
},
|
||||
|
||||
// With a nested expression
|
||||
// @todo tests to assure nil values; omitting due to the gval issue
|
||||
{
|
||||
name: "nested expression",
|
||||
rows: []simpleRow{
|
||||
{"v": 1},
|
||||
{"v": 5},
|
||||
{"v": 35},
|
||||
{"v": 11.5},
|
||||
},
|
||||
out: simpleRow{"sum": float64(60.5)},
|
||||
attrubutes: []simpleAttribute{
|
||||
{ident: "sum", expr: "sum(v + 2)"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
agg := Aggregator()
|
||||
for _, a := range tc.attrubutes {
|
||||
require.NoError(t, agg.AddAggregateE(a.ident, a.expr))
|
||||
}
|
||||
|
||||
for _, r := range tc.rows {
|
||||
require.NoError(t, agg.Aggregate(ctx, r))
|
||||
}
|
||||
|
||||
out := make(simpleRow)
|
||||
require.NoError(t, agg.Scan(out))
|
||||
require.Equal(t, tc.out, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregatorInit(t *testing.T) {
|
||||
t.Run("non supported agg. op.", func(t *testing.T) {
|
||||
agg := Aggregator()
|
||||
require.Error(t, agg.AddAggregateE("count", "div(v)"))
|
||||
})
|
||||
}
|
||||
370
server/pkg/dal/alteration.go
Normal file
370
server/pkg/dal/alteration.go
Normal file
@@ -0,0 +1,370 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
Alteration struct {
|
||||
ID uint64
|
||||
BatchID uint64
|
||||
DependsOn uint64
|
||||
Resource string
|
||||
ResourceType string
|
||||
ConnectionID uint64
|
||||
|
||||
AttributeAdd *AttributeAdd
|
||||
AttributeDelete *AttributeDelete
|
||||
AttributeReType *AttributeReType
|
||||
AttributeReEncode *AttributeReEncode
|
||||
ModelAdd *ModelAdd
|
||||
ModelDelete *ModelDelete
|
||||
}
|
||||
|
||||
AlterationSet []*Alteration
|
||||
|
||||
AttributeAdd struct {
|
||||
Attr *Attribute `json:"attr"`
|
||||
}
|
||||
|
||||
AttributeDelete struct {
|
||||
Attr *Attribute `json:"attr"`
|
||||
}
|
||||
|
||||
AttributeReType struct {
|
||||
Attr *Attribute `json:"attr"`
|
||||
To Type `json:"to"`
|
||||
}
|
||||
|
||||
AttributeReEncode struct {
|
||||
Attr *Attribute `json:"attr"`
|
||||
To Codec `json:"to"`
|
||||
}
|
||||
|
||||
ModelAdd struct {
|
||||
Model *Model `json:"model"`
|
||||
}
|
||||
|
||||
ModelDelete struct {
|
||||
Model *Model `json:"model"`
|
||||
}
|
||||
|
||||
// auxAttributeType is a helper struct used for marshaling/unmarshaling
|
||||
//
|
||||
// This is required since the Type inside the Attribute is an interface and we
|
||||
// need to help the encoding/json a bit.
|
||||
auxAttributeReType struct {
|
||||
Attr *Attribute `json:"attr"`
|
||||
To *auxAttributeType `json:"to"`
|
||||
}
|
||||
|
||||
// auxAttributeReEncode is a helper struct used for marshaling/unmarshaling
|
||||
//
|
||||
// This is required since the Codec inside the Attribute is an interface and we
|
||||
// need to help the encoding/json a bit.
|
||||
auxAttributeReEncode struct {
|
||||
Attr *Attribute `json:"attr"`
|
||||
To *auxStoreCodec `json:"to"`
|
||||
}
|
||||
)
|
||||
|
||||
// Merge merges the two alteration slices
|
||||
func (aa AlterationSet) Merge(bb AlterationSet) (cc AlterationSet) {
|
||||
// @todo the Merge function currently just merges the two slices together
|
||||
// and removes any duplicates that would occur due to the merge.
|
||||
// We should also handle overlapping/transitive alterations to reduce
|
||||
// the amount of needles processing.
|
||||
//
|
||||
// A quick list of overlapping alterations:
|
||||
// * attribute A added and then renamed from A to A'
|
||||
// * attribute A renamed to A' and then renamed to A''
|
||||
// * attribute A deleted and then created
|
||||
|
||||
aux := append(aa, bb...)
|
||||
seen := make(map[int]bool, len(aux)/2)
|
||||
|
||||
for i, a := range aux {
|
||||
if seen[i] {
|
||||
continue
|
||||
}
|
||||
|
||||
found := false
|
||||
for j := i + 1; j < len(aux); j++ {
|
||||
if seen[j] {
|
||||
continue
|
||||
}
|
||||
|
||||
if a.compare(*aux[j]) {
|
||||
if !found {
|
||||
cc = append(cc, aux[j])
|
||||
}
|
||||
|
||||
seen[j] = true
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
cc = append(cc, a)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a AttributeReType) MarshalJSON() ([]byte, error) {
|
||||
aux := auxAttributeReType{
|
||||
Attr: a.Attr,
|
||||
To: &auxAttributeType{},
|
||||
}
|
||||
|
||||
switch t := a.To.(type) {
|
||||
case *TypeID:
|
||||
aux.To.Type = "ID"
|
||||
aux.To.ID = t
|
||||
|
||||
case *TypeRef:
|
||||
aux.To.Type = "Ref"
|
||||
aux.To.Ref = t
|
||||
|
||||
case *TypeTimestamp:
|
||||
aux.To.Type = "Timestamp"
|
||||
aux.To.Timestamp = t
|
||||
|
||||
case *TypeTime:
|
||||
aux.To.Type = "Time"
|
||||
aux.To.Time = t
|
||||
|
||||
case *TypeDate:
|
||||
aux.To.Type = "Date"
|
||||
aux.To.Date = t
|
||||
|
||||
case *TypeNumber:
|
||||
aux.To.Type = "Number"
|
||||
aux.To.Number = t
|
||||
|
||||
case *TypeText:
|
||||
aux.To.Type = "Text"
|
||||
aux.To.Text = t
|
||||
|
||||
case *TypeBoolean:
|
||||
aux.To.Type = "Boolean"
|
||||
aux.To.Boolean = t
|
||||
|
||||
case *TypeEnum:
|
||||
aux.To.Type = "Enum"
|
||||
aux.To.Enum = t
|
||||
|
||||
case *TypeGeometry:
|
||||
aux.To.Type = "Geometry"
|
||||
aux.To.Geometry = t
|
||||
|
||||
case *TypeJSON:
|
||||
aux.To.Type = "JSON"
|
||||
aux.To.JSON = t
|
||||
|
||||
case *TypeBlob:
|
||||
aux.To.Type = "Blob"
|
||||
aux.To.Blob = t
|
||||
|
||||
case *TypeUUID:
|
||||
aux.To.Type = "UUID"
|
||||
aux.To.UUID = t
|
||||
}
|
||||
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
func (a *AttributeReType) UnmarshalJSON(data []byte) (err error) {
|
||||
aux := &auxAttributeReType{}
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a == nil {
|
||||
*a = AttributeReType{}
|
||||
}
|
||||
|
||||
a.Attr = aux.Attr
|
||||
|
||||
switch aux.To.Type {
|
||||
case "ID":
|
||||
a.To = aux.To.ID
|
||||
|
||||
case "Ref":
|
||||
a.To = aux.To.Ref
|
||||
|
||||
case "Timestamp":
|
||||
a.To = aux.To.Timestamp
|
||||
|
||||
case "Time":
|
||||
a.To = aux.To.Time
|
||||
|
||||
case "Date":
|
||||
a.To = aux.To.Date
|
||||
|
||||
case "Number":
|
||||
a.To = aux.To.Number
|
||||
|
||||
case "Text":
|
||||
a.To = aux.To.Text
|
||||
|
||||
case "Boolean":
|
||||
a.To = aux.To.Boolean
|
||||
|
||||
case "Enum":
|
||||
a.To = aux.To.Enum
|
||||
|
||||
case "Geometry":
|
||||
a.To = aux.To.Geometry
|
||||
|
||||
case "JSON":
|
||||
a.To = aux.To.JSON
|
||||
|
||||
case "Blob":
|
||||
a.To = aux.To.Blob
|
||||
|
||||
case "UUID":
|
||||
a.To = aux.To.UUID
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a AttributeReEncode) MarshalJSON() ([]byte, error) {
|
||||
aux := auxAttributeReEncode{
|
||||
Attr: a.Attr,
|
||||
To: &auxStoreCodec{},
|
||||
}
|
||||
|
||||
switch t := a.To.(type) {
|
||||
case *CodecPlain:
|
||||
aux.To.Type = "CodecPlain"
|
||||
aux.To.CodecPlain = t
|
||||
case *CodecRecordValueSetJSON:
|
||||
aux.To.Type = "CodecRecordValueSetJSON"
|
||||
aux.To.CodecRecordValueSetJSON = t
|
||||
case *CodecAlias:
|
||||
aux.To.Type = "CodecAlias"
|
||||
aux.To.CodecAlias = t
|
||||
}
|
||||
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
func (a *AttributeReEncode) UnmarshalJSON(data []byte) (err error) {
|
||||
aux := &auxAttributeReEncode{}
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a == nil {
|
||||
*a = AttributeReEncode{}
|
||||
}
|
||||
|
||||
a.Attr = aux.Attr
|
||||
|
||||
switch aux.To.Type {
|
||||
case "CodecPlain":
|
||||
a.To = aux.To.CodecPlain
|
||||
|
||||
case "CodecRecordValueSetJSON":
|
||||
a.To = aux.To.CodecRecordValueSetJSON
|
||||
|
||||
case "CodecAlias":
|
||||
a.To = aux.To.CodecAlias
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a Alteration) compare(b Alteration) (cmp bool) {
|
||||
if a.AttributeAdd == nil && b.AttributeAdd != nil {
|
||||
return false
|
||||
}
|
||||
if a.AttributeDelete == nil && b.AttributeDelete != nil {
|
||||
return false
|
||||
}
|
||||
if a.AttributeReType == nil && b.AttributeReType != nil {
|
||||
return false
|
||||
}
|
||||
if a.AttributeReEncode == nil && b.AttributeReEncode != nil {
|
||||
return false
|
||||
}
|
||||
if a.ModelAdd == nil && b.ModelAdd != nil {
|
||||
return false
|
||||
}
|
||||
if a.ModelDelete == nil && b.ModelDelete != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch {
|
||||
case a.AttributeAdd != nil:
|
||||
return a.compareAttributeAdd(b)
|
||||
case a.AttributeDelete != nil:
|
||||
return a.compareAttributeDelete(b)
|
||||
case a.AttributeReType != nil:
|
||||
return a.compareAttributeReType(b)
|
||||
case a.AttributeReEncode != nil:
|
||||
return a.compareAttributeReEncode(b)
|
||||
case a.ModelAdd != nil:
|
||||
return a.compareModelAdd(b)
|
||||
case a.ModelDelete != nil:
|
||||
return a.compareModelDelete(b)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported alteration type %v", a))
|
||||
}
|
||||
|
||||
func (a Alteration) compareAttributeAdd(b Alteration) bool {
|
||||
if a.AttributeAdd == nil || b.AttributeAdd == nil {
|
||||
return a.AttributeAdd == b.AttributeAdd
|
||||
}
|
||||
return a.AttributeAdd.Attr.Compare(b.AttributeAdd.Attr)
|
||||
}
|
||||
|
||||
func (a Alteration) compareAttributeDelete(b Alteration) bool {
|
||||
if a.AttributeDelete == nil || b.AttributeDelete == nil {
|
||||
return a.AttributeDelete == b.AttributeDelete
|
||||
}
|
||||
return a.AttributeDelete.Attr.Compare(b.AttributeDelete.Attr)
|
||||
}
|
||||
|
||||
func (a Alteration) compareAttributeReType(b Alteration) bool {
|
||||
if !a.AttributeReType.Attr.Compare(b.AttributeReType.Attr) {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.AttributeReType == nil || b.AttributeReType == nil {
|
||||
return a.AttributeReType == b.AttributeReType
|
||||
}
|
||||
return a.AttributeReType.To.Type() == b.AttributeReType.To.Type()
|
||||
}
|
||||
|
||||
func (a Alteration) compareAttributeReEncode(b Alteration) bool {
|
||||
if !a.AttributeReEncode.Attr.Compare(b.AttributeReEncode.Attr) {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.AttributeReEncode == nil || b.AttributeReEncode == nil {
|
||||
return a.AttributeReEncode == b.AttributeReEncode
|
||||
}
|
||||
return a.AttributeReEncode.To.Type() == b.AttributeReEncode.To.Type()
|
||||
}
|
||||
|
||||
func (a Alteration) compareModelAdd(b Alteration) bool {
|
||||
if a.ModelAdd == nil || b.ModelAdd == nil {
|
||||
return a.ModelAdd == b.ModelAdd
|
||||
}
|
||||
return a.ModelAdd.Model.Compare(*b.ModelAdd.Model)
|
||||
}
|
||||
|
||||
func (a Alteration) compareModelDelete(b Alteration) bool {
|
||||
if a.ModelDelete == nil || b.ModelDelete == nil {
|
||||
return a.ModelDelete == b.ModelDelete
|
||||
}
|
||||
return a.ModelDelete.Model.Compare(*b.ModelDelete.Model)
|
||||
}
|
||||
600
server/pkg/dal/alteration_test.go
Normal file
600
server/pkg/dal/alteration_test.go
Normal file
@@ -0,0 +1,600 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestMerge(t *testing.T) {
|
||||
tcc := []struct {
|
||||
name string
|
||||
aa AlterationSet
|
||||
bb AlterationSet
|
||||
cc AlterationSet
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
aa: AlterationSet{},
|
||||
bb: AlterationSet{},
|
||||
cc: AlterationSet{},
|
||||
},
|
||||
{
|
||||
name: "aa empty",
|
||||
aa: AlterationSet{},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bb empty",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remove duplicates",
|
||||
aa: AlterationSet{},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "un matching types",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match AttributeAdd",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match AttributeAdd",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "bar",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: "bar",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match AttributeDelete",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match AttributeDelete",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "bar",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: &Attribute{
|
||||
Ident: "bar",
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match AttributeReType",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match AttributeReType",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeText{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeBoolean{Nullable: false},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
},
|
||||
To: &TypeText{Nullable: false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match AttributeReEncode",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match AttributeReEncode",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecAlias{Ident: "foo2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: &Attribute{Ident: "foo"},
|
||||
To: &CodecAlias{Ident: "foo2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match ModelAdd",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match ModelAdd",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
ModelAdd: &ModelAdd{
|
||||
Model: &Model{Ident: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: "match ModelDelete",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not match ModelDelete",
|
||||
aa: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
bb: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cc: AlterationSet{
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "foo"},
|
||||
},
|
||||
},
|
||||
&Alteration{
|
||||
ModelDelete: &ModelDelete{
|
||||
Model: &Model{Ident: "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req := require.New(t)
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cc := tc.aa.Merge(tc.bb)
|
||||
req.Len(cc, len(tc.cc))
|
||||
for i, c := range cc {
|
||||
cmp := c.compare(*tc.cc[i])
|
||||
req.True(cmp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReTypeMarshling(t *testing.T) {
|
||||
a := &AttributeReType{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeBlob{},
|
||||
Store: &CodecPlain{},
|
||||
},
|
||||
To: &TypeBoolean{},
|
||||
}
|
||||
|
||||
bb, err := json.Marshal(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := &AttributeReType{}
|
||||
err = json.Unmarshal(bb, &b)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, reflect.DeepEqual(a, b))
|
||||
}
|
||||
|
||||
func TestReEncodeMarshling(t *testing.T) {
|
||||
a := &AttributeReEncode{
|
||||
Attr: &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeBlob{},
|
||||
Store: &CodecPlain{},
|
||||
},
|
||||
To: &CodecAlias{Ident: "bar"},
|
||||
}
|
||||
|
||||
bb, err := json.Marshal(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := &AttributeReEncode{}
|
||||
err = json.Unmarshal(bb, &b)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, reflect.DeepEqual(a, b))
|
||||
}
|
||||
69
server/pkg/dal/attribute_codec.go
Normal file
69
server/pkg/dal/attribute_codec.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package dal
|
||||
|
||||
type (
|
||||
AttributeCodecType string
|
||||
|
||||
// Codec defines how values for a specific model attribute
|
||||
// are retrieved or stored
|
||||
//
|
||||
// If attribute does not have store strategy set
|
||||
// store driver should use attribute name to determinate
|
||||
// source/destination of the value (table column, json document property)
|
||||
Codec interface {
|
||||
Type() AttributeCodecType
|
||||
SingleValueOnly() bool
|
||||
}
|
||||
|
||||
CodecPlain struct{}
|
||||
|
||||
// CodecRecordValueSetJSON defines that values are encoded/decoded into
|
||||
// a JSON simple document { [_: string]: Array<unknown> }
|
||||
//
|
||||
// This only solves
|
||||
// Attribute{Ident: "foo", Store: StoreCodecRecordValueJSON{ Ident: "bar" }
|
||||
// => "bar"->'foo'->0
|
||||
CodecRecordValueSetJSON struct {
|
||||
Ident string
|
||||
}
|
||||
|
||||
// handling complex JSON documents
|
||||
//StoreCodecJSON struct {
|
||||
// Ident string
|
||||
// Path []any
|
||||
//}
|
||||
//
|
||||
// { "@value": ... "@type": .... }
|
||||
// StoreCodecJSONLD struct { Ident string; Path []any }
|
||||
|
||||
// StoreCodecXML
|
||||
//StoreCodecXML struct {}
|
||||
|
||||
// CodecAlias defines case when value is not stored
|
||||
// under the same column (in case of an SQL table)
|
||||
//
|
||||
// Value of CodecAlias.Ident is used as base
|
||||
// and value of Attribute.Ident holding CodecAlias is used
|
||||
// as an alias!
|
||||
//
|
||||
// Attribute{Ident: "foo", Store: CodecAlias{ Ident: "bar" }
|
||||
// => "bar" as "foo"
|
||||
CodecAlias struct {
|
||||
Ident string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
AttributeCodecPlain AttributeCodecType = "corteza::dal:attribute-codec:plain"
|
||||
AttributeCodecRecordValueSetJSON AttributeCodecType = "corteza::dal:attribute-codec:record-value-set-json"
|
||||
AttributeCodecAlias AttributeCodecType = "corteza::dal:attribute-codec:alias"
|
||||
)
|
||||
|
||||
func (*CodecPlain) Type() AttributeCodecType { return AttributeCodecPlain }
|
||||
func (*CodecRecordValueSetJSON) Type() AttributeCodecType {
|
||||
return AttributeCodecRecordValueSetJSON
|
||||
}
|
||||
func (*CodecAlias) Type() AttributeCodecType { return AttributeCodecAlias }
|
||||
|
||||
func (*CodecPlain) SingleValueOnly() bool { return true }
|
||||
func (*CodecRecordValueSetJSON) SingleValueOnly() bool { return false }
|
||||
func (*CodecAlias) SingleValueOnly() bool { return true }
|
||||
203
server/pkg/dal/attribute_types.go
Normal file
203
server/pkg/dal/attribute_types.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package dal
|
||||
|
||||
type (
|
||||
AttributeType string
|
||||
|
||||
// temp
|
||||
Type interface {
|
||||
Type() AttributeType
|
||||
IsNullable() bool
|
||||
}
|
||||
|
||||
// TypeID handles ID (uint64) coding
|
||||
//
|
||||
// Encoding/decoding might be different depending on
|
||||
// 1) underlying store (and dialect)
|
||||
// 2) value codec (raw, json ...)
|
||||
//
|
||||
// It is always Corteza ID
|
||||
TypeID struct {
|
||||
GeneratedByStore bool
|
||||
Nullable bool
|
||||
|
||||
HasDefault bool
|
||||
DefaultValue uint64
|
||||
}
|
||||
|
||||
// TypeRef handles ID (uint64) coding + reference info
|
||||
//
|
||||
// Encoding/decoding might be different depending on
|
||||
// 1) underlying store (and dialect)
|
||||
// 2) value codec (raw, json ...)
|
||||
TypeRef struct {
|
||||
// defaults to ID
|
||||
RefAttribute string
|
||||
RefModel *ModelRef
|
||||
|
||||
Nullable bool
|
||||
|
||||
HasDefault bool
|
||||
DefaultValue uint64
|
||||
}
|
||||
|
||||
// TypeTimestamp handles timestamp coding
|
||||
//
|
||||
// Encoding/decoding might be different depending on
|
||||
// 1) underlying store (and dialect)
|
||||
// 2) value codec (raw, json ...)
|
||||
TypeTimestamp struct {
|
||||
Timezone bool
|
||||
Precision int
|
||||
Nullable bool
|
||||
|
||||
DefaultCurrentTimestamp bool
|
||||
}
|
||||
|
||||
// TypeTime handles time coding
|
||||
//
|
||||
// Encoding/decoding might be different depending on
|
||||
// 1) underlying store (and dialect)
|
||||
// 2) value codec (raw, json ...)
|
||||
TypeTime struct {
|
||||
Timezone bool
|
||||
TimezonePrecision bool
|
||||
Precision int
|
||||
Nullable bool
|
||||
|
||||
DefaultCurrentTimestamp bool
|
||||
}
|
||||
|
||||
// TypeDate handles date coding
|
||||
//
|
||||
// Encoding/decoding might be different depending on
|
||||
// 1) underlying store (and dialect)
|
||||
// 2) value codec (raw, jsonb...)
|
||||
TypeDate struct {
|
||||
//
|
||||
Nullable bool
|
||||
|
||||
DefaultCurrentTimestamp bool
|
||||
}
|
||||
|
||||
// TypeNumber handles number coding
|
||||
//
|
||||
// Encoding/decoding might be different depending on
|
||||
// 1) underlying store (and dialect)
|
||||
// 2) value codec (raw, jsonb...)
|
||||
TypeNumber struct {
|
||||
Precision int
|
||||
Scale int
|
||||
Nullable bool
|
||||
|
||||
HasDefault bool
|
||||
DefaultValue float64
|
||||
Meta map[string]any
|
||||
}
|
||||
|
||||
// TypeText handles string coding
|
||||
//
|
||||
// Encoding/decoding might be different depending on
|
||||
// 1) underlying store (and dialect)
|
||||
// 2) value codec (raw, jsonb...)
|
||||
TypeText struct {
|
||||
Length uint
|
||||
Nullable bool
|
||||
|
||||
HasDefault bool
|
||||
DefaultValue string
|
||||
}
|
||||
|
||||
// TypeBoolean
|
||||
TypeBoolean struct {
|
||||
//
|
||||
Nullable bool
|
||||
|
||||
HasDefault bool
|
||||
DefaultValue bool
|
||||
}
|
||||
|
||||
// TypeEnum
|
||||
TypeEnum struct {
|
||||
Values []string
|
||||
Nullable bool
|
||||
|
||||
HasDefault bool
|
||||
DefaultValue string
|
||||
}
|
||||
|
||||
// TypeGeometry
|
||||
TypeGeometry struct {
|
||||
//
|
||||
Nullable bool
|
||||
}
|
||||
|
||||
// TypeJSON handles coding of arbitrary data into JSON structure
|
||||
// NOT TO BE CONFUSED with encodedField
|
||||
//
|
||||
// Encoding/decoding might be different depending on
|
||||
// 1) underlying store (and dialect)
|
||||
// 2) value codec (raw, json ...)
|
||||
TypeJSON struct {
|
||||
//
|
||||
Nullable bool
|
||||
|
||||
HasDefault bool
|
||||
DefaultValue any
|
||||
}
|
||||
|
||||
// TypeBlob store/return data as
|
||||
TypeBlob struct {
|
||||
//
|
||||
Nullable bool
|
||||
}
|
||||
|
||||
TypeUUID struct {
|
||||
Nullable bool
|
||||
}
|
||||
|
||||
TypeNumberStoreNativeType string
|
||||
)
|
||||
|
||||
const (
|
||||
AttributeTypeID AttributeType = "corteza::dal:attribute-type:id"
|
||||
AttributeTypeRef AttributeType = "corteza::dal:attribute-type:ref"
|
||||
AttributeTypeTimestamp AttributeType = "corteza::dal:attribute-type:timestamp"
|
||||
AttributeTypeTime AttributeType = "corteza::dal:attribute-type:time"
|
||||
AttributeTypeDate AttributeType = "corteza::dal:attribute-type:date"
|
||||
AttributeTypeNumber AttributeType = "corteza::dal:attribute-type:number"
|
||||
AttributeTypeText AttributeType = "corteza::dal:attribute-type:text"
|
||||
AttributeTypeBoolean AttributeType = "corteza::dal:attribute-type:boolean"
|
||||
AttributeTypeEnum AttributeType = "corteza::dal:attribute-type:enum"
|
||||
AttributeTypeGeometry AttributeType = "corteza::dal:attribute-type:geometry"
|
||||
AttributeTypejson AttributeType = "corteza::dal:attribute-type:json"
|
||||
AttributeTypeBlob AttributeType = "corteza::dal:attribute-type:blob"
|
||||
AttributeTypeUUID AttributeType = "corteza::dal:attribute-type:uuid"
|
||||
)
|
||||
|
||||
func (t TypeID) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeRef) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeTimestamp) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeTime) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeDate) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeNumber) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeText) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeBoolean) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeEnum) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeGeometry) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeJSON) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeBlob) IsNullable() bool { return t.Nullable }
|
||||
func (t TypeUUID) IsNullable() bool { return t.Nullable }
|
||||
|
||||
func (t TypeID) Type() AttributeType { return AttributeTypeID }
|
||||
func (t TypeRef) Type() AttributeType { return AttributeTypeRef }
|
||||
func (t TypeTimestamp) Type() AttributeType { return AttributeTypeTimestamp }
|
||||
func (t TypeTime) Type() AttributeType { return AttributeTypeTime }
|
||||
func (t TypeDate) Type() AttributeType { return AttributeTypeDate }
|
||||
func (t TypeNumber) Type() AttributeType { return AttributeTypeNumber }
|
||||
func (t TypeText) Type() AttributeType { return AttributeTypeText }
|
||||
func (t TypeBoolean) Type() AttributeType { return AttributeTypeBoolean }
|
||||
func (t TypeEnum) Type() AttributeType { return AttributeTypeEnum }
|
||||
func (t TypeGeometry) Type() AttributeType { return AttributeTypeGeometry }
|
||||
func (t TypeJSON) Type() AttributeType { return AttributeTypejson }
|
||||
func (t TypeBlob) Type() AttributeType { return AttributeTypeBlob }
|
||||
func (t TypeUUID) Type() AttributeType { return AttributeTypeUUID }
|
||||
36
server/pkg/dal/buffer.go
Normal file
36
server/pkg/dal/buffer.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
)
|
||||
|
||||
type (
|
||||
// OrderedBuffer provides the buffered data in the specified order
|
||||
OrderedBuffer interface {
|
||||
Buffer
|
||||
|
||||
// InOrder makes the buffer provide the stored data in the specified order
|
||||
InOrder(ss ...*filter.SortExpr) (err error)
|
||||
}
|
||||
|
||||
// Buffer provides a place where you can buffer the data provided by DAL
|
||||
// @note Buffers are currently primarily used for testing
|
||||
Buffer interface {
|
||||
// Seek moves the index pointer to the specified location
|
||||
// After the Seek call, a Next() call is required
|
||||
Seek(context.Context, int) error
|
||||
|
||||
// Len returns the number of elements in the buffer
|
||||
Len() int
|
||||
|
||||
Iterator
|
||||
Adder
|
||||
}
|
||||
|
||||
Adder interface {
|
||||
// Add adds a new ValueGetter to the buffer
|
||||
Add(context.Context, ValueGetter) (err error)
|
||||
}
|
||||
)
|
||||
42
server/pkg/dal/conn.go
Normal file
42
server/pkg/dal/conn.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package dal
|
||||
|
||||
import "regexp"
|
||||
|
||||
type (
|
||||
ConnectionWrap struct {
|
||||
ID uint64
|
||||
|
||||
connection Connection
|
||||
params ConnectionParams
|
||||
Config ConnectionConfig
|
||||
}
|
||||
|
||||
ConnectionConfig struct {
|
||||
SensitivityLevelID uint64
|
||||
Label string
|
||||
|
||||
// When model does not specify the ident (table name for example), fallback to this
|
||||
ModelIdent string
|
||||
|
||||
// when a new model is added on a connection, it's ident
|
||||
// is verified against this regexp
|
||||
//
|
||||
// ident is considered valid if it matches one of the expressions
|
||||
// or if the list of checks is empty
|
||||
ModelIdentCheck []*regexp.Regexp
|
||||
}
|
||||
)
|
||||
|
||||
func checkIdent(ident string, rr ...*regexp.Regexp) bool {
|
||||
if len(rr) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, r := range rr {
|
||||
if r.MatchString(ident) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
53
server/pkg/dal/conn_test.go
Normal file
53
server/pkg/dal/conn_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_checkIdent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ident string
|
||||
rr []*regexp.Regexp
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
ident: "",
|
||||
rr: []*regexp.Regexp{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "one",
|
||||
ident: "foo",
|
||||
rr: []*regexp.Regexp{regexp.MustCompile("foo")},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "false",
|
||||
ident: "foo",
|
||||
rr: []*regexp.Regexp{regexp.MustCompile("bar")},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "two",
|
||||
ident: "bar",
|
||||
rr: []*regexp.Regexp{regexp.MustCompile("foo"), regexp.MustCompile("bar")},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "two failed",
|
||||
ident: "foo",
|
||||
rr: []*regexp.Regexp{regexp.MustCompile("bar"), regexp.MustCompile("baz")},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := checkIdent(tt.ident, tt.rr...); got != tt.want {
|
||||
t.Errorf("checkIdent() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
299
server/pkg/dal/def_aggregate.go
Normal file
299
server/pkg/dal/def_aggregate.go
Normal file
@@ -0,0 +1,299 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
)
|
||||
|
||||
type (
|
||||
// Aggregate produces a series of aggregated rows from the provided sources based
|
||||
// on the specified group.
|
||||
Aggregate struct {
|
||||
Ident string
|
||||
RelSource string
|
||||
Filter filter.Filter
|
||||
filter internalFilter
|
||||
|
||||
Group []AggregateAttr
|
||||
OutAttributes []AggregateAttr
|
||||
|
||||
SourceAttributes []AttributeMapping
|
||||
|
||||
rel PipelineStep
|
||||
plan aggregatePlan
|
||||
analysis map[string]OpAnalysis
|
||||
}
|
||||
|
||||
// aggregatePlan outlines how the optimizer determined the dataset should be
|
||||
// aggregated
|
||||
aggregatePlan struct {
|
||||
// partialScan indicates we can partially pull data from the two sources
|
||||
// as the data is provided in the correct order.
|
||||
partialScan bool
|
||||
}
|
||||
|
||||
groupKey []any
|
||||
aggregateGroup struct {
|
||||
key groupKey
|
||||
agg *aggregator
|
||||
}
|
||||
|
||||
// AggregateAttr is a simple wrapper to outline aggregated attribute definitions
|
||||
AggregateAttr struct {
|
||||
MultiValue bool
|
||||
Key bool
|
||||
|
||||
// @todo change; temporary for compose service
|
||||
RawExpr string
|
||||
|
||||
Identifier string
|
||||
Label string
|
||||
Expression *ql.ASTNode
|
||||
Type Type
|
||||
Store Codec
|
||||
}
|
||||
)
|
||||
|
||||
func (def *Aggregate) Identifier() string {
|
||||
return def.Ident
|
||||
}
|
||||
|
||||
func (def *Aggregate) Sources() []string {
|
||||
return []string{def.RelSource}
|
||||
}
|
||||
|
||||
func (def *Aggregate) Attributes() [][]AttributeMapping {
|
||||
aa := append(def.Group, def.OutAttributes...)
|
||||
out := make([]AttributeMapping, 0, len(aa))
|
||||
|
||||
for _, a := range aa {
|
||||
out = append(out, a.toSimpleAttr())
|
||||
}
|
||||
|
||||
return [][]AttributeMapping{out}
|
||||
}
|
||||
|
||||
func (def *Aggregate) Analyze(ctx context.Context) (err error) {
|
||||
// @todo proper analysis; for now we'll leave this as defaults
|
||||
def.analysis = map[string]OpAnalysis{
|
||||
OpAnalysisIterate: {
|
||||
ScanCost: CostUnknown,
|
||||
SearchCost: CostUnknown,
|
||||
FilterCost: CostUnknown,
|
||||
SortCost: CostUnknown,
|
||||
OutputSize: SizeUnknown,
|
||||
},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (def *Aggregate) Analysis() map[string]OpAnalysis {
|
||||
return def.analysis
|
||||
}
|
||||
|
||||
func (def *Aggregate) Optimize(reqFilter internalFilter) (rspFilter internalFilter, err error) {
|
||||
err = fmt.Errorf("not implemented")
|
||||
return
|
||||
}
|
||||
|
||||
// iterator initializes an iterator based on the provided pipeline step definition
|
||||
func (def *Aggregate) iterator(ctx context.Context, src Iterator) (out Iterator, err error) {
|
||||
exec, err := def.init(ctx, src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return exec, exec.init(ctx)
|
||||
}
|
||||
|
||||
// dryrun performs step execution without interacting with the data
|
||||
// @todo consider rewording this
|
||||
func (def *Aggregate) dryrun(ctx context.Context) (err error) {
|
||||
_, err = def.init(ctx, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (def *Aggregate) init(ctx context.Context, src Iterator) (exec *aggregate, err error) {
|
||||
exec = &aggregate{
|
||||
source: src,
|
||||
}
|
||||
|
||||
// Convert the provided filter into an internal filter
|
||||
if def.Filter != nil {
|
||||
def.filter, err = toInternalFilter(def.Filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Collect attributes from the underlaying step in case own are not provided
|
||||
if len(def.SourceAttributes) == 0 {
|
||||
def.SourceAttributes = collectAttributes(def.rel)
|
||||
}
|
||||
|
||||
// Index source attributes for group/aggregate definition validation
|
||||
srcAttrs := indexAttrs(def.SourceAttributes...)
|
||||
pp := newQlParser(func(ident ql.Ident) (_ ql.Ident, err error) {
|
||||
if _, ok := srcAttrs[ident.Value]; !ok {
|
||||
return ident, fmt.Errorf("unknown attribute %s", ident.Value)
|
||||
}
|
||||
return ident, nil
|
||||
})
|
||||
|
||||
prepAttr := func(attr AggregateAttr) (_ AggregateAttr, err error) {
|
||||
if attr.RawExpr != "" {
|
||||
// Parse (it already validates) raw expressions
|
||||
attr.Expression, err = pp.Parse(attr.RawExpr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Manually validate already parsed expressions
|
||||
err = attr.Expression.Traverse(func(a *ql.ASTNode) (bool, *ql.ASTNode, error) {
|
||||
if a.Symbol == "" {
|
||||
return true, a, nil
|
||||
}
|
||||
if _, ok := srcAttrs[a.Symbol]; !ok {
|
||||
return false, nil, fmt.Errorf("unknown attribute %s", a.Symbol)
|
||||
}
|
||||
|
||||
return true, a, nil
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return def.determineAttrType(attr, def.SourceAttributes)
|
||||
}
|
||||
|
||||
// Convert & validate group definitions
|
||||
// - groups
|
||||
outAttrs := make(map[string]bool, len(def.Group)+len(def.OutAttributes))
|
||||
for i, attr := range def.Group {
|
||||
attr, err = prepAttr(attr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
def.Group[i] = attr
|
||||
idtf := attr.Identifier
|
||||
|
||||
exec.groupDefs = append(exec.groupDefs, attr)
|
||||
outAttrs[idtf] = true
|
||||
}
|
||||
// - aggregates
|
||||
for i, attr := range def.OutAttributes {
|
||||
// @todo change when needed; currently, all aggregates are numbers
|
||||
attr.Type = &TypeNumber{}
|
||||
attr, err = prepAttr(attr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
def.OutAttributes[i] = attr
|
||||
idtf := attr.Identifier
|
||||
|
||||
exec.aggregateDefs = append(exec.aggregateDefs, attr)
|
||||
outAttrs[idtf] = true
|
||||
}
|
||||
|
||||
// Generic validation
|
||||
if len(def.Group) == 0 {
|
||||
err = fmt.Errorf("no group attributes specified")
|
||||
return
|
||||
}
|
||||
|
||||
if len(def.OutAttributes) == 0 {
|
||||
err = fmt.Errorf("no output attributes specified")
|
||||
return
|
||||
}
|
||||
|
||||
if len(def.SourceAttributes) == 0 {
|
||||
err = fmt.Errorf("no source attributes specified")
|
||||
return
|
||||
}
|
||||
|
||||
// order
|
||||
for _, s := range def.filter.OrderBy() {
|
||||
if _, ok := outAttrs[s.Column]; !ok {
|
||||
err = fmt.Errorf("order by attribute %s does not exist", s.Column)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Finishup
|
||||
exec.filter = def.filter
|
||||
exec.def = *def
|
||||
return
|
||||
}
|
||||
|
||||
// determineAttrType determines the type of the AggregateAttr based on it's definition
|
||||
// and source attributes
|
||||
func (def *Aggregate) determineAttrType(base AggregateAttr, ss []AttributeMapping) (out AggregateAttr, err error) {
|
||||
out = base
|
||||
if out.Type != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var root *ql.ASTNode
|
||||
var t Type
|
||||
|
||||
// If we have a symbol, then we'll use it to determine the type.
|
||||
// All current operations should return the same output type as the input one.
|
||||
//
|
||||
// In case of a function, use the output type of the root most function which has
|
||||
// a known type.
|
||||
//
|
||||
// Note, some refs (group and add for example) may not know their types so we need
|
||||
// to dig deeper.
|
||||
base.Expression.Traverse(func(a *ql.ASTNode) (bool, *ql.ASTNode, error) {
|
||||
if a.Symbol != "" {
|
||||
root = a
|
||||
return false, a, nil
|
||||
}
|
||||
|
||||
if a.Ref != "" {
|
||||
tmp := refToGvalExp[strings.ToLower(a.Ref)]
|
||||
if tmp == nil || tmp.OutType == nil || tmp.OutTypeUnknown {
|
||||
return true, a, nil
|
||||
}
|
||||
|
||||
if tmp.OutType != nil {
|
||||
t = tmp.OutType
|
||||
return false, a, nil
|
||||
}
|
||||
}
|
||||
return true, a, nil
|
||||
})
|
||||
|
||||
if root != nil {
|
||||
for _, s := range ss {
|
||||
if s.Identifier() == root.Symbol {
|
||||
t = s.Properties().Type
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out.Type = t
|
||||
return
|
||||
}
|
||||
|
||||
func (a AggregateAttr) toSimpleAttr() SimpleAttr {
|
||||
return SimpleAttr{
|
||||
Ident: a.Identifier,
|
||||
Props: MapProperties{
|
||||
Label: a.Label,
|
||||
IsPrimary: a.Key,
|
||||
Type: a.Type,
|
||||
},
|
||||
|
||||
// @todo won't matter for now
|
||||
Expr: "",
|
||||
Src: "",
|
||||
}
|
||||
}
|
||||
362
server/pkg/dal/def_datasource.go
Normal file
362
server/pkg/dal/def_datasource.go
Normal file
@@ -0,0 +1,362 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
)
|
||||
|
||||
type (
|
||||
// Datasource is a simple passthrough step for underlaying datasources.
|
||||
// It exists primarily to make operations consistent.
|
||||
Datasource struct {
|
||||
Ident string
|
||||
Filter filter.Filter
|
||||
ModelRef ModelRef
|
||||
filter internalFilter
|
||||
|
||||
OutAttributes []AttributeMapping
|
||||
|
||||
analysis map[string]OpAnalysis
|
||||
connection *ConnectionWrap
|
||||
model *Model
|
||||
|
||||
// clobbered lists all of the steps that are offloaded into the datasource.
|
||||
// The list is provided in order; the first step is the first step which should execute
|
||||
// in the report pipeline.
|
||||
//
|
||||
// @todo change to generic step; doing this because I can for now
|
||||
clobbered []*Aggregate
|
||||
|
||||
// provided in the init step so we can omit some code in the exec step
|
||||
// @todo consider removing this
|
||||
auxIter Iterator
|
||||
}
|
||||
)
|
||||
|
||||
func (def *Datasource) Identifier() string {
|
||||
return def.Ident
|
||||
}
|
||||
|
||||
func (def *Datasource) Sources() []string {
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (def *Datasource) Attributes() [][]AttributeMapping {
|
||||
if len(def.clobbered) > 0 {
|
||||
return def.offloadedAttributes()
|
||||
}
|
||||
|
||||
return def.ownAttributes()
|
||||
}
|
||||
|
||||
func (def *Datasource) Analyze(ctx context.Context) (err error) {
|
||||
a, err := def.connection.connection.Analyze(ctx, def.model)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
def.analysis = map[string]OpAnalysis{
|
||||
OpAnalysisIterate: {
|
||||
ScanCost: CostUnknown,
|
||||
SearchCost: CostUnknown,
|
||||
FilterCost: CostUnknown,
|
||||
SortCost: CostUnknown,
|
||||
OutputSize: SizeUnknown,
|
||||
},
|
||||
}
|
||||
|
||||
if _, ok := a[OpAnalysisAggregate]; ok {
|
||||
def.analysis[OpAnalysisAggregate] = a[OpAnalysisAggregate]
|
||||
}
|
||||
if _, ok := a[OpAnalysisJoin]; ok {
|
||||
def.analysis[OpAnalysisJoin] = a[OpAnalysisJoin]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (def *Datasource) Analysis() map[string]OpAnalysis {
|
||||
return def.analysis
|
||||
}
|
||||
|
||||
func (def *Datasource) Optimize(req internalFilter) (res internalFilter, err error) {
|
||||
return internalFilter{}, fmt.Errorf("optimization not implemented")
|
||||
}
|
||||
|
||||
func (def *Datasource) init(ctx context.Context) (err error) {
|
||||
if def.model == nil {
|
||||
return fmt.Errorf("cannot initialize datasource: model not set")
|
||||
}
|
||||
if def.connection == nil {
|
||||
return fmt.Errorf("cannot initialize datasource: connection not set")
|
||||
}
|
||||
|
||||
if def.Filter != nil {
|
||||
def.filter, err = toInternalFilter(def.Filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(def.OutAttributes) == 0 {
|
||||
def.OutAttributes = def.outAttrsFromModel(def.model)
|
||||
}
|
||||
|
||||
pp := make([]string, 0, len(def.OutAttributes)/2+1)
|
||||
for _, a := range def.OutAttributes {
|
||||
if a.Properties().IsPrimary {
|
||||
pp = append(pp, a.Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
// Assure and attempt to correct the provided sort to conform with the data set and the
|
||||
// paging cursor (if any)
|
||||
def.filter, err = assureSort(def.filter, pp)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Firstly validate the base
|
||||
err = def.validate()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(def.clobbered) > 0 {
|
||||
// @todo currently, we can only do one; change this when we tweak the DB
|
||||
// offloading code.
|
||||
ag := def.clobbered[0]
|
||||
|
||||
// Invoke the aggregation's init to perform the validation and preparation logic
|
||||
var wa *aggregate
|
||||
wa, err = ag.init(ctx, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Preprocess the filters to conform to connection API
|
||||
f, having, err := def.getAggregationFilters(def.filter, wa.filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
def.auxIter, err = def.connection.connection.Aggregate(ctx, def.model, f, wa.groupDefs, wa.aggregateDefs, having)
|
||||
return err
|
||||
}
|
||||
|
||||
def.auxIter, err = def.connection.connection.Search(ctx, def.model, def.filter)
|
||||
return err
|
||||
}
|
||||
|
||||
func (def *Datasource) exec(ctx context.Context) (out Iterator, err error) {
|
||||
if def.auxIter == nil {
|
||||
return nil, fmt.Errorf("datasource not initialized")
|
||||
}
|
||||
|
||||
return def.auxIter, nil
|
||||
}
|
||||
|
||||
func (def *Datasource) validate() (err error) {
|
||||
err = func() (err error) {
|
||||
if len(def.OutAttributes) == 0 {
|
||||
return fmt.Errorf("no attributes specified")
|
||||
}
|
||||
|
||||
return
|
||||
}()
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid definition: %v", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (def *Datasource) outAttrsFromModel(model *Model) (attrs []AttributeMapping) {
|
||||
for _, attr := range model.Attributes {
|
||||
if attr.Type == nil {
|
||||
panic(fmt.Sprintf("impossible state: attribute %s has no type", attr.Ident))
|
||||
}
|
||||
|
||||
attrs = append(attrs, SimpleAttr{
|
||||
Ident: attr.Ident,
|
||||
Src: attr.Ident,
|
||||
Props: MapProperties{
|
||||
IsPrimary: attr.PrimaryKey,
|
||||
IsSystem: attr.System,
|
||||
IsFilterable: attr.Filterable,
|
||||
IsSortable: attr.Sortable,
|
||||
Nullable: attr.Type.IsNullable(),
|
||||
Type: attr.Type,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (def *Datasource) shouldClobberAggregation() bool {
|
||||
costs, ok := def.analysis[OpAnalysisAggregate]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// @todo more to it; check and compare costs; for now we know that all rdbms
|
||||
// offloads will be faster.
|
||||
|
||||
_ = costs
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (def *Datasource) ownAttributes() [][]AttributeMapping {
|
||||
return [][]AttributeMapping{def.OutAttributes}
|
||||
}
|
||||
|
||||
func (def *Datasource) offloadedAttributes() [][]AttributeMapping {
|
||||
// The last offloaded step's attributes is what this step's iterator returns
|
||||
return def.clobbered[len(def.clobbered)-1].Attributes()
|
||||
}
|
||||
|
||||
func (def *Datasource) clobber(s PipelineStep) (ok bool) {
|
||||
switch cs := s.(type) {
|
||||
case *Aggregate:
|
||||
if !def.shouldClobberAggregation() {
|
||||
return false
|
||||
}
|
||||
|
||||
def.clobbered = append(def.clobbered, cs)
|
||||
return true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getAggregationFilters transforms the base and the aggregation filters to conform
|
||||
// to the store/dal's model.Aggregate API
|
||||
//
|
||||
// - The full filter.Filter return parameter is applied to the base dataset
|
||||
// - The QL node return parameter is applied only to the aggregated dataset
|
||||
//
|
||||
// @todo should we change the store's API to accept 2x filter.Filter? I think
|
||||
// that would allow the underlaying driver to decide how to handle them
|
||||
// instead of relying on what SQL does.
|
||||
func (def *Datasource) getAggregationFilters(base, agg internalFilter) (filter internalFilter, having *ql.ASTNode, err error) {
|
||||
var typedV expr.TypedValue
|
||||
filter = base
|
||||
|
||||
// Move the aggregation's order and limit to the base filter because those
|
||||
// are applied to the final output (per SQL)
|
||||
//
|
||||
// Leave base filtering parameters (filter, constraints, ...) as those are apploed
|
||||
// to the dataset before aggregation (which is correct).
|
||||
filter.orderBy = agg.orderBy
|
||||
filter.limit = agg.limit
|
||||
|
||||
// Convert the rest of the filtering parameters defined on the aggregation's filter
|
||||
// to the QL node.
|
||||
// ALl of that is applied after the aggregation which is correct.
|
||||
var nConstraints *ql.ASTNode
|
||||
if len(agg.constraints) > 0 {
|
||||
nConstraints = &ql.ASTNode{
|
||||
Ref: "and",
|
||||
}
|
||||
for k, c := range agg.constraints {
|
||||
arg := &ql.ASTNode{
|
||||
Ref: "or",
|
||||
}
|
||||
|
||||
for _, v := range c {
|
||||
typedV, err = expr.Typify(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
arg.Args = append(arg.Args, &ql.ASTNode{
|
||||
Symbol: k,
|
||||
Value: ql.WrapValue(typedV),
|
||||
})
|
||||
}
|
||||
|
||||
nConstraints.Args = append(nConstraints.Args, &ql.ASTNode{
|
||||
Ref: "group",
|
||||
Args: ql.ASTNodeSet{arg},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var nStateConstraints *ql.ASTNode
|
||||
if len(agg.stateConstraints) > 0 {
|
||||
nStateConstraints = &ql.ASTNode{
|
||||
Ref: "and",
|
||||
}
|
||||
for k, c := range agg.stateConstraints {
|
||||
typedV, err = expr.Typify(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nStateConstraints.Args = append(nStateConstraints.Args, &ql.ASTNode{
|
||||
Symbol: k,
|
||||
Value: ql.WrapValue(typedV),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var nMetaConstraints *ql.ASTNode
|
||||
if len(agg.stateConstraints) > 0 {
|
||||
nMetaConstraints = &ql.ASTNode{
|
||||
Ref: "and",
|
||||
}
|
||||
for k, c := range agg.stateConstraints {
|
||||
typedV, err = expr.Typify(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
nMetaConstraints.Args = append(nMetaConstraints.Args, &ql.ASTNode{
|
||||
Symbol: k,
|
||||
Value: ql.WrapValue(typedV),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
nExpression := agg.expParsed
|
||||
|
||||
var nCursor *ql.ASTNode
|
||||
if agg.cursor != nil {
|
||||
nCursor, err = agg.cursor.ToAST(nil, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
having = &ql.ASTNode{
|
||||
Ref: "and",
|
||||
}
|
||||
|
||||
if nConstraints != nil {
|
||||
having.Args = append(having.Args, nConstraints)
|
||||
}
|
||||
if nStateConstraints != nil {
|
||||
having.Args = append(having.Args, nStateConstraints)
|
||||
}
|
||||
if nMetaConstraints != nil {
|
||||
having.Args = append(having.Args, nMetaConstraints)
|
||||
}
|
||||
if nExpression != nil {
|
||||
having.Args = append(having.Args, nExpression)
|
||||
}
|
||||
if nCursor != nil {
|
||||
having.Args = append(having.Args, nCursor)
|
||||
}
|
||||
|
||||
// In case everything is empty, no need for the having part
|
||||
if len(having.Ref) == 0 {
|
||||
having = nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
218
server/pkg/dal/def_join.go
Normal file
218
server/pkg/dal/def_join.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
)
|
||||
|
||||
type (
|
||||
// Join produces a series of joined rows from the provided sources based
|
||||
// on the JoinPredicate.
|
||||
//
|
||||
// The join step produces an SQL left join-like output where all right rows
|
||||
// have a corresponding left row.
|
||||
Join struct {
|
||||
Ident string
|
||||
RelLeft string
|
||||
RelRight string
|
||||
// @todo allow multiple join predicates; for now (for easier indexing)
|
||||
// only allow one (this is the same as we had before)
|
||||
On JoinPredicate
|
||||
Filter filter.Filter
|
||||
filter internalFilter
|
||||
|
||||
OutAttributes []AttributeMapping
|
||||
LeftAttributes []AttributeMapping
|
||||
RightAttributes []AttributeMapping
|
||||
|
||||
relLeft PipelineStep
|
||||
relRight PipelineStep
|
||||
plan joinPlan
|
||||
analysis map[string]OpAnalysis
|
||||
}
|
||||
|
||||
// JoinPredicate determines the attributes the two datasets should get joined on
|
||||
JoinPredicate struct {
|
||||
Left string
|
||||
Right string
|
||||
}
|
||||
|
||||
// joinPlan outlines how the optimizer determined the two datasets should be
|
||||
// joined on.
|
||||
joinPlan struct {
|
||||
// @todo add strategy when we have different strategies implemented
|
||||
// strategy string
|
||||
|
||||
// partialScan indicates we can partially pull data from the two sources
|
||||
// as the data is provided in the correct order.
|
||||
partialScan bool
|
||||
}
|
||||
)
|
||||
|
||||
func (def *Join) Identifier() string {
|
||||
return def.Ident
|
||||
}
|
||||
|
||||
func (def *Join) Sources() []string {
|
||||
return []string{def.RelLeft, def.RelRight}
|
||||
}
|
||||
|
||||
func (def *Join) Attributes() [][]AttributeMapping {
|
||||
return [][]AttributeMapping{def.OutAttributes}
|
||||
}
|
||||
|
||||
func (def *Join) Analyze(ctx context.Context) (err error) {
|
||||
// @todo proper analysis; for now we'll leave this as defaults
|
||||
def.analysis = map[string]OpAnalysis{
|
||||
OpAnalysisIterate: {
|
||||
ScanCost: CostUnknown,
|
||||
SearchCost: CostUnknown,
|
||||
FilterCost: CostUnknown,
|
||||
SortCost: CostUnknown,
|
||||
OutputSize: SizeUnknown,
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (def *Join) Analysis() map[string]OpAnalysis {
|
||||
return def.analysis
|
||||
}
|
||||
|
||||
func (def *Join) Optimize(req internalFilter) (res internalFilter, err error) {
|
||||
err = fmt.Errorf("not implemented")
|
||||
return
|
||||
}
|
||||
|
||||
// iterator initializes an iterator based on the provided pipeline step definition
|
||||
func (def *Join) iterator(ctx context.Context, left, right Iterator) (out Iterator, err error) {
|
||||
exec, err := def.init(ctx, left, right)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get pred. type
|
||||
// @todo should validate that both pred. types are the same/compatible
|
||||
var pt Type
|
||||
for _, a := range def.LeftAttributes {
|
||||
if a.Identifier() == def.On.Left {
|
||||
pt = a.Properties().Type
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return exec, exec.init(ctx, pt)
|
||||
}
|
||||
|
||||
// dryrun performs step execution without interacting with the data
|
||||
// @todo consider rewording this
|
||||
func (def *Join) dryrun(ctx context.Context) (err error) {
|
||||
_, err = def.init(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (def *Join) init(ctx context.Context, left, right Iterator) (exec *joinLeft, err error) {
|
||||
exec = &joinLeft{
|
||||
leftSource: left,
|
||||
rightSource: right,
|
||||
}
|
||||
|
||||
// Convert the provided filter into an internal filter
|
||||
if def.Filter != nil {
|
||||
def.filter, err = toInternalFilter(def.Filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Collect attributes from the underlaying step in case own are not provided
|
||||
if len(def.LeftAttributes) == 0 {
|
||||
def.LeftAttributes = collectAttributes(def.relLeft)
|
||||
}
|
||||
if len(def.RightAttributes) == 0 {
|
||||
def.RightAttributes = collectAttributes(def.relRight)
|
||||
}
|
||||
|
||||
// When the output attributes are not provided, we determine them based on the input
|
||||
// attributes.
|
||||
//
|
||||
// The join step prefixes each attribute with the identifier of the source they came from.
|
||||
// This avoids name collisions.
|
||||
// @todo consider improving this to only prefix when there is a name collision
|
||||
// and allow the nested attr. to be more permissive; for example, instead of
|
||||
// requiring a.b.c only b.c or c would be enough.
|
||||
if len(def.OutAttributes) == 0 {
|
||||
ins := func(srcIdent string, aa []AttributeMapping) {
|
||||
for _, a := range aa {
|
||||
p := a.Properties()
|
||||
def.OutAttributes = append(def.OutAttributes, SimpleAttr{
|
||||
Ident: fmt.Sprintf("%s%s%s", srcIdent, attributeNestingSeparator, a.Identifier()),
|
||||
Expr: a.Expression(),
|
||||
Src: a.Identifier(),
|
||||
Props: p,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
def.OutAttributes = make([]AttributeMapping, 0, len(def.LeftAttributes)+len(def.RightAttributes))
|
||||
ins(def.RelLeft, def.LeftAttributes)
|
||||
ins(def.RelRight, def.RightAttributes)
|
||||
}
|
||||
|
||||
// Assure and attempt to correct the provided sort to conform with the data set and the
|
||||
// paging cursor (if any)
|
||||
def.filter, err = assureSort(def.filter, exec.collectPrimaryAttributes(def.OutAttributes))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Index attrs for validations
|
||||
leftSrcAttrs := indexAttrs(def.LeftAttributes...)
|
||||
rightSrcAttrs := indexAttrs(def.RightAttributes...)
|
||||
outAttrs := indexAttrs(def.OutAttributes...)
|
||||
|
||||
for _, a := range def.OutAttributes {
|
||||
if !leftSrcAttrs[a.Source()] && !rightSrcAttrs[a.Source()] {
|
||||
return nil, fmt.Errorf("unknown attribute %s", a.Source())
|
||||
}
|
||||
}
|
||||
|
||||
// Generic validation
|
||||
if !leftSrcAttrs[def.On.Left] {
|
||||
return nil, fmt.Errorf("unknown join predicate attribute %s", def.On.Left)
|
||||
}
|
||||
if !rightSrcAttrs[def.On.Right] {
|
||||
return nil, fmt.Errorf("unknown join predicate attribute %s", def.On.Right)
|
||||
}
|
||||
|
||||
if len(def.OutAttributes) == 0 {
|
||||
return nil, fmt.Errorf("no attributes specified")
|
||||
}
|
||||
if len(def.LeftAttributes) == 0 {
|
||||
return nil, fmt.Errorf("no left attributes specified")
|
||||
}
|
||||
if len(def.RightAttributes) == 0 {
|
||||
return nil, fmt.Errorf("no right attributes specified")
|
||||
}
|
||||
|
||||
if def.On.Left == "" {
|
||||
return nil, fmt.Errorf("no left attribute in the join predicate specified")
|
||||
}
|
||||
if def.On.Right == "" {
|
||||
return nil, fmt.Errorf("no right attribute in the join predicate specified")
|
||||
}
|
||||
|
||||
// order
|
||||
for _, s := range def.filter.OrderBy() {
|
||||
if _, ok := outAttrs[s.Column]; !ok {
|
||||
return nil, fmt.Errorf("order attribute %s does not exist", s.Column)
|
||||
}
|
||||
}
|
||||
|
||||
exec.filter = def.filter
|
||||
exec.def = *def
|
||||
return
|
||||
}
|
||||
289
server/pkg/dal/def_link.go
Normal file
289
server/pkg/dal/def_link.go
Normal file
@@ -0,0 +1,289 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
)
|
||||
|
||||
type (
|
||||
// Link produces a series of left and corresponding right rows based on the
|
||||
// provided sources and the LinkPredicate.
|
||||
//
|
||||
// The Link step produces an SQL left join-like output where left and right
|
||||
// rows are served separately and the left rows are not duplicated.
|
||||
Link struct {
|
||||
Ident string
|
||||
RelLeft string
|
||||
RelRight string
|
||||
// @todo allow multiple link predicates; for now (for easier indexing)
|
||||
// only allow one (this is the same as we had before)
|
||||
On LinkPredicate
|
||||
// @todo consider splitting filter into left and right filter
|
||||
filter internalFilter
|
||||
Filter filter.Filter
|
||||
|
||||
OutLeftAttributes []AttributeMapping
|
||||
OutRightAttributes []AttributeMapping
|
||||
LeftAttributes []AttributeMapping
|
||||
RightAttributes []AttributeMapping
|
||||
|
||||
relLeft PipelineStep
|
||||
relRight PipelineStep
|
||||
|
||||
plan linkPlan
|
||||
analysis map[string]OpAnalysis
|
||||
}
|
||||
|
||||
// LinkPredicate determines the attributes the two datasets should get joined on
|
||||
LinkPredicate struct {
|
||||
Left string
|
||||
Right string
|
||||
}
|
||||
|
||||
// linkPlan outlines how the optimizer determined the two datasets should be
|
||||
// joined on.
|
||||
linkPlan struct {
|
||||
// @todo add strategy when we have different strategies implemented
|
||||
// strategy string
|
||||
|
||||
// partialScan indicates we can partially pull data from the two sources
|
||||
// as the data is provided in the correct order.
|
||||
partialScan bool
|
||||
}
|
||||
|
||||
rowLink struct {
|
||||
a, b ValueGetter
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
LinkRefIdent = "$sys.ref"
|
||||
)
|
||||
|
||||
func (def *Link) Identifier() string {
|
||||
return def.Ident
|
||||
}
|
||||
|
||||
func (def *Link) Sources() []string {
|
||||
return []string{def.RelLeft, def.RelRight}
|
||||
}
|
||||
|
||||
func (def *Link) Attributes() [][]AttributeMapping {
|
||||
return [][]AttributeMapping{def.OutLeftAttributes, def.OutRightAttributes}
|
||||
}
|
||||
|
||||
func (def *Link) Analyze(ctx context.Context) (err error) {
|
||||
// @todo proper analysis; for now we'll leave this as defaults
|
||||
def.analysis = map[string]OpAnalysis{
|
||||
OpAnalysisIterate: {
|
||||
ScanCost: CostUnknown,
|
||||
SearchCost: CostUnknown,
|
||||
FilterCost: CostUnknown,
|
||||
SortCost: CostUnknown,
|
||||
OutputSize: SizeUnknown,
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (def *Link) Analysis() map[string]OpAnalysis {
|
||||
return def.analysis
|
||||
}
|
||||
|
||||
func (def *Link) Optimize(req internalFilter) (res internalFilter, err error) {
|
||||
err = fmt.Errorf("not implemented")
|
||||
return
|
||||
}
|
||||
|
||||
// iterator initializes an iterator based on the provided pipeline step definition
|
||||
func (def *Link) iterator(ctx context.Context, left, right Iterator) (out Iterator, err error) {
|
||||
x, err := def.init(ctx, left, right)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get pred. type
|
||||
// @todo should validate that both pred. types are the same/compatible
|
||||
var pt Type
|
||||
for _, a := range def.LeftAttributes {
|
||||
if a.Identifier() == def.On.Left {
|
||||
pt = a.Properties().Type
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Collect order by attributes which appear in the right source
|
||||
rightAttrs := indexAttrs(def.RightAttributes...)
|
||||
rightOrderAttrs := make([]string, 0, len(def.filter.OrderBy()))
|
||||
for _, o := range def.filter.OrderBy() {
|
||||
if rightAttrs[o.Column] {
|
||||
rightOrderAttrs = append(rightOrderAttrs, o.Column)
|
||||
}
|
||||
}
|
||||
|
||||
return x, x.init(ctx, pt, rightOrderAttrs)
|
||||
}
|
||||
|
||||
// dryrun performs step execution without interacting with the data
|
||||
// @todo consider rewording this
|
||||
func (def *Link) dryrun(ctx context.Context) (err error) {
|
||||
_, err = def.init(ctx, nil, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (def *Link) init(ctx context.Context, left, right Iterator) (exec *linkLeft, err error) {
|
||||
exec = &linkLeft{
|
||||
leftSource: left,
|
||||
rightSource: right,
|
||||
}
|
||||
|
||||
// Convert the provided filter into an internal filter
|
||||
if def.Filter != nil {
|
||||
def.filter, err = toInternalFilter(def.Filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Collect attributes from the underlaying step in case own are not provided
|
||||
if len(def.LeftAttributes) == 0 {
|
||||
def.LeftAttributes = collectAttributes(def.relLeft)
|
||||
}
|
||||
if len(def.RightAttributes) == 0 {
|
||||
def.RightAttributes = collectAttributes(def.relRight)
|
||||
}
|
||||
|
||||
// @todo this isn't quite ok -- the ident of left/right must become the src of the out.
|
||||
// An edge-case but it should be covered.
|
||||
if len(def.OutLeftAttributes) == 0 {
|
||||
def.OutLeftAttributes = def.LeftAttributes
|
||||
}
|
||||
if len(def.OutRightAttributes) == 0 {
|
||||
def.OutRightAttributes = def.RightAttributes
|
||||
}
|
||||
|
||||
// Index source attributes for group/aggregate definition validation
|
||||
leftSrcAttrs := indexAttrs(def.LeftAttributes...)
|
||||
rightSrcAttrs := indexAttrs(def.RightAttributes...)
|
||||
outAttrs := indexAttrs(def.OutLeftAttributes...)
|
||||
indexAttrsInto(outAttrs, def.OutRightAttributes...)
|
||||
|
||||
// Check if source attributes are present
|
||||
for _, a := range append(def.OutLeftAttributes, def.OutRightAttributes...) {
|
||||
if !leftSrcAttrs[a.Source()] && !rightSrcAttrs[a.Source()] {
|
||||
return nil, fmt.Errorf("unknown attribute %s", a.Source())
|
||||
}
|
||||
}
|
||||
|
||||
// Check link predicates
|
||||
if !leftSrcAttrs[def.On.Left] {
|
||||
return nil, fmt.Errorf("left link predicate %s does not exist", def.On.Left)
|
||||
}
|
||||
if !rightSrcAttrs[def.On.Right] {
|
||||
return nil, fmt.Errorf("right link predicate %s does not exist", def.On.Right)
|
||||
}
|
||||
|
||||
// General validation
|
||||
if len(def.OutLeftAttributes) == 0 {
|
||||
return nil, fmt.Errorf("no left output attributes specified")
|
||||
}
|
||||
if len(def.OutRightAttributes) == 0 {
|
||||
return nil, fmt.Errorf("no right output attributes specified")
|
||||
}
|
||||
if len(def.LeftAttributes) == 0 {
|
||||
return nil, fmt.Errorf("no left attributes specified")
|
||||
}
|
||||
if len(def.RightAttributes) == 0 {
|
||||
return nil, fmt.Errorf("no right attributes specified")
|
||||
}
|
||||
|
||||
if def.On.Left == "" {
|
||||
return nil, fmt.Errorf("no left attribute in the link predicate specified")
|
||||
}
|
||||
if def.On.Right == "" {
|
||||
return nil, fmt.Errorf("no right attribute in the link predicate specified")
|
||||
}
|
||||
|
||||
for _, s := range def.filter.OrderBy() {
|
||||
if _, ok := outAttrs[s.Column]; !ok {
|
||||
return nil, fmt.Errorf("order attribute %s does not exist", s.Column)
|
||||
}
|
||||
}
|
||||
|
||||
exec.filter = def.filter
|
||||
exec.def = *def
|
||||
return
|
||||
}
|
||||
|
||||
func (r *rowLink) SelectGVal(ctx context.Context, k string) (interface{}, error) {
|
||||
// @todo this way of determining multivalue is not entirely ok but we can get away with it
|
||||
// since we have special functions to work with those.
|
||||
// We need to somehow inform this bit as to what fields are multi value.
|
||||
row, counts := r.getGetter(k)
|
||||
if row == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
cc := counts[k]
|
||||
if cc == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if cc == 1 {
|
||||
return row.GetValue(k, 0)
|
||||
}
|
||||
|
||||
out := make([]any, cc)
|
||||
for i := uint(0); i < cc; i++ {
|
||||
out[i], _ = row.GetValue(k, i)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *rowLink) GetValue(name string, pos uint) (v any, err error) {
|
||||
row, a := r.getGetter(name)
|
||||
if row == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if cc, ok := a[name]; ok {
|
||||
if pos >= cc {
|
||||
return nil, nil
|
||||
}
|
||||
return row.GetValue(name, pos)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (r *rowLink) CountValues() (out map[string]uint) {
|
||||
out = make(map[string]uint)
|
||||
|
||||
for k, c := range r.a.CountValues() {
|
||||
out[k] = c
|
||||
}
|
||||
for k, c := range r.b.CountValues() {
|
||||
out[k] = c
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getGetter returns the ValueGetter based on if the value is in a or b
|
||||
// If neither, it returns nil
|
||||
func (r *rowLink) getGetter(k string) (row ValueGetter, cc map[string]uint) {
|
||||
cc = r.a.CountValues()
|
||||
if _, ok := cc[k]; ok {
|
||||
return r.a, cc
|
||||
}
|
||||
|
||||
cc = r.b.CountValues()
|
||||
if _, ok := cc[k]; ok {
|
||||
return r.b, cc
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
189
server/pkg/dal/diff.go
Normal file
189
server/pkg/dal/diff.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package dal
|
||||
|
||||
type (
|
||||
modelDiffType string
|
||||
// ModelDiff defines one identified missmatch between two models
|
||||
ModelDiff struct {
|
||||
Type modelDiffType
|
||||
// Original will be nil when a new attribute is being added
|
||||
Original *Attribute
|
||||
// Asserted will be nil wen an existing attribute is being removed
|
||||
Asserted *Attribute
|
||||
}
|
||||
|
||||
ModelDiffSet []*ModelDiff
|
||||
)
|
||||
|
||||
const (
|
||||
AttributeMissing modelDiffType = "attributeMissing"
|
||||
AttributeTypeMissmatch modelDiffType = "typeMissmatch"
|
||||
AttributeSensitivityMismatch modelDiffType = "sensitivityMismatch"
|
||||
AttributeCodecMismatch modelDiffType = "codecMismatch"
|
||||
)
|
||||
|
||||
// Diff calculates the diff between models a and b where a is used as base
|
||||
func (a *Model) Diff(b *Model) (out ModelDiffSet) {
|
||||
if a == nil {
|
||||
a = &Model{}
|
||||
}
|
||||
if b == nil {
|
||||
b = &Model{}
|
||||
}
|
||||
|
||||
bIndex := make(map[string]struct {
|
||||
found bool
|
||||
attr *Attribute
|
||||
})
|
||||
for _, _attr := range b.Attributes {
|
||||
attr := _attr
|
||||
bIndex[attr.Ident] = struct {
|
||||
found bool
|
||||
attr *Attribute
|
||||
}{
|
||||
attr: attr,
|
||||
}
|
||||
}
|
||||
|
||||
aIndex := make(map[string]struct {
|
||||
found bool
|
||||
attr *Attribute
|
||||
})
|
||||
for _, _attr := range a.Attributes {
|
||||
attr := _attr
|
||||
aIndex[attr.Ident] = struct {
|
||||
found bool
|
||||
attr *Attribute
|
||||
}{
|
||||
attr: attr,
|
||||
}
|
||||
}
|
||||
|
||||
// Deleted and update ones
|
||||
for _, _attrA := range a.Attributes {
|
||||
attrA := _attrA
|
||||
// store is an interface to something that could be a pointer.
|
||||
// we need to copy it to make sure we don't get a nil pointer
|
||||
// make sure not to modify this since it would modify the original
|
||||
attrA.Store = _attrA.Store
|
||||
|
||||
// Missmatches
|
||||
attrBAux, ok := bIndex[attrA.Ident]
|
||||
if !ok {
|
||||
out = append(out, &ModelDiff{
|
||||
Type: AttributeMissing,
|
||||
Original: attrA,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Typecheck
|
||||
if attrA.Type.Type() != attrBAux.attr.Type.Type() {
|
||||
out = append(out, &ModelDiff{
|
||||
Type: AttributeTypeMissmatch,
|
||||
Original: attrA,
|
||||
Asserted: attrBAux.attr,
|
||||
})
|
||||
}
|
||||
|
||||
// Other stuff
|
||||
// @todo improve; for now it'll do
|
||||
if attrA.SensitivityLevelID != attrBAux.attr.SensitivityLevelID {
|
||||
out = append(out, &ModelDiff{
|
||||
Type: AttributeSensitivityMismatch,
|
||||
Original: attrA,
|
||||
Asserted: attrBAux.attr,
|
||||
})
|
||||
}
|
||||
if attrA.Store.Type() != attrBAux.attr.Store.Type() {
|
||||
out = append(out, &ModelDiff{
|
||||
Type: AttributeCodecMismatch,
|
||||
Original: attrA,
|
||||
Asserted: attrBAux.attr,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// New
|
||||
for _, _attrB := range b.Attributes {
|
||||
attrB := _attrB
|
||||
|
||||
// Missmatches
|
||||
_, ok := aIndex[attrB.Ident]
|
||||
if !ok {
|
||||
out = append(out, &ModelDiff{
|
||||
Type: AttributeMissing,
|
||||
Original: nil,
|
||||
Asserted: attrB,
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (dd ModelDiffSet) Alterations() (out []*Alteration) {
|
||||
add := func(a *Alteration) {
|
||||
out = append(out, a)
|
||||
}
|
||||
|
||||
for _, d := range dd {
|
||||
switch d.Type {
|
||||
case AttributeMissing:
|
||||
if d.Asserted == nil {
|
||||
// @todo if this was the last attribute we can consider dropping this column
|
||||
if d.Original.Store.Type() == AttributeCodecRecordValueSetJSON {
|
||||
break
|
||||
}
|
||||
|
||||
add(&Alteration{
|
||||
AttributeDelete: &AttributeDelete{
|
||||
Attr: d.Original,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
if d.Asserted.Store.Type() == AttributeCodecRecordValueSetJSON {
|
||||
add(&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: &Attribute{
|
||||
Ident: d.Asserted.StoreIdent(),
|
||||
Type: &TypeJSON{Nullable: false},
|
||||
Store: &CodecPlain{},
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
add(&Alteration{
|
||||
AttributeAdd: &AttributeAdd{
|
||||
Attr: d.Asserted,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
case AttributeTypeMissmatch:
|
||||
// @todo we might have to do some validation earlier on
|
||||
if d.Original.Store.Type() == AttributeCodecRecordValueSetJSON {
|
||||
break
|
||||
}
|
||||
|
||||
add(&Alteration{
|
||||
AttributeReType: &AttributeReType{
|
||||
Attr: d.Asserted,
|
||||
To: d.Asserted.Type,
|
||||
},
|
||||
})
|
||||
|
||||
case AttributeCodecMismatch:
|
||||
add(&Alteration{
|
||||
AttributeReEncode: &AttributeReEncode{
|
||||
Attr: d.Asserted,
|
||||
To: d.Asserted.Store,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
116
server/pkg/dal/diff_test.go
Normal file
116
server/pkg/dal/diff_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDiff_same(t *testing.T) {
|
||||
a := &Model{
|
||||
Attributes: AttributeSet{{
|
||||
Ident: "F1",
|
||||
Type: TypeText{},
|
||||
Store: &CodecPlain{},
|
||||
}},
|
||||
}
|
||||
|
||||
dd := a.Diff(a)
|
||||
require.Len(t, dd, 0)
|
||||
}
|
||||
|
||||
func TestDiff_wrongAttrType(t *testing.T) {
|
||||
a := &Model{
|
||||
Attributes: AttributeSet{{
|
||||
Ident: "F1",
|
||||
Type: TypeText{},
|
||||
Store: &CodecPlain{},
|
||||
}},
|
||||
}
|
||||
b := &Model{
|
||||
Attributes: AttributeSet{{
|
||||
Ident: "F1",
|
||||
Type: TypeBlob{},
|
||||
Store: &CodecPlain{},
|
||||
}},
|
||||
}
|
||||
|
||||
dd := a.Diff(b)
|
||||
require.Len(t, dd, 1)
|
||||
require.Equal(t, AttributeTypeMissmatch, dd[0].Type)
|
||||
}
|
||||
|
||||
func TestDiff_removedAttr(t *testing.T) {
|
||||
a := &Model{
|
||||
Attributes: AttributeSet{{
|
||||
Ident: "F1",
|
||||
Type: TypeText{},
|
||||
Store: &CodecPlain{},
|
||||
}, {
|
||||
Ident: "F2",
|
||||
Type: TypeText{},
|
||||
Store: &CodecPlain{},
|
||||
}},
|
||||
}
|
||||
b := &Model{
|
||||
Attributes: AttributeSet{{
|
||||
Ident: "F1",
|
||||
Type: TypeText{},
|
||||
Store: &CodecPlain{},
|
||||
}},
|
||||
}
|
||||
|
||||
dd := a.Diff(b)
|
||||
require.Len(t, dd, 1)
|
||||
require.Equal(t, AttributeMissing, dd[0].Type)
|
||||
require.NotNil(t, dd[0].Original)
|
||||
require.Nil(t, dd[0].Asserted)
|
||||
}
|
||||
|
||||
func TestDiff_addedAttr(t *testing.T) {
|
||||
a := &Model{
|
||||
Attributes: AttributeSet{{
|
||||
Ident: "F1",
|
||||
Type: TypeText{},
|
||||
Store: &CodecPlain{},
|
||||
}},
|
||||
}
|
||||
b := &Model{
|
||||
Attributes: AttributeSet{{
|
||||
Ident: "F1",
|
||||
Type: TypeText{},
|
||||
Store: &CodecPlain{},
|
||||
}, {
|
||||
Ident: "F2",
|
||||
Type: TypeText{},
|
||||
Store: &CodecPlain{},
|
||||
}},
|
||||
}
|
||||
|
||||
dd := a.Diff(b)
|
||||
require.Len(t, dd, 1)
|
||||
require.Equal(t, AttributeMissing, dd[0].Type)
|
||||
require.Nil(t, dd[0].Original)
|
||||
require.NotNil(t, dd[0].Asserted)
|
||||
}
|
||||
|
||||
func TestDiff_changedCodec(t *testing.T) {
|
||||
a := &Model{
|
||||
Attributes: AttributeSet{{
|
||||
Ident: "F1",
|
||||
Type: TypeText{},
|
||||
Store: &CodecPlain{},
|
||||
}},
|
||||
}
|
||||
b := &Model{
|
||||
Attributes: AttributeSet{{
|
||||
Ident: "F1",
|
||||
Type: TypeText{},
|
||||
Store: &CodecRecordValueSetJSON{},
|
||||
}},
|
||||
}
|
||||
|
||||
dd := a.Diff(b)
|
||||
require.Len(t, dd, 1)
|
||||
require.Equal(t, AttributeCodecMismatch, dd[0].Type)
|
||||
}
|
||||
257
server/pkg/dal/driver.go
Normal file
257
server/pkg/dal/driver.go
Normal file
@@ -0,0 +1,257 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
PKValues map[string]any
|
||||
|
||||
ConnectionParams struct {
|
||||
Type string `json:"type"`
|
||||
Params map[string]any `json:"params"`
|
||||
}
|
||||
|
||||
Connection interface {
|
||||
// Meta
|
||||
|
||||
// Models returns all the models the underlying connection already supports
|
||||
//
|
||||
// This is useful when adding support for new models since we can find out what
|
||||
// can work out of the box.
|
||||
Models(context.Context) (ModelSet, error)
|
||||
|
||||
// Operations returns all of the operations the given store supports
|
||||
Operations() OperationSet
|
||||
|
||||
// Can returns true if this store can handle the given operations
|
||||
Can(operations ...Operation) bool
|
||||
|
||||
// DML stuff
|
||||
|
||||
// Create stores the given data into the underlying database
|
||||
Create(ctx context.Context, m *Model, rr ...ValueGetter) error
|
||||
|
||||
// Update updates the given value in the underlying connection
|
||||
Update(ctx context.Context, m *Model, r ValueGetter) error
|
||||
|
||||
// Lookup returns one bit of data
|
||||
Lookup(context.Context, *Model, ValueGetter, ValueSetter) error
|
||||
|
||||
// Search returns an iterator which can be used to access all if the bits
|
||||
Search(context.Context, *Model, filter.Filter) (Iterator, error)
|
||||
|
||||
// Count returns the total number of rows matching the filter
|
||||
Count(context.Context, *Model, filter.Filter) (uint, error)
|
||||
|
||||
// Analyze returns the operation analysis the connection can perform for the model
|
||||
Analyze(ctx context.Context, m *Model) (map[string]OpAnalysis, error)
|
||||
|
||||
// Aggregate returns the iterator with aggregated data from the base model
|
||||
Aggregate(ctx context.Context, m *Model, f filter.Filter, groupBy []AggregateAttr, aggrExpr []AggregateAttr, having *ql.ASTNode) (i Iterator, _ error)
|
||||
|
||||
// Delete deletes the given value
|
||||
Delete(ctx context.Context, m *Model, pkv ValueGetter) error
|
||||
|
||||
// Truncate deletes all the data for the given model
|
||||
Truncate(ctx context.Context, m *Model) error
|
||||
|
||||
// DDL stuff
|
||||
|
||||
// // returns all attribute types that driver supports
|
||||
// AttributeTypes() []data.AttributeType
|
||||
|
||||
// CreateModel adds support for the given models to the underlying database
|
||||
//
|
||||
// The operation returns an error if any of the models already exists.
|
||||
CreateModel(context.Context, ...*Model) error
|
||||
|
||||
// DeleteModel removes support for the given model from the underlying database
|
||||
DeleteModel(context.Context, ...*Model) error
|
||||
|
||||
// UpdateModel requests for metadata changes to the existing model
|
||||
//
|
||||
// Only metadata (such as idents) are affected; attributes can not be changed here
|
||||
UpdateModel(ctx context.Context, old *Model, new *Model) error
|
||||
|
||||
// AssertSchemaAlterations returns a new set of Alterations based on what the underlying
|
||||
// schema already provides -- it discards alterations for column additions that already exist, etc.
|
||||
AssertSchemaAlterations(ctx context.Context, sch *Model, aa ...*Alteration) ([]*Alteration, error)
|
||||
|
||||
// ApplyAlteration applies the given alterations to the underlying schema
|
||||
ApplyAlteration(ctx context.Context, sch *Model, aa ...*Alteration) []error
|
||||
}
|
||||
|
||||
ConnectionCloser interface {
|
||||
// Close closes the store connection allowing the driver to perform potential
|
||||
// cleanup operations
|
||||
Close(ctx context.Context) error
|
||||
}
|
||||
|
||||
// Store provides an interface which CRS uses to interact with the underlying database
|
||||
|
||||
ValueGetter interface {
|
||||
CountValues() map[string]uint
|
||||
GetValue(string, uint) (any, error)
|
||||
}
|
||||
|
||||
ValueSetter interface {
|
||||
SetValue(string, uint, any) error
|
||||
}
|
||||
|
||||
ConnectorFn func(ctx context.Context, dsn string) (Connection, error)
|
||||
|
||||
DriverConnectionParam struct {
|
||||
Key string `json:"key"`
|
||||
ValueType string `json:"valueType"`
|
||||
MultiValue bool `json:"multiValue"`
|
||||
}
|
||||
|
||||
DriverConnectionConfig struct {
|
||||
Type string `json:"type"`
|
||||
Params []DriverConnectionParam `json:"params"`
|
||||
}
|
||||
|
||||
Driver struct {
|
||||
Type string `json:"type"`
|
||||
Connection DriverConnectionConfig `json:"connection"`
|
||||
Operations OperationSet `json:"operations"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
registeredConnectors = make(map[string]ConnectorFn)
|
||||
registeredDrivers = make(map[string]Driver)
|
||||
)
|
||||
|
||||
func (pkv PKValues) CountValues() map[string]uint {
|
||||
c := make(map[string]uint)
|
||||
for k := range pkv {
|
||||
c[k] = 1
|
||||
}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (pkv PKValues) GetValue(key string, _ uint) (any, error) {
|
||||
if val, has := pkv[key]; has {
|
||||
return val, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("missing")
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterConnector registers a new connector for the given DSN schema
|
||||
//
|
||||
// In case of a duplicate schema the latter overwrites the prior
|
||||
func RegisterConnector(fn ConnectorFn, tt ...string) {
|
||||
for _, t := range tt {
|
||||
registeredConnectors[t] = fn
|
||||
}
|
||||
}
|
||||
|
||||
func RegisterDriver(d Driver) {
|
||||
registeredDrivers[d.Type] = d
|
||||
}
|
||||
|
||||
// connect opens a new StoreConnection for the given CRS
|
||||
func connect(ctx context.Context, log *zap.Logger, isDevelopment bool, cp ConnectionParams) (Connection, error) {
|
||||
if cp.Type != "corteza::dal:connection:dsn" {
|
||||
return nil, fmt.Errorf("cannot open connection: only DSN connections supported (got: %q)", cp.Type)
|
||||
}
|
||||
if cp.Params == nil {
|
||||
return nil, fmt.Errorf("cannot open connection: connection parameters not defined")
|
||||
}
|
||||
if _, ok := cp.Params["dsn"]; !ok {
|
||||
return nil, fmt.Errorf("cannot open connection: DSN not provided")
|
||||
}
|
||||
|
||||
dsn := cp.Params["dsn"].(string)
|
||||
|
||||
if isDevelopment {
|
||||
if strings.Contains(dsn, "{version}") {
|
||||
log.Warn("You're using DB_DSN with {version}, It is still in EXPERIMENTAL phase")
|
||||
log.Warn("Should be used only for development mode")
|
||||
log.Warn("You may experience instability")
|
||||
}
|
||||
expr := regexp.MustCompile(`[.\-]+`)
|
||||
version := expr.ReplaceAllString(os.Getenv("BUILD_VERSION"), "_")
|
||||
dsn = strings.Replace(dsn, "{version}", version, 1)
|
||||
}
|
||||
|
||||
var storeType = strings.SplitN(dsn, "://", 2)[0]
|
||||
if storeType == "" {
|
||||
// Backward compatibility
|
||||
storeType = "mysql"
|
||||
}
|
||||
|
||||
if conn, ok := registeredConnectors[storeType]; ok {
|
||||
return conn(ctx, dsn)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unknown store type used: %q (check your database configuration)", storeType)
|
||||
}
|
||||
}
|
||||
|
||||
func NewDSNDriverConnectionConfig() DriverConnectionConfig {
|
||||
return DriverConnectionConfig{
|
||||
Type: "corteza::dal:connection:dsn",
|
||||
Params: []DriverConnectionParam{{
|
||||
Key: "dsn",
|
||||
ValueType: "string",
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
//func NewHTTPDriverConnectionConfig() DriverConnectionConfig {
|
||||
// panic("not implemented NewHTTPDriverConnectionConfig")
|
||||
// return DriverConnectionConfig{
|
||||
// Type: "corteza::dal:connection:http",
|
||||
// Params: []DriverConnectionParam{{}},
|
||||
// }
|
||||
//}
|
||||
//func NewFederatedNodeDriverConnectionConfig() DriverConnectionConfig {
|
||||
// panic("not implemented NewFederatedNodeDriverConnectionConfig")
|
||||
// return DriverConnectionConfig{
|
||||
// Type: "corteza::dal:connection:federated-node",
|
||||
// Params: []DriverConnectionParam{{}},
|
||||
// }
|
||||
//}
|
||||
|
||||
//func NewDSNConnection(dsn string) ConnectionParams {
|
||||
// return ConnectionParams{
|
||||
// Type: "corteza::dal:connection:dsn",
|
||||
// Params: map[string]any{
|
||||
// "dsn": dsn,
|
||||
// },
|
||||
// }
|
||||
//}
|
||||
|
||||
//func NewHTTPConnection(url string, headers, query map[string][]string) ConnectionParams {
|
||||
// return ConnectionParams{
|
||||
// Type: "corteza::dal:connection:http",
|
||||
// Params: map[string]any{
|
||||
// "url": url,
|
||||
// "headers": headers,
|
||||
// "query": query,
|
||||
// },
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//func NewFederatedNodeConnection(url string, pairToken, authToken string) ConnectionParams {
|
||||
// return ConnectionParams{
|
||||
// Type: "corteza::dal:connection:federation-node",
|
||||
// Params: map[string]any{
|
||||
// "baseURL": url,
|
||||
// "pairToken": pairToken,
|
||||
// "authToken": authToken,
|
||||
// },
|
||||
// }
|
||||
//}
|
||||
166
server/pkg/dal/errors.go
Normal file
166
server/pkg/dal/errors.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
)
|
||||
|
||||
// Generic errors
|
||||
|
||||
func errModelNotFound(modelID uint64) error {
|
||||
return errors.NotFound("model %d does not exist", modelID)
|
||||
}
|
||||
|
||||
func errConnectionNotFound(connectionID uint64) error {
|
||||
return errors.NotFound("connection %d does not exist", connectionID)
|
||||
}
|
||||
|
||||
// Sensitivity level errors
|
||||
// - remove
|
||||
func errSensitivityLevelRemoveNotFound(sensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot remove sensitivity level %d: sensitivity level does not exist", sensitivityLevelID)
|
||||
}
|
||||
|
||||
// Connection errors
|
||||
// - create
|
||||
func errConnectionCreateMissingSensitivityLevel(connectionID, sensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot create connection %d: sensitivity level does not exist %d", connectionID, sensitivityLevelID)
|
||||
}
|
||||
func errConnectionCreateConnectionFailed(connectionID uint64, err error) error {
|
||||
return fmt.Errorf("cannot create connection %d: connection failed: %v", connectionID, err)
|
||||
}
|
||||
func errConnectionDeleteNotFound(connectionID uint64) error {
|
||||
return fmt.Errorf("cannot delete connection %d: connection does not exist", connectionID)
|
||||
}
|
||||
func errConnectionDeleteCloserFailed(connectionID uint64, err error) error {
|
||||
return fmt.Errorf("cannot delete connection %d: connection's driver failed to close: %v", connectionID, err)
|
||||
}
|
||||
|
||||
// - update
|
||||
func errConnectionUpdateNotFound(connectionID uint64) error {
|
||||
return fmt.Errorf("cannot update connection %d: connection does not exist", connectionID)
|
||||
}
|
||||
func errConnectionUpdateMissingSensitivityLevel(connectionID, sensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot update connection %d: sensitivity level %d does not exist", connectionID, sensitivityLevelID)
|
||||
}
|
||||
|
||||
// Model errors
|
||||
// - create
|
||||
func errModelCreateProblematicConnection(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot create model %d on connection %d: connection has issues", modelID, connectionID)
|
||||
}
|
||||
func errModelCreateMissingConnection(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot create model %d on connection %d: connection does not exist", modelID, connectionID)
|
||||
}
|
||||
func errModelCreateMissingSensitivityLevel(connectionID, modelID, sensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot create model %d on connection %d: sensitivity level %d does not exist", modelID, connectionID, sensitivityLevelID)
|
||||
}
|
||||
func errModelCreateGreaterSensitivityLevel(connectionID, modelID, modelSensitivityLevelID, connSensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot create model %d on connection %d: sensitivity level %d exceeds connection supported sensitivity level %d", modelID, connectionID, modelSensitivityLevelID, connSensitivityLevelID)
|
||||
}
|
||||
func errModelCreateMissingAttributeSensitivityLevel(connectionID, modelID, sensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot create model %d on connection %d: attribute sensitivity level %d does not exist", modelID, connectionID, sensitivityLevelID)
|
||||
}
|
||||
func errModelCreateGreaterAttributeSensitivityLevel(connectionID, modelID, attrSensitivityLevelID, modelSensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot create model %d on connection %d: attribute sensitivity level %d exceeds model supported sensitivity level %d", modelID, connectionID, attrSensitivityLevelID, modelSensitivityLevelID)
|
||||
}
|
||||
func errModelCreateConnectionModelUnsupported(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot create model %d on connection %d: model already exists for connection but is not compatible with provided definition", modelID, connectionID)
|
||||
}
|
||||
func errModelCreateInvalidIdent(connectionID, modelID uint64, ident string) error {
|
||||
return fmt.Errorf("cannot create model %d on connection %d: malformed model ident %s", modelID, connectionID, ident)
|
||||
}
|
||||
|
||||
// - update
|
||||
func errModelUpdateProblematicConnection(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot update model %d on connection %d: connection has issues", modelID, connectionID)
|
||||
}
|
||||
func errModelUpdateMissingConnection(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot update model %d on connection %d: connection does not exist", modelID, connectionID)
|
||||
}
|
||||
func errModelUpdateConnectionModelUnsupported(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot update model %d on connection %d: model already exists for connection but is not compatible with provided definition", modelID, connectionID)
|
||||
}
|
||||
func errModelUpdateMissingOldModel(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot update model %d on connection %d: model does not exist", modelID, connectionID)
|
||||
}
|
||||
func errModelUpdateDuplicate(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot update model %d on connection %d: model already exists", modelID, connectionID)
|
||||
}
|
||||
func errModelUpdateConnectionMissmatch(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot update model %d on connection %d: cannot change model connection", modelID, connectionID)
|
||||
}
|
||||
func errModelUpdateMissingSensitivityLevel(connectionID, modelID, sensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot update model %d on connection %d: sensitivity level %d does not exist", modelID, connectionID, sensitivityLevelID)
|
||||
}
|
||||
func errModelUpdateGreaterSensitivityLevel(connectionID, modelID, modelSensitivityLevelID, connSensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot update model %d on connection %d: sensitivity level %d exceeds connection supported sensitivity level %d", modelID, connectionID, modelSensitivityLevelID, connSensitivityLevelID)
|
||||
}
|
||||
|
||||
// - alterations
|
||||
func errModelRequiresAlteration(connectionID, modelID, batchID uint64) error {
|
||||
return fmt.Errorf("model %d on connection %d requires schema alterations: alteration batchID %d", modelID, connectionID, batchID)
|
||||
}
|
||||
|
||||
// Attribute errors
|
||||
// - Update
|
||||
func errAttributeUpdateProblematicConnection(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot update attribute for model %d on connection %d: connection has issues", modelID, connectionID)
|
||||
}
|
||||
func errAttributeUpdateMissingModel(connectionID, modelID uint64) error {
|
||||
return fmt.Errorf("cannot update attribute for model %d on connection %d: model does not exist", modelID, connectionID)
|
||||
}
|
||||
func errAttributeUpdateMissingSensitivityLevel(connectionID, modelID, sensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot update attribute for model %d on connection %d: sensitivity level %d does not exist", modelID, connectionID, sensitivityLevelID)
|
||||
}
|
||||
func errAttributeUpdateGreaterSensitivityLevel(connectionID, modelID, attrSensitivityLevelID, modelSensitivityLevelID uint64) error {
|
||||
return fmt.Errorf("cannot update attribute for model %d on connection %d: sensitivity level %d exceeds model supported sensitivity level %d", modelID, connectionID, attrSensitivityLevelID, modelSensitivityLevelID)
|
||||
}
|
||||
|
||||
// Record errors
|
||||
|
||||
func errRecordOpProblematicConnection(connectionID uint64) error {
|
||||
return fmt.Errorf("cannot perform record operation: connection %d has issues", connectionID)
|
||||
}
|
||||
func errRecordOpProblematicModel(modelID uint64) error {
|
||||
return fmt.Errorf("cannot perform record operation: model %d has issues", modelID)
|
||||
}
|
||||
|
||||
// func errModelHigherSensitivity(model, connection string) error {
|
||||
// return errors.New(
|
||||
// errors.KindSensitiveData,
|
||||
|
||||
// "model sensitivity surpasses connection sensitivity",
|
||||
|
||||
// errors.Meta("type", "invalid sensitivity"),
|
||||
|
||||
// // Translation namespace & key
|
||||
// errors.Meta(locale.ErrorMetaNamespace{}, "internal"),
|
||||
// errors.Meta(locale.ErrorMetaKey{}, "dal.sensitivity.model-exceeds-connection"),
|
||||
// errors.Meta("model", model),
|
||||
// errors.Meta("connection", connection),
|
||||
|
||||
// errors.StackSkip(1),
|
||||
// errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"),
|
||||
// )
|
||||
// }
|
||||
|
||||
// func errAttributeHigherSensitivity(model, attribute string) error {
|
||||
// return errors.New(
|
||||
// errors.KindSensitiveData,
|
||||
|
||||
// "attribute sensitivity surpasses model sensitivity",
|
||||
|
||||
// errors.Meta("type", "invalid sensitivity"),
|
||||
|
||||
// // Translation namespace & key
|
||||
// errors.Meta(locale.ErrorMetaNamespace{}, "internal"),
|
||||
// errors.Meta(locale.ErrorMetaKey{}, "dal.sensitivity.attribute-exceeds-model"),
|
||||
// errors.Meta("model", model),
|
||||
// errors.Meta("attribute", attribute),
|
||||
|
||||
// errors.StackSkip(1),
|
||||
// errors.StackTrimAtFn("http.HandlerFunc.ServeHTTP"),
|
||||
// )
|
||||
// }
|
||||
589
server/pkg/dal/exec_aggregate.go
Normal file
589
server/pkg/dal/exec_aggregate.go
Normal file
@@ -0,0 +1,589 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
"github.com/tidwall/btree"
|
||||
)
|
||||
|
||||
type (
|
||||
aggregate struct {
|
||||
def Aggregate
|
||||
filter internalFilter
|
||||
|
||||
source Iterator
|
||||
err error
|
||||
scanRow *Row
|
||||
planned bool
|
||||
|
||||
groupDefs []AggregateAttr
|
||||
aggregateDefs []AggregateAttr
|
||||
|
||||
rowTester tester
|
||||
keyWalker keyWalker
|
||||
|
||||
// Keep track of the registered groupIndex so we can easier implement the iterator.
|
||||
// Simple index tracking won't be enough, because btree is self-balancing -- things change
|
||||
//
|
||||
// @todo worth considering using the inmembuffer and fork it around...
|
||||
groupIndex *btree.Generic[*aggregateGroup]
|
||||
groups []*aggregateGroup
|
||||
i int
|
||||
|
||||
ctr int
|
||||
}
|
||||
|
||||
keyWalker func(context.Context, ValueGetter, func(context.Context, groupKey, ValueGetter) error) error
|
||||
)
|
||||
|
||||
// init initializes the execution step's state
|
||||
func (xs *aggregate) init(ctx context.Context) (err error) {
|
||||
// Initialize state variables
|
||||
xs.scanRow = xs.initScanRow()
|
||||
xs.groupIndex = btree.NewGeneric[*aggregateGroup](xs.compareGroupKeys)
|
||||
xs.groups = make([]*aggregateGroup, 0, 128)
|
||||
xs.rowTester, err = prepareGenericRowTester(xs.filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize the key maker
|
||||
kk := make([]*ql.ASTNode, 0, len(xs.groupDefs))
|
||||
for _, a := range xs.groupDefs {
|
||||
kk = append(kk, a.Expression)
|
||||
}
|
||||
|
||||
xs.keyWalker, err = aggregateGroupKeyWalker(kk...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply the decided aggregation plan
|
||||
return xs.applyPlan(ctx)
|
||||
}
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // // //
|
||||
// Iterator methods
|
||||
|
||||
func (xs *aggregate) Next(ctx context.Context) (more bool) {
|
||||
// Assure aggregation plan was taken into account
|
||||
xs.err = xs.applyPlan(ctx)
|
||||
if xs.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// Go next...
|
||||
more, xs.err = xs.next(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
// next prepares the next scannable row
|
||||
//
|
||||
// The method is adjusted based on the defined aggregation plan.
|
||||
// The method performs appropriate filtering/sorting (if needed).
|
||||
func (xs *aggregate) next(ctx context.Context) (more bool, err error) {
|
||||
var g *aggregateGroup
|
||||
for {
|
||||
if xs.limitExceeded() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Make sure it's cleared out and ready for fresh data
|
||||
xs.scanRow.Reset()
|
||||
|
||||
// Make sure we have data
|
||||
err = xs.prepareNext(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Next group
|
||||
g, err = xs.nextGroup(ctx)
|
||||
if err != nil || g == nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Make sure the key is in there
|
||||
// @todo potential optimization where we don't scan entire composited keys
|
||||
err = xs.scanKey(g, xs.scanRow)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Scan the data
|
||||
err = g.agg.Scan(xs.scanRow)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we want to keep it
|
||||
k, err := xs.keep(ctx, xs.scanRow)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !k {
|
||||
continue
|
||||
}
|
||||
|
||||
xs.ctr++
|
||||
break
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (xs *aggregate) limitExceeded() bool {
|
||||
return xs.filter.limit > 0 && xs.ctr >= int(xs.filter.limit)
|
||||
}
|
||||
|
||||
func (xs *aggregate) More(limit uint, v ValueGetter) (err error) {
|
||||
// Redo the cursor
|
||||
xs.filter.cursor, err = filter.PagingCursorFrom(xs.filter.OrderBy(), v, xs.collectPrimaryAttributes()...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Redo the row tester
|
||||
xs.rowTester, err = prepareGenericRowTester(xs.filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Redo the state
|
||||
// @todo adjust based on aggregation plan; reuse buffered, etc.
|
||||
xs.scanRow.Reset()
|
||||
xs.groupIndex = btree.NewGeneric[*aggregateGroup](xs.compareGroupKeys)
|
||||
xs.groups = make([]*aggregateGroup, 0, 128)
|
||||
xs.planned = false
|
||||
xs.i = 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *aggregate) Err() error { return s.err }
|
||||
|
||||
func (s *aggregate) Scan(dst ValueSetter) (err error) {
|
||||
if s.i < 0 {
|
||||
return fmt.Errorf("@todo err not initialized; next first")
|
||||
}
|
||||
|
||||
var v any
|
||||
for name, cc := range s.scanRow.CountValues() {
|
||||
for i := uint(0); i < cc; i++ {
|
||||
// omitting err here since it won't happen
|
||||
v, _ = s.scanRow.GetValue(name, i)
|
||||
err = dst.SetValue(name, i, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *aggregate) Close() error {
|
||||
if s.source != nil {
|
||||
return s.source.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *aggregate) BackCursor(v ValueGetter) (*filter.PagingCursor, error) {
|
||||
c, err := filter.PagingCursorFrom(s.filter.OrderBy(), v, s.collectPrimaryAttributes()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.ROrder = true
|
||||
c.LThen = s.filter.OrderBy().Reversed()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *aggregate) ForwardCursor(v ValueGetter) (*filter.PagingCursor, error) {
|
||||
c, err := filter.PagingCursorFrom(s.filter.OrderBy(), v, s.collectPrimaryAttributes()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // // //
|
||||
// Utilities
|
||||
|
||||
// applyPlan runs additional initialization code based on the defined aggregation plan
|
||||
func (xs *aggregate) applyPlan(ctx context.Context) (err error) {
|
||||
if xs.planned || xs.err != nil {
|
||||
return xs.err
|
||||
}
|
||||
|
||||
xs.planned = true
|
||||
switch {
|
||||
case !xs.def.plan.partialScan:
|
||||
return xs.pullEntireSource(ctx)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *aggregate) prepareNext(ctx context.Context) (err error) {
|
||||
// Pull next chunk from source if not entirely buffered
|
||||
if xs.def.plan.partialScan {
|
||||
err = xs.pullNextChunk(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// pullEntireSource pulls the entire source into aggregators
|
||||
//
|
||||
// The entire source should be pulled when underlaying datasources can't provide
|
||||
// more appropriately ordered data due to costs.
|
||||
func (xs *aggregate) pullEntireSource(ctx context.Context) (err error) {
|
||||
if xs.source == nil {
|
||||
return fmt.Errorf("unable to pull data: no source defined")
|
||||
}
|
||||
|
||||
// @todo consider pre-populating the hashmaps
|
||||
r := &Row{
|
||||
counters: make(map[string]uint),
|
||||
values: make(valueSet),
|
||||
}
|
||||
|
||||
// Drain the source
|
||||
for xs.source.Next(ctx) {
|
||||
err = xs.source.Scan(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get the key for this row
|
||||
// @todo we probably can reuse the key or at least cache keys and avoid re-computation.
|
||||
// My fairly hacky attempt boosted performance by ~20%
|
||||
err = xs.keyWalker(ctx, r, xs.addToGroup)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.Reset()
|
||||
}
|
||||
xs.err = xs.source.Err()
|
||||
if xs.err != nil {
|
||||
return xs.err
|
||||
}
|
||||
|
||||
xs.sortGroups()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// pullNextChunk pulls the next chunk into the aggregators
|
||||
//
|
||||
// Source should be pulled in chunks when the underlaying datasource can provide
|
||||
// more appropriately ordered data.
|
||||
func (xs *aggregate) pullNextChunk(ctx context.Context) (err error) {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (s *aggregate) addToGroup(ctx context.Context, key groupKey, r ValueGetter) (err error) {
|
||||
if s.groupIndex == nil {
|
||||
s.groupIndex = btree.NewGeneric[*aggregateGroup](s.compareGroupKeys)
|
||||
}
|
||||
|
||||
// Try to get the existing one; if it doesn't exist, push a new one
|
||||
// @todo this causes a bit of a memory pressure; investigate
|
||||
g, ok := s.groupIndex.Get(&aggregateGroup{key: key})
|
||||
if !ok {
|
||||
g, err = s.wrapGroup(ctx, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.groupIndex.Set(g)
|
||||
|
||||
s.groups = append(s.groups, g)
|
||||
}
|
||||
|
||||
// if it exists/was added, add it to the aggregator
|
||||
err = g.agg.Aggregate(ctx, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Group key comparator
|
||||
// @todo can this be moved to the struct's Less method?
|
||||
func (t *aggregate) compareGroupKeys(a, b *aggregateGroup) (out bool) {
|
||||
for i := range a.key {
|
||||
va := a.key[i]
|
||||
vb := b.key[i]
|
||||
out = out || compareValues(va, vb) < 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *aggregate) wrapGroup(ctx context.Context, key groupKey) (g *aggregateGroup, err error) {
|
||||
agg := Aggregator()
|
||||
|
||||
for _, a := range s.aggregateDefs {
|
||||
err = agg.AddAggregate(a.Identifier, a.Expression)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
g = &aggregateGroup{
|
||||
key: key,
|
||||
agg: agg,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *aggregate) nextGroup(ctx context.Context) (_ *aggregateGroup, err error) {
|
||||
if xs.i >= len(xs.groups) {
|
||||
return
|
||||
}
|
||||
|
||||
xs.i++
|
||||
return xs.groups[xs.i-1], nil
|
||||
}
|
||||
|
||||
func (xs *aggregate) scanKey(g *aggregateGroup, dst *Row) (err error) {
|
||||
for i, attr := range xs.groupDefs {
|
||||
// @todo multi value support?
|
||||
// omitting err; internal row won't raise them
|
||||
dst.SetValue(attr.Identifier, 0, g.key[i])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *aggregate) keep(ctx context.Context, r *Row) (bool, error) {
|
||||
if s.rowTester == nil {
|
||||
return true, nil
|
||||
}
|
||||
return s.rowTester.Test(ctx, r)
|
||||
}
|
||||
|
||||
// Each group key is a PK candidate; all together form a composite key
|
||||
func (s *aggregate) collectPrimaryAttributes() (out []string) {
|
||||
out = make([]string, 0, 2)
|
||||
for _, m := range s.def.Group {
|
||||
out = append(out, m.Identifier)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *aggregate) sortGroups() {
|
||||
sort.SliceStable(s.groups, func(i, j int) bool {
|
||||
ga := s.groups[i]
|
||||
gb := s.groups[j]
|
||||
|
||||
var (
|
||||
va any
|
||||
vb any
|
||||
)
|
||||
|
||||
for _, o := range s.filter.OrderBy() {
|
||||
x := inKeys(s.def.Group, o.Column)
|
||||
if x > -1 {
|
||||
va = ga.key[x]
|
||||
} else {
|
||||
x := inKeys(s.def.OutAttributes, o.Column)
|
||||
va = ga.agg.aggregates[x]
|
||||
}
|
||||
|
||||
x = inKeys(s.def.Group, o.Column)
|
||||
if x > -1 {
|
||||
vb = gb.key[x]
|
||||
} else {
|
||||
x := inKeys(s.def.OutAttributes, o.Column)
|
||||
vb = gb.agg.aggregates[x]
|
||||
}
|
||||
|
||||
cmp := compareValues(va, vb)
|
||||
if cmp != 0 {
|
||||
if o.Descending {
|
||||
return cmp > 0
|
||||
}
|
||||
return cmp < 0
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func inKeys(kk []AggregateAttr, ident string) int {
|
||||
for i, k := range kk {
|
||||
if k.Identifier == ident {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (xs *aggregate) initScanRow() (out *Row) {
|
||||
// base
|
||||
out = &Row{
|
||||
counters: make(map[string]uint),
|
||||
values: make(valueSet),
|
||||
}
|
||||
|
||||
// pre-populate with known attrs
|
||||
for _, attr := range append(xs.def.Group, xs.def.OutAttributes...) {
|
||||
out.values[attr.Identifier] = make([]any, 0, 2)
|
||||
out.counters[attr.Identifier] = 0
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// aggregateGroupKeyWalker prepares a function which runs the provided function over all group keys
|
||||
//
|
||||
// This is required to handle multi-value attributes when used in group keys.
|
||||
//
|
||||
// Algorithm TL;DR
|
||||
//
|
||||
// We're going backwards in the slice of idents we need to handle (going the other way around)
|
||||
// should also work but I chose to do it like so.
|
||||
//
|
||||
// For every ident, keep track of the number of elements and the index of the last unhandled ident.
|
||||
//
|
||||
// For every iteration, collect all of the values pointed to by the unused index.
|
||||
// After the key is processed, increment the counter for the current ident.
|
||||
// If the counter exceeds the number of elements, reset the counter for the current ident and move
|
||||
// the ident pointer backwards (repeat if that ident's counter also exceeds the limit).
|
||||
//
|
||||
// When we find an indent which still has some items to process (counter doesn't exceed limit),
|
||||
// reset the ident pointer to the end of the slice and repeat the whole thing.
|
||||
func aggregateGroupKeyWalker(kk ...*ql.ASTNode) (out keyWalker, err error) {
|
||||
// @todo option to copy constants and idents
|
||||
runners, err := makeExprRunners(kk...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// We'll sort the idents to keep the output consistent; the order doesn't matter
|
||||
// but it will simplify testing.
|
||||
idents, hasConstants := keysFromExpr(kk...)
|
||||
sort.Strings(idents)
|
||||
|
||||
out = func(ctx context.Context, vg ValueGetter, run func(context.Context, groupKey, ValueGetter) error) error {
|
||||
// Edgecase for when all of the expressions return constant values
|
||||
if len(idents) == 0 {
|
||||
// This should be impossible but better safe then sorry
|
||||
if !hasConstants {
|
||||
return nil
|
||||
}
|
||||
|
||||
k, err := makeGroupKey(ctx, runners, vg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return run(ctx, k, vg)
|
||||
}
|
||||
|
||||
// For every value combination of idents used in agg. key expressions
|
||||
// construct a key and run the runner.
|
||||
|
||||
limits := vg.CountValues()
|
||||
ptr := len(idents) - 1
|
||||
counts := make(map[string]uint, len(limits))
|
||||
for k := range limits {
|
||||
counts[k] = 0
|
||||
}
|
||||
|
||||
handle := func() (err error) {
|
||||
aux := make(map[string]any, len(limits))
|
||||
|
||||
for k, i := range counts {
|
||||
aux[k], err = vg.GetValue(k, i)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
kk, err := makeGroupKey(ctx, runners, aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return run(ctx, kk, vg)
|
||||
}
|
||||
|
||||
outer:
|
||||
for ptr >= 0 {
|
||||
handle()
|
||||
|
||||
counts[idents[ptr]]++
|
||||
for {
|
||||
// There are still values to process so we can skip the rest
|
||||
if counts[idents[ptr]] < limits[idents[ptr]] {
|
||||
continue outer
|
||||
}
|
||||
|
||||
// Reset the counter for the current ptr since we'll move back.
|
||||
// The outer loop will have this reset all of the counters after the current ptr.
|
||||
counts[idents[ptr]] = 0
|
||||
|
||||
ptr--
|
||||
if ptr < 0 {
|
||||
break outer
|
||||
}
|
||||
|
||||
counts[idents[ptr]]++
|
||||
|
||||
if counts[idents[ptr]] >= limits[idents[ptr]] {
|
||||
continue
|
||||
}
|
||||
|
||||
// We need to reset to the end so the next ident gets all of the values
|
||||
// that appear after it.
|
||||
ptr = len(idents) - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeExprRunners(kk ...*ql.ASTNode) (out []*runnerGval, err error) {
|
||||
out = make([]*runnerGval, len(kk))
|
||||
|
||||
for i, k := range kk {
|
||||
out[i], err = newRunnerGvalParsed(k)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func makeGroupKey(ctx context.Context, runners []*runnerGval, vals any) (gk groupKey, err error) {
|
||||
gk = make(groupKey, len(runners))
|
||||
for i, r := range runners {
|
||||
v, err := r.Eval(ctx, vals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gk[i] = v
|
||||
}
|
||||
return gk, nil
|
||||
}
|
||||
83
server/pkg/dal/exec_aggregate_bench_test.go
Normal file
83
server/pkg/dal/exec_aggregate_bench_test.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func benchmarkExecAggregate(b *testing.B, n int) {
|
||||
b.StopTimer()
|
||||
ctx := context.Background()
|
||||
group := []simpleAttribute{{ident: "g", expr: "g"}}
|
||||
outAttributes := []simpleAttribute{{ident: "sum", expr: "sum(s1_v)"}}
|
||||
sourceAttributes := []simpleAttribute{
|
||||
{ident: "k"},
|
||||
{ident: "g"},
|
||||
{ident: "s1_v"},
|
||||
}
|
||||
|
||||
ggs := []string{"a", "b", "c", "d", "e", "f", "g"}
|
||||
|
||||
// Inmem buffer for example
|
||||
buff := InMemoryBuffer()
|
||||
for i := 0; i < n; i++ {
|
||||
require.NoError(b, buff.Add(ctx, simpleRow{"k": i + 1, "g": ggs[rand.Intn(len(ggs))], "s1_v": rand.Intn(200)}))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
def := Aggregate{
|
||||
Ident: "agg",
|
||||
Group: saToAggAttr(group...),
|
||||
OutAttributes: saToAggAttr(outAttributes...),
|
||||
SourceAttributes: saToMapping(sourceAttributes...),
|
||||
}
|
||||
|
||||
b.StartTimer()
|
||||
_, err := def.iterator(ctx, buff)
|
||||
require.NoError(b, err)
|
||||
b.StopTimer()
|
||||
|
||||
buff.Seek(ctx, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/cortezaproject/corteza/server/pkg/dal
|
||||
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
|
||||
// BenchmarkExecAggregate_20000-12 63 17902131 ns/op 1423343 B/op 80432 allocs/op
|
||||
// BenchmarkExecAggregate_40000-12 33 36179481 ns/op 2783435 B/op 160432 allocs/op
|
||||
// BenchmarkExecAggregate_60000-12 21 53681403 ns/op 4143471 B/op 240433 allocs/op
|
||||
// BenchmarkExecAggregate_80000-12 15 72234005 ns/op 5503370 B/op 320432 allocs/op
|
||||
// BenchmarkExecAggregate_100000-12 12 92145195 ns/op 6863674 B/op 400434 allocs/op
|
||||
// BenchmarkExecAggregate_120000-12 10 109842507 ns/op 8223711 B/op 480433 allocs/op
|
||||
// BenchmarkExecAggregate_140000-12 8 129250726 ns/op 9583616 B/op 560433 allocs/op
|
||||
// BenchmarkExecAggregate_160000-12 7 144896012 ns/op 10943776 B/op 640434 allocs/op
|
||||
// BenchmarkExecAggregate_180000-12 7 162705594 ns/op 12303718 B/op 720434 allocs/op
|
||||
// BenchmarkExecAggregate_200000-12 6 181970479 ns/op 13663848 B/op 800434 allocs/op
|
||||
// BenchmarkExecAggregate_220000-12 5 204168375 ns/op 15024054 B/op 880435 allocs/op
|
||||
// BenchmarkExecAggregate_240000-12 5 218573247 ns/op 16383470 B/op 960432 allocs/op
|
||||
// BenchmarkExecAggregate_260000-12 5 238612939 ns/op 17744016 B/op 1040435 allocs/op
|
||||
// BenchmarkExecAggregate_280000-12 4 261442973 ns/op 19103558 B/op 1120433 allocs/op
|
||||
// BenchmarkExecAggregate_300000-12 4 342276668 ns/op 20464244 B/op 1200436 allocs/op
|
||||
|
||||
func BenchmarkExecAggregate_20000(b *testing.B) { benchmarkExecAggregate(b, 20000) }
|
||||
func BenchmarkExecAggregate_40000(b *testing.B) { benchmarkExecAggregate(b, 40000) }
|
||||
func BenchmarkExecAggregate_60000(b *testing.B) { benchmarkExecAggregate(b, 60000) }
|
||||
func BenchmarkExecAggregate_80000(b *testing.B) { benchmarkExecAggregate(b, 80000) }
|
||||
func BenchmarkExecAggregate_100000(b *testing.B) { benchmarkExecAggregate(b, 100000) }
|
||||
func BenchmarkExecAggregate_120000(b *testing.B) { benchmarkExecAggregate(b, 120000) }
|
||||
func BenchmarkExecAggregate_140000(b *testing.B) { benchmarkExecAggregate(b, 140000) }
|
||||
func BenchmarkExecAggregate_160000(b *testing.B) { benchmarkExecAggregate(b, 160000) }
|
||||
func BenchmarkExecAggregate_180000(b *testing.B) { benchmarkExecAggregate(b, 180000) }
|
||||
func BenchmarkExecAggregate_200000(b *testing.B) { benchmarkExecAggregate(b, 200000) }
|
||||
func BenchmarkExecAggregate_220000(b *testing.B) { benchmarkExecAggregate(b, 220000) }
|
||||
func BenchmarkExecAggregate_240000(b *testing.B) { benchmarkExecAggregate(b, 240000) }
|
||||
func BenchmarkExecAggregate_260000(b *testing.B) { benchmarkExecAggregate(b, 260000) }
|
||||
func BenchmarkExecAggregate_280000(b *testing.B) { benchmarkExecAggregate(b, 280000) }
|
||||
func BenchmarkExecAggregate_300000(b *testing.B) { benchmarkExecAggregate(b, 300000) }
|
||||
1478
server/pkg/dal/exec_aggregate_test.go
Normal file
1478
server/pkg/dal/exec_aggregate_test.go
Normal file
File diff suppressed because it is too large
Load Diff
417
server/pkg/dal/exec_join_left.go
Normal file
417
server/pkg/dal/exec_join_left.go
Normal file
@@ -0,0 +1,417 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/tidwall/btree"
|
||||
)
|
||||
|
||||
type (
|
||||
// Considerations for optimizations
|
||||
// - Rework how values are stored internally.
|
||||
// Potentially rework the pipeline input/output interfaces, how ValueGetter/Setter work.
|
||||
// - Skip initial left row scan/sort when sorting not requested.
|
||||
// - Rework row struct to use slices instead of hashmaps.
|
||||
// With how values are now the performance gain is not that impactful and the
|
||||
// rework complexity is a bit too high for now.
|
||||
// - Use `.More` to filter rows by key.
|
||||
// - When data is provided in a satisfactory order, use that to pull data in chunks.
|
||||
joinLeft struct {
|
||||
def Join
|
||||
filter internalFilter
|
||||
|
||||
leftSource Iterator
|
||||
rightSource Iterator
|
||||
err error
|
||||
scanRow ValueGetter
|
||||
planned bool
|
||||
filtered bool
|
||||
|
||||
rowTester tester
|
||||
|
||||
// Index to keep track of related rows
|
||||
relIndex *relIndex
|
||||
|
||||
// Output placeholder for sorted rows
|
||||
// @todo consider a generic slice for cases when sorting is not needed.
|
||||
// This will probably save up on memory/time since we don't even need
|
||||
// to pull everything.
|
||||
outSorted *btree.Generic[ValueGetter]
|
||||
i int
|
||||
}
|
||||
)
|
||||
|
||||
func (xs *joinLeft) init(ctx context.Context, joinPredType Type) (err error) {
|
||||
xs.relIndex, err = newRelIndex(joinPredType)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
xs.rowTester, err = prepareGenericRowTester(xs.filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// @note careful here if you throw routines into the mix; see the NoLocks flag.
|
||||
// Enabling locks does have a performance impact so you might be better off by
|
||||
// constructing multiple of these but then you'll also need to complicate
|
||||
// the .Next methods a bit.
|
||||
xs.outSorted = btree.NewGenericOptions[ValueGetter](makeRowComparator(xs.filter.OrderBy()...), btree.Options{NoLocks: true})
|
||||
|
||||
return xs.applyPlan(ctx)
|
||||
}
|
||||
|
||||
func (xs *joinLeft) Next(ctx context.Context) (more bool) {
|
||||
xs.err = xs.applyPlan(ctx)
|
||||
if xs.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
more, xs.err = xs.next(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *joinLeft) More(limit uint, v ValueGetter) (err error) {
|
||||
xs.filter.cursor, err = filter.PagingCursorFrom(xs.filter.OrderBy(), v, xs.collectPrimaryAttributes(xs.def.OutAttributes)...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Redo row tester
|
||||
xs.rowTester, err = prepareGenericRowTester(xs.filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Redo the state
|
||||
// @todo adjust based on aggregation plan; reuse buffered, etc.
|
||||
xs.relIndex.Clear()
|
||||
xs.outSorted = btree.NewGenericOptions[ValueGetter](makeRowComparator(xs.filter.OrderBy()...), btree.Options{NoLocks: true})
|
||||
xs.scanRow = nil
|
||||
xs.planned = false
|
||||
xs.i = 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *joinLeft) Err() error { return xs.err }
|
||||
|
||||
func (xs *joinLeft) Scan(s ValueSetter) (err error) {
|
||||
for k, cc := range xs.scanRow.CountValues() {
|
||||
for i := uint(0); i < cc; i++ {
|
||||
// @note internal row won't raise errors so we can safely omit them
|
||||
v, _ := xs.scanRow.GetValue(k, i)
|
||||
err = s.SetValue(k, i, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *joinLeft) Close() (err error) {
|
||||
if xs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cc := []io.Closer{
|
||||
xs.leftSource,
|
||||
xs.rightSource,
|
||||
}
|
||||
|
||||
for _, c := range cc {
|
||||
if c != nil {
|
||||
err = c.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *joinLeft) BackCursor(v ValueGetter) (pc *filter.PagingCursor, err error) {
|
||||
pc, err = filter.PagingCursorFrom(xs.filter.OrderBy(), v, xs.collectPrimaryAttributes(xs.def.OutAttributes)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc.ROrder = true
|
||||
pc.LThen = xs.filter.OrderBy().Reversed()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *joinLeft) ForwardCursor(v ValueGetter) (pc *filter.PagingCursor, err error) {
|
||||
pc, err = filter.PagingCursorFrom(xs.filter.OrderBy(), v, xs.collectPrimaryAttributes(xs.def.OutAttributes)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // // //
|
||||
// Utility methods
|
||||
|
||||
// next prepares the next scan row based on the defined join plan
|
||||
func (xs *joinLeft) next(ctx context.Context) (more bool, err error) {
|
||||
if xs.limitExceeded() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
more, err = xs.pullNext(ctx)
|
||||
if !more || err != nil {
|
||||
return more, err
|
||||
}
|
||||
|
||||
xs.scanRow, _ = xs.outSorted.GetAt(xs.i)
|
||||
xs.i++
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (xs *joinLeft) limitExceeded() bool {
|
||||
return xs.filter.limit > 0 && xs.i >= int(xs.filter.limit)
|
||||
}
|
||||
|
||||
// pullNext pulls additional data so we can produce more
|
||||
//
|
||||
// This step may be omitted based on the join plan.
|
||||
func (xs *joinLeft) pullNext(ctx context.Context) (more bool, err error) {
|
||||
// Pull next chunk from source if not entirely buffered
|
||||
if xs.def.plan.partialScan {
|
||||
// @todo this case is currently not implemented so we're erroring it out
|
||||
return false, fmt.Errorf("partialScan join plan strategy not implemented")
|
||||
}
|
||||
|
||||
// Check if buffer has more
|
||||
if xs.i >= xs.outSorted.Len() {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// applyPlan runs plan specific logic to prepare the state
|
||||
func (xs *joinLeft) applyPlan(ctx context.Context) (err error) {
|
||||
if xs.planned || xs.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
xs.planned = true
|
||||
switch {
|
||||
case !xs.def.plan.partialScan:
|
||||
return xs.pullEntireSource(ctx)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// pullEntireSource pulls both sources into memory and indexes them for later use
|
||||
func (xs *joinLeft) pullEntireSource(ctx context.Context) (err error) {
|
||||
// This bit does the filtering so just mark it of as such here
|
||||
xs.filtered = true
|
||||
|
||||
// First the right source
|
||||
err = xs.pullEntireRightSource(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Next the left source
|
||||
err = xs.pullEntireLeftSource(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// pullEntireRightSource pulls and indexes all of the right bits
|
||||
func (xs *joinLeft) pullEntireRightSource(ctx context.Context) (err error) {
|
||||
for xs.rightSource.Next(ctx) {
|
||||
r := &Row{
|
||||
counters: make(map[string]uint),
|
||||
values: make(valueSet),
|
||||
}
|
||||
|
||||
err = xs.rightSource.Scan(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = xs.indexRightRow(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return xs.rightSource.Err()
|
||||
}
|
||||
|
||||
// pullEntireLeftSource pulls left bits and attempts to do as much of the joining
|
||||
// work on this stage
|
||||
func (xs *joinLeft) pullEntireLeftSource(ctx context.Context) (err error) {
|
||||
for xs.leftSource.Next(ctx) {
|
||||
l := &Row{
|
||||
counters: make(map[string]uint),
|
||||
values: make(valueSet),
|
||||
}
|
||||
|
||||
err = xs.leftSource.Scan(l)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = xs.joinRight(ctx, l)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return xs.leftSource.Err()
|
||||
}
|
||||
|
||||
// joinRight finds related right rows for the given left row and matches them up
|
||||
//
|
||||
// @note for sorting, we use a b-tree as it's self sorting.
|
||||
// Benchmarking shows that using a slice is negligibly faster if faster at all.
|
||||
func (xs *joinLeft) joinRight(ctx context.Context, left *Row) (err error) {
|
||||
bb, ok, err := xs.getRelatedBuffers(left)
|
||||
if err != nil || !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range bb {
|
||||
for _, right := range b.rows {
|
||||
// Merge the two
|
||||
xs.mergeRows(xs.def.OutAttributes, right, left, right)
|
||||
|
||||
// Assert if we want to keep
|
||||
k, err := xs.keep(ctx, right)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !k {
|
||||
continue
|
||||
}
|
||||
|
||||
xs.outSorted.Set(right)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// getRelatedBuffers returns all of the right rows corresponding to the given left row
|
||||
func (xs *joinLeft) getRelatedBuffers(l *Row) (out []*relIndexBuffer, ok bool, err error) {
|
||||
var aux *relIndexBuffer
|
||||
for c := uint(0); c < l.CountValues()[xs.def.On.Left]; c++ {
|
||||
// @note internal Row struct never errors
|
||||
v, _ := l.GetValue(xs.def.On.Left, c)
|
||||
aux, ok = xs.relIndex.Get(v)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, aux)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// indexRightRow pushes the provided row onto the rel index
|
||||
// @todo consider moving most of this logic to the relIndex struct.
|
||||
func (xs *joinLeft) indexRightRow(r *Row) (err error) {
|
||||
for i := uint(0); i < r.CountValues()[xs.def.On.Right]; i++ {
|
||||
v, err := r.GetValue(xs.def.On.Right, i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xs.relIndex.Add(v, r)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// keep checks if the row should be kept or discarded
|
||||
func (xs *joinLeft) keep(ctx context.Context, r *Row) (bool, error) {
|
||||
if xs.rowTester == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return xs.rowTester.Test(ctx, r)
|
||||
}
|
||||
|
||||
// collectPrimaryAttributes returns all of the attributes of the composited key
|
||||
//
|
||||
// For joins, all primary attributes from both of the sides should be in here
|
||||
// since that is what always uniquely identifies a joined row.
|
||||
//
|
||||
// @todo consider applying PK candidates and filter out some of these. I don't
|
||||
// think it'll provide much of a performance boost but worth a shot later on.
|
||||
func (xs *joinLeft) collectPrimaryAttributes(mm []AttributeMapping) (out []string) {
|
||||
out = make([]string, 0, 2)
|
||||
for _, m := range mm {
|
||||
if m.Properties().IsPrimary {
|
||||
out = append(out, m.Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// mergeRows merges the left and right rows based on the provided attrs
|
||||
func (xs *joinLeft) mergeRows(attrs []AttributeMapping, out, left, right *Row) {
|
||||
var (
|
||||
identSide int
|
||||
srcIdent string
|
||||
)
|
||||
|
||||
for _, attr := range attrs {
|
||||
srcIdent = attr.Identifier()
|
||||
identSide = xs.identSide(srcIdent)
|
||||
|
||||
if identSide == -1 {
|
||||
xs.mergeValuesFrom(attr.Source(), attr.Identifier(), out, left)
|
||||
} else if identSide == 1 {
|
||||
xs.mergeValuesFrom(attr.Source(), attr.Identifier(), out, right)
|
||||
} else {
|
||||
xs.mergeValuesFrom(attr.Source(), attr.Identifier(), out, left, right)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (xs *joinLeft) mergeValuesFrom(inIdent, outIdent string, out *Row, sources ...*Row) {
|
||||
var (
|
||||
aux any
|
||||
)
|
||||
for _, src := range sources {
|
||||
for c := uint(0); c < src.CountValues()[inIdent]; c++ {
|
||||
aux, _ = src.GetValue(inIdent, c)
|
||||
out.SetValue(outIdent, c, aux)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// identSide returns -1 if the ident belongs to the left source, 1 if it belongs
|
||||
// to the right side, and 0 if it's either.
|
||||
//
|
||||
// @todo consider adding an additional flag to identify what side it is on.
|
||||
// For now, this should be fine.
|
||||
func (xs *joinLeft) identSide(ident string) int {
|
||||
pp := strings.Split(ident, attributeNestingSeparator)
|
||||
if len(pp) > 1 {
|
||||
if pp[0] == xs.def.RelLeft {
|
||||
return -1
|
||||
} else if pp[0] == xs.def.RelRight {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
116
server/pkg/dal/exec_join_left_bench_test.go
Normal file
116
server/pkg/dal/exec_join_left_bench_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func benchmarkExecJoin_local(b *testing.B, n int) {
|
||||
ctx := context.Background()
|
||||
attrs := []simpleAttribute{
|
||||
{ident: "l_k"},
|
||||
{ident: "l_v1"},
|
||||
{ident: "l_v2"},
|
||||
{ident: "f_k"},
|
||||
{ident: "f_ref"},
|
||||
{ident: "f_v1"},
|
||||
{ident: "f_v2"},
|
||||
}
|
||||
|
||||
la := []simpleAttribute{
|
||||
{ident: "l_k", t: TypeID{}},
|
||||
{ident: "l_v1"},
|
||||
{ident: "l_v2"},
|
||||
}
|
||||
fa := []simpleAttribute{
|
||||
{ident: "f_k", t: TypeID{}},
|
||||
{ident: "f_ref", t: TypeID{}},
|
||||
{ident: "f_v1"},
|
||||
{ident: "f_v2"},
|
||||
}
|
||||
|
||||
// Inmem buffer for example
|
||||
l := InMemoryBuffer()
|
||||
f := InMemoryBuffer()
|
||||
for i := 0; i < n; i++ {
|
||||
require.NoError(b, l.Add(ctx, simpleRow{"l_k": i + 1, "l_v1": "a", "l_v2": rand.Intn(200)}))
|
||||
require.NoError(b, f.Add(ctx, simpleRow{"f_k": i + 1, "f_ref": i + 1, "f_v1": "a", "f_v2": rand.Intn(200)}))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
def := Join{
|
||||
Ident: "join",
|
||||
OutAttributes: saToMapping(attrs...),
|
||||
LeftAttributes: saToMapping(la...),
|
||||
RightAttributes: saToMapping(fa...),
|
||||
filter: internalFilter{orderBy: filter.SortExprSet{{Column: "f_k"}}},
|
||||
On: JoinPredicate{Left: "l_k", Right: "f_ref"},
|
||||
}
|
||||
|
||||
def.iterator(ctx, l, f)
|
||||
|
||||
l.Seek(ctx, 0)
|
||||
f.Seek(ctx, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/cortezaproject/corteza/server/pkg/dal
|
||||
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
|
||||
// BenchmarkExecJoin_local_200-12 1620 718632 ns/op
|
||||
// BenchmarkExecJoin_local_400-12 801 1474321 ns/op
|
||||
// BenchmarkExecJoin_local_600-12 504 2344433 ns/op
|
||||
// BenchmarkExecJoin_local_800-12 388 3098124 ns/op
|
||||
// BenchmarkExecJoin_local_1000-12 304 3876453 ns/op
|
||||
// BenchmarkExecJoin_local_1200-12 258 4653349 ns/op
|
||||
// BenchmarkExecJoin_local_1400-12 218 5406631 ns/op
|
||||
// BenchmarkExecJoin_local_1600-12 192 6215687 ns/op
|
||||
// BenchmarkExecJoin_local_1800-12 168 7185540 ns/op
|
||||
// BenchmarkExecJoin_local_2000-12 148 8021597 ns/op
|
||||
// BenchmarkExecJoin_local_2200-12 122 9350466 ns/op
|
||||
// BenchmarkExecJoin_local_2400-12 100 10035371 ns/op
|
||||
// BenchmarkExecJoin_local_2600-12 117 10247223 ns/op
|
||||
// BenchmarkExecJoin_local_2800-12 100 11282374 ns/op
|
||||
// BenchmarkExecJoin_local_3000-12 99 12591232 ns/op
|
||||
// BenchmarkExecJoin_local_3200-12 86 13047472 ns/op
|
||||
// BenchmarkExecJoin_local_3400-12 80 13859590 ns/op
|
||||
// BenchmarkExecJoin_local_3600-12 76 14986832 ns/op
|
||||
// BenchmarkExecJoin_local_3800-12 70 15826467 ns/op
|
||||
// BenchmarkExecJoin_local_4000-12 61 16537483 ns/op
|
||||
// BenchmarkExecJoin_local_4200-12 68 17226297 ns/op
|
||||
// BenchmarkExecJoin_local_4400-12 67 17996956 ns/op
|
||||
// BenchmarkExecJoin_local_4600-12 62 18622211 ns/op
|
||||
// BenchmarkExecJoin_local_4800-12 61 19948914 ns/op
|
||||
// BenchmarkExecJoin_local_5000-12 57 20614964 ns/op
|
||||
func BenchmarkExecJoin_local_200(b *testing.B) { benchmarkExecJoin_local(b, 200) }
|
||||
func BenchmarkExecJoin_local_400(b *testing.B) { benchmarkExecJoin_local(b, 400) }
|
||||
func BenchmarkExecJoin_local_600(b *testing.B) { benchmarkExecJoin_local(b, 600) }
|
||||
func BenchmarkExecJoin_local_800(b *testing.B) { benchmarkExecJoin_local(b, 800) }
|
||||
func BenchmarkExecJoin_local_1000(b *testing.B) { benchmarkExecJoin_local(b, 1000) }
|
||||
func BenchmarkExecJoin_local_1200(b *testing.B) { benchmarkExecJoin_local(b, 1200) }
|
||||
func BenchmarkExecJoin_local_1400(b *testing.B) { benchmarkExecJoin_local(b, 1400) }
|
||||
func BenchmarkExecJoin_local_1600(b *testing.B) { benchmarkExecJoin_local(b, 1600) }
|
||||
func BenchmarkExecJoin_local_1800(b *testing.B) { benchmarkExecJoin_local(b, 1800) }
|
||||
func BenchmarkExecJoin_local_2000(b *testing.B) { benchmarkExecJoin_local(b, 2000) }
|
||||
func BenchmarkExecJoin_local_2200(b *testing.B) { benchmarkExecJoin_local(b, 2200) }
|
||||
func BenchmarkExecJoin_local_2400(b *testing.B) { benchmarkExecJoin_local(b, 2400) }
|
||||
func BenchmarkExecJoin_local_2600(b *testing.B) { benchmarkExecJoin_local(b, 2600) }
|
||||
func BenchmarkExecJoin_local_2800(b *testing.B) { benchmarkExecJoin_local(b, 2800) }
|
||||
func BenchmarkExecJoin_local_3000(b *testing.B) { benchmarkExecJoin_local(b, 3000) }
|
||||
func BenchmarkExecJoin_local_3200(b *testing.B) { benchmarkExecJoin_local(b, 3200) }
|
||||
func BenchmarkExecJoin_local_3400(b *testing.B) { benchmarkExecJoin_local(b, 3400) }
|
||||
func BenchmarkExecJoin_local_3600(b *testing.B) { benchmarkExecJoin_local(b, 3600) }
|
||||
func BenchmarkExecJoin_local_3800(b *testing.B) { benchmarkExecJoin_local(b, 3800) }
|
||||
func BenchmarkExecJoin_local_4000(b *testing.B) { benchmarkExecJoin_local(b, 4000) }
|
||||
func BenchmarkExecJoin_local_4200(b *testing.B) { benchmarkExecJoin_local(b, 4200) }
|
||||
func BenchmarkExecJoin_local_4400(b *testing.B) { benchmarkExecJoin_local(b, 4400) }
|
||||
func BenchmarkExecJoin_local_4600(b *testing.B) { benchmarkExecJoin_local(b, 4600) }
|
||||
func BenchmarkExecJoin_local_4800(b *testing.B) { benchmarkExecJoin_local(b, 4800) }
|
||||
func BenchmarkExecJoin_local_5000(b *testing.B) { benchmarkExecJoin_local(b, 5000) }
|
||||
1610
server/pkg/dal/exec_join_left_test.go
Normal file
1610
server/pkg/dal/exec_join_left_test.go
Normal file
File diff suppressed because it is too large
Load Diff
505
server/pkg/dal/exec_link_left.go
Normal file
505
server/pkg/dal/exec_link_left.go
Normal file
@@ -0,0 +1,505 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
linkLeft struct {
|
||||
def Link
|
||||
filter internalFilter
|
||||
|
||||
leftSource Iterator
|
||||
rightSource Iterator
|
||||
err error
|
||||
scanRow *Row
|
||||
planned bool
|
||||
filtered bool
|
||||
|
||||
rightSortAttrs []string
|
||||
|
||||
rowTester tester
|
||||
|
||||
// Buffer to keep track of pulled left rows
|
||||
leftRows []*Row
|
||||
relIndex *relIndex
|
||||
keepLeft bool
|
||||
leftIndex int
|
||||
rightIndex int
|
||||
|
||||
// Some helper fields for temporary data
|
||||
leftRow *Row
|
||||
rightRow *Row
|
||||
relScanBuffer *relIndexBuffer
|
||||
}
|
||||
)
|
||||
|
||||
func (xs *linkLeft) init(ctx context.Context, linkPredType Type, rightSort []string) (err error) {
|
||||
// @note the index is keeping track of the right source attributes so we can
|
||||
// simplify the sorting logic.
|
||||
xs.relIndex, err = newRelIndex(linkPredType, rightSort...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
xs.rightSortAttrs = rightSort
|
||||
|
||||
xs.rowTester, err = prepareGenericRowTester(xs.filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return xs.applyPlan(ctx)
|
||||
}
|
||||
|
||||
func (xs *linkLeft) Next(ctx context.Context) (more bool) {
|
||||
xs.err = xs.applyPlan(ctx)
|
||||
if xs.err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
more, xs.err = xs.next(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *linkLeft) More(limit uint, v ValueGetter) (err error) {
|
||||
xs.filter.cursor, err = xs.ForwardCursor(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Redo row tester
|
||||
xs.rowTester, err = prepareGenericRowTester(xs.filter)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Redo the state
|
||||
// @todo adjust based on aggregation plan; reuse buffered, etc.
|
||||
xs.relIndex.Clear()
|
||||
xs.leftRows = make([]*Row, 0, 128)
|
||||
xs.scanRow = nil
|
||||
xs.planned = false
|
||||
xs.keepLeft = false
|
||||
xs.leftIndex = 0
|
||||
xs.rightIndex = 0
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *linkLeft) Err() error { return xs.err }
|
||||
|
||||
func (xs *linkLeft) Scan(s ValueSetter) (err error) {
|
||||
for k, cc := range xs.scanRow.CountValues() {
|
||||
for i := uint(0); i < cc; i++ {
|
||||
// @note internal row won't raise errors so we can safely omit them
|
||||
v, _ := xs.scanRow.GetValue(k, i)
|
||||
err = s.SetValue(k, i, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *linkLeft) Close() (err error) {
|
||||
if xs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cc := []io.Closer{
|
||||
xs.leftSource,
|
||||
xs.rightSource,
|
||||
}
|
||||
|
||||
for _, c := range cc {
|
||||
if c != nil {
|
||||
err = c.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *linkLeft) BackCursor(v ValueGetter) (pc *filter.PagingCursor, err error) {
|
||||
g := &rowLink{
|
||||
a: xs.leftRow,
|
||||
b: v,
|
||||
}
|
||||
pc, err = filter.PagingCursorFrom(xs.filter.OrderBy(), g, xs.collectPrimaryAttributes()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pc.ROrder = true
|
||||
pc.LThen = xs.filter.OrderBy().Reversed()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *linkLeft) ForwardCursor(v ValueGetter) (pc *filter.PagingCursor, err error) {
|
||||
g := &rowLink{
|
||||
a: xs.leftRow,
|
||||
b: v,
|
||||
}
|
||||
pc, err = filter.PagingCursorFrom(xs.filter.OrderBy(), g, xs.collectPrimaryAttributes()...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// // // // // // // // // // // // // // // // // // // // // // // // //
|
||||
// Utility methods
|
||||
|
||||
// next prepares the next scan row based on the defined link plan
|
||||
func (xs *linkLeft) next(ctx context.Context) (more bool, err error) {
|
||||
more, err = xs.pullNext(ctx)
|
||||
if !more || err != nil {
|
||||
return more, err
|
||||
}
|
||||
|
||||
return xs.nextBuffered()
|
||||
}
|
||||
|
||||
// pullNext pulls additional data so we can produce more
|
||||
//
|
||||
// This step may be omitted based on the join plan.
|
||||
func (xs *linkLeft) pullNext(ctx context.Context) (more bool, err error) {
|
||||
// Pull next chunk from source if not entirely buffered
|
||||
if xs.def.plan.partialScan {
|
||||
return false, fmt.Errorf("partialScan join plan strategy not implemented")
|
||||
}
|
||||
|
||||
// Check if buffer has more
|
||||
// We need to check for gt because the right bits may still be relevant
|
||||
if xs.leftIndex > len(xs.leftRows) {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// nextBuffered prepares the next scan row from the buffers
|
||||
func (xs *linkLeft) nextBuffered() (more bool, err error) {
|
||||
// keepLeft indicates if we should keep the left row and move onto the next
|
||||
// in the right buffers.
|
||||
//
|
||||
// If we're keeping it, take the next row from the other side, else take the
|
||||
// next left one and reset right counters.
|
||||
|
||||
var (
|
||||
ok bool
|
||||
)
|
||||
|
||||
for {
|
||||
if !xs.keepLeft {
|
||||
// Go for the next left row
|
||||
if xs.leftIndex >= len(xs.leftRows) {
|
||||
return
|
||||
}
|
||||
xs.leftRow = xs.leftRows[xs.leftIndex]
|
||||
xs.leftIndex++
|
||||
|
||||
// Go for the next right buffer
|
||||
xs.relScanBuffer, ok, err = xs.getRelatedBuffer(xs.leftRow)
|
||||
if !ok || err != nil {
|
||||
return
|
||||
}
|
||||
// if len(rel) > 1 {
|
||||
// // @todo implement this; not entirely sure how it should be so I'll block it for now
|
||||
// // @todo move this check futher up
|
||||
// return false, fmt.Errorf("multi-value link predicates not supported")
|
||||
// }
|
||||
// xs.relScanBuffer = rel[0]
|
||||
xs.rightIndex = 0
|
||||
|
||||
xs.scanRow = xs.leftRow
|
||||
xs.keepLeft = true
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Related buffer done
|
||||
if xs.rightIndex >= len(xs.relScanBuffer.rows) {
|
||||
xs.keepLeft = false
|
||||
continue
|
||||
}
|
||||
|
||||
xs.rightRow = xs.relScanBuffer.rows[xs.rightIndex]
|
||||
xs.rightIndex++
|
||||
xs.scanRow = xs.rightRow
|
||||
|
||||
// Sys defined value to indicate the source
|
||||
// @todo consider using some wrap struct instead
|
||||
xs.scanRow.SetValue(LinkRefIdent, 0, xs.def.RelRight)
|
||||
break
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getRelatedBuffer returns all of the right rows corresponding to the given left row
|
||||
func (xs *linkLeft) getRelatedBuffer(l *Row) (out *relIndexBuffer, ok bool, err error) {
|
||||
|
||||
// @todo mv link predicate attrs
|
||||
v, _ := l.GetValue(xs.def.On.Left, 0)
|
||||
out, ok = xs.relIndex.Get(v)
|
||||
return
|
||||
}
|
||||
|
||||
// applyPlan runs plan specific logic to prepare the state
|
||||
func (xs *linkLeft) applyPlan(ctx context.Context) (err error) {
|
||||
if xs.planned || xs.err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
xs.planned = true
|
||||
switch {
|
||||
case !xs.def.plan.partialScan:
|
||||
return xs.pullEntireSource(ctx)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// pullEntireSource pulls both sources into memory and indexes them for later use
|
||||
func (xs *linkLeft) pullEntireSource(ctx context.Context) (err error) {
|
||||
// This bit does the filtering so just mark it of as such here
|
||||
xs.filtered = true
|
||||
|
||||
// First the right source
|
||||
err = xs.pullEntireRightSource(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Next the left source
|
||||
err = xs.pullEntireLeftSource(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Sort if needed
|
||||
return xs.sortLeftRows()
|
||||
}
|
||||
|
||||
// pullEntireRightSource pulls and indexes all of the right bits
|
||||
func (xs *linkLeft) pullEntireRightSource(ctx context.Context) (err error) {
|
||||
// Drain the source
|
||||
for xs.rightSource.Next(ctx) {
|
||||
r := &Row{
|
||||
counters: make(map[string]uint),
|
||||
values: make(valueSet),
|
||||
}
|
||||
|
||||
err = xs.rightSource.Scan(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = xs.indexRightRow(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return xs.rightSource.Err()
|
||||
}
|
||||
|
||||
// pullEntireLeftSource pulls and indexes all of the left bits
|
||||
func (xs *linkLeft) pullEntireLeftSource(ctx context.Context) (err error) {
|
||||
var (
|
||||
rel *relIndexBuffer
|
||||
ok bool
|
||||
)
|
||||
|
||||
// Drain the source
|
||||
for xs.leftSource.Next(ctx) {
|
||||
l := &Row{
|
||||
counters: make(map[string]uint),
|
||||
values: make(valueSet),
|
||||
}
|
||||
|
||||
err = xs.leftSource.Scan(l)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rel, ok, err = xs.getRelatedBuffer(l)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
k, err := xs.keep(ctx, l, rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !k {
|
||||
continue
|
||||
}
|
||||
|
||||
xs.leftRows = append(xs.leftRows, l)
|
||||
}
|
||||
return xs.leftSource.Err()
|
||||
}
|
||||
|
||||
// indexRightRow pushes the provided row onto the rel index
|
||||
// @todo consider moving most of this logic to the relIndex struct.
|
||||
func (xs *linkLeft) indexRightRow(r *Row) (err error) {
|
||||
// @todo mv link predicate attrs; should be prevented higher up for now
|
||||
v, err := r.GetValue(xs.def.On.Right, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
xs.relIndex.Add(v, r)
|
||||
return
|
||||
}
|
||||
|
||||
// sortLeftRows sorts the left rows into the correct order
|
||||
//
|
||||
// Algorithm outline:
|
||||
// Compare the two left rows based on the defined sort order.
|
||||
// If the two left rows match up, or we're primarily sorting using right rows,
|
||||
// check the min/max stats values produced by the index buffer.
|
||||
//
|
||||
// If some min/max of a chunk is greater/lesser then the other, no row in the other
|
||||
// can appear before/after the prior.
|
||||
func (xs *linkLeft) sortLeftRows() (err error) {
|
||||
var (
|
||||
leftRelBufferA *relIndexBuffer
|
||||
leftRelBufferB *relIndexBuffer
|
||||
|
||||
a, b any
|
||||
)
|
||||
|
||||
// Use stable sort just so we don't needlesly messup the initial order if we decide
|
||||
// to preserve sorts produced by further steps.
|
||||
sort.SliceStable(xs.leftRows, func(i, j int) bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, s := range xs.filter.OrderBy() {
|
||||
// Prepare the data
|
||||
leftRowA := xs.leftRows[i]
|
||||
leftRowB := xs.leftRows[j]
|
||||
|
||||
leftRowA = convertStringToIntForColumn(s.Column, leftRowA)
|
||||
leftRowB = convertStringToIntForColumn(s.Column, leftRowB)
|
||||
|
||||
leftRelBufferA, _, err = xs.getRelatedBuffer(leftRowA)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
leftRelBufferB, _, err = xs.getRelatedBuffer(leftRowB)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if !xs.isRightSortAttr(s.Column) {
|
||||
// This bit here orders based on the left attributes
|
||||
less, skip := evalCmpResult(compareGetters(leftRowA, leftRowB, leftRowA.counters, leftRowB.counters, s.Column), s)
|
||||
if !skip {
|
||||
return less
|
||||
}
|
||||
} else {
|
||||
// This bit here orders based on the right attributes
|
||||
// Check the stats of the buffer; make sure to adjust based on direction
|
||||
|
||||
// Use chunk's values
|
||||
if !s.Descending {
|
||||
a = leftRelBufferA.min[s.Column]
|
||||
b = leftRelBufferB.min[s.Column]
|
||||
} else {
|
||||
a = leftRelBufferA.max[s.Column]
|
||||
b = leftRelBufferB.max[s.Column]
|
||||
}
|
||||
|
||||
less, skip := evalCmpResult(compareValues(a, b), s)
|
||||
if !skip {
|
||||
return less
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (xs *linkLeft) isRightSortAttr(ident string) bool {
|
||||
for _, i := range xs.rightSortAttrs {
|
||||
if i == ident {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// keep checks if the row should be kept or discarded
|
||||
//
|
||||
// Link's keep is a bit more complicated and it looks at the related buffer as well.
|
||||
func (xs *linkLeft) keep(ctx context.Context, left *Row, buffer *relIndexBuffer) (keep bool, err error) {
|
||||
// If no buffer, we won't keep -- left inner join like behavior
|
||||
if buffer == nil {
|
||||
return false, nil
|
||||
}
|
||||
// No tester include all ok rows
|
||||
if xs.rowTester == nil {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
ch := &rowLink{a: left}
|
||||
for _, ch.b = range buffer.rows {
|
||||
k, err := xs.rowTester.Test(ctx, ch)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !k {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (xs *linkLeft) collectPrimaryAttributes() (out []string) {
|
||||
out = make([]string, 0, 2)
|
||||
for _, m := range xs.def.OutLeftAttributes {
|
||||
if m.Properties().IsPrimary {
|
||||
out = append(out, m.Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
for _, m := range xs.def.OutRightAttributes {
|
||||
if m.Properties().IsPrimary {
|
||||
out = append(out, m.Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// convertStringToIntForColumn converts the string value that have numeric data in the specified column
|
||||
// and updates the Row value datatype to int, if the conversion is successful
|
||||
func convertStringToIntForColumn(column string, row *Row) *Row {
|
||||
if strValue, ok := row.values[column][0].(string); ok {
|
||||
if rowVal, err := strconv.Atoi(strValue); err == nil {
|
||||
row.values[column][0] = rowVal
|
||||
}
|
||||
}
|
||||
return row
|
||||
}
|
||||
119
server/pkg/dal/exec_link_left_bench_test.go
Normal file
119
server/pkg/dal/exec_link_left_bench_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func benchmarkExecLink_left(b *testing.B, n int) {
|
||||
ctx := context.Background()
|
||||
lattrs := []simpleAttribute{
|
||||
{ident: "l_k"},
|
||||
{ident: "l_v1"},
|
||||
{ident: "l_v2"},
|
||||
}
|
||||
fattrs := []simpleAttribute{
|
||||
{ident: "f_k"},
|
||||
{ident: "f_ref"},
|
||||
{ident: "f_v1"},
|
||||
{ident: "f_v2"},
|
||||
}
|
||||
|
||||
la := []simpleAttribute{
|
||||
{ident: "l_k", t: TypeID{}},
|
||||
{ident: "l_v1"},
|
||||
{ident: "l_v2"},
|
||||
}
|
||||
fa := []simpleAttribute{
|
||||
{ident: "f_k", t: TypeID{}},
|
||||
{ident: "f_ref", t: TypeID{}},
|
||||
{ident: "f_v1"},
|
||||
{ident: "f_v2"},
|
||||
}
|
||||
|
||||
// Inmem buffer for example
|
||||
l := InMemoryBuffer()
|
||||
f := InMemoryBuffer()
|
||||
for i := 0; i < n; i++ {
|
||||
require.NoError(b, l.Add(ctx, simpleRow{"l_k": i + 1, "l_v1": "a", "l_v2": rand.Intn(200)}))
|
||||
require.NoError(b, f.Add(ctx, simpleRow{"f_k": i + 1, "f_ref": i + 1, "f_v1": "a", "f_v2": rand.Intn(200)}))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
def := Link{
|
||||
Ident: "link",
|
||||
OutLeftAttributes: saToMapping(lattrs...),
|
||||
OutRightAttributes: saToMapping(fattrs...),
|
||||
LeftAttributes: saToMapping(la...),
|
||||
RightAttributes: saToMapping(fa...),
|
||||
filter: internalFilter{orderBy: filter.SortExprSet{{Column: "f_k"}}},
|
||||
On: LinkPredicate{Left: "l_k", Right: "f_ref"},
|
||||
}
|
||||
|
||||
def.iterator(ctx, l, f)
|
||||
|
||||
l.Seek(ctx, 0)
|
||||
f.Seek(ctx, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/cortezaproject/corteza/server/pkg/dal
|
||||
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
|
||||
// BenchmarkExecLink_left_200-12 1874 633602 ns/op
|
||||
// BenchmarkExecLink_left_400-12 910 1369591 ns/op
|
||||
// BenchmarkExecLink_left_600-12 590 2162512 ns/op
|
||||
// BenchmarkExecLink_left_800-12 451 2725109 ns/op
|
||||
// BenchmarkExecLink_left_1000-12 358 3568684 ns/op
|
||||
// BenchmarkExecLink_left_1200-12 260 4059573 ns/op
|
||||
// BenchmarkExecLink_left_1400-12 250 4742408 ns/op
|
||||
// BenchmarkExecLink_left_1600-12 219 5383900 ns/op
|
||||
// BenchmarkExecLink_left_1800-12 181 6880839 ns/op
|
||||
// BenchmarkExecLink_left_2000-12 165 6821432 ns/op
|
||||
// BenchmarkExecLink_left_2200-12 134 8386884 ns/op
|
||||
// BenchmarkExecLink_left_2400-12 142 8471661 ns/op
|
||||
// BenchmarkExecLink_left_2600-12 135 8702328 ns/op
|
||||
// BenchmarkExecLink_left_2800-12 126 9375744 ns/op
|
||||
// BenchmarkExecLink_left_3000-12 114 10156339 ns/op
|
||||
// BenchmarkExecLink_left_3200-12 96 10682149 ns/op
|
||||
// BenchmarkExecLink_left_3400-12 104 11361907 ns/op
|
||||
// BenchmarkExecLink_left_3600-12 80 12616645 ns/op
|
||||
// BenchmarkExecLink_left_3800-12 73 13761230 ns/op
|
||||
// BenchmarkExecLink_left_4000-12 81 13795284 ns/op
|
||||
// BenchmarkExecLink_left_4200-12 69 15898381 ns/op
|
||||
// BenchmarkExecLink_left_4400-12 72 15162550 ns/op
|
||||
// BenchmarkExecLink_left_4600-12 74 15650527 ns/op
|
||||
// BenchmarkExecLink_left_4800-12 70 19573447 ns/op
|
||||
// BenchmarkExecLink_left_5000-12 69 17214960 ns/op
|
||||
func BenchmarkExecLink_left_200(b *testing.B) { benchmarkExecLink_left(b, 200) }
|
||||
func BenchmarkExecLink_left_400(b *testing.B) { benchmarkExecLink_left(b, 400) }
|
||||
func BenchmarkExecLink_left_600(b *testing.B) { benchmarkExecLink_left(b, 600) }
|
||||
func BenchmarkExecLink_left_800(b *testing.B) { benchmarkExecLink_left(b, 800) }
|
||||
func BenchmarkExecLink_left_1000(b *testing.B) { benchmarkExecLink_left(b, 1000) }
|
||||
func BenchmarkExecLink_left_1200(b *testing.B) { benchmarkExecLink_left(b, 1200) }
|
||||
func BenchmarkExecLink_left_1400(b *testing.B) { benchmarkExecLink_left(b, 1400) }
|
||||
func BenchmarkExecLink_left_1600(b *testing.B) { benchmarkExecLink_left(b, 1600) }
|
||||
func BenchmarkExecLink_left_1800(b *testing.B) { benchmarkExecLink_left(b, 1800) }
|
||||
func BenchmarkExecLink_left_2000(b *testing.B) { benchmarkExecLink_left(b, 2000) }
|
||||
func BenchmarkExecLink_left_2200(b *testing.B) { benchmarkExecLink_left(b, 2200) }
|
||||
func BenchmarkExecLink_left_2400(b *testing.B) { benchmarkExecLink_left(b, 2400) }
|
||||
func BenchmarkExecLink_left_2600(b *testing.B) { benchmarkExecLink_left(b, 2600) }
|
||||
func BenchmarkExecLink_left_2800(b *testing.B) { benchmarkExecLink_left(b, 2800) }
|
||||
func BenchmarkExecLink_left_3000(b *testing.B) { benchmarkExecLink_left(b, 3000) }
|
||||
func BenchmarkExecLink_left_3200(b *testing.B) { benchmarkExecLink_left(b, 3200) }
|
||||
func BenchmarkExecLink_left_3400(b *testing.B) { benchmarkExecLink_left(b, 3400) }
|
||||
func BenchmarkExecLink_left_3600(b *testing.B) { benchmarkExecLink_left(b, 3600) }
|
||||
func BenchmarkExecLink_left_3800(b *testing.B) { benchmarkExecLink_left(b, 3800) }
|
||||
func BenchmarkExecLink_left_4000(b *testing.B) { benchmarkExecLink_left(b, 4000) }
|
||||
func BenchmarkExecLink_left_4200(b *testing.B) { benchmarkExecLink_left(b, 4200) }
|
||||
func BenchmarkExecLink_left_4400(b *testing.B) { benchmarkExecLink_left(b, 4400) }
|
||||
func BenchmarkExecLink_left_4600(b *testing.B) { benchmarkExecLink_left(b, 4600) }
|
||||
func BenchmarkExecLink_left_4800(b *testing.B) { benchmarkExecLink_left(b, 4800) }
|
||||
func BenchmarkExecLink_left_5000(b *testing.B) { benchmarkExecLink_left(b, 5000) }
|
||||
1092
server/pkg/dal/exec_link_left_test.go
Normal file
1092
server/pkg/dal/exec_link_left_test.go
Normal file
File diff suppressed because it is too large
Load Diff
189
server/pkg/dal/filter.go
Normal file
189
server/pkg/dal/filter.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
)
|
||||
|
||||
type (
|
||||
// internalFilter is a wrap to simplify interactions for cases where we might
|
||||
// want to interact with the received filters; i.e. when optimizing pipeline steps
|
||||
internalFilter struct {
|
||||
constraints map[string][]any
|
||||
stateConstraints map[string]filter.State
|
||||
metaConstraints map[string]any
|
||||
expression string
|
||||
expParsed *ql.ASTNode
|
||||
orderBy filter.SortExprSet
|
||||
limit uint
|
||||
cursor *filter.PagingCursor
|
||||
}
|
||||
|
||||
parsedFilter interface {
|
||||
ExpressionParsed() *ql.ASTNode
|
||||
}
|
||||
)
|
||||
|
||||
func (f internalFilter) Constraints() map[string][]any { return f.constraints }
|
||||
func (f internalFilter) StateConstraints() map[string]filter.State { return f.stateConstraints }
|
||||
func (f internalFilter) MetaConstraints() map[string]any { return f.metaConstraints }
|
||||
func (f internalFilter) Expression() string { return f.expression }
|
||||
func (f internalFilter) ExpressionParsed() *ql.ASTNode { return f.expParsed }
|
||||
func (f internalFilter) OrderBy() filter.SortExprSet { return f.orderBy }
|
||||
func (f internalFilter) Limit() uint { return f.limit }
|
||||
func (f internalFilter) Cursor() *filter.PagingCursor { return f.cursor }
|
||||
|
||||
// toInternalFilter converts filter.Filter to internalFilter for easier manipulation
|
||||
//
|
||||
// nil filters are returned as zeroed-out internalFilters.
|
||||
func toInternalFilter(f filter.Filter) (out internalFilter, err error) {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Base
|
||||
out = internalFilter{
|
||||
constraints: f.Constraints(),
|
||||
stateConstraints: f.StateConstraints(),
|
||||
expression: f.Expression(),
|
||||
orderBy: f.OrderBy(),
|
||||
limit: f.Limit(),
|
||||
cursor: f.Cursor(),
|
||||
}
|
||||
|
||||
pf, ok := f.(parsedFilter)
|
||||
if ok {
|
||||
out.expParsed = pf.ExpressionParsed()
|
||||
|
||||
// In case the filter was already provided, we need to make sure the idents
|
||||
// are wrapped
|
||||
out.expParsed.Traverse(func(a *ql.ASTNode) (bool, *ql.ASTNode, error) {
|
||||
a.Symbol = wrapNestedGvalIdent(a.Symbol)
|
||||
return true, a, nil
|
||||
})
|
||||
}
|
||||
if out.expParsed != nil {
|
||||
// We can't mix the two so just clear it out to avoid confusion
|
||||
out.expression = ""
|
||||
}
|
||||
|
||||
// Parse expression for later use
|
||||
if out.expression != "" {
|
||||
out.expParsed, err = newQlParser().Parse(out.expression)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func FilterFromExpr(n *ql.ASTNode) internalFilter {
|
||||
// @todo consider adding string expr for consistency
|
||||
return internalFilter{
|
||||
expParsed: n,
|
||||
}
|
||||
}
|
||||
|
||||
// MergeFilters returns a new filter based on a overwritten by b
|
||||
func (a internalFilter) MergeFilters(b filter.Filter) (c internalFilter, err error) {
|
||||
// In case we got a generic b filter, convert it to the internal one for easier handling
|
||||
aux, ok := b.(internalFilter)
|
||||
if !ok {
|
||||
var err error
|
||||
aux, err = toInternalFilter(b)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
}
|
||||
|
||||
return a.mergeFilters(aux), nil
|
||||
}
|
||||
|
||||
func (a internalFilter) mergeFilters(b internalFilter) (c internalFilter) {
|
||||
|
||||
c = a
|
||||
|
||||
// expression
|
||||
if b.expParsed != nil {
|
||||
if c.expParsed == nil {
|
||||
c.expParsed = b.expParsed
|
||||
c.expression = b.expression
|
||||
} else {
|
||||
parsedA := a.expParsed
|
||||
rawA := a.expression
|
||||
if parsedA.Ref != "group" {
|
||||
parsedA = &ql.ASTNode{
|
||||
Ref: "group",
|
||||
Args: []*ql.ASTNode{
|
||||
parsedA,
|
||||
},
|
||||
}
|
||||
rawA = fmt.Sprintf("(%s)", rawA)
|
||||
}
|
||||
|
||||
parsedB := b.expParsed
|
||||
rawB := b.expression
|
||||
if parsedB.Ref != "group" {
|
||||
parsedB = &ql.ASTNode{
|
||||
Ref: "group",
|
||||
Args: []*ql.ASTNode{
|
||||
parsedB,
|
||||
},
|
||||
}
|
||||
rawB = fmt.Sprintf("(%s)", rawB)
|
||||
}
|
||||
|
||||
c.expParsed = &ql.ASTNode{
|
||||
Ref: "and",
|
||||
Args: ql.ASTNodeSet{parsedA, parsedB},
|
||||
}
|
||||
c.expression = fmt.Sprintf("%s && %s", rawA, rawB)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// constraints
|
||||
if b.constraints != nil {
|
||||
if c.constraints == nil {
|
||||
c.constraints = make(map[string][]any)
|
||||
}
|
||||
|
||||
for k, v := range b.constraints {
|
||||
c.constraints[k] = append(c.constraints[k], v...)
|
||||
}
|
||||
}
|
||||
|
||||
// state constraints
|
||||
if b.stateConstraints != nil {
|
||||
if c.stateConstraints == nil {
|
||||
c.stateConstraints = make(map[string]filter.State)
|
||||
}
|
||||
for k, v := range b.stateConstraints {
|
||||
c.stateConstraints[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// order by
|
||||
if b.orderBy != nil {
|
||||
c.orderBy = append(c.orderBy, b.orderBy...)
|
||||
}
|
||||
|
||||
// cursor
|
||||
// always use the latest paging cursor
|
||||
c.cursor = b.cursor
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// empty reports if the filter is completely empty or not
|
||||
func (f internalFilter) empty() bool {
|
||||
return f.constraints == nil &&
|
||||
f.stateConstraints == nil &&
|
||||
f.expression == "" &&
|
||||
f.orderBy == nil &&
|
||||
f.limit == 0 &&
|
||||
f.cursor == nil
|
||||
}
|
||||
268
server/pkg/dal/filter_test.go
Normal file
268
server/pkg/dal/filter_test.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInternalFilterMergeFilters_expression(t *testing.T) {
|
||||
t.Run("b no expr", func(t *testing.T) {
|
||||
a, err := toInternalFilter(internalFilter{expression: "a == b"})
|
||||
require.NoError(t, err)
|
||||
b, err := toInternalFilter(internalFilter{expression: ""})
|
||||
require.NoError(t, err)
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, "a == b", c.expression)
|
||||
require.Equal(t, "eq(a, b)", c.expParsed.String())
|
||||
})
|
||||
|
||||
t.Run("a no expr", func(t *testing.T) {
|
||||
a, err := toInternalFilter(internalFilter{expression: ""})
|
||||
require.NoError(t, err)
|
||||
b, err := toInternalFilter(internalFilter{expression: "a == b"})
|
||||
require.NoError(t, err)
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, "a == b", c.expression)
|
||||
require.Equal(t, "eq(a, b)", c.expParsed.String())
|
||||
})
|
||||
|
||||
t.Run("both missing", func(t *testing.T) {
|
||||
a, err := toInternalFilter(internalFilter{expression: ""})
|
||||
require.NoError(t, err)
|
||||
b, err := toInternalFilter(internalFilter{expression: ""})
|
||||
require.NoError(t, err)
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, "", c.expression)
|
||||
require.Equal(t, "<nil>", c.expParsed.String())
|
||||
})
|
||||
|
||||
t.Run("combine", func(t *testing.T) {
|
||||
a, err := toInternalFilter(internalFilter{expression: "a == b"})
|
||||
require.NoError(t, err)
|
||||
b, err := toInternalFilter(internalFilter{expression: "c == d"})
|
||||
require.NoError(t, err)
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, "(a == b) && (c == d)", c.expression)
|
||||
require.Equal(t, "and(group(eq(a, b)), group(eq(c, d)))", c.expParsed.String())
|
||||
})
|
||||
}
|
||||
|
||||
func TestInternalFilterMergeFilters_constraints(t *testing.T) {
|
||||
t.Run("b no constr", func(t *testing.T) {
|
||||
a := internalFilter{constraints: map[string][]any{"a": {"b"}}}
|
||||
b := internalFilter{constraints: nil}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, map[string][]any{"a": {"b"}}, c.constraints)
|
||||
})
|
||||
|
||||
t.Run("a no constr", func(t *testing.T) {
|
||||
a := internalFilter{constraints: nil}
|
||||
b := internalFilter{constraints: map[string][]any{"a": {"b"}}}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, map[string][]any{"a": {"b"}}, c.constraints)
|
||||
})
|
||||
|
||||
t.Run("both missing", func(t *testing.T) {
|
||||
a := internalFilter{constraints: nil}
|
||||
b := internalFilter{constraints: nil}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
// Use this assertion because equal asserts type also
|
||||
require.Nil(t, c.constraints)
|
||||
})
|
||||
|
||||
t.Run("combine", func(t *testing.T) {
|
||||
a := internalFilter{constraints: map[string][]any{"a": {"b"}}}
|
||||
b := internalFilter{constraints: map[string][]any{"a": {"c"}, "d": {"e"}}}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, map[string][]any{"a": {"b", "c"}, "d": {"e"}}, c.constraints)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInternalFilterMergeFilters_stateConstraints(t *testing.T) {
|
||||
t.Run("b no constr", func(t *testing.T) {
|
||||
a := internalFilter{stateConstraints: map[string]filter.State{"a": filter.StateInclusive}}
|
||||
b := internalFilter{stateConstraints: nil}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, map[string]filter.State{"a": filter.StateInclusive}, c.stateConstraints)
|
||||
})
|
||||
|
||||
t.Run("a no constr", func(t *testing.T) {
|
||||
a := internalFilter{stateConstraints: nil}
|
||||
b := internalFilter{stateConstraints: map[string]filter.State{"a": filter.StateInclusive}}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, map[string]filter.State{"a": filter.StateInclusive}, c.stateConstraints)
|
||||
})
|
||||
|
||||
t.Run("both missing", func(t *testing.T) {
|
||||
a := internalFilter{stateConstraints: nil}
|
||||
b := internalFilter{stateConstraints: nil}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
// Use this assertion because equal asserts type also
|
||||
require.Nil(t, c.stateConstraints)
|
||||
})
|
||||
|
||||
t.Run("combine", func(t *testing.T) {
|
||||
a := internalFilter{stateConstraints: map[string]filter.State{"a": filter.StateInclusive}}
|
||||
b := internalFilter{stateConstraints: map[string]filter.State{"a": filter.StateExcluded, "d": filter.StateExclusive}}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, map[string]filter.State{"a": filter.StateExcluded, "d": filter.StateExclusive}, c.stateConstraints)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInternalFilterMergeFilters_order(t *testing.T) {
|
||||
t.Run("b no order", func(t *testing.T) {
|
||||
a := internalFilter{orderBy: filter.SortExprSet{{Column: "a"}}}
|
||||
b := internalFilter{orderBy: nil}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, filter.SortExprSet{{Column: "a"}}, c.orderBy)
|
||||
})
|
||||
|
||||
t.Run("a no order", func(t *testing.T) {
|
||||
a := internalFilter{orderBy: nil}
|
||||
b := internalFilter{orderBy: filter.SortExprSet{{Column: "a"}}}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, filter.SortExprSet{{Column: "a"}}, c.orderBy)
|
||||
})
|
||||
|
||||
t.Run("both missing", func(t *testing.T) {
|
||||
a := internalFilter{orderBy: nil}
|
||||
b := internalFilter{orderBy: nil}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
// Use this assertion because equal asserts type also
|
||||
require.Nil(t, c.orderBy)
|
||||
})
|
||||
|
||||
t.Run("combine", func(t *testing.T) {
|
||||
a := internalFilter{orderBy: filter.SortExprSet{{Column: "a"}}}
|
||||
b := internalFilter{orderBy: filter.SortExprSet{{Column: "b"}}}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, filter.SortExprSet{{Column: "a"}, {Column: "b"}}, c.orderBy)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInternalFilterMergeFilters_cursor(t *testing.T) {
|
||||
t.Run("b no cursor", func(t *testing.T) {
|
||||
a := internalFilter{cursor: &filter.PagingCursor{ROrder: true}}
|
||||
b := internalFilter{orderBy: nil}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Nil(t, c.cursor)
|
||||
})
|
||||
|
||||
t.Run("a no constr", func(t *testing.T) {
|
||||
a := internalFilter{orderBy: nil}
|
||||
b := internalFilter{cursor: &filter.PagingCursor{ROrder: true}}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, &filter.PagingCursor{ROrder: true}, c.cursor)
|
||||
})
|
||||
|
||||
t.Run("both missing", func(t *testing.T) {
|
||||
a := internalFilter{orderBy: nil}
|
||||
b := internalFilter{orderBy: nil}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
// Use this assertion because equal asserts type also
|
||||
require.Nil(t, c.cursor)
|
||||
})
|
||||
|
||||
t.Run("combine", func(t *testing.T) {
|
||||
a := internalFilter{cursor: &filter.PagingCursor{ROrder: true}}
|
||||
b := internalFilter{cursor: &filter.PagingCursor{ROrder: false}}
|
||||
|
||||
c := a.mergeFilters(b)
|
||||
require.Equal(t, &filter.PagingCursor{ROrder: false}, c.cursor)
|
||||
})
|
||||
}
|
||||
|
||||
func TestInternalFilterEmpty(t *testing.T) {
|
||||
tcc := []struct {
|
||||
name string
|
||||
f internalFilter
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "completely empty",
|
||||
f: internalFilter{},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "only constraints",
|
||||
f: internalFilter{
|
||||
constraints: map[string][]any{
|
||||
"a": {"b"},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only state constraints",
|
||||
f: internalFilter{
|
||||
stateConstraints: map[string]filter.State{
|
||||
"a": filter.StateInclusive,
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only expression",
|
||||
f: internalFilter{
|
||||
expression: "true",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only order",
|
||||
f: internalFilter{
|
||||
orderBy: filter.SortExprSet{{Column: "a"}},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "only cursor",
|
||||
f: internalFilter{
|
||||
cursor: &filter.PagingCursor{ROrder: true},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "all",
|
||||
f: internalFilter{
|
||||
constraints: map[string][]any{
|
||||
"a": {"b"},
|
||||
},
|
||||
stateConstraints: map[string]filter.State{
|
||||
"a": filter.StateInclusive,
|
||||
},
|
||||
orderBy: filter.SortExprSet{{Column: "a"}},
|
||||
cursor: &filter.PagingCursor{ROrder: true},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := tc.f.empty()
|
||||
require.Equal(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
212
server/pkg/dal/inmem_buffer.go
Normal file
212
server/pkg/dal/inmem_buffer.go
Normal file
@@ -0,0 +1,212 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
)
|
||||
|
||||
type (
|
||||
// inmemBuffer isa simple DAL buffer which holds all of the data in memory
|
||||
//
|
||||
// This buffer should be used for small datasets
|
||||
inmemBuffer struct {
|
||||
rows []ValueGetter
|
||||
ctrs []map[string]uint
|
||||
|
||||
i int
|
||||
cap int
|
||||
closed bool
|
||||
|
||||
more bool
|
||||
err error
|
||||
|
||||
sort filter.SortExprSet
|
||||
}
|
||||
)
|
||||
|
||||
// InMemoryBuffer initializes a new DAL buffer where the data is kept in memory
|
||||
func InMemoryBuffer() *inmemBuffer {
|
||||
return &inmemBuffer{
|
||||
// @note we'll buffer the value counters along side the actual value counter
|
||||
// because constant hashmap initialization is quite memory intensive and introduces
|
||||
// a bit of a bottleneck.
|
||||
//
|
||||
// @todo investigate if this can be optimized at a lower level
|
||||
rows: make([]ValueGetter, 0, 100),
|
||||
ctrs: make([]map[string]uint, 0, 100),
|
||||
|
||||
// i starts off as -1 so we don't need an extra state flag when doing the first Next
|
||||
i: -1,
|
||||
more: true,
|
||||
}
|
||||
}
|
||||
|
||||
// InMemoryBufferWith returns a new buffer with the given value getters
|
||||
func InMemoryBufferWith(ctx context.Context, vv ...ValueGetter) (Buffer, error) {
|
||||
b := InMemoryBuffer()
|
||||
for _, v := range vv {
|
||||
if err := b.Add(ctx, v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) InOrder(ss ...*filter.SortExpr) (err error) {
|
||||
b.sort = ss
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Single makes the buffer only keep one element at the time
|
||||
// @todo make this option convert the buffer into a circular buffer
|
||||
func (b *inmemBuffer) Single() {
|
||||
b.cap = 1
|
||||
b.rows = make([]ValueGetter, 1)
|
||||
b.ctrs = make([]map[string]uint, 1)
|
||||
b.i = -1
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) Seek(_ context.Context, i int) (err error) {
|
||||
// we go one level further and start at -1
|
||||
b.i = i - 1
|
||||
|
||||
b.more = i < b.Len()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) isSingle() bool {
|
||||
return b.cap == 1
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) isSorted() bool {
|
||||
return len(b.sort) > 0
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) Add(ctx context.Context, v ValueGetter) (err error) {
|
||||
if b.i != -1 && b.isSorted() {
|
||||
return fmt.Errorf("cannot buffer items after an access occurred and the buffer is ordered")
|
||||
}
|
||||
|
||||
if b.isSingle() {
|
||||
b.rows[0] = v
|
||||
b.ctrs[0] = v.CountValues()
|
||||
} else {
|
||||
b.rows = append(b.rows, v)
|
||||
b.ctrs = append(b.ctrs, v.CountValues())
|
||||
}
|
||||
|
||||
b.more = true
|
||||
return
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) Next(ctx context.Context) bool {
|
||||
if !b.more {
|
||||
return false
|
||||
}
|
||||
if b.closed {
|
||||
panic("cannot call Next on closed buffer")
|
||||
}
|
||||
|
||||
// @todo use something like a b-tree to sort while inserting
|
||||
if b.i == -1 && len(b.sort) > 0 {
|
||||
sort.Sort(b)
|
||||
}
|
||||
|
||||
b.i++
|
||||
if b.isSingle() {
|
||||
b.i = b.i % b.cap
|
||||
}
|
||||
|
||||
b.more = !b.isSingle() && b.i+1 < b.Len()
|
||||
|
||||
return b.i < b.Len()
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) More(uint, ValueGetter) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) Err() error {
|
||||
return b.err
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) Scan(s ValueSetter) (err error) {
|
||||
if b.closed {
|
||||
panic("cannot call Scan on closed buffer")
|
||||
}
|
||||
|
||||
r, ctrs, err := b.rowAt(b.i)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var v any
|
||||
for name, count := range ctrs {
|
||||
for i := uint(0); i < count; i++ {
|
||||
v, err = r.GetValue(name, i)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = s.SetValue(name, i, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// @todo should this get a context for things like IO ops
|
||||
func (b *inmemBuffer) Close() error {
|
||||
b.ctrs = nil
|
||||
b.rows = nil
|
||||
b.closed = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) BackCursor(ValueGetter) (*filter.PagingCursor, error) {
|
||||
return nil, fmt.Errorf("not supported")
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) ForwardCursor(ValueGetter) (*filter.PagingCursor, error) {
|
||||
return nil, fmt.Errorf("not supported")
|
||||
}
|
||||
|
||||
// rowAt returns the row at a given index
|
||||
func (b *inmemBuffer) rowAt(i int) (ValueGetter, map[string]uint, error) {
|
||||
if i >= b.Len() {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
return b.rows[i], b.ctrs[i], nil
|
||||
}
|
||||
|
||||
// sort.Interface methods
|
||||
|
||||
func (b *inmemBuffer) Len() int {
|
||||
return len(b.rows)
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) Less(i, j int) bool {
|
||||
ra, _, err := b.rowAt(i)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rb, _, err := b.rowAt(j)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return makeRowComparator(b.sort...)(ra, rb)
|
||||
}
|
||||
|
||||
func (b *inmemBuffer) Swap(i, j int) {
|
||||
b.rows[i], b.rows[j] = b.rows[j], b.rows[i]
|
||||
b.ctrs[i], b.ctrs[j] = b.ctrs[j], b.ctrs[i]
|
||||
}
|
||||
298
server/pkg/dal/inmem_buffer_test.go
Normal file
298
server/pkg/dal/inmem_buffer_test.go
Normal file
@@ -0,0 +1,298 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestInmemBuffer_rw(t *testing.T) {
|
||||
gBuff := InMemoryBuffer()
|
||||
ctx := context.Background()
|
||||
|
||||
tcc := []struct {
|
||||
name string
|
||||
prep func() *inmemBuffer
|
||||
in []simpleRow
|
||||
test func(t *testing.T, in []simpleRow, buff *inmemBuffer)
|
||||
}{{
|
||||
name: "fresh insert",
|
||||
prep: func() *inmemBuffer {
|
||||
return gBuff
|
||||
},
|
||||
in: []simpleRow{
|
||||
{
|
||||
"r1_k1": "r1_k1",
|
||||
"r1_k2": "r1_k2",
|
||||
"r1_k3": "r1_k3",
|
||||
}},
|
||||
test: func(t *testing.T, in []simpleRow, buff *inmemBuffer) {
|
||||
r := make(simpleRow)
|
||||
|
||||
require.True(t, buff.Next(ctx))
|
||||
require.NoError(t, buff.Scan(r))
|
||||
require.Equal(t, in[0], r)
|
||||
|
||||
require.False(t, buff.Next(ctx))
|
||||
},
|
||||
}, {
|
||||
name: "insert into existing",
|
||||
prep: func() *inmemBuffer {
|
||||
return gBuff
|
||||
},
|
||||
in: []simpleRow{
|
||||
{
|
||||
"r2_k1": "r2_k1",
|
||||
"r2_k2": "r2_k2",
|
||||
"r2_k3": "r2_k3",
|
||||
}},
|
||||
test: func(t *testing.T, in []simpleRow, buff *inmemBuffer) {
|
||||
r := make(simpleRow)
|
||||
|
||||
require.True(t, buff.Next(ctx))
|
||||
require.NoError(t, buff.Scan(r))
|
||||
require.Equal(t, in[0], r)
|
||||
|
||||
require.False(t, buff.Next(ctx))
|
||||
},
|
||||
}}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
buff := c.prep()
|
||||
for _, in := range c.in {
|
||||
require.NoError(t, buff.Add(ctx, in))
|
||||
}
|
||||
c.test(t, c.in, buff)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInmemBuffer_single_rw(t *testing.T) {
|
||||
mk := func() *inmemBuffer {
|
||||
buff := InMemoryBuffer()
|
||||
buff.Single()
|
||||
return buff
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
tcc := []struct {
|
||||
name string
|
||||
prep func() *inmemBuffer
|
||||
in []simpleRow
|
||||
test func(t *testing.T, in []simpleRow, buff *inmemBuffer)
|
||||
}{{
|
||||
name: "one",
|
||||
prep: func() *inmemBuffer {
|
||||
return mk()
|
||||
},
|
||||
in: []simpleRow{
|
||||
{
|
||||
"r1_k1": "r1_k1",
|
||||
"r1_k2": "r1_k2",
|
||||
"r1_k3": "r1_k3",
|
||||
}},
|
||||
test: func(t *testing.T, in []simpleRow, buff *inmemBuffer) {
|
||||
r := make(simpleRow)
|
||||
|
||||
require.True(t, buff.Next(ctx))
|
||||
require.NoError(t, buff.Scan(r))
|
||||
require.Equal(t, in[0], r)
|
||||
|
||||
require.False(t, buff.Next(ctx))
|
||||
},
|
||||
}, {
|
||||
name: "two",
|
||||
prep: func() *inmemBuffer {
|
||||
return mk()
|
||||
},
|
||||
in: []simpleRow{
|
||||
{
|
||||
"r1_k1": "r1_k1",
|
||||
"r1_k2": "r1_k2",
|
||||
"r1_k3": "r1_k3",
|
||||
}, {
|
||||
"r2_k1": "r2_k1",
|
||||
"r2_k2": "r2_k2",
|
||||
"r2_k3": "r2_k3",
|
||||
}},
|
||||
test: func(t *testing.T, in []simpleRow, buff *inmemBuffer) {
|
||||
r := make(simpleRow)
|
||||
|
||||
require.True(t, buff.Next(ctx))
|
||||
require.NoError(t, buff.Scan(r))
|
||||
require.Equal(t, in[1], r)
|
||||
|
||||
require.False(t, buff.Next(ctx))
|
||||
},
|
||||
}}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
buff := c.prep()
|
||||
for _, in := range c.in {
|
||||
require.NoError(t, buff.Add(ctx, in))
|
||||
}
|
||||
c.test(t, c.in, buff)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInmemBuffer_single_writeMidRead(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
buff := InMemoryBuffer()
|
||||
buff.Single()
|
||||
|
||||
a := simpleRow{
|
||||
"r1_k1": "r1_k1",
|
||||
"r1_k2": "r1_k2",
|
||||
"r1_k3": "r1_k3",
|
||||
}
|
||||
|
||||
b := simpleRow{
|
||||
"r2_k1": "r2_k1",
|
||||
"r2_k2": "r2_k2",
|
||||
"r2_k3": "r2_k3",
|
||||
}
|
||||
|
||||
c := simpleRow{
|
||||
"r3_k1": "r3_k1",
|
||||
"r3_k2": "r3_k2",
|
||||
"r3_k3": "r3_k3",
|
||||
}
|
||||
|
||||
// Write
|
||||
require.NoError(t, buff.Add(ctx, a))
|
||||
|
||||
// Read
|
||||
tmp := simpleRow{}
|
||||
require.True(t, buff.Next(ctx))
|
||||
require.NoError(t, buff.Err())
|
||||
require.NoError(t, buff.Scan(tmp))
|
||||
require.Equal(t, a, tmp)
|
||||
|
||||
// Next write
|
||||
require.NoError(t, buff.Add(ctx, b))
|
||||
// Next write
|
||||
require.NoError(t, buff.Add(ctx, c))
|
||||
|
||||
// Read
|
||||
tmp = simpleRow{}
|
||||
require.True(t, buff.Next(ctx))
|
||||
require.NoError(t, buff.Err())
|
||||
require.NoError(t, buff.Scan(tmp))
|
||||
require.Equal(t, c, tmp)
|
||||
|
||||
// Read empty
|
||||
require.False(t, buff.Next(ctx))
|
||||
require.NoError(t, buff.Err())
|
||||
}
|
||||
|
||||
func TestInmemBuffer_inOrder(t *testing.T) {
|
||||
mk := func() *inmemBuffer {
|
||||
buff := InMemoryBuffer()
|
||||
return buff
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
tcc := []struct {
|
||||
name string
|
||||
prep func() *inmemBuffer
|
||||
in []simpleRow
|
||||
out []simpleRow
|
||||
sort filter.SortExprSet
|
||||
}{{
|
||||
name: "in order",
|
||||
prep: func() *inmemBuffer {
|
||||
return mk()
|
||||
},
|
||||
in: []simpleRow{
|
||||
{
|
||||
"order": 3,
|
||||
}, {
|
||||
"order": 2,
|
||||
}, {
|
||||
"order": 4,
|
||||
}, {
|
||||
"order": 1,
|
||||
}},
|
||||
out: []simpleRow{
|
||||
{
|
||||
"order": 1,
|
||||
}, {
|
||||
"order": 2,
|
||||
}, {
|
||||
"order": 3,
|
||||
}, {
|
||||
"order": 4,
|
||||
}},
|
||||
sort: filter.SortExprSet{{
|
||||
Column: "order",
|
||||
Descending: false,
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
buff := c.prep()
|
||||
buff.InOrder(c.sort...)
|
||||
for _, in := range c.in {
|
||||
require.NoError(t, buff.Add(ctx, in))
|
||||
}
|
||||
|
||||
out := make([]simpleRow, 0, 4)
|
||||
for buff.Next(ctx) {
|
||||
require.NoError(t, buff.Err())
|
||||
|
||||
r := simpleRow{}
|
||||
require.NoError(t, buff.Scan(r))
|
||||
out = append(out, r)
|
||||
}
|
||||
|
||||
require.Equal(t, c.out, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInmemBuffer_seek(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
buff := InMemoryBuffer()
|
||||
|
||||
a := simpleRow{
|
||||
"k": "k1",
|
||||
}
|
||||
b := simpleRow{
|
||||
"k": "k2",
|
||||
}
|
||||
c := simpleRow{
|
||||
"k": "k3",
|
||||
}
|
||||
|
||||
tmp := simpleRow{}
|
||||
|
||||
// Write
|
||||
require.NoError(t, buff.Add(ctx, a))
|
||||
require.NoError(t, buff.Add(ctx, b))
|
||||
require.NoError(t, buff.Add(ctx, c))
|
||||
|
||||
// Seek to end
|
||||
require.NoError(t, buff.Seek(ctx, 4))
|
||||
require.False(t, buff.Next(ctx))
|
||||
|
||||
// Seek to start
|
||||
require.NoError(t, buff.Seek(ctx, 0))
|
||||
require.True(t, buff.Next(ctx))
|
||||
require.NoError(t, buff.Scan(tmp))
|
||||
require.Equal(t, a, tmp)
|
||||
|
||||
// Seek to middle
|
||||
require.NoError(t, buff.Seek(ctx, 2))
|
||||
require.True(t, buff.Next(ctx))
|
||||
require.NoError(t, buff.Scan(tmp))
|
||||
require.Equal(t, c, tmp)
|
||||
}
|
||||
163
server/pkg/dal/issues.go
Normal file
163
server/pkg/dal/issues.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package dal
|
||||
|
||||
import "go.uber.org/zap"
|
||||
|
||||
type (
|
||||
Issue struct {
|
||||
err error
|
||||
Issue string `json:"issue,omitempty"`
|
||||
|
||||
Kind issueKind `json:"kind,omitempty"`
|
||||
Meta map[string]any `json:"meta,omitempty"`
|
||||
}
|
||||
issueSet []Issue
|
||||
|
||||
issueHelper struct {
|
||||
// these two will be used to help clear out unneeded errors
|
||||
connections []uint64
|
||||
models []uint64
|
||||
|
||||
connectionIssues dalIssueIndex
|
||||
modelIssues dalIssueIndex
|
||||
}
|
||||
|
||||
issueKind string
|
||||
dalIssueIndex map[uint64]issueSet
|
||||
)
|
||||
|
||||
const (
|
||||
connectionIssue issueKind = "connection"
|
||||
modelIssue issueKind = "model"
|
||||
)
|
||||
|
||||
func newIssueHelper() *issueHelper {
|
||||
return &issueHelper{
|
||||
connectionIssues: make(dalIssueIndex),
|
||||
modelIssues: make(dalIssueIndex),
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *service) SearchConnectionIssues(connectionID uint64) (out []Issue) {
|
||||
return svc.connectionIssues[connectionID]
|
||||
}
|
||||
|
||||
func (svc *service) SearchModelIssues(resourceID uint64) (out []Issue) {
|
||||
return svc.modelIssues[resourceID]
|
||||
}
|
||||
|
||||
func (svc *service) SearchResourceIssues(resourceType, resource string) (out []Issue) {
|
||||
var m *Model
|
||||
for _, ax := range svc.models {
|
||||
m = ax.FindByResourceIdent(resourceType, resource)
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return
|
||||
}
|
||||
|
||||
return svc.modelIssues[m.ResourceID]
|
||||
}
|
||||
|
||||
func (svc *service) hasConnectionIssues(connectionID uint64) bool {
|
||||
return len(svc.SearchConnectionIssues(connectionID)) > 0
|
||||
}
|
||||
|
||||
func (svc *service) hasModelIssues(modelID uint64) bool {
|
||||
return len(svc.SearchModelIssues(modelID)) > 0
|
||||
}
|
||||
|
||||
func (svc *service) updateIssues(issues *issueHelper) {
|
||||
for _, connectionID := range issues.connections {
|
||||
delete(svc.connectionIssues, connectionID)
|
||||
}
|
||||
for connectionID, issues := range issues.connectionIssues {
|
||||
svc.connectionIssues[connectionID] = issues
|
||||
}
|
||||
|
||||
for _, modelID := range issues.models {
|
||||
delete(svc.modelIssues, modelID)
|
||||
}
|
||||
for modelID, issues := range issues.modelIssues {
|
||||
svc.modelIssues[modelID] = issues
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *service) clearModelIssues() {
|
||||
svc.modelIssues = make(dalIssueIndex)
|
||||
}
|
||||
|
||||
func (rd *issueHelper) addConnection(connectionID uint64) *issueHelper {
|
||||
rd.connections = append(rd.connections, connectionID)
|
||||
return rd
|
||||
}
|
||||
|
||||
func (rd *issueHelper) addModel(modelID uint64) *issueHelper {
|
||||
rd.models = append(rd.models, modelID)
|
||||
return rd
|
||||
}
|
||||
|
||||
func (rd *issueHelper) addConnectionIssue(connectionID uint64, i Issue) {
|
||||
i.Kind = connectionIssue
|
||||
i.Issue = i.err.Error()
|
||||
rd.connectionIssues[connectionID] = append(rd.connectionIssues[connectionID], i)
|
||||
}
|
||||
|
||||
func (rd *issueHelper) addModelIssue(resourceID uint64, i Issue) {
|
||||
i.Kind = modelIssue
|
||||
i.Issue = i.err.Error()
|
||||
rd.modelIssues[resourceID] = append(rd.modelIssues[resourceID], i)
|
||||
}
|
||||
|
||||
func (rd *issueHelper) hasConnectionIssues() bool {
|
||||
return len(rd.connectionIssues) > 0
|
||||
}
|
||||
|
||||
func (rd *issueHelper) hasModelIssues() bool {
|
||||
return len(rd.modelIssues) > 0
|
||||
}
|
||||
|
||||
func (a *issueHelper) mergeWith(b *issueHelper) {
|
||||
if b == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for connectionID, issues := range b.connectionIssues {
|
||||
a.connectionIssues[connectionID] = append(a.connectionIssues[connectionID], issues...)
|
||||
}
|
||||
|
||||
for modelID, issues := range b.modelIssues {
|
||||
a.modelIssues[modelID] = append(a.modelIssues[modelID], issues...)
|
||||
}
|
||||
}
|
||||
|
||||
// Op check utils
|
||||
func (svc *service) canOpData(ref ModelRef) (err error) {
|
||||
if svc.hasConnectionIssues(ref.ConnectionID) {
|
||||
for _, i := range svc.connectionIssues[ref.ConnectionID] {
|
||||
svc.logger.Debug(
|
||||
"can not perform data operation due to connection issue: "+i.err.Error(),
|
||||
zap.Any("ref", ref.ResourceID),
|
||||
)
|
||||
}
|
||||
|
||||
return errRecordOpProblematicConnection(ref.ConnectionID)
|
||||
}
|
||||
|
||||
var mod = svc.FindModelByRef(ref)
|
||||
if mod == nil {
|
||||
return errModelNotFound(ref.ResourceID)
|
||||
}
|
||||
|
||||
if svc.hasModelIssues(mod.ResourceID) {
|
||||
for _, i := range svc.modelIssues[mod.ResourceID] {
|
||||
svc.logger.Debug(
|
||||
"can not perform data operation due to model issue: "+i.err.Error(),
|
||||
zap.Any("ref", ref.ResourceID),
|
||||
)
|
||||
}
|
||||
|
||||
return errRecordOpProblematicModel(mod.ResourceID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
119
server/pkg/dal/iterator.go
Normal file
119
server/pkg/dal/iterator.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
)
|
||||
|
||||
type (
|
||||
// Iterator provides an interface for loading data from the underlying store
|
||||
Iterator interface {
|
||||
Next(ctx context.Context) bool
|
||||
More(uint, ValueGetter) error
|
||||
Err() error
|
||||
Scan(ValueSetter) error
|
||||
Close() error
|
||||
|
||||
BackCursor(ValueGetter) (*filter.PagingCursor, error)
|
||||
ForwardCursor(ValueGetter) (*filter.PagingCursor, error)
|
||||
|
||||
// // -1 means unknown
|
||||
// Total() int
|
||||
// Cursor() any
|
||||
// // ... do we need anything else here?
|
||||
}
|
||||
|
||||
iterator interface {
|
||||
Preload(context.Context, uint, *filter.PagingCursor) error
|
||||
Sorting() filter.SortExprSet
|
||||
}
|
||||
)
|
||||
|
||||
// IteratorEncodeJSON helper function that encodes each item from the iterator as JSON
|
||||
// and writes it to th given io.Writer.
|
||||
//
|
||||
// target initialization function is intentionally used to avoid use of reflection
|
||||
func IteratorEncodeJSON(ctx context.Context, w io.Writer, iter Iterator, initTarget func() ValueSetter) (err error) {
|
||||
var (
|
||||
target ValueSetter
|
||||
firstOut = false
|
||||
)
|
||||
|
||||
for iter.Next(ctx) {
|
||||
if err = iter.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if firstOut {
|
||||
if _, err = w.Write([]byte(`,`)); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
firstOut = true
|
||||
|
||||
target = initTarget()
|
||||
|
||||
if err = iter.Scan(target); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = json.NewEncoder(w).Encode(target)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// PreLoadCursor into iterator and check it exist then return the cursor
|
||||
// @todo this should be move to the Iterator
|
||||
func PreLoadCursor(ctx context.Context, iter Iterator, limit uint, reverse bool, r ValueGetter, fx func(Iterator) (bool, error)) (out *filter.PagingCursor, err error) {
|
||||
makeCursor := func() (*filter.PagingCursor, error) {
|
||||
if reverse {
|
||||
return iter.BackCursor(r)
|
||||
} else {
|
||||
return iter.ForwardCursor(r)
|
||||
}
|
||||
}
|
||||
|
||||
out, err = makeCursor()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = iter.(iterator).Preload(ctx, limit, out)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
for {
|
||||
if !iter.Next(ctx) {
|
||||
out = nil
|
||||
return
|
||||
}
|
||||
|
||||
ok, err := fx(iter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ok {
|
||||
return out, err
|
||||
|
||||
// // @todo Skip the things we don't have access to; could cause some edge cases so probably not
|
||||
// // It adds some performance since we skip unneeded stuff but could some records change in the mean time?
|
||||
// // return makeCursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IteratorSorting return iterator sorting
|
||||
// @todo this should be move to the Iterator
|
||||
func IteratorSorting(iter Iterator) filter.SortExprSet {
|
||||
return iter.(iterator).Sorting()
|
||||
}
|
||||
113
server/pkg/dal/main_test.go
Normal file
113
server/pkg/dal/main_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type (
|
||||
simpleAttribute struct {
|
||||
ident string
|
||||
source string
|
||||
expr string
|
||||
primary bool
|
||||
multivalue bool
|
||||
t Type
|
||||
}
|
||||
|
||||
simpleRow map[string]any
|
||||
)
|
||||
|
||||
func saToMapping(sa ...simpleAttribute) []AttributeMapping {
|
||||
out := make([]AttributeMapping, 0, len(sa))
|
||||
for _, a := range sa {
|
||||
out = append(out, a)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func saToAggAttr(sa ...simpleAttribute) []AggregateAttr {
|
||||
out := make([]AggregateAttr, 0, len(sa))
|
||||
for _, a := range sa {
|
||||
aux := AggregateAttr{
|
||||
RawExpr: a.expr,
|
||||
Identifier: a.ident,
|
||||
Type: a.t,
|
||||
}
|
||||
if aux.RawExpr == "" {
|
||||
aux.RawExpr = a.source
|
||||
}
|
||||
if aux.RawExpr == "" {
|
||||
aux.RawExpr = a.ident
|
||||
}
|
||||
out = append(out, aux)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (sa simpleAttribute) Properties() MapProperties {
|
||||
return MapProperties{
|
||||
IsPrimary: sa.primary,
|
||||
Type: sa.t,
|
||||
IsMultivalue: sa.multivalue,
|
||||
}
|
||||
}
|
||||
|
||||
func (sa simpleAttribute) Identifier() (ident string) {
|
||||
return sa.ident
|
||||
}
|
||||
|
||||
func (sa simpleAttribute) Source() (expr string) {
|
||||
if sa.source != "" {
|
||||
return sa.source
|
||||
}
|
||||
|
||||
return sa.ident
|
||||
}
|
||||
|
||||
func (sa simpleAttribute) Expression() (expr string) {
|
||||
if sa.expr != "" {
|
||||
return sa.expr
|
||||
}
|
||||
|
||||
if sa.source != "" {
|
||||
return sa.source
|
||||
}
|
||||
|
||||
return sa.ident
|
||||
}
|
||||
|
||||
func bootstrapAggregate(rootTest *testing.T, run func(context.Context, *testing.T, *Aggregate, Buffer)) {
|
||||
bootstrapAggregateNoOpt(rootTest, run)
|
||||
}
|
||||
|
||||
func bootstrapAggregateNoOpt(rootTest *testing.T, run func(context.Context, *testing.T, *Aggregate, Buffer)) {
|
||||
rootTest.Run("no optimization", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
buff := InMemoryBuffer()
|
||||
agg := &Aggregate{}
|
||||
|
||||
run(ctx, t, agg, buff)
|
||||
})
|
||||
}
|
||||
|
||||
func (r simpleRow) CountValues() map[string]uint {
|
||||
out := make(map[string]uint)
|
||||
|
||||
for k := range r {
|
||||
out[k]++
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (r simpleRow) GetValue(k string, place uint) (any, error) {
|
||||
return r[k], nil
|
||||
}
|
||||
|
||||
func (r simpleRow) SetValue(k string, place uint, v any) error {
|
||||
r[k] = v
|
||||
return nil
|
||||
}
|
||||
578
server/pkg/dal/model.go
Normal file
578
server/pkg/dal/model.go
Normal file
@@ -0,0 +1,578 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/handle"
|
||||
"github.com/modern-go/reflect2"
|
||||
)
|
||||
|
||||
type (
|
||||
// ModelRef is used to retrieve a model from the DAL based on given params
|
||||
ModelRef struct {
|
||||
ConnectionID uint64
|
||||
|
||||
ResourceID uint64
|
||||
|
||||
ResourceType string
|
||||
Resource string
|
||||
|
||||
Refs map[string]any
|
||||
}
|
||||
|
||||
// Model describes the underlying data and its shape
|
||||
Model struct {
|
||||
ConnectionID uint64 `json:"connectionID"`
|
||||
Ident string `json:"ident"`
|
||||
Label string `json:"label"`
|
||||
|
||||
Resource string `json:"resource"`
|
||||
ResourceID uint64 `json:"resourceID"`
|
||||
ResourceType string `json:"resourceType"`
|
||||
|
||||
// Refs is an arbitrary map to identify a model
|
||||
// @todo consider reworking this; I'm not the biggest fan
|
||||
Refs map[string]any `json:"refs"`
|
||||
|
||||
SensitivityLevelID uint64 `json:"sensitivityLevelID"`
|
||||
|
||||
Attributes AttributeSet `json:"attributes"`
|
||||
|
||||
Constraints map[string][]any `json:"constraints"`
|
||||
Indexes IndexSet `json:"indexes"`
|
||||
}
|
||||
|
||||
ModelSet []*Model
|
||||
|
||||
// Attribute describes a specific value of the dataset
|
||||
Attribute struct {
|
||||
Ident string `json:"ident"`
|
||||
Label string `json:"label"`
|
||||
|
||||
SensitivityLevelID uint64 `json:"sensitivityLevelID"`
|
||||
|
||||
MultiValue bool `json:"multiValue"`
|
||||
|
||||
PrimaryKey bool `json:"primaryKey"`
|
||||
|
||||
// If attribute has SoftDeleteFlag=true we use it
|
||||
// when filtering out deleted items
|
||||
SoftDeleteFlag bool `json:"softDeleteFlag"`
|
||||
|
||||
// System indicates the attribute was defined by the system
|
||||
System bool `json:"system"`
|
||||
|
||||
// Is attribute sortable?
|
||||
// Note: all primary keys are sortable
|
||||
Sortable bool `json:"sortable"`
|
||||
|
||||
// Can attribute be used in query expression?
|
||||
Filterable bool `json:"filterable"`
|
||||
|
||||
// Store describes the strategy the underlying storage system should
|
||||
// apply to the underlying value
|
||||
Store Codec `json:"store"`
|
||||
|
||||
// Type describes what the value represents and how it should be
|
||||
// encoded/decoded
|
||||
Type Type `json:"type"`
|
||||
}
|
||||
|
||||
// auxAttribute is a helper struct used for marshaling/unmarshaling
|
||||
//
|
||||
// This is required since some fields are interfaces
|
||||
auxAttribute struct {
|
||||
Ident string `json:"ident"`
|
||||
Label string `json:"label"`
|
||||
SensitivityLevelID uint64 `json:"sensitivityLevelID"`
|
||||
MultiValue bool `json:"multiValue"`
|
||||
PrimaryKey bool `json:"primaryKey"`
|
||||
SoftDeleteFlag bool `json:"softDeleteFlag"`
|
||||
System bool `json:"system"`
|
||||
Sortable bool `json:"sortable"`
|
||||
Filterable bool `json:"filterable"`
|
||||
|
||||
Store *auxAttributeStore `json:"store"`
|
||||
Type *auxAttributeType `json:"type"`
|
||||
}
|
||||
|
||||
// auxAttributeStore is a helper struct used for marshaling/unmarshaling
|
||||
//
|
||||
// This is required since some fields are interfaces
|
||||
auxAttributeStore struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
Plain *CodecPlain `json:"plain,omitempty"`
|
||||
RecordValueSetJSON *CodecRecordValueSetJSON `json:"recordValueSetJSON,omitempty"`
|
||||
Alias *CodecAlias `json:"alias,omitempty"`
|
||||
}
|
||||
|
||||
// auxAttributeType is a helper struct used for marshaling/unmarshaling
|
||||
//
|
||||
// This is required since some fields are interfaces
|
||||
auxAttributeType struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
ID *TypeID `json:"id,omitempty"`
|
||||
Ref *TypeRef `json:"ref,omitempty"`
|
||||
Timestamp *TypeTimestamp `json:"timestamp,omitempty"`
|
||||
Time *TypeTime `json:"time,omitempty"`
|
||||
Date *TypeDate `json:"date,omitempty"`
|
||||
Number *TypeNumber `json:"number,omitempty"`
|
||||
Text *TypeText `json:"text,omitempty"`
|
||||
Boolean *TypeBoolean `json:"boolean,omitempty"`
|
||||
Enum *TypeEnum `json:"enum,omitempty"`
|
||||
Geometry *TypeGeometry `json:"geometry,omitempty"`
|
||||
JSON *TypeJSON `json:"jSON,omitempty"`
|
||||
Blob *TypeBlob `json:"blob,omitempty"`
|
||||
UUID *TypeUUID `json:"uuid,omitempty"`
|
||||
}
|
||||
|
||||
// auxStoreCodec is a helper struct used for marshaling/unmarshaling
|
||||
//
|
||||
// This is required since some fields are interfaces
|
||||
auxStoreCodec struct {
|
||||
Type string `json:"type"`
|
||||
|
||||
CodecPlain *CodecPlain `json:"codecPlain"`
|
||||
CodecRecordValueSetJSON *CodecRecordValueSetJSON `json:"codecRecordValueSetJSON"`
|
||||
CodecAlias *CodecAlias `json:"codecAlias"`
|
||||
}
|
||||
|
||||
AttributeSet []*Attribute
|
||||
|
||||
Index struct {
|
||||
Ident string
|
||||
Type string
|
||||
Unique bool
|
||||
|
||||
Fields []*IndexField
|
||||
|
||||
Predicate string
|
||||
}
|
||||
|
||||
IndexField struct {
|
||||
AttributeIdent string
|
||||
Modifiers []IndexFieldModifier
|
||||
Sort IndexFieldSort
|
||||
Nulls IndexFieldNulls
|
||||
}
|
||||
|
||||
IndexSet []*Index
|
||||
|
||||
IndexFieldModifier string
|
||||
IndexFieldSort int
|
||||
IndexFieldNulls int
|
||||
)
|
||||
|
||||
const (
|
||||
IndexFieldSortDesc IndexFieldSort = -1
|
||||
IndexFieldSortAsc IndexFieldSort = 1
|
||||
|
||||
IndexFieldNullsLast IndexFieldNulls = -1
|
||||
IndexFieldNullsFirst IndexFieldNulls = 1
|
||||
|
||||
IndexFieldModifierLower = "LOWERCASE"
|
||||
)
|
||||
|
||||
func PrimaryAttribute(ident string, codec Codec) *Attribute {
|
||||
out := FullAttribute(ident, TypeID{}, codec)
|
||||
out.Type = &TypeID{}
|
||||
out.PrimaryKey = true
|
||||
return out
|
||||
}
|
||||
|
||||
func FullAttribute(ident string, at Type, codec Codec) *Attribute {
|
||||
return &Attribute{
|
||||
Ident: ident,
|
||||
Label: ident,
|
||||
Sortable: true,
|
||||
Filterable: true,
|
||||
Store: codec,
|
||||
Type: at,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Attribute) WithSoftDelete() *Attribute {
|
||||
a.SoftDeleteFlag = true
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Attribute) WithMultiValue() *Attribute {
|
||||
a.MultiValue = true
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *Attribute) StoreIdent() string {
|
||||
switch s := a.Store.(type) {
|
||||
case *CodecRecordValueSetJSON:
|
||||
return s.Ident
|
||||
|
||||
case *CodecAlias:
|
||||
return s.Ident
|
||||
|
||||
default:
|
||||
return a.Ident
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Compare the two attributes
|
||||
func (a *Attribute) Compare(b *Attribute) bool {
|
||||
if a == nil || b == nil {
|
||||
return a == b
|
||||
}
|
||||
|
||||
out := true
|
||||
out = out && a.Ident == b.Ident
|
||||
out = out && a.Label == b.Label
|
||||
out = out && a.SensitivityLevelID == b.SensitivityLevelID
|
||||
out = out && a.MultiValue == b.MultiValue
|
||||
out = out && a.PrimaryKey == b.PrimaryKey
|
||||
out = out && a.SoftDeleteFlag == b.SoftDeleteFlag
|
||||
out = out && a.System == b.System
|
||||
out = out && a.Sortable == b.Sortable
|
||||
out = out && a.Filterable == b.Filterable
|
||||
if a.Store == nil || b.Store == nil {
|
||||
out = out && a.Store == b.Store
|
||||
} else {
|
||||
out = out && a.Store.Type() == b.Store.Type()
|
||||
}
|
||||
if a.Type == nil || b.Type == nil {
|
||||
out = out && a.Type == b.Type
|
||||
} else {
|
||||
out = out && a.Type.Type() == b.Type.Type()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (mm ModelSet) FindByResourceID(resourceID uint64) *Model {
|
||||
for _, m := range mm {
|
||||
if m.ResourceID == resourceID {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mm ModelSet) FindByResourceIdent(resourceType, resourceIdent string) *Model {
|
||||
for _, m := range mm {
|
||||
if m.ResourceType != resourceType {
|
||||
continue
|
||||
}
|
||||
|
||||
if m.Resource != resourceIdent {
|
||||
continue
|
||||
}
|
||||
|
||||
return m
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mm ModelSet) FindByIdent(ident string) *Model {
|
||||
for _, m := range mm {
|
||||
if m.Ident == ident {
|
||||
return m
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindByRefs returns the first Model that matches the given refs
|
||||
func (mm ModelSet) FindByRefs(refs map[string]any) *Model {
|
||||
for _, model := range mm {
|
||||
for k, v := range refs {
|
||||
ref, ok := model.Refs[k]
|
||||
if !ok {
|
||||
goto skip
|
||||
}
|
||||
if v != ref {
|
||||
goto skip
|
||||
}
|
||||
}
|
||||
|
||||
return model
|
||||
|
||||
skip:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterByReferenced returns all of the models that reference b
|
||||
func (aa ModelSet) FilterByReferenced(b *Model) (out ModelSet) {
|
||||
for _, aModel := range aa {
|
||||
if aModel.Resource == b.Resource {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, aAttribute := range aModel.Attributes {
|
||||
switch casted := aAttribute.Type.(type) {
|
||||
case *TypeRef:
|
||||
if casted.RefModel.Resource == b.Resource {
|
||||
out = append(out, aModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (m Model) ToFilter() ModelRef {
|
||||
return ModelRef{
|
||||
ConnectionID: m.ConnectionID,
|
||||
|
||||
ResourceID: m.ResourceID,
|
||||
|
||||
ResourceType: m.ResourceType,
|
||||
Resource: m.Resource,
|
||||
}
|
||||
}
|
||||
|
||||
// HasAttribute returns true when the model includes the specified attribute
|
||||
func (m Model) HasAttribute(ident string) bool {
|
||||
return m.Attributes.FindByIdent(ident) != nil
|
||||
}
|
||||
|
||||
func (aa AttributeSet) FindByIdent(ident string) *Attribute {
|
||||
for _, a := range aa {
|
||||
if strings.EqualFold(a.Ident, ident) {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (aa AttributeSet) FindByStoreIdent(ident string) *Attribute {
|
||||
for _, a := range aa {
|
||||
if strings.EqualFold(a.StoreIdent(), ident) {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate performs a base model validation before it is passed down
|
||||
func (m Model) Validate() error {
|
||||
if m.Resource == "" {
|
||||
return fmt.Errorf("resource not defined")
|
||||
}
|
||||
|
||||
seen := make(map[string]bool)
|
||||
for _, attr := range m.Attributes {
|
||||
if attr.Ident == "" {
|
||||
return fmt.Errorf("invalid attribute ident: ident must not be empty")
|
||||
}
|
||||
|
||||
if !handle.IsValid(attr.Ident) {
|
||||
return fmt.Errorf("invalid attribute ident: %s is not a valid handle", attr.Ident)
|
||||
}
|
||||
|
||||
if seen[attr.Ident] {
|
||||
return fmt.Errorf("invalid attribute %s: duplicate attributes are not allowed", attr.Ident)
|
||||
}
|
||||
seen[attr.Ident] = true
|
||||
|
||||
if reflect2.IsNil(attr.Type) {
|
||||
return fmt.Errorf("attribute does not define a type: %s", attr.Ident)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compare the two models
|
||||
//
|
||||
// This only checks model metadata, the attributes are excluded.
|
||||
func (a Model) Compare(b Model) bool {
|
||||
out := true
|
||||
|
||||
out = out && a.ConnectionID == b.ConnectionID
|
||||
out = out && a.Ident == b.Ident
|
||||
out = out && a.Label == b.Label
|
||||
out = out && a.Resource == b.Resource
|
||||
out = out && a.ResourceID == b.ResourceID
|
||||
out = out && a.ResourceType == b.ResourceType
|
||||
out = out && a.SensitivityLevelID == b.SensitivityLevelID
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *Attribute) MarshalJSON() ([]byte, error) {
|
||||
aux := &auxAttribute{
|
||||
Ident: a.Ident,
|
||||
Label: a.Label,
|
||||
SensitivityLevelID: a.SensitivityLevelID,
|
||||
MultiValue: a.MultiValue,
|
||||
PrimaryKey: a.PrimaryKey,
|
||||
SoftDeleteFlag: a.SoftDeleteFlag,
|
||||
System: a.System,
|
||||
Sortable: a.Sortable,
|
||||
Filterable: a.Filterable,
|
||||
|
||||
Store: &auxAttributeStore{},
|
||||
Type: &auxAttributeType{},
|
||||
}
|
||||
|
||||
switch s := a.Store.(type) {
|
||||
case *CodecPlain:
|
||||
aux.Store.Type = "plain"
|
||||
aux.Store.Plain = s
|
||||
|
||||
case *CodecRecordValueSetJSON:
|
||||
aux.Store.Type = "recordValueSetJSON"
|
||||
aux.Store.RecordValueSetJSON = s
|
||||
|
||||
case *CodecAlias:
|
||||
aux.Store.Type = "alias"
|
||||
aux.Store.Alias = s
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown store codec type: %T", s)
|
||||
}
|
||||
|
||||
switch t := a.Type.(type) {
|
||||
case *TypeID:
|
||||
aux.Type.Type = "ID"
|
||||
aux.Type.ID = t
|
||||
|
||||
case *TypeRef:
|
||||
aux.Type.Type = "Ref"
|
||||
aux.Type.Ref = t
|
||||
|
||||
case *TypeTimestamp:
|
||||
aux.Type.Type = "Timestamp"
|
||||
aux.Type.Timestamp = t
|
||||
|
||||
case *TypeTime:
|
||||
aux.Type.Type = "Time"
|
||||
aux.Type.Time = t
|
||||
|
||||
case *TypeDate:
|
||||
aux.Type.Type = "Date"
|
||||
aux.Type.Date = t
|
||||
|
||||
case *TypeNumber:
|
||||
aux.Type.Type = "Number"
|
||||
aux.Type.Number = t
|
||||
|
||||
case *TypeText:
|
||||
aux.Type.Type = "Text"
|
||||
aux.Type.Text = t
|
||||
|
||||
case *TypeBoolean:
|
||||
aux.Type.Type = "Boolean"
|
||||
aux.Type.Boolean = t
|
||||
|
||||
case *TypeEnum:
|
||||
aux.Type.Type = "Enum"
|
||||
aux.Type.Enum = t
|
||||
|
||||
case *TypeGeometry:
|
||||
aux.Type.Type = "Geometry"
|
||||
aux.Type.Geometry = t
|
||||
|
||||
case *TypeJSON:
|
||||
aux.Type.Type = "JSON"
|
||||
aux.Type.JSON = t
|
||||
|
||||
case *TypeBlob:
|
||||
aux.Type.Type = "Blob"
|
||||
aux.Type.Blob = t
|
||||
|
||||
case *TypeUUID:
|
||||
aux.Type.Type = "UUID"
|
||||
aux.Type.UUID = t
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown attribute type type: %T", t)
|
||||
}
|
||||
|
||||
return json.Marshal(aux)
|
||||
}
|
||||
|
||||
func (a *Attribute) UnmarshalJSON(data []byte) (err error) {
|
||||
aux := &auxAttribute{}
|
||||
err = json.Unmarshal(data, &aux)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a == nil {
|
||||
*a = Attribute{}
|
||||
}
|
||||
|
||||
a.Ident = aux.Ident
|
||||
a.Label = aux.Label
|
||||
a.SensitivityLevelID = aux.SensitivityLevelID
|
||||
a.MultiValue = aux.MultiValue
|
||||
a.PrimaryKey = aux.PrimaryKey
|
||||
a.SoftDeleteFlag = aux.SoftDeleteFlag
|
||||
a.System = aux.System
|
||||
a.Sortable = aux.Sortable
|
||||
a.Filterable = aux.Filterable
|
||||
|
||||
switch aux.Store.Type {
|
||||
case "plain":
|
||||
a.Store = aux.Store.Plain
|
||||
|
||||
case "recordValueSetJSON":
|
||||
a.Store = aux.Store.RecordValueSetJSON
|
||||
|
||||
case "alias":
|
||||
a.Store = aux.Store.Alias
|
||||
}
|
||||
|
||||
switch aux.Type.Type {
|
||||
case "ID":
|
||||
a.Type = aux.Type.ID
|
||||
|
||||
case "Ref":
|
||||
a.Type = aux.Type.Ref
|
||||
|
||||
case "Timestamp":
|
||||
a.Type = aux.Type.Timestamp
|
||||
|
||||
case "Time":
|
||||
a.Type = aux.Type.Time
|
||||
|
||||
case "Date":
|
||||
a.Type = aux.Type.Date
|
||||
|
||||
case "Number":
|
||||
a.Type = aux.Type.Number
|
||||
|
||||
case "Text":
|
||||
a.Type = aux.Type.Text
|
||||
|
||||
case "Boolean":
|
||||
a.Type = aux.Type.Boolean
|
||||
|
||||
case "Enum":
|
||||
a.Type = aux.Type.Enum
|
||||
|
||||
case "Geometry":
|
||||
a.Type = aux.Type.Geometry
|
||||
|
||||
case "JSON":
|
||||
a.Type = aux.Type.JSON
|
||||
|
||||
case "Blob":
|
||||
a.Type = aux.Type.Blob
|
||||
|
||||
case "UUID":
|
||||
a.Type = aux.Type.UUID
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown attribute type type: %s", aux.Type.Type)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
80
server/pkg/dal/model_test.go
Normal file
80
server/pkg/dal/model_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestModelFindByRefs(t *testing.T) {
|
||||
tcc := []struct {
|
||||
name string
|
||||
in ModelSet
|
||||
refs map[string]interface{}
|
||||
found bool
|
||||
}{
|
||||
{
|
||||
name: "one ref found",
|
||||
in: ModelSet{
|
||||
{Refs: map[string]any{"a": 1}},
|
||||
},
|
||||
refs: map[string]any{"a": 1},
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "one ref not found",
|
||||
in: ModelSet{
|
||||
{Refs: map[string]any{"a": 1}},
|
||||
},
|
||||
refs: map[string]any{"b": 1},
|
||||
found: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "n refs found",
|
||||
in: ModelSet{
|
||||
{Refs: map[string]any{"a": 1, "b": 1, "c": 1}},
|
||||
},
|
||||
refs: map[string]any{"a": 1, "b": 1},
|
||||
found: true,
|
||||
},
|
||||
{
|
||||
name: "n refs not found",
|
||||
in: ModelSet{
|
||||
{Refs: map[string]any{"a": 1, "b": 1}},
|
||||
},
|
||||
refs: map[string]any{"a": 1, "b": 2},
|
||||
found: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
m := tc.in.FindByRefs(tc.refs)
|
||||
if tc.found {
|
||||
require.NotNil(t, m)
|
||||
} else {
|
||||
require.Nil(t, m)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttributeMarshling(t *testing.T) {
|
||||
a := &Attribute{
|
||||
Ident: "foo",
|
||||
Type: &TypeBlob{},
|
||||
Store: &CodecPlain{},
|
||||
}
|
||||
|
||||
bb, err := json.Marshal(a)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := &Attribute{}
|
||||
err = json.Unmarshal(bb, &b)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, reflect.DeepEqual(a, b))
|
||||
}
|
||||
54
server/pkg/dal/normalizer.go
Normal file
54
server/pkg/dal/normalizer.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package dal
|
||||
|
||||
import "strings"
|
||||
|
||||
// NormalizeAttrNames normalizes system identifiers to attribute identifiers
|
||||
// @todo this is temporary until we re-visit field referencing
|
||||
//
|
||||
// The normalization step must exist since legacy versions allowed multiple
|
||||
// identifier variations for the same system field, such as created_at and createdAt.
|
||||
func NormalizeAttrNames(name string) string {
|
||||
switch strings.ToLower(name) {
|
||||
case
|
||||
"recordid",
|
||||
"record_id",
|
||||
"id":
|
||||
return "ID"
|
||||
|
||||
case "moduleid",
|
||||
"module_id",
|
||||
"module":
|
||||
return "moduleID"
|
||||
|
||||
case "owned_by",
|
||||
"ownedby":
|
||||
return "ownedBy"
|
||||
|
||||
case "created_by",
|
||||
"createdby":
|
||||
return "createdBy"
|
||||
|
||||
case "updated_by",
|
||||
"updatedby":
|
||||
return "updatedBy"
|
||||
|
||||
case "deleted_by",
|
||||
"deletedby":
|
||||
return "deletedBy"
|
||||
|
||||
case "created_at",
|
||||
"createdat":
|
||||
return "createdAt"
|
||||
|
||||
case "updated_at",
|
||||
"updatedat":
|
||||
return "updatedAt"
|
||||
|
||||
case "deleted_at",
|
||||
"deletedat":
|
||||
return "deletedAt"
|
||||
}
|
||||
|
||||
return name
|
||||
|
||||
}
|
||||
155
server/pkg/dal/operations.go
Normal file
155
server/pkg/dal/operations.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package dal
|
||||
|
||||
type (
|
||||
Operation string
|
||||
OperationSet []Operation
|
||||
)
|
||||
|
||||
const (
|
||||
Create Operation = "corteza::dal:operation:create"
|
||||
Update Operation = "corteza::dal:operation:update"
|
||||
Delete Operation = "corteza::dal:operation:delete"
|
||||
Search Operation = "corteza::dal:operation:search"
|
||||
Lookup Operation = "corteza::dal:operation:lookup"
|
||||
Paging Operation = "corteza::dal:operation:paging"
|
||||
Sorting Operation = "corteza::dal:operation:sorting"
|
||||
)
|
||||
|
||||
var (
|
||||
full = OperationSet{
|
||||
Create,
|
||||
Update,
|
||||
Delete,
|
||||
Search,
|
||||
Lookup,
|
||||
Paging,
|
||||
Sorting,
|
||||
}
|
||||
|
||||
createOperations = OperationSet{
|
||||
Create,
|
||||
}
|
||||
|
||||
updateOperations = OperationSet{
|
||||
Update,
|
||||
}
|
||||
|
||||
deleteOperations = OperationSet{
|
||||
Update,
|
||||
}
|
||||
|
||||
searchOperations = OperationSet{
|
||||
Search,
|
||||
Paging,
|
||||
Sorting,
|
||||
}
|
||||
|
||||
lookupOperations = OperationSet{
|
||||
Lookup,
|
||||
}
|
||||
)
|
||||
|
||||
// FullOperations returns all base system defined operations
|
||||
func FullOperations() (cc OperationSet) {
|
||||
// Doing an union just to make a fresh copy
|
||||
return full.Union(nil)
|
||||
}
|
||||
|
||||
// CreateOperations returns only requested operations used for Create operations
|
||||
func CreateOperations(requested ...Operation) (required OperationSet) {
|
||||
return common(createOperations, requested)
|
||||
}
|
||||
|
||||
// UpdateOperations returns only requested operations used for Update operations
|
||||
func UpdateOperations(requested ...Operation) (required OperationSet) {
|
||||
return common(updateOperations, requested)
|
||||
}
|
||||
|
||||
// DeleteOperations returns only requested operations used for delete operations
|
||||
func DeleteOperations(requested ...Operation) (required OperationSet) {
|
||||
return common(deleteOperations, requested)
|
||||
}
|
||||
|
||||
// SearchOperations returns only requested operations used for Search operations
|
||||
func SearchOperations(requested ...Operation) (required OperationSet) {
|
||||
return common(searchOperations, requested)
|
||||
}
|
||||
|
||||
// LookupOperations returns only requested operations used for Search operations
|
||||
func LookupOperations(requested ...Operation) (required OperationSet) {
|
||||
return common(lookupOperations, requested)
|
||||
}
|
||||
|
||||
func common(aa, bb OperationSet) OperationSet {
|
||||
return aa.Intersect(bb)
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
// IsSuperset is inverse IsSubset
|
||||
//
|
||||
// IsSuperset checks if all bb operations are inside aa
|
||||
func (aa OperationSet) IsSuperset(bb ...Operation) bool {
|
||||
return OperationSet(bb).IsSubset(aa...)
|
||||
}
|
||||
|
||||
// IsSubset checks if all aa operations are inside bb
|
||||
func (aa OperationSet) IsSubset(bb ...Operation) bool {
|
||||
if len(aa) > len(bb) {
|
||||
return false
|
||||
}
|
||||
|
||||
// When A is subset of B, the difference between the two must be 0
|
||||
return len(aa.Diff(bb)) == 0
|
||||
}
|
||||
|
||||
// Intersect returns the intersection between the two sets
|
||||
func (aa OperationSet) Intersect(bb OperationSet) (cc OperationSet) {
|
||||
cc = make(OperationSet, 0, len(aa))
|
||||
for _, a := range aa {
|
||||
for _, b := range bb {
|
||||
if a == b {
|
||||
cc = append(cc, a)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Intersect returns the union between the two sets
|
||||
//
|
||||
// Duplicates are omitted
|
||||
func (aa OperationSet) Union(bb OperationSet) (cc OperationSet) {
|
||||
ix := make(map[Operation]bool)
|
||||
for _, c := range append(aa, bb...) {
|
||||
if !ix[c] {
|
||||
ix[c] = true
|
||||
cc = append(cc, c)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Diff calculates the difference between the two operation sets
|
||||
//
|
||||
// The diff uses aa as base
|
||||
func (aa OperationSet) Diff(bb OperationSet) (cc OperationSet) {
|
||||
for _, a := range aa {
|
||||
found := false
|
||||
for _, b := range bb {
|
||||
found = a == b
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
cc = append(cc, a)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
127
server/pkg/dal/operations_test.go
Normal file
127
server/pkg/dal/operations_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCommonOperations(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
aa OperationSet
|
||||
bb OperationSet
|
||||
cc OperationSet
|
||||
}{{
|
||||
name: "regular",
|
||||
aa: OperationSet{
|
||||
Create,
|
||||
Update,
|
||||
},
|
||||
bb: OperationSet{
|
||||
Update,
|
||||
Delete,
|
||||
},
|
||||
cc: OperationSet{
|
||||
Update,
|
||||
},
|
||||
}, {
|
||||
name: "no commons",
|
||||
aa: OperationSet{
|
||||
Create,
|
||||
},
|
||||
bb: OperationSet{
|
||||
Delete,
|
||||
},
|
||||
cc: OperationSet{},
|
||||
}, {
|
||||
name: "aa empty",
|
||||
aa: nil,
|
||||
bb: OperationSet{
|
||||
Update,
|
||||
Delete,
|
||||
},
|
||||
cc: OperationSet{},
|
||||
}, {
|
||||
name: "bb empty",
|
||||
aa: OperationSet{
|
||||
Create,
|
||||
Update,
|
||||
},
|
||||
bb: nil,
|
||||
cc: OperationSet{},
|
||||
}}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
cc := common(c.aa, c.bb)
|
||||
require.Equal(t, c.cc, cc)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationChecking(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
support OperationSet
|
||||
require OperationSet
|
||||
out bool
|
||||
}{{
|
||||
name: "passing: complete match",
|
||||
support: OperationSet{
|
||||
Create,
|
||||
Update,
|
||||
},
|
||||
require: OperationSet{
|
||||
Create,
|
||||
Update,
|
||||
},
|
||||
out: true,
|
||||
}, {
|
||||
name: "passing: supports more then required",
|
||||
support: OperationSet{
|
||||
Create,
|
||||
Update,
|
||||
Delete,
|
||||
},
|
||||
require: OperationSet{
|
||||
Create,
|
||||
Update,
|
||||
},
|
||||
out: true,
|
||||
}, {
|
||||
name: "passing: no required",
|
||||
support: OperationSet{
|
||||
Create,
|
||||
Update,
|
||||
Delete,
|
||||
},
|
||||
require: OperationSet{},
|
||||
out: true,
|
||||
}, {
|
||||
name: "passing: no required nor supported",
|
||||
support: OperationSet{},
|
||||
require: OperationSet{},
|
||||
out: true,
|
||||
}, {
|
||||
name: "failing: missing support",
|
||||
support: OperationSet{
|
||||
Create,
|
||||
Update,
|
||||
},
|
||||
require: OperationSet{
|
||||
Delete,
|
||||
},
|
||||
out: false,
|
||||
}}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
out := c.support.IsSuperset(c.require...)
|
||||
require.Equal(t, c.out, out)
|
||||
|
||||
out = c.require.IsSubset(c.support...)
|
||||
require.Equal(t, c.out, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
148
server/pkg/dal/optimization.go
Normal file
148
server/pkg/dal/optimization.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package dal
|
||||
|
||||
import "fmt"
|
||||
|
||||
type (
|
||||
// opCost provides a general idea of expensive an operation is for a
|
||||
// specific pipeline step.
|
||||
//
|
||||
// dsSize provides a general idea of how large an underlaying dataset is.
|
||||
opCost int
|
||||
dsSize int
|
||||
|
||||
OpAnalysis struct {
|
||||
ScanCost opCost
|
||||
SearchCost opCost
|
||||
FilterCost opCost
|
||||
SortCost opCost
|
||||
|
||||
OutputSize dsSize
|
||||
}
|
||||
|
||||
ppStepWrap struct {
|
||||
step PipelineStep
|
||||
|
||||
parent *ppStepWrap
|
||||
child []*ppStepWrap
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
pipelineOptimizers = []func(Pipeline) (Pipeline, error){
|
||||
pipelineClobberSteps,
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// operation computation indicators
|
||||
CostUnknown opCost = iota
|
||||
CostFree
|
||||
CostCheep
|
||||
CostAcceptable
|
||||
CostExpensive
|
||||
CostInfinite
|
||||
)
|
||||
|
||||
const (
|
||||
// dataset size indicators
|
||||
SizeUnknown dsSize = iota
|
||||
SizeTiny
|
||||
SizeSmall
|
||||
SizeMedium
|
||||
SizeLarge
|
||||
)
|
||||
|
||||
const (
|
||||
OpAnalysisIterate string = "iterate"
|
||||
OpAnalysisAggregate string = "aggregate"
|
||||
OpAnalysisJoin string = "join"
|
||||
)
|
||||
|
||||
// wrapPpSteps wraps the pipeline steps in a more processing friendly format
|
||||
// and returns a slice of leave nodes
|
||||
//
|
||||
// @todo the pipeline representation might change which will make this obsolete
|
||||
func wrapPpSteps(pp Pipeline) (leaves []*ppStepWrap) {
|
||||
ix := make(map[PipelineStep]*ppStepWrap)
|
||||
|
||||
for _, p := range pp {
|
||||
ix[p] = &ppStepWrap{step: p}
|
||||
}
|
||||
|
||||
for _, p := range pp {
|
||||
switch c := p.(type) {
|
||||
case *Aggregate:
|
||||
ix[p].child = append(ix[p].child, ix[c.rel])
|
||||
ix[c.rel].parent = ix[p]
|
||||
|
||||
case *Join:
|
||||
ix[p].child = append(ix[p].child, ix[c.relLeft])
|
||||
ix[c.relLeft].parent = ix[p]
|
||||
|
||||
ix[p].child = append(ix[p].child, ix[c.relRight])
|
||||
ix[c.relRight].parent = ix[p]
|
||||
|
||||
case *Link:
|
||||
ix[p].child = append(ix[p].child, ix[c.relLeft])
|
||||
ix[c.relLeft].parent = ix[p]
|
||||
|
||||
ix[p].child = append(ix[p].child, ix[c.relRight])
|
||||
ix[c.relRight].parent = ix[p]
|
||||
|
||||
case *Datasource:
|
||||
continue
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("impossible state: unknown pipeline step type %v", p))
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range ix {
|
||||
if len(a.child) == 0 {
|
||||
leaves = append(leaves, a)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// unwrapPpSteps unwraps the wrapped pipeline step into the classic Pipeline
|
||||
//
|
||||
// @todo the pipeline representation might change which will make this obsolete
|
||||
func unwrapPpSteps(n *ppStepWrap, seen map[*ppStepWrap]bool) (out Pipeline) {
|
||||
for i, c := range n.child {
|
||||
switch s := n.step.(type) {
|
||||
case *Aggregate:
|
||||
s.rel = c.step
|
||||
s.RelSource = c.step.Identifier()
|
||||
case *Join:
|
||||
if i == 0 {
|
||||
s.relLeft = c.step
|
||||
s.RelLeft = c.step.Identifier()
|
||||
} else {
|
||||
s.relRight = c.step
|
||||
s.RelRight = c.step.Identifier()
|
||||
}
|
||||
case *Link:
|
||||
if i == 0 {
|
||||
s.relLeft = c.step
|
||||
s.RelLeft = c.step.Identifier()
|
||||
} else {
|
||||
s.relRight = c.step
|
||||
s.RelRight = c.step.Identifier()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @todo potentially cancancel sooner
|
||||
if !seen[n] {
|
||||
out = append(out, n.step)
|
||||
}
|
||||
seen[n] = true
|
||||
|
||||
if n.parent != nil {
|
||||
out = append(unwrapPpSteps(n.parent, seen), out...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
69
server/pkg/dal/optimization_clobber.go
Normal file
69
server/pkg/dal/optimization_clobber.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package dal
|
||||
|
||||
// pipelineClobberSteps tries to reduce the amount of pipeline steps by offloading
|
||||
// higher operations to the lower levels
|
||||
//
|
||||
// As an example; the aggregation can be offloaded to the database for faster
|
||||
// execution.
|
||||
func pipelineClobberSteps(in Pipeline) (Pipeline, error) {
|
||||
if len(in) <= 1 {
|
||||
// Can't optimize further :upsidedownface:
|
||||
return in, nil
|
||||
}
|
||||
|
||||
// Outline
|
||||
// - get a nicer pipeline representation
|
||||
// @todo make the pipeline nicer in the first place
|
||||
// - traverse from the lief nodes up; try to clobber if the lief node allows it
|
||||
//
|
||||
// The clobbering for a branch ends when there is a node that can't be clobbered.
|
||||
// The progression ends because application level nodes can't be offloaded to.
|
||||
ll := wrapPpSteps(in)
|
||||
for _, l := range ll {
|
||||
for {
|
||||
// When there is no parent, we can't progress further
|
||||
if l.parent == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// if step can't clobber, skip
|
||||
cs, ok := l.step.(clobberableStep)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
|
||||
// if child fails to clobber parent, skip to the next child
|
||||
// @note for now we can end the clobbering if any of the steps
|
||||
// can't be clobbered as all of the application defined steps
|
||||
// are focused on the single op. and can't do anything else.
|
||||
if !cs.clobber(l.parent.step) {
|
||||
break
|
||||
}
|
||||
|
||||
// if clobbered successfully, update references
|
||||
if l.parent != nil && l.parent.parent != nil {
|
||||
// - update child ref of the parent's parent
|
||||
for i, c := range l.parent.parent.child {
|
||||
if c == l.parent {
|
||||
l.parent.parent.child[i] = l
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
l.parent = l.parent.parent
|
||||
|
||||
// @todo for now, clobbering ends after one successfull instance; this is due
|
||||
// to the current DB implementation doesn't allow nested things.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// convert back to the pipeline representation; update deps in the process
|
||||
out := make(Pipeline, 0, len(in))
|
||||
seen := make(map[*ppStepWrap]bool, len(in))
|
||||
for _, l := range ll {
|
||||
out = append(out, unwrapPpSteps(l, seen)...)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
179
server/pkg/dal/optimization_clobber_test.go
Normal file
179
server/pkg/dal/optimization_clobber_test.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func makeAnalysisDsAggregate() map[string]OpAnalysis {
|
||||
return map[string]OpAnalysis{
|
||||
OpAnalysisAggregate: {},
|
||||
}
|
||||
}
|
||||
|
||||
func TestClobberStep(t *testing.T) {
|
||||
t.Run("no steps", func(t *testing.T) {
|
||||
out, err := pipelineClobberSteps(nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, out)
|
||||
})
|
||||
|
||||
t.Run("one step no optimize", func(t *testing.T) {
|
||||
out, err := pipelineClobberSteps(Pipeline{&Datasource{}})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 1)
|
||||
})
|
||||
|
||||
t.Run("agg ds", func(t *testing.T) {
|
||||
ds := &Datasource{
|
||||
Ident: "ds_1",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
agg := &Aggregate{
|
||||
Ident: "agg_1",
|
||||
RelSource: "ds_1",
|
||||
rel: ds,
|
||||
}
|
||||
|
||||
out, err := pipelineClobberSteps(Pipeline{agg, ds})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 1)
|
||||
c := out[0].(*Datasource)
|
||||
require.Len(t, c.clobbered, 1)
|
||||
})
|
||||
|
||||
t.Run("agg agg ds", func(t *testing.T) {
|
||||
// @note for now we're only offloading one aggregation
|
||||
ds := &Datasource{
|
||||
Ident: "ds_1",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
agg1 := &Aggregate{
|
||||
Ident: "agg_1",
|
||||
RelSource: "ds_1",
|
||||
rel: ds,
|
||||
}
|
||||
agg2 := &Aggregate{
|
||||
Ident: "agg_2",
|
||||
RelSource: "agg_1",
|
||||
rel: agg1,
|
||||
}
|
||||
|
||||
out, err := pipelineClobberSteps(Pipeline{agg2, agg1, ds})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 2)
|
||||
c := out[1].(*Datasource)
|
||||
require.Len(t, c.clobbered, 1)
|
||||
})
|
||||
|
||||
t.Run("join ds ds", func(t *testing.T) {
|
||||
ds1 := &Datasource{
|
||||
Ident: "ds_1",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
ds2 := &Datasource{
|
||||
Ident: "ds_2",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
|
||||
join := &Join{
|
||||
Ident: "join_1",
|
||||
RelLeft: "ds_1",
|
||||
RelRight: "ds_2",
|
||||
|
||||
relLeft: ds1,
|
||||
relRight: ds2,
|
||||
}
|
||||
|
||||
out, err := pipelineClobberSteps(Pipeline{join, ds1, ds2})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 3)
|
||||
})
|
||||
|
||||
t.Run("join join ds ds ds", func(t *testing.T) {
|
||||
ds1 := &Datasource{
|
||||
Ident: "ds_1",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
ds2 := &Datasource{
|
||||
Ident: "ds_2",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
ds3 := &Datasource{
|
||||
Ident: "ds_3",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
|
||||
join1 := &Join{
|
||||
Ident: "join_1",
|
||||
RelLeft: "ds_1",
|
||||
RelRight: "ds_2",
|
||||
|
||||
relLeft: ds1,
|
||||
relRight: ds2,
|
||||
}
|
||||
join2 := &Join{
|
||||
Ident: "join_2",
|
||||
RelLeft: "join_1",
|
||||
RelRight: "ds_3",
|
||||
|
||||
relLeft: join1,
|
||||
relRight: ds3,
|
||||
}
|
||||
|
||||
out, err := pipelineClobberSteps(Pipeline{join2, join1, ds1, ds2, ds3})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 5)
|
||||
})
|
||||
|
||||
t.Run("join join agg ds ds ds", func(t *testing.T) {
|
||||
ds1 := &Datasource{
|
||||
Ident: "ds_1",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
agg := &Aggregate{
|
||||
Ident: "agg_1",
|
||||
RelSource: "ds_1",
|
||||
rel: ds1,
|
||||
}
|
||||
ds2 := &Datasource{
|
||||
Ident: "ds_2",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
ds3 := &Datasource{
|
||||
Ident: "ds_3",
|
||||
analysis: makeAnalysisDsAggregate(),
|
||||
}
|
||||
|
||||
join1 := &Join{
|
||||
Ident: "join_1",
|
||||
RelLeft: "agg_1",
|
||||
RelRight: "ds_2",
|
||||
|
||||
relLeft: agg,
|
||||
relRight: ds2,
|
||||
}
|
||||
join2 := &Join{
|
||||
Ident: "join_2",
|
||||
RelLeft: "join_1",
|
||||
RelRight: "ds_3",
|
||||
|
||||
relLeft: join1,
|
||||
relRight: ds3,
|
||||
}
|
||||
|
||||
out, err := pipelineClobberSteps(Pipeline{join2, join1, agg, ds1, ds2, ds3})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Len(t, out, 5)
|
||||
|
||||
require.Len(t, ds1.clobbered, 1)
|
||||
})
|
||||
}
|
||||
162
server/pkg/dal/pipeline.go
Normal file
162
server/pkg/dal/pipeline.go
Normal file
@@ -0,0 +1,162 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type (
|
||||
// Pipeline defines a series of steps performed over the data
|
||||
//
|
||||
// General outline of how a pipeline is used:
|
||||
// 1. Populate the pipeline with steps to define the desired outcome
|
||||
// 2. Analyze the pipeline to collect statistics and assist optimizers
|
||||
// 3. Run structure optimization which may reorder/drop steps
|
||||
// 4. Run step optimization which may re-configure individual steps such as
|
||||
// push parts of a filter to lower level, request specific sort order, ...
|
||||
// 5. Use the pipeline as an iterator to pull the data from.
|
||||
Pipeline []PipelineStep
|
||||
|
||||
// PipelineStep defines an operation performed over the data in the pipeline
|
||||
PipelineStep interface {
|
||||
Identifier() string
|
||||
Sources() []string
|
||||
Attributes() [][]AttributeMapping
|
||||
|
||||
Analyze(ctx context.Context) error
|
||||
Analysis() map[string]OpAnalysis
|
||||
}
|
||||
|
||||
// clobberableStep can have other steps offload their work to it.
|
||||
// This is primarily used to offload the work to the database.
|
||||
clobberableStep interface {
|
||||
clobber(PipelineStep) bool
|
||||
}
|
||||
|
||||
// Attribute mapping outlines specific attributes within a pipeline
|
||||
// @todo reconsider this interface; potentially remove it or split it up
|
||||
AttributeMapping interface {
|
||||
Identifier() string
|
||||
Expression() (expression string)
|
||||
Source() (ident string)
|
||||
Properties() MapProperties
|
||||
}
|
||||
|
||||
// MapProperties describe the attribute such as it's type and constraints
|
||||
MapProperties struct {
|
||||
Label string
|
||||
IsPrimary bool
|
||||
IsSystem bool
|
||||
IsFilterable bool
|
||||
IsSortable bool
|
||||
IsMultivalue bool
|
||||
MultivalueDelimiter string
|
||||
Nullable bool
|
||||
Type Type
|
||||
}
|
||||
)
|
||||
|
||||
// LinkSteps links related steps into a tree structure
|
||||
//
|
||||
// @todo make it return a new slice and not mutate the original
|
||||
func (pp Pipeline) LinkSteps() (err error) {
|
||||
// map steps by identifiers
|
||||
steps := make(map[string]PipelineStep)
|
||||
for _, s := range pp {
|
||||
steps[s.Identifier()] = s
|
||||
}
|
||||
|
||||
// Link up...
|
||||
err = func() (err error) {
|
||||
for _, s := range pp {
|
||||
switch rs := s.(type) {
|
||||
case *Aggregate:
|
||||
rs.rel = steps[rs.RelSource]
|
||||
if rs.rel == nil {
|
||||
return fmt.Errorf("aggregate: missing source relation %s", rs.RelSource)
|
||||
}
|
||||
|
||||
case *Join:
|
||||
rs.relLeft = steps[rs.RelLeft]
|
||||
rs.relRight = steps[rs.RelRight]
|
||||
if rs.relLeft == nil {
|
||||
return fmt.Errorf("join: missing left relation %s", rs.relLeft)
|
||||
}
|
||||
if rs.relRight == nil {
|
||||
return fmt.Errorf("join: missing right relation %s", rs.relRight)
|
||||
}
|
||||
|
||||
case *Link:
|
||||
rs.relLeft = steps[rs.RelLeft]
|
||||
rs.relRight = steps[rs.RelRight]
|
||||
if rs.relLeft == nil {
|
||||
return fmt.Errorf("link: missing left relation %s", rs.relLeft)
|
||||
}
|
||||
if rs.relRight == nil {
|
||||
return fmt.Errorf("link: missing right relation %s", rs.relRight)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to link steps: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pp Pipeline) root() PipelineStep {
|
||||
ix := make(map[string]PipelineStep)
|
||||
|
||||
for _, s := range pp {
|
||||
ix[s.Identifier()] = s
|
||||
}
|
||||
|
||||
for _, s := range pp {
|
||||
for _, src := range s.Sources() {
|
||||
delete(ix, src)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range ix {
|
||||
return ix[s.Identifier()]
|
||||
}
|
||||
|
||||
panic("impossible state: no root-level pipeline step")
|
||||
}
|
||||
|
||||
// Slice returns a new pipeline with all steps of the subtree with ident as root
|
||||
func (pp Pipeline) Slice(ident string) (out Pipeline) {
|
||||
// Find root
|
||||
var r PipelineStep
|
||||
for _, p := range pp {
|
||||
if p.Identifier() == ident {
|
||||
r = p
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return pp.slice(r)
|
||||
}
|
||||
|
||||
// slice is the recursive counterpart for the Slice method
|
||||
func (pp Pipeline) slice(s PipelineStep) (out Pipeline) {
|
||||
out = append(out, s)
|
||||
|
||||
switch n := s.(type) {
|
||||
case *Datasource:
|
||||
return
|
||||
|
||||
case *Aggregate:
|
||||
return append(out, pp.slice(n.rel)...)
|
||||
|
||||
case *Join:
|
||||
return append(out, append(pp.slice(n.relLeft), pp.slice(n.relRight)...)...)
|
||||
|
||||
case *Link:
|
||||
return append(out, append(pp.slice(n.relLeft), pp.slice(n.relRight)...)...)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
146
server/pkg/dal/rel_index.go
Normal file
146
server/pkg/dal/rel_index.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type (
|
||||
// relIndex is a generic struct for indexing data which join/link can use
|
||||
//
|
||||
// The current index implementation utilizes a series of hashmaps based on what
|
||||
// type of values we're indexing on.
|
||||
// As we currently only support single value predicate, the hashmaps proved
|
||||
// to be a bit more efficient then b-trees (other consideration) when testing
|
||||
// on larger datasets.
|
||||
//
|
||||
// @todo when we support multiple join predicates, this should probably change
|
||||
// into a b-tree as it might be a bit faster thennested hashmaps.
|
||||
//
|
||||
// @todo do some benchmarks in regards to using generics for key
|
||||
relIndex struct {
|
||||
track []string
|
||||
|
||||
keyType Type
|
||||
|
||||
ints map[int64]*relIndexBuffer
|
||||
strings map[string]*relIndexBuffer
|
||||
ids map[uint64]*relIndexBuffer
|
||||
}
|
||||
)
|
||||
|
||||
// newRelIndex initializes a new relIndex with the specified tracked attributes
|
||||
// @todo benchmark with generics as key
|
||||
func newRelIndex(t Type, track ...string) (out *relIndex, err error) {
|
||||
out = &relIndex{
|
||||
track: track,
|
||||
keyType: t,
|
||||
}
|
||||
|
||||
return out, out.initHashmaps()
|
||||
}
|
||||
|
||||
// Add adds a new row to the index under the specified key
|
||||
func (ri *relIndex) Add(k any, r *Row) {
|
||||
switch ri.keyType.(type) {
|
||||
case TypeNumber, *TypeNumber:
|
||||
ri.addInt(cast.ToInt64(k), r)
|
||||
return
|
||||
|
||||
case TypeText, *TypeText:
|
||||
ri.addString(cast.ToString(k), r)
|
||||
return
|
||||
|
||||
case TypeID, *TypeID,
|
||||
TypeRef, *TypeRef:
|
||||
ri.addID(cast.ToUint64(k), r)
|
||||
return
|
||||
}
|
||||
|
||||
// @note this is validated when initializing
|
||||
panic(fmt.Sprintf("cannot use type %s as index key", ri.keyType.Type()))
|
||||
}
|
||||
|
||||
func (ri *relIndex) Get(k any) (out *relIndexBuffer, ok bool) {
|
||||
switch ri.keyType.(type) {
|
||||
case TypeNumber, *TypeNumber:
|
||||
return ri.getInt(cast.ToInt64(k))
|
||||
|
||||
case TypeText, *TypeText:
|
||||
return ri.getString(cast.ToString(k))
|
||||
|
||||
case TypeID, *TypeID,
|
||||
TypeRef, *TypeRef:
|
||||
return ri.getID(cast.ToUint64(k))
|
||||
}
|
||||
|
||||
// @note this is validated when initializing
|
||||
panic(fmt.Sprintf("cannot use type %s as index key", ri.keyType.Type()))
|
||||
}
|
||||
|
||||
// Clear clears out the index omitting the need to reinitialize
|
||||
func (ri *relIndex) Clear() (err error) {
|
||||
return ri.initHashmaps()
|
||||
}
|
||||
|
||||
func (ri *relIndex) initHashmaps() (err error) {
|
||||
// @note initHashmaps initializes only the ones we need to save up on space
|
||||
switch ri.keyType.(type) {
|
||||
case TypeNumber, *TypeNumber:
|
||||
ri.ints = make(map[int64]*relIndexBuffer, 512)
|
||||
return
|
||||
|
||||
case TypeText, *TypeText:
|
||||
ri.strings = make(map[string]*relIndexBuffer, 512)
|
||||
return
|
||||
|
||||
case TypeID, *TypeID,
|
||||
TypeRef, *TypeRef:
|
||||
ri.ids = make(map[uint64]*relIndexBuffer, 512)
|
||||
return
|
||||
}
|
||||
return fmt.Errorf("cannot use type %s as index key", ri.keyType.Type())
|
||||
}
|
||||
|
||||
func (ri *relIndex) addInt(k int64, r *Row) {
|
||||
c, ok := ri.getInt(k)
|
||||
if !ok {
|
||||
c = newRelIndexBuffer(ri.track...)
|
||||
ri.ints[k] = c
|
||||
}
|
||||
c.add(r)
|
||||
}
|
||||
|
||||
func (ri *relIndex) getInt(k int64) (out *relIndexBuffer, ok bool) {
|
||||
out, ok = ri.ints[k]
|
||||
return
|
||||
}
|
||||
|
||||
func (ri *relIndex) addString(k string, r *Row) {
|
||||
c, ok := ri.getString(k)
|
||||
if !ok {
|
||||
c = newRelIndexBuffer(ri.track...)
|
||||
ri.strings[k] = c
|
||||
}
|
||||
c.add(r)
|
||||
}
|
||||
|
||||
func (ri *relIndex) getString(k string) (out *relIndexBuffer, ok bool) {
|
||||
out, ok = ri.strings[k]
|
||||
return
|
||||
}
|
||||
|
||||
func (ri *relIndex) addID(k uint64, r *Row) {
|
||||
c, ok := ri.getID(k)
|
||||
if !ok {
|
||||
c = newRelIndexBuffer(ri.track...)
|
||||
ri.ids[k] = c
|
||||
}
|
||||
c.add(r)
|
||||
}
|
||||
|
||||
func (ri *relIndex) getID(k uint64) (out *relIndexBuffer, ok bool) {
|
||||
out, ok = ri.ids[k]
|
||||
return
|
||||
}
|
||||
71
server/pkg/dal/rel_index_buffer.go
Normal file
71
server/pkg/dal/rel_index_buffer.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package dal
|
||||
|
||||
type (
|
||||
relIndexBuffer struct {
|
||||
// attribute statistics to track
|
||||
//
|
||||
// These stats can be used when performing operations like sorting and
|
||||
// binary searching for specific values.
|
||||
track []string
|
||||
|
||||
min map[string]any
|
||||
max map[string]any
|
||||
|
||||
// @todo make b-tree for sorting? will probably be small so simple slice sort
|
||||
// should be ok ig...
|
||||
//
|
||||
// Probably don't need to sort yet
|
||||
rows []*Row
|
||||
}
|
||||
)
|
||||
|
||||
// newRelIndexBuffer initializes a new relIndexBuffer tracking the given attributes
|
||||
func newRelIndexBuffer(tt ...string) *relIndexBuffer {
|
||||
return &relIndexBuffer{
|
||||
min: make(map[string]any),
|
||||
max: make(map[string]any),
|
||||
track: tt,
|
||||
}
|
||||
}
|
||||
|
||||
// add adds a new *row to the buffer
|
||||
func (lc *relIndexBuffer) add(r *Row) {
|
||||
if len(lc.rows) == 0 {
|
||||
for _, ix := range lc.track {
|
||||
lc.min[ix] = r.values[ix][0]
|
||||
lc.max[ix] = r.values[ix][0]
|
||||
}
|
||||
|
||||
lc.rows = append(lc.rows, r)
|
||||
return
|
||||
}
|
||||
|
||||
lc.updMin(r)
|
||||
lc.updMax(r)
|
||||
|
||||
lc.rows = append(lc.rows, r)
|
||||
}
|
||||
|
||||
// updMin updates the min stat
|
||||
func (lc *relIndexBuffer) updMin(r *Row) {
|
||||
for _, ix := range lc.track {
|
||||
for i := uint(0); i < r.CountValues()[ix]; i++ {
|
||||
v := r.values[ix][i]
|
||||
if compareValues(v, lc.min[ix]) < 0 {
|
||||
lc.min[ix] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updMax updates the max stat
|
||||
func (lc *relIndexBuffer) updMax(r *Row) {
|
||||
for _, ix := range lc.track {
|
||||
for i := uint(0); i < r.CountValues()[ix]; i++ {
|
||||
v := r.values[ix][i]
|
||||
if compareValues(v, lc.max[ix]) > 0 {
|
||||
lc.max[ix] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
31
server/pkg/dal/rel_index_buffer_test.go
Normal file
31
server/pkg/dal/rel_index_buffer_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRelIndexBuffer(t *testing.T) {
|
||||
cc := newRelIndexBuffer("a", "b")
|
||||
|
||||
cc.add((&Row{}).WithValue("a", 0, 1).WithValue("b", 0, "a"))
|
||||
|
||||
require.Len(t, cc.rows, 1)
|
||||
require.Equal(t, 1, cc.min["a"])
|
||||
require.Equal(t, 1, cc.max["a"])
|
||||
require.Equal(t, "a", cc.min["b"])
|
||||
require.Equal(t, "a", cc.max["b"])
|
||||
|
||||
cc.add((&Row{}).WithValue("a", 0, -1).WithValue("b", 0, "a"))
|
||||
require.Equal(t, -1, cc.min["a"])
|
||||
require.Equal(t, 1, cc.max["a"])
|
||||
require.Equal(t, "a", cc.min["b"])
|
||||
require.Equal(t, "a", cc.max["b"])
|
||||
|
||||
cc.add((&Row{}).WithValue("a", 0, 2).WithValue("b", 0, "aa"))
|
||||
require.Equal(t, -1, cc.min["a"])
|
||||
require.Equal(t, 2, cc.max["a"])
|
||||
require.Equal(t, "a", cc.min["b"])
|
||||
require.Equal(t, "aa", cc.max["b"])
|
||||
}
|
||||
19
server/pkg/dal/runner.go
Normal file
19
server/pkg/dal/runner.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type (
|
||||
tester interface {
|
||||
// Test returns a boolean output based on the expression, mostly used for filters
|
||||
// @todo remove the error and rely on validator so make sure everything is valid
|
||||
Test(ctx context.Context, params any) (bool, error)
|
||||
}
|
||||
|
||||
evaluator interface {
|
||||
// Eval returns some value based on the expression, mostly used for attribute eval
|
||||
// @todo remove the error and rely on validator so make sure eve
|
||||
Eval(ctx context.Context, params any) (any, error)
|
||||
}
|
||||
)
|
||||
472
server/pkg/dal/runner_gval.go
Normal file
472
server/pkg/dal/runner_gval.go
Normal file
@@ -0,0 +1,472 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/PaesslerAG/gval"
|
||||
"github.com/cortezaproject/corteza/server/pkg/expr"
|
||||
"github.com/cortezaproject/corteza/server/pkg/gvalfnc"
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
)
|
||||
|
||||
type (
|
||||
runnerGval struct {
|
||||
eval gval.Evaluable
|
||||
}
|
||||
converterGval struct {
|
||||
parser *ql.Parser
|
||||
}
|
||||
exprHandlerGval struct {
|
||||
Handler func(...string) string
|
||||
|
||||
// @todo temporarily added to support the current aggregate attribute type calculation
|
||||
OutType Type
|
||||
OutTypeUnknown bool
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
globalGvalConverter converterGval
|
||||
)
|
||||
|
||||
var (
|
||||
refToGvalExp = map[string]*exprHandlerGval{
|
||||
// operators
|
||||
"not": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("(! %s)", args[0])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
|
||||
// - bool
|
||||
"and": {
|
||||
Handler: func(args ...string) string {
|
||||
return strings.Join(args, " && ")
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
"or": {
|
||||
Handler: func(args ...string) string {
|
||||
return strings.Join(args, " || ")
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
"xor": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("(%s != %s)", args[0], args[1])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
|
||||
// - comp.
|
||||
"eq": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s == %s", args[0], args[1])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
"ne": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s != %s", args[0], args[1])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
"lt": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s < %s", args[0], args[1])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
"le": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s <= %s", args[0], args[1])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
"gt": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s > %s", args[0], args[1])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
"ge": {
|
||||
// Handler: makeGenericCompHandler(">="),
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s >= %s", args[0], args[1])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
|
||||
// - math
|
||||
"add": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s + %s", args[0], args[1])
|
||||
},
|
||||
OutTypeUnknown: true,
|
||||
},
|
||||
"sub": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s - %s", args[0], args[1])
|
||||
},
|
||||
OutTypeUnknown: true,
|
||||
},
|
||||
"mult": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s * %s", args[0], args[1])
|
||||
},
|
||||
OutTypeUnknown: true,
|
||||
},
|
||||
"div": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("%s / %s", args[0], args[1])
|
||||
},
|
||||
OutTypeUnknown: true,
|
||||
},
|
||||
|
||||
"in": {
|
||||
Handler: func(args ...string) string {
|
||||
// The arguments must be reversed!!
|
||||
return fmt.Sprintf("has(%s, %s)", args[1], args[0])
|
||||
},
|
||||
OutTypeUnknown: true,
|
||||
},
|
||||
"nin": {
|
||||
Handler: func(args ...string) string {
|
||||
// The arguments must be reversed!!
|
||||
return fmt.Sprintf("!has(%s, %s)", args[1], args[0])
|
||||
},
|
||||
OutTypeUnknown: true,
|
||||
},
|
||||
|
||||
// - strings
|
||||
"concat": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("concat(%s)", strings.Join(args, ", "))
|
||||
},
|
||||
OutType: &TypeText{},
|
||||
},
|
||||
"instr": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("instr(%s, %s)", args[0], args[1])
|
||||
},
|
||||
OutType: &TypeNumber{},
|
||||
},
|
||||
|
||||
// @todo implement; the commented versions are not good enough
|
||||
// "like": {
|
||||
// Handler: func(args ...string) string {
|
||||
// // @todo better regex construction
|
||||
// nn := strings.Replace(strings.Trim(args[1], "\""), "%", ".*", -1)
|
||||
// nn = strings.Replace(nn, "_", ".[1]", -1)
|
||||
|
||||
// return fmt.Sprintf("%s =~ ^%s$", args[0], nn)
|
||||
// },
|
||||
// },
|
||||
// "nlike": {
|
||||
// Handler: func(args ...string) string {
|
||||
// // @todo better regex construction
|
||||
// nn := strings.Replace(args[1], "%", ".*", -1)
|
||||
// nn = strings.Replace(nn, "_", ".[1]", -1)
|
||||
|
||||
// return fmt.Sprintf("!(%s =~ ^%s$)", args[0], nn)
|
||||
// },
|
||||
// },
|
||||
|
||||
// "is": {
|
||||
// Handler: func(args ...string) string {
|
||||
// return fmt.Sprintf("%s == %s", args[0], args[1])
|
||||
// }
|
||||
// }
|
||||
|
||||
// - filtering
|
||||
"now": {
|
||||
Handler: func(args ...string) string {
|
||||
return "now()"
|
||||
},
|
||||
OutType: &TypeTimestamp{},
|
||||
},
|
||||
"quarter": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("quarter(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeNumber{},
|
||||
},
|
||||
"year": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("year(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeNumber{},
|
||||
},
|
||||
"month": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("month(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeNumber{},
|
||||
},
|
||||
"date": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("date(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeDate{},
|
||||
},
|
||||
"day": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("day(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeNumber{},
|
||||
},
|
||||
"week": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("week(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeNumber{},
|
||||
},
|
||||
"date_format": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("strftime(%s, %s)", args[0], args[1])
|
||||
},
|
||||
OutType: &TypeText{},
|
||||
},
|
||||
|
||||
// generic stuff
|
||||
"null": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("isNil(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
"nnull": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("!isNil(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
"exists": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("!isNil(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeBoolean{},
|
||||
},
|
||||
|
||||
// - typecast
|
||||
"float": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("float(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeNumber{},
|
||||
},
|
||||
"int": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("int(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeNumber{},
|
||||
},
|
||||
"string": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("string(%s)", args[0])
|
||||
},
|
||||
OutType: &TypeText{},
|
||||
},
|
||||
|
||||
"group": {
|
||||
Handler: func(args ...string) string {
|
||||
return fmt.Sprintf("(%s)", args[0])
|
||||
},
|
||||
OutTypeUnknown: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// newConverterGval initializes a new gval exp. converter
|
||||
func newConverterGval(ii ...ql.IdentHandler) converterGval {
|
||||
if globalGvalConverter.parser == nil {
|
||||
globalGvalConverter = converterGval{
|
||||
parser: newQlParser(append(
|
||||
ii,
|
||||
// This should always happen for gval expressions
|
||||
func(i ql.Ident) (ql.Ident, error) {
|
||||
i.Value = wrapNestedGvalIdent(i.Value)
|
||||
return i, nil
|
||||
},
|
||||
)...),
|
||||
}
|
||||
}
|
||||
|
||||
return globalGvalConverter
|
||||
}
|
||||
|
||||
// newRunnerGval initializes a new gval exp. runner from the provided expression
|
||||
func newRunnerGval(expr string) (out *runnerGval, err error) {
|
||||
out = &runnerGval{}
|
||||
|
||||
c := newConverterGval()
|
||||
n, err := c.Parse(expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expr, err = c.Convert(n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out.eval, err = newGval(expr)
|
||||
return
|
||||
}
|
||||
|
||||
// newRunnerGvalParsed initializes a new gval exp. runner from the pre-parsed expression
|
||||
func newRunnerGvalParsed(n *ql.ASTNode) (out *runnerGval, err error) {
|
||||
out = &runnerGval{}
|
||||
c := newConverterGval()
|
||||
|
||||
expr, err := c.Convert(n)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
out.eval, err = newGval(expr)
|
||||
return
|
||||
}
|
||||
|
||||
// newGval initializes a new gval evaluatable for the provided expression
|
||||
//
|
||||
// The eval. includes a small subset of supported expr. functions which may be
|
||||
// used in the pipeline.
|
||||
//
|
||||
// @note the subset is limited to simplify the (eventual) offloading to the DB.
|
||||
// At some point, more functions will be supported, and the ones which can't
|
||||
// be offloaded will be performed in some exec. step.
|
||||
func newGval(e string) (gval.Evaluable, error) {
|
||||
return gval.Full(
|
||||
// Extra functions we'll need
|
||||
// @note don't bring in all of the expr. pkg functions as we'll need to
|
||||
// support these on the DB as well
|
||||
gval.Function("now", gvalfnc.Now),
|
||||
gval.Function("quarter", gvalfnc.Quarter),
|
||||
gval.Function("year", gvalfnc.Year),
|
||||
gval.Function("month", gvalfnc.Month),
|
||||
gval.Function("strftime", gvalfnc.StrfTime),
|
||||
gval.Function("date", gvalfnc.Date),
|
||||
gval.Function("day", gvalfnc.Day),
|
||||
gval.Function("isNil", gvalfnc.IsNil),
|
||||
gval.Function("float", gvalfnc.CastFloat),
|
||||
gval.Function("int", gvalfnc.CastInt),
|
||||
gval.Function("string", gvalfnc.CastString),
|
||||
gval.Function("concat", gvalfnc.ConcatStrings),
|
||||
gval.Function("has", arrHas),
|
||||
).NewEvaluable(e)
|
||||
}
|
||||
|
||||
func newQlParser(onIdent ...ql.IdentHandler) *ql.Parser {
|
||||
pp := ql.NewParser()
|
||||
pp.OnIdent = func(ident ql.Ident) (_ ql.Ident, err error) {
|
||||
ident.Value = NormalizeAttrNames(ident.Value)
|
||||
|
||||
for _, oi := range onIdent {
|
||||
ident, err = oi(ident)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return ident, err
|
||||
}
|
||||
|
||||
return pp
|
||||
}
|
||||
|
||||
func (e *runnerGval) Test(ctx context.Context, rows any) (bool, error) {
|
||||
o, err := e.eval(ctx, rows)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
v, ok := o.(bool)
|
||||
return ok && v, nil
|
||||
}
|
||||
|
||||
func (e *runnerGval) Eval(ctx context.Context, rows any) (any, error) {
|
||||
o, err := e.eval(ctx, rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// Parse parses the QL expression into QL ASTNodes
|
||||
func (c converterGval) Parse(expr string) (*ql.ASTNode, error) {
|
||||
return c.parser.Parse(expr)
|
||||
}
|
||||
|
||||
// Convert converts the given nodes into a GVal expression
|
||||
// @todo add more validation so we can potentially omit exec. error checks
|
||||
func (c converterGval) Convert(n *ql.ASTNode) (expr string, err error) {
|
||||
return c.convert(n)
|
||||
}
|
||||
|
||||
func (c converterGval) convert(n *ql.ASTNode) (_ string, err error) {
|
||||
switch {
|
||||
case n.Symbol != "":
|
||||
return n.Symbol, nil
|
||||
case n.Value != nil:
|
||||
// @todo I don't think this is quite ok, but it works for now so it'll do for now
|
||||
if n.Value.V.Type() == "String" {
|
||||
return fmt.Sprintf("\"%v\"", n.Value.V.Get()), nil
|
||||
}
|
||||
return fmt.Sprintf("%v", n.Value.V.Get()), nil
|
||||
}
|
||||
|
||||
args := make([]string, len(n.Args))
|
||||
for i, a := range n.Args {
|
||||
args[i], err = c.convert(a)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return c.refHandler(n, args...)
|
||||
}
|
||||
|
||||
func (c converterGval) refHandler(n *ql.ASTNode, args ...string) (out string, err error) {
|
||||
r := strings.ToLower(n.Ref)
|
||||
if refToGvalExp[r] == nil {
|
||||
return "", fmt.Errorf("unknown ref %q", n.Ref)
|
||||
}
|
||||
return refToGvalExp[r].Handler(args...), nil
|
||||
}
|
||||
|
||||
// arrHas is a helper to assure the expr.Has always gets an array (or map) as
|
||||
// the first argument
|
||||
//
|
||||
// @todo this is needed because how the ValueGetters returns multi-value fields so
|
||||
// an edge case where a field would have [a] but here, it would be presented
|
||||
// as a.
|
||||
// This would become obsolete when we address the actual issue.
|
||||
func arrHas(arr interface{}, vv ...interface{}) (b bool, err error) {
|
||||
arr = expr.UntypedValue(arr)
|
||||
|
||||
if isMap(arr) {
|
||||
return expr.Has(arr, vv...)
|
||||
}
|
||||
|
||||
var (
|
||||
c = reflect.ValueOf(arr)
|
||||
)
|
||||
|
||||
switch c.Kind() {
|
||||
case reflect.Slice:
|
||||
return expr.Has(arr, vv...)
|
||||
|
||||
default:
|
||||
return expr.Has([]any{arr}, vv...)
|
||||
}
|
||||
}
|
||||
|
||||
func isMap(v interface{}) bool {
|
||||
return reflect.TypeOf(v).Kind() == reflect.Map
|
||||
}
|
||||
181
server/pkg/dal/runner_gval_test.go
Normal file
181
server/pkg/dal/runner_gval_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRowEvaluatorTest(t *testing.T) {
|
||||
tc := []struct {
|
||||
name string
|
||||
expr string
|
||||
in *Row
|
||||
out bool
|
||||
}{{
|
||||
name: "single row ok",
|
||||
expr: `test == 10`,
|
||||
in: (&Row{}).WithValue("test", 0, 10),
|
||||
out: true,
|
||||
}, {
|
||||
name: "single row nok",
|
||||
expr: `test == 11`,
|
||||
in: (&Row{}).WithValue("test", 0, 10),
|
||||
out: false,
|
||||
}}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
evl, err := newRunnerGval(c.expr)
|
||||
require.NoError(t, err)
|
||||
tt, err := evl.Test(ctx, c.in)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.out, tt)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandlers(t *testing.T) {
|
||||
yr := time.Now().Year()
|
||||
mnt := int(time.Now().Month())
|
||||
qtr := mnt / 4
|
||||
dy := time.Now().Day()
|
||||
|
||||
// @todo some of the tests were omitted because they would be too flaky
|
||||
// or can not yet pass (the gval nil handlers primarily)
|
||||
tcc := []struct {
|
||||
name string
|
||||
expr string
|
||||
in simpleRow
|
||||
out any
|
||||
}{
|
||||
{
|
||||
name: "not",
|
||||
expr: `!true`,
|
||||
in: simpleRow{},
|
||||
out: false,
|
||||
},
|
||||
{
|
||||
name: "and",
|
||||
expr: `true && false`,
|
||||
in: simpleRow{},
|
||||
out: false,
|
||||
},
|
||||
{
|
||||
name: "or",
|
||||
expr: `false || true`,
|
||||
in: simpleRow{},
|
||||
out: true,
|
||||
},
|
||||
{
|
||||
name: "xor",
|
||||
expr: `'a' xor 'b'`,
|
||||
in: simpleRow{},
|
||||
out: true,
|
||||
},
|
||||
{
|
||||
name: "eq",
|
||||
expr: `1 == 2`,
|
||||
in: simpleRow{},
|
||||
out: false,
|
||||
},
|
||||
{
|
||||
name: "ne",
|
||||
expr: `3 != 2`,
|
||||
in: simpleRow{},
|
||||
out: true,
|
||||
},
|
||||
{
|
||||
name: "lt",
|
||||
expr: `1 < 2`,
|
||||
in: simpleRow{},
|
||||
out: true,
|
||||
},
|
||||
{
|
||||
name: "le",
|
||||
expr: `1 <= 2`,
|
||||
in: simpleRow{},
|
||||
out: true,
|
||||
},
|
||||
{
|
||||
name: "gt",
|
||||
expr: `2 > 1`,
|
||||
in: simpleRow{},
|
||||
out: true,
|
||||
},
|
||||
{
|
||||
name: "ge",
|
||||
expr: `1 >= 2`,
|
||||
in: simpleRow{},
|
||||
out: false,
|
||||
},
|
||||
{
|
||||
name: "add",
|
||||
expr: `1+3`,
|
||||
in: simpleRow{},
|
||||
out: float64(4),
|
||||
},
|
||||
{
|
||||
name: "sub",
|
||||
expr: `3-5`,
|
||||
in: simpleRow{},
|
||||
out: float64(-2),
|
||||
},
|
||||
{
|
||||
name: "mult",
|
||||
expr: `1*2`,
|
||||
in: simpleRow{},
|
||||
out: float64(2),
|
||||
},
|
||||
{
|
||||
name: "div",
|
||||
expr: `2/2`,
|
||||
in: simpleRow{},
|
||||
out: float64(1),
|
||||
},
|
||||
{
|
||||
name: "concat",
|
||||
expr: `concat('a', 'b')`,
|
||||
in: simpleRow{},
|
||||
out: "ab",
|
||||
},
|
||||
{
|
||||
name: "quarter",
|
||||
expr: `quarter(now())`,
|
||||
in: simpleRow{},
|
||||
out: qtr,
|
||||
},
|
||||
{
|
||||
name: "year",
|
||||
expr: `year(now())`,
|
||||
in: simpleRow{},
|
||||
out: yr,
|
||||
},
|
||||
{
|
||||
name: "month",
|
||||
expr: `month(now())`,
|
||||
in: simpleRow{},
|
||||
out: mnt,
|
||||
},
|
||||
{
|
||||
name: "day",
|
||||
expr: `day(now())`,
|
||||
in: simpleRow{},
|
||||
out: dy,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcc {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
evl, err := newRunnerGval(tc.expr)
|
||||
require.NoError(t, err)
|
||||
out, err := evl.Eval(context.Background(), tc.in)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.out, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
109
server/pkg/dal/sensitivity_level.go
Normal file
109
server/pkg/dal/sensitivity_level.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package dal
|
||||
|
||||
import "sort"
|
||||
|
||||
type (
|
||||
SensitivityLevel struct {
|
||||
Handle string
|
||||
ID uint64
|
||||
Level int
|
||||
}
|
||||
SensitivityLevelSet []SensitivityLevel
|
||||
|
||||
sensitivityLevelIndex struct {
|
||||
set SensitivityLevelSet
|
||||
|
||||
byHandle map[string]int
|
||||
byID map[uint64]int
|
||||
}
|
||||
|
||||
SensitivityLevelUsage struct {
|
||||
connections []map[string]any
|
||||
modules []map[string]any
|
||||
fields []map[string]any
|
||||
}
|
||||
)
|
||||
|
||||
func SensitivityLevelIndex(levels ...SensitivityLevel) *sensitivityLevelIndex {
|
||||
out := &sensitivityLevelIndex{
|
||||
set: make(SensitivityLevelSet, len(levels)),
|
||||
byHandle: make(map[string]int),
|
||||
byID: make(map[uint64]int),
|
||||
}
|
||||
|
||||
for i, l := range levels {
|
||||
out.set[i] = l
|
||||
out.byHandle[l.Handle] = i
|
||||
out.byID[l.ID] = i
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (sli sensitivityLevelIndex) with(levels ...SensitivityLevel) *sensitivityLevelIndex {
|
||||
slvls := append(sli.set, levels...)
|
||||
sort.Sort(slvls)
|
||||
|
||||
return SensitivityLevelIndex(slvls...)
|
||||
}
|
||||
|
||||
func (sli sensitivityLevelIndex) without(levels ...SensitivityLevel) *sensitivityLevelIndex {
|
||||
nn := make(SensitivityLevelSet, 0, len(sli.set))
|
||||
|
||||
remIndex := SensitivityLevelIndex(levels...)
|
||||
|
||||
for _, existing := range sli.set {
|
||||
if !remIndex.includes(existing.ID) {
|
||||
nn = append(nn, existing)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(nn)
|
||||
|
||||
return SensitivityLevelIndex(nn...)
|
||||
}
|
||||
|
||||
func (sli sensitivityLevelIndex) includes(l uint64) (ok bool) {
|
||||
if l == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
if sli.byID == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
_, ok = sli.byID[l]
|
||||
return
|
||||
}
|
||||
|
||||
func (sli sensitivityLevelIndex) isSubset(a, b uint64) (ok bool) {
|
||||
// Edgecases
|
||||
// If A is zero theneverything is possible
|
||||
if a == 0 {
|
||||
return true
|
||||
}
|
||||
// If B is zero, then A must also be zero
|
||||
if b == 0 {
|
||||
return a == 0
|
||||
}
|
||||
|
||||
var lvlA, lvlB int
|
||||
|
||||
if lvlA, ok = sli.byID[a]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if lvlB, ok = sli.byID[b]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return lvlA <= lvlB
|
||||
}
|
||||
|
||||
func (a SensitivityLevelSet) Len() int { return len(a) }
|
||||
func (a SensitivityLevelSet) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a SensitivityLevelSet) Less(i, j int) bool { return a[i].Level < a[j].Level }
|
||||
|
||||
func (u SensitivityLevelUsage) Empty() bool {
|
||||
return len(u.connections)+len(u.fields)+len(u.modules) == 0
|
||||
}
|
||||
95
server/pkg/dal/sensitivity_level_test.go
Normal file
95
server/pkg/dal/sensitivity_level_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSensitivityLevelIncluded(t *testing.T) {
|
||||
t.Run("does not include; empty", func(t *testing.T) {
|
||||
ll := sensitivityLevelIndex{}
|
||||
|
||||
require.False(t, ll.includes(1))
|
||||
})
|
||||
|
||||
t.Run("does not include; not in there", func(t *testing.T) {
|
||||
ll := SensitivityLevelIndex(SensitivityLevel{
|
||||
Handle: "a",
|
||||
ID: 0,
|
||||
Level: 1,
|
||||
})
|
||||
|
||||
require.False(t, ll.includes(1))
|
||||
})
|
||||
|
||||
t.Run("includes", func(t *testing.T) {
|
||||
ll := SensitivityLevelIndex(SensitivityLevel{
|
||||
Handle: "a",
|
||||
ID: 1,
|
||||
Level: 1,
|
||||
})
|
||||
|
||||
require.True(t, ll.includes(1))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSensitivityLevelSubset(t *testing.T) {
|
||||
commonLevels := SensitivityLevelSet{{
|
||||
ID: 1,
|
||||
Handle: "a",
|
||||
Level: 1,
|
||||
}, {
|
||||
ID: 2,
|
||||
Handle: "b",
|
||||
Level: 2,
|
||||
}, {
|
||||
ID: 3,
|
||||
Handle: "c",
|
||||
Level: 3,
|
||||
}}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
a uint64
|
||||
b uint64
|
||||
out bool
|
||||
}{{
|
||||
name: "lower is zero",
|
||||
a: 0,
|
||||
b: 1,
|
||||
out: true,
|
||||
}, {
|
||||
name: "both zero",
|
||||
a: 0,
|
||||
b: 0,
|
||||
out: true,
|
||||
}, {
|
||||
name: "upper zero, lower not",
|
||||
a: 1,
|
||||
b: 0,
|
||||
out: false,
|
||||
}, {
|
||||
name: "lower less",
|
||||
a: 1,
|
||||
b: 2,
|
||||
out: true,
|
||||
}, {
|
||||
name: "equal",
|
||||
a: 2,
|
||||
b: 2,
|
||||
out: true,
|
||||
}, {
|
||||
name: "lower higher",
|
||||
a: 3,
|
||||
b: 2,
|
||||
out: false,
|
||||
}}
|
||||
|
||||
ix := SensitivityLevelIndex(commonLevels...)
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
require.Equal(t, c.out, ix.isSubset(c.a, c.b))
|
||||
})
|
||||
}
|
||||
}
|
||||
1378
server/pkg/dal/service.go
Normal file
1378
server/pkg/dal/service.go
Normal file
File diff suppressed because it is too large
Load Diff
512
server/pkg/dal/utils.go
Normal file
512
server/pkg/dal/utils.go
Normal file
@@ -0,0 +1,512 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/cortezaproject/corteza/server/pkg/ql"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
type (
|
||||
// @todo consider reworking this
|
||||
SimpleAttr struct {
|
||||
Ident string
|
||||
Expr string
|
||||
Src string
|
||||
Props MapProperties
|
||||
}
|
||||
|
||||
// Row is a generic implementation for ValueGetter and ValueSetter
|
||||
//
|
||||
// Primarily used within DAL pipeline execution steps, but may also be used
|
||||
// outside.
|
||||
Row struct {
|
||||
counters map[string]uint
|
||||
values valueSet
|
||||
|
||||
// Metadata to make it easier to work with
|
||||
// @todo add when needed
|
||||
}
|
||||
|
||||
valueSet map[string][]any
|
||||
)
|
||||
|
||||
const (
|
||||
attributeNestingSeparator = "."
|
||||
attributeNestingGvalSeparator = "___DLTR___"
|
||||
)
|
||||
|
||||
func (sa SimpleAttr) Identifier() string { return sa.Ident }
|
||||
func (sa SimpleAttr) Expression() (expression string) { return sa.Expr }
|
||||
func (sa SimpleAttr) Source() (ident string) { return sa.Src }
|
||||
func (sa SimpleAttr) Properties() MapProperties { return sa.Props }
|
||||
|
||||
// WithValue is a simple helper to construct rows with populated values
|
||||
//
|
||||
// @note The main use is for tests so restrain from using it in code.
|
||||
func (r *Row) WithValue(name string, pos uint, v any) *Row {
|
||||
err := r.SetValue(name, pos, v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r Row) SelectGVal(ctx context.Context, wk string) (interface{}, error) {
|
||||
// @todo this way of determining multivalue is not entirely ok but we can get away with it
|
||||
// since we have special functions to work with those.
|
||||
// We need to somehow inform this bit as to what fields are multi value.
|
||||
|
||||
k := unwrapNestedGvalIdent(wk)
|
||||
cc := r.CountValues()[k]
|
||||
if cc == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if cc == 1 {
|
||||
return r.GetValue(k, 0)
|
||||
}
|
||||
|
||||
out := make([]any, cc)
|
||||
for i := uint(0); i < cc; i++ {
|
||||
out[i], _ = r.GetValue(k, i)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Reset clears out the row so the same instance can be reused where possible
|
||||
//
|
||||
// Important: Reset only clears out the counters and does not re-init/clear out
|
||||
// the underlaying values. Don't directly iterate over the values, but use the
|
||||
// counters.
|
||||
func (r *Row) Reset() {
|
||||
for k := range r.counters {
|
||||
r.counters[k] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Row) SetValue(name string, pos uint, v any) error {
|
||||
if r.values == nil {
|
||||
r.values = make(valueSet)
|
||||
}
|
||||
if r.counters == nil {
|
||||
r.counters = make(map[string]uint)
|
||||
}
|
||||
|
||||
// Make sure there is space for it
|
||||
// @note benchmarking proves that the rest of the function introduces a lot of memory pressure.
|
||||
// Investigate options on reworking this/reducing allocations.
|
||||
if int(pos)+1 > len(r.values[name]) {
|
||||
r.values[name] = append(r.values[name], make([]any, (int(pos)+1)-len(r.values[name]))...)
|
||||
}
|
||||
|
||||
r.values[name][pos] = v
|
||||
if pos >= r.counters[name] {
|
||||
r.counters[name]++
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Row) CountValues() map[string]uint {
|
||||
return r.counters
|
||||
}
|
||||
|
||||
func (r *Row) GetValue(name string, pos uint) (any, error) {
|
||||
if r.values == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if r.counters == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if pos >= r.counters[name] {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return r.values[name][pos], nil
|
||||
}
|
||||
|
||||
func (r *Row) String() string {
|
||||
out := make([]string, 0, 20)
|
||||
|
||||
for k, cc := range r.counters {
|
||||
for i := uint(0); i < cc; i++ {
|
||||
v := r.values[k][i]
|
||||
out = append(out, fmt.Sprintf("%s [%d] %v", k, i, v))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(out, " | ")
|
||||
}
|
||||
|
||||
// compareGetters compares the two ValueGetters
|
||||
// -1: a is less then b
|
||||
// 0: a is equal to b
|
||||
// 1: a is greater then b
|
||||
//
|
||||
// Multi value rules:
|
||||
// - if a has less items then b, a is less then b (-1)
|
||||
// - if a has more items then b, a is more then b (1)
|
||||
// - if a and b have the same amount of items; if any of the corresponding values
|
||||
// are different, that outcome is used as the result
|
||||
//
|
||||
// This function is used to satisfy sort's less function requirement.
|
||||
func compareGetters(a, b ValueGetter, ac, bc map[string]uint, attr string) int {
|
||||
// If a has less values then b, then a is less then b
|
||||
if ac[attr] < bc[attr] {
|
||||
return -1
|
||||
} else if ac[attr] > bc[attr] {
|
||||
return 1
|
||||
}
|
||||
|
||||
// If a and b have the same number of values, then we need to compare them
|
||||
for i := uint(0); i < ac[attr]; i++ {
|
||||
va, err := a.GetValue(attr, i)
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
vb, err := b.GetValue(attr, i)
|
||||
if err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Continue the cmp. until we find two values that are different
|
||||
cmp := compareValues(va, vb)
|
||||
if cmp != 0 {
|
||||
return cmp
|
||||
}
|
||||
}
|
||||
|
||||
// If any value is different from the other, the loop above would end; so
|
||||
// here, we can safely say they are the same
|
||||
return 0
|
||||
}
|
||||
|
||||
// compareValues compares the two values
|
||||
// @todo identify what other types we should support
|
||||
// -1: a is less then b
|
||||
// 0: a is equal to b
|
||||
// 1: a is greater then b
|
||||
//
|
||||
// @note I considered using GVal here but it introduces more overhead then
|
||||
// what I've conjured here.
|
||||
// @todo look into using generics or some wrapping types here
|
||||
func compareValues(va, vb any) int {
|
||||
// simple/edge cases
|
||||
if va == vb {
|
||||
return 0
|
||||
}
|
||||
if va == nil {
|
||||
return -1
|
||||
}
|
||||
if vb == nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Compare based on type
|
||||
switch ca := va.(type) {
|
||||
case string:
|
||||
cb, err := cast.ToStringE(vb)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if ca < cb {
|
||||
return -1
|
||||
}
|
||||
if ca > cb {
|
||||
return 1
|
||||
}
|
||||
|
||||
case int, int8, int16, int32, int64:
|
||||
// this one can't error since we know it's an ok value
|
||||
xa := cast.ToInt64(va)
|
||||
cb, err := cast.ToInt64E(vb)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if xa < cb {
|
||||
return -1
|
||||
}
|
||||
if xa > cb {
|
||||
return 1
|
||||
}
|
||||
|
||||
case uint, uint8, uint16, uint32, uint64:
|
||||
// this one can't error since we know it's an ok value
|
||||
xa := cast.ToUint64(va)
|
||||
cb, err := cast.ToUint64E(vb)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if xa < cb {
|
||||
return -1
|
||||
}
|
||||
if xa > cb {
|
||||
return 1
|
||||
}
|
||||
|
||||
case float32, float64:
|
||||
// this one can't error since we know it's an ok value
|
||||
xa := cast.ToFloat64(va)
|
||||
cb, err := cast.ToFloat64E(vb)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if xa < cb {
|
||||
return -1
|
||||
}
|
||||
if xa > cb {
|
||||
return 1
|
||||
}
|
||||
|
||||
case time.Time, *time.Time:
|
||||
// this one can't error since we know it's an ok value
|
||||
xa := cast.ToTime(va)
|
||||
cb, err := cast.ToTimeE(vb)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
if xa.Before(cb) {
|
||||
return -1
|
||||
}
|
||||
if xa.After(cb) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("unsupported type for values %v, %v", va, vb))
|
||||
}
|
||||
|
||||
// constraintsToExpression converts the given constraints map to a ql parsable expression
|
||||
func constraintsToExpression(cc map[string][]any) string {
|
||||
out := make([]string, 0, 10)
|
||||
|
||||
for k, vv := range cc {
|
||||
part := make([]string, len(vv))
|
||||
for i, v := range vv {
|
||||
if vs, ok := v.(string); ok {
|
||||
part[i] = fmt.Sprintf(`%s == '%s'`, k, vs)
|
||||
} else {
|
||||
part[i] = fmt.Sprintf("%s == %v", k, v)
|
||||
}
|
||||
}
|
||||
|
||||
pt := strings.Join(part, " || ")
|
||||
if len(cc) > 1 {
|
||||
out = append(out, fmt.Sprintf("(%s)", pt))
|
||||
} else {
|
||||
out = append(out, pt)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return strings.Join(out, " && ")
|
||||
}
|
||||
|
||||
// stateConstraintsToExpression converts the given state expression to a ql parsable expression
|
||||
func stateConstraintsToExpression(cc map[string]filter.State) string {
|
||||
out := make([]string, 0, 10)
|
||||
|
||||
for k, s := range cc {
|
||||
// Inclusive one is omitted since the condition always evaluates
|
||||
// as true (field == null || field != null => true)
|
||||
switch s {
|
||||
case filter.StateExcluded:
|
||||
out = append(out, fmt.Sprintf("%s == null", k))
|
||||
|
||||
// Only these ones
|
||||
case filter.StateExclusive:
|
||||
out = append(out, fmt.Sprintf("%s != null", k))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(out, " && ")
|
||||
}
|
||||
|
||||
// @todo see if the rest of the "conversion" functions should return a QL node
|
||||
// like the cursor one does.
|
||||
func prepareGenericRowTester(f internalFilter) (_ tester, err error) {
|
||||
var (
|
||||
parts = make([]string, 0, 5)
|
||||
pcNode *ql.ASTNode
|
||||
exprNode *ql.ASTNode
|
||||
)
|
||||
|
||||
{
|
||||
// Convert the regular constraints
|
||||
if cc := f.Constraints(); len(cc) != 0 {
|
||||
parts = append(parts, constraintsToExpression(cc))
|
||||
}
|
||||
|
||||
// Convert state constraints
|
||||
// @todo check if the attributes in the state constraints are nullable.
|
||||
if sc := f.StateConstraints(); len(sc) != 0 {
|
||||
parts = append(parts, stateConstraintsToExpression(sc))
|
||||
}
|
||||
|
||||
exprNode = f.ExpressionParsed()
|
||||
if exprNode == nil {
|
||||
expr := f.Expression()
|
||||
if expr != "" {
|
||||
parts = append(parts, f.Expression())
|
||||
}
|
||||
}
|
||||
|
||||
// Convert the paging cursor
|
||||
if pc := f.Cursor(); pc != nil {
|
||||
pcNode, err = f.Cursor().ToAST(nil, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pcNode.Traverse(func(a *ql.ASTNode) (bool, *ql.ASTNode, error) {
|
||||
if a.Symbol != "" {
|
||||
a.Symbol = wrapNestedGvalIdent(a.Symbol)
|
||||
}
|
||||
return true, a, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
expr := strings.Join(parts, " && ")
|
||||
|
||||
// Everything is empty, not doing anything
|
||||
if len(expr) == 0 && exprNode == nil && pcNode == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
args := make([]*ql.ASTNode, 0, 5)
|
||||
|
||||
// Paging cursors
|
||||
if pcNode != nil {
|
||||
args = append(args, pcNode)
|
||||
}
|
||||
|
||||
// Parsed filter expression
|
||||
if exprNode != nil {
|
||||
args = append(args, exprNode)
|
||||
}
|
||||
|
||||
// Rest of the generated expression string
|
||||
if len(expr) > 0 {
|
||||
expr, err := newConverterGval().Parse(expr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, expr)
|
||||
}
|
||||
|
||||
return newRunnerGvalParsed(
|
||||
&ql.ASTNode{
|
||||
Ref: "and",
|
||||
Args: args,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// makeRowComparator returns a ValueGetter comparator for the given sort expr
|
||||
func makeRowComparator(ss ...*filter.SortExpr) func(a, b ValueGetter) bool {
|
||||
return func(a, b ValueGetter) bool {
|
||||
for _, s := range ss {
|
||||
cmp := compareGetters(a, b, a.CountValues(), b.CountValues(), s.Column)
|
||||
|
||||
less, skip := evalCmpResult(cmp, s)
|
||||
if !skip {
|
||||
return less
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func evalCmpResult(cmp int, s *filter.SortExpr) (less, skip bool) {
|
||||
if cmp != 0 {
|
||||
if s.Descending {
|
||||
return cmp > 0, false
|
||||
}
|
||||
return cmp < 0, false
|
||||
}
|
||||
|
||||
return false, true
|
||||
}
|
||||
|
||||
func indexAttrs(aa ...AttributeMapping) (out map[string]bool) {
|
||||
out = make(map[string]bool, len(aa))
|
||||
indexAttrsInto(out, aa...)
|
||||
return
|
||||
}
|
||||
|
||||
func indexAttrsInto(dst map[string]bool, aa ...AttributeMapping) {
|
||||
for _, a := range aa {
|
||||
dst[a.Identifier()] = true
|
||||
}
|
||||
}
|
||||
|
||||
// keysFromExpr returns all of the identifiers used in agg. group expressions
|
||||
//
|
||||
// The hasConstants return argument is true if any of the expressions returns a
|
||||
// constant value, such as year(now()) or 42
|
||||
func keysFromExpr(nn ...*ql.ASTNode) (out []string, hasConstants bool) {
|
||||
out = make([]string, 0, (len(nn)+1)*2)
|
||||
auxOut := make(map[string]bool, (len(nn)+1)*2)
|
||||
|
||||
for _, n := range nn {
|
||||
symbols := n.CollectSymbols()
|
||||
if len(symbols) == 0 {
|
||||
hasConstants = true
|
||||
}
|
||||
|
||||
for _, s := range symbols {
|
||||
auxOut[s] = true
|
||||
}
|
||||
}
|
||||
|
||||
for k := range auxOut {
|
||||
out = append(out, k)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Assure sort validates that the filter's definition includes all of the primary
|
||||
// keys and that the paging cursor's sort is compatible
|
||||
func assureSort(f internalFilter, primaries []string) (out internalFilter, err error) {
|
||||
out = f
|
||||
|
||||
// make sure all primary keys are in there
|
||||
for _, p := range primaries {
|
||||
if out.orderBy.Get(p) == nil {
|
||||
out.orderBy = append(out.orderBy, &filter.SortExpr{
|
||||
Column: p,
|
||||
Descending: out.orderBy.LastDescending(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// No cursor, no problem
|
||||
if f.cursor == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure the cursor can handle this sort def
|
||||
out.orderBy, err = out.cursor.Sort(out.orderBy)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func wrapNestedGvalIdent(ident string) string {
|
||||
return strings.ReplaceAll(ident, attributeNestingSeparator, attributeNestingGvalSeparator)
|
||||
}
|
||||
|
||||
func unwrapNestedGvalIdent(ident string) string {
|
||||
return strings.ReplaceAll(ident, attributeNestingGvalSeparator, attributeNestingSeparator)
|
||||
}
|
||||
42
server/pkg/dal/utils_bench_test.go
Normal file
42
server/pkg/dal/utils_bench_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
)
|
||||
|
||||
// goos: linux
|
||||
// goarch: amd64
|
||||
// pkg: github.com/cortezaproject/corteza/server/pkg/dal
|
||||
// cpu: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
|
||||
// BenchmarkRowComparator-12 161826 7015 ns/op 5120 B/op 40 allocs/op
|
||||
// PASS
|
||||
func BenchmarkRowComparator(b *testing.B) {
|
||||
cmp := makeRowComparator(
|
||||
&filter.SortExpr{Column: "a", Descending: false},
|
||||
&filter.SortExpr{Column: "b", Descending: true},
|
||||
&filter.SortExpr{Column: "c", Descending: true},
|
||||
&filter.SortExpr{Column: "d", Descending: false},
|
||||
)
|
||||
|
||||
r1 := simpleRow{"a": 10, "b": "aa", "c": 33, "d": 500}
|
||||
r2 := simpleRow{"a": 50, "b": "aaaaa", "c": 31, "d": 10}
|
||||
r3 := simpleRow{"a": 31, "b": "a", "c": 11, "d": 1000}
|
||||
r4 := simpleRow{"a": 42, "b": "", "c": 0, "d": 300}
|
||||
r5 := simpleRow{"a": 22, "b": "aaaaaaaaaa", "c": -25, "d": 21}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
cmp(r1, r2)
|
||||
cmp(r1, r3)
|
||||
cmp(r1, r4)
|
||||
cmp(r1, r5)
|
||||
cmp(r2, r3)
|
||||
cmp(r2, r4)
|
||||
cmp(r2, r5)
|
||||
cmp(r3, r4)
|
||||
cmp(r3, r5)
|
||||
cmp(r4, r5)
|
||||
}
|
||||
}
|
||||
466
server/pkg/dal/utils_test.go
Normal file
466
server/pkg/dal/utils_test.go
Normal file
@@ -0,0 +1,466 @@
|
||||
package dal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/filter"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCompareValues(t *testing.T) {
|
||||
n := time.Now()
|
||||
m := n.Add(time.Second)
|
||||
|
||||
tcc := []struct {
|
||||
name string
|
||||
a any
|
||||
b any
|
||||
out int
|
||||
}{
|
||||
{
|
||||
name: "two ints; eq",
|
||||
a: 10,
|
||||
b: 10,
|
||||
out: 0,
|
||||
}, {
|
||||
name: "two ints; lt",
|
||||
a: 9,
|
||||
b: 10,
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two ints; gt",
|
||||
a: 10,
|
||||
b: 9,
|
||||
out: 1,
|
||||
}, {
|
||||
name: "two ints; a nil",
|
||||
a: nil,
|
||||
b: 10,
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two ints; b nil",
|
||||
a: 10,
|
||||
b: nil,
|
||||
out: 1,
|
||||
},
|
||||
|
||||
{
|
||||
name: "two strings; eq",
|
||||
a: "aa",
|
||||
b: "aa",
|
||||
out: 0,
|
||||
}, {
|
||||
name: "two strings; lt",
|
||||
a: "a",
|
||||
b: "aa",
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two strings; gt",
|
||||
a: "aa",
|
||||
b: "a",
|
||||
out: 1,
|
||||
}, {
|
||||
name: "two strings; a nil",
|
||||
a: nil,
|
||||
b: "aa",
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two strings; b nil",
|
||||
a: "aa",
|
||||
b: nil,
|
||||
out: 1,
|
||||
},
|
||||
|
||||
{
|
||||
name: "two uints; eq",
|
||||
a: uint(10),
|
||||
b: uint(10),
|
||||
out: 0,
|
||||
}, {
|
||||
name: "two uints; lt",
|
||||
a: uint(9),
|
||||
b: uint(10),
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two uints; gt",
|
||||
a: uint(10),
|
||||
b: uint(9),
|
||||
out: 1,
|
||||
}, {
|
||||
name: "two uints; a nil",
|
||||
a: nil,
|
||||
b: uint(10),
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two uints; b nil",
|
||||
a: uint(10),
|
||||
b: nil,
|
||||
out: 1,
|
||||
},
|
||||
|
||||
{
|
||||
name: "two floats; eq",
|
||||
a: float64(10),
|
||||
b: float64(10),
|
||||
out: 0,
|
||||
}, {
|
||||
name: "two floats; lt",
|
||||
a: float64(9),
|
||||
b: float64(10),
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two floats; gt",
|
||||
a: float64(10),
|
||||
b: float64(9),
|
||||
out: 1,
|
||||
}, {
|
||||
name: "two floats; a nil",
|
||||
a: nil,
|
||||
b: float64(10),
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two floats; b nil",
|
||||
a: float64(10),
|
||||
b: nil,
|
||||
out: 1,
|
||||
},
|
||||
|
||||
{
|
||||
name: "two times; eq",
|
||||
a: n,
|
||||
b: n,
|
||||
out: 0,
|
||||
}, {
|
||||
name: "two times; lt",
|
||||
a: n,
|
||||
b: m,
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two times; gt",
|
||||
a: m,
|
||||
b: n,
|
||||
out: 1,
|
||||
}, {
|
||||
name: "two times; a nil",
|
||||
a: nil,
|
||||
b: n,
|
||||
out: -1,
|
||||
}, {
|
||||
name: "two times; b nil",
|
||||
a: n,
|
||||
b: nil,
|
||||
out: 1,
|
||||
},
|
||||
|
||||
{
|
||||
name: "two nils",
|
||||
a: nil,
|
||||
b: nil,
|
||||
out: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
require.Equal(t, c.out, compareValues(c.a, c.b))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareGetters(t *testing.T) {
|
||||
tcc := []struct {
|
||||
name string
|
||||
a ValueGetter
|
||||
b ValueGetter
|
||||
attr string
|
||||
out int
|
||||
}{{
|
||||
name: "single eq",
|
||||
a: simpleRow{"a": 10},
|
||||
b: simpleRow{"a": 10},
|
||||
attr: "a",
|
||||
out: 0,
|
||||
}, {
|
||||
name: "single lt",
|
||||
a: simpleRow{"a": 9},
|
||||
b: simpleRow{"a": 10},
|
||||
attr: "a",
|
||||
out: -1,
|
||||
}, {
|
||||
name: "single gt",
|
||||
a: simpleRow{"a": 10},
|
||||
b: simpleRow{"a": 9},
|
||||
attr: "a",
|
||||
out: 1,
|
||||
},
|
||||
|
||||
{
|
||||
name: "multi eq both empty",
|
||||
a: &Row{},
|
||||
b: &Row{},
|
||||
attr: "a",
|
||||
out: 0,
|
||||
}, {
|
||||
name: "multi eq same values",
|
||||
a: (&Row{}).WithValue("a", 0, 1),
|
||||
b: (&Row{}).WithValue("a", 0, 1),
|
||||
attr: "a",
|
||||
out: 0,
|
||||
}, {
|
||||
name: "multi lt a less items",
|
||||
a: (&Row{}),
|
||||
b: (&Row{}).WithValue("a", 0, 1),
|
||||
attr: "a",
|
||||
out: -1,
|
||||
}, {
|
||||
name: "multi lt a item less",
|
||||
a: (&Row{}).WithValue("a", 0, 0),
|
||||
b: (&Row{}).WithValue("a", 0, 1),
|
||||
attr: "a",
|
||||
out: -1,
|
||||
}, {
|
||||
name: "multi gt a more items",
|
||||
a: (&Row{}).WithValue("a", 0, 1),
|
||||
b: (&Row{}),
|
||||
attr: "a",
|
||||
out: 1,
|
||||
}, {
|
||||
name: "multi gt a item more",
|
||||
a: (&Row{}).WithValue("a", 0, 1),
|
||||
b: (&Row{}).WithValue("a", 0, 0),
|
||||
attr: "a",
|
||||
out: 1,
|
||||
}}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
require.Equal(t, c.out, compareGetters(c.a, c.b, c.a.CountValues(), c.b.CountValues(), c.attr))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstraintsToExpression(t *testing.T) {
|
||||
tcc := []struct {
|
||||
name string
|
||||
cc map[string][]any
|
||||
out []string
|
||||
}{{
|
||||
name: "one constraint, one value",
|
||||
cc: map[string][]any{
|
||||
"k1": {"v1"},
|
||||
},
|
||||
out: []string{`k1 == 'v1'`},
|
||||
}, {
|
||||
name: "one constraint, multiple values",
|
||||
cc: map[string][]any{
|
||||
"k1": {"v1", 10, true},
|
||||
},
|
||||
out: []string{`k1 == 'v1' || k1 == 10 || k1 == true`},
|
||||
}, {
|
||||
name: "multiple constraints, multiple values",
|
||||
cc: map[string][]any{
|
||||
"k1": {"v1", 10, true},
|
||||
"k2": {"v2", 42, false},
|
||||
},
|
||||
out: []string{
|
||||
`(k1 == 'v1' || k1 == 10 || k1 == true)`,
|
||||
`(k2 == 'v2' || k2 == 42 || k2 == false)`,
|
||||
},
|
||||
}}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
// Do it like so since map order is not defined and the test would be flaky
|
||||
got := constraintsToExpression(c.cc)
|
||||
for _, o := range c.out {
|
||||
require.Contains(t, got, o)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateConstraintsToExpression(t *testing.T) {
|
||||
tcc := []struct {
|
||||
name string
|
||||
cc map[string]filter.State
|
||||
out []string
|
||||
}{{
|
||||
name: "exclude these ones",
|
||||
cc: map[string]filter.State{
|
||||
"k1": filter.StateExcluded,
|
||||
"k2": filter.StateExcluded,
|
||||
},
|
||||
out: []string{`k1 == null`, `k2 == null`},
|
||||
}, {
|
||||
name: "only these ones",
|
||||
cc: map[string]filter.State{
|
||||
"k1": filter.StateExclusive,
|
||||
"k2": filter.StateExclusive,
|
||||
},
|
||||
out: []string{`k1 != null`, `k2 != null`},
|
||||
}, {
|
||||
name: "both",
|
||||
cc: map[string]filter.State{
|
||||
"k1": filter.StateExclusive,
|
||||
"k2": filter.StateExclusive,
|
||||
},
|
||||
out: []string{},
|
||||
}, {
|
||||
name: "mix and match",
|
||||
cc: map[string]filter.State{
|
||||
"k1": filter.StateExcluded,
|
||||
"k2": filter.StateInclusive,
|
||||
"k3": filter.StateExclusive,
|
||||
},
|
||||
out: []string{
|
||||
`k1 == null`,
|
||||
`k3 != null`,
|
||||
},
|
||||
}}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
// Do it like so since map order is not defined and the test would be flaky
|
||||
got := stateConstraintsToExpression(c.cc)
|
||||
for _, o := range c.out {
|
||||
require.Contains(t, got, o)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowComparator(t *testing.T) {
|
||||
// @todo add some more extreme cases
|
||||
tcc := []struct {
|
||||
name string
|
||||
a ValueGetter
|
||||
b ValueGetter
|
||||
ss filter.SortExprSet
|
||||
less bool
|
||||
}{
|
||||
// Simple one col cases
|
||||
{
|
||||
name: "single column simple asc less",
|
||||
a: simpleRow{"a": 10},
|
||||
b: simpleRow{"a": 20},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: false}},
|
||||
less: true,
|
||||
},
|
||||
{
|
||||
name: "single column simple asc more",
|
||||
a: simpleRow{"a": 20},
|
||||
b: simpleRow{"a": 10},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: false}},
|
||||
less: false,
|
||||
},
|
||||
{
|
||||
name: "single column simple asc equal",
|
||||
a: simpleRow{"a": 10},
|
||||
b: simpleRow{"a": 10},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: false}},
|
||||
less: false,
|
||||
},
|
||||
{
|
||||
name: "single column simple desc less",
|
||||
a: simpleRow{"a": 20},
|
||||
b: simpleRow{"a": 10},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: true}},
|
||||
less: true,
|
||||
},
|
||||
{
|
||||
name: "single column simple desc more",
|
||||
a: simpleRow{"a": 10},
|
||||
b: simpleRow{"a": 20},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: true}},
|
||||
less: false,
|
||||
},
|
||||
{
|
||||
name: "single column simple desc equal",
|
||||
a: simpleRow{"a": 10},
|
||||
b: simpleRow{"a": 10},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: true}},
|
||||
less: false,
|
||||
},
|
||||
|
||||
// basic 2 col cases
|
||||
{
|
||||
name: "two column asc less first priority",
|
||||
a: simpleRow{"a": 10, "b": 100},
|
||||
b: simpleRow{"a": 20, "b": 1},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: false}, {Column: "b", Descending: false}},
|
||||
less: true,
|
||||
},
|
||||
{
|
||||
name: "two column asc less first equal",
|
||||
a: simpleRow{"a": 10, "b": 1},
|
||||
b: simpleRow{"a": 10, "b": 2},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: false}, {Column: "b", Descending: false}},
|
||||
less: true,
|
||||
},
|
||||
{
|
||||
name: "two column asc equal",
|
||||
a: simpleRow{"a": 10, "b": 1},
|
||||
b: simpleRow{"a": 10, "b": 1},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: false}, {Column: "b", Descending: false}},
|
||||
less: false,
|
||||
},
|
||||
{
|
||||
name: "two column desc less first priority",
|
||||
a: simpleRow{"a": 20, "b": 1},
|
||||
b: simpleRow{"a": 10, "b": 100},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: true}, {Column: "b", Descending: true}},
|
||||
less: true,
|
||||
},
|
||||
{
|
||||
name: "two column desc less first equal",
|
||||
a: simpleRow{"a": 10, "b": 2},
|
||||
b: simpleRow{"a": 10, "b": 1},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: true}, {Column: "b", Descending: true}},
|
||||
less: true,
|
||||
},
|
||||
{
|
||||
name: "two column desc equal",
|
||||
a: simpleRow{"a": 10, "b": 1},
|
||||
b: simpleRow{"a": 10, "b": 1},
|
||||
ss: filter.SortExprSet{{Column: "a", Descending: true}, {Column: "b", Descending: true}},
|
||||
less: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range tcc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
||||
less := makeRowComparator(c.ss...)(c.a, c.b)
|
||||
require.Equal(t, c.less, less)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRowResetting(t *testing.T) {
|
||||
r := &Row{}
|
||||
|
||||
gv := func(ident string, pos uint) any {
|
||||
v, err := r.GetValue(ident, pos)
|
||||
require.NoError(t, err)
|
||||
return v
|
||||
}
|
||||
|
||||
r.SetValue("a", 0, 1)
|
||||
r.SetValue("a", 0, 2)
|
||||
r.SetValue("a", 1, 1)
|
||||
|
||||
require.Equal(t, 2, gv("a", 0))
|
||||
require.Equal(t, 1, gv("a", 1))
|
||||
require.Equal(t, uint(2), r.counters["a"])
|
||||
|
||||
r.Reset()
|
||||
require.Equal(t, uint(0), r.counters["a"])
|
||||
|
||||
r.SetValue("a", 0, 3)
|
||||
r.SetValue("b", 0, 4)
|
||||
require.Equal(t, 3, gv("a", 0))
|
||||
require.Equal(t, uint(1), r.counters["a"])
|
||||
require.Equal(t, 4, gv("b", 0))
|
||||
require.Equal(t, uint(1), r.counters["b"])
|
||||
}
|
||||
Reference in New Issue
Block a user