Implement pipeline link (left) execution step

This commit is contained in:
Tomaž Jerman
2022-09-01 16:55:20 +02:00
parent bad80e5d81
commit f8595ca827
4 changed files with 1528 additions and 0 deletions
+41
View File
@@ -48,6 +48,10 @@ type (
// as the data is provided in the correct order.
partialScan bool
}
rowLink struct {
a, b ValueGetter
}
)
func (def *Link) Identifier() string {
@@ -128,3 +132,40 @@ func (def *Link) validate(ii []Iterator) (err error) {
}
return
}
func (r *rowLink) SelectGVal(ctx context.Context, k string) (interface{}, error) {
return r.GetValue(k, 0)
}
func (r *rowLink) GetValue(name string, pos uint) (v any, err error) {
a := r.a.CountValues()
if cc, ok := a[name]; ok {
if pos >= cc {
return nil, nil
}
return r.a.GetValue(name, pos)
}
b := r.b.CountValues()
if cc, ok := b[name]; ok {
if pos >= cc {
return nil, nil
}
return r.b.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
}
+549
View File
@@ -0,0 +1,549 @@
package dal
import (
"context"
"fmt"
"io"
"sort"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/spf13/cast"
)
type (
linkLeft struct {
def Link
filter internalFilter
leftSource Iterator
rightSource Iterator
err error
scanRow *row
planned bool
filtered bool
// Index the attributes for easier lookups later on
outLeftAttrIndex map[string]int
outRightAttrIndex map[string]int
leftAttrIndex map[string]int
rightAttrIndex map[string]int
linkRightAttr AttributeMapping
linkLeftAttr AttributeMapping
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) (err error) {
// @note the index is keeping track of the right source attributes so we can
// simplify the sorting logic.
xs.relIndex = newRelIndex(xs.rightSortAttrs...)
xs.indexAttributes()
// basic sort breadown
for _, o := range xs.filter.OrderBy() {
if _, ok := xs.rightAttrIndex[o.Column]; ok {
xs.rightSortAttrs = append(xs.rightSortAttrs, o.Column)
}
}
xs.linkLeftAttr = xs.def.LeftAttributes[xs.leftAttrIndex[xs.def.On.Left]]
xs.linkRightAttr = xs.def.RightAttributes[xs.rightAttrIndex[xs.def.On.Right]]
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 = newRelIndex(xs.rightSortAttrs...)
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
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) {
attrIdent := xs.linkLeftAttr.Identifier()
attrType := xs.linkLeftAttr.Properties().Type
// @todo mv link predicate attrs
v, _ := l.GetValue(attrIdent, 0)
switch attrType.(type) {
case TypeNumber:
out, ok = xs.relIndex.GetInt(cast.ToInt64(v))
return
case TypeText:
out, ok = xs.relIndex.GetString(cast.ToString(v))
return
case TypeID,
TypeRef:
out, ok = xs.relIndex.GetID(cast.ToUint64(v))
return
default:
// @note this should be validated way before
err = fmt.Errorf("cannot use type %s ad join predicate", attrType.Type())
}
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
}
if !xs.keep(ctx, l, rel) {
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) {
attrIdent := xs.linkRightAttr.Identifier()
attrType := xs.linkRightAttr.Properties().Type
// @todo mv link predicate attrs; should be prevented higher up for now
v, err := r.GetValue(attrIdent, 0)
if err != nil {
return err
}
// @todo not so sure about this switch; see above coment about moving this out
switch attrType.(type) {
case TypeNumber:
xs.relIndex.AddInt(cast.ToInt64(v), r)
return
case TypeText:
xs.relIndex.AddString(cast.ToString(v), r)
return
case TypeID,
TypeRef:
xs.relIndex.AddID(cast.ToUint64(v), r)
return
default:
// @note this should be validated way before
return fmt.Errorf("cannot use type %s as link predicate", attrType.Type())
}
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
}
// Prepare the data
leftRowA := xs.leftRows[i]
leftRowB := xs.leftRows[j]
leftRelBufferA, _, err = xs.getRelatedBuffer(leftRowA)
if err != nil {
return false
}
leftRelBufferB, _, err = xs.getRelatedBuffer(leftRowB)
if err != nil {
return false
}
for _, s := range xs.filter.OrderBy() {
if _, ok := xs.leftAttrIndex[s.Column]; ok {
// 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
}
// 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) {
// If no buffer, we won't keep -- left inner join like behavior
if buffer == nil {
return false
}
// No tester include all ok rows
if xs.rowTester == nil {
return true
}
ch := &rowLink{a: left}
for _, ch.b = range buffer.rows {
if !xs.rowTester.Test(ctx, ch) {
return false
}
}
return true
}
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
}
func (xs *linkLeft) indexAttributes() {
xs.outLeftAttrIndex = make(map[string]int)
for i, a := range xs.def.LeftAttributes {
xs.outLeftAttrIndex[a.Identifier()] = i
}
xs.outRightAttrIndex = make(map[string]int)
for i, a := range xs.def.RightAttributes {
xs.outRightAttrIndex[a.Identifier()] = i
}
xs.leftAttrIndex = make(map[string]int)
for i, a := range xs.def.LeftAttributes {
xs.leftAttrIndex[a.Identifier()] = i
}
xs.rightAttrIndex = make(map[string]int)
for i, a := range xs.def.RightAttributes {
xs.rightAttrIndex[a.Identifier()] = i
}
}
+119
View 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.Initialize(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) }
+819
View File
@@ -0,0 +1,819 @@
package dal
import (
"context"
"testing"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/stretchr/testify/require"
)
func TestStepLinkleft(t *testing.T) {
crs1 := &filter.PagingCursor{}
crs1.Set("l_pk", 1, false)
crs1.Set("l_val", "l1 v1", false)
crs1.Set("f_pk", 2, false)
crs1.Set("f_fk", 1, false)
crs1.Set("f_val", "f2 v1", false)
basicLeftAttrs := []simpleAttribute{
{ident: "l_pk", t: TypeID{}},
{ident: "l_val", t: TypeText{}},
}
basicRightAttrs := []simpleAttribute{
{ident: "f_pk", t: TypeID{}},
{ident: "f_fk", t: TypeRef{}},
{ident: "f_val", t: TypeText{}},
}
basicOutLeftAttrs := []simpleAttribute{
{ident: "l_pk", t: TypeID{}},
{ident: "l_val", t: TypeText{}},
}
basicOutRightAttrs := []simpleAttribute{
{ident: "f_pk", t: TypeID{}},
{ident: "f_fk", t: TypeRef{}},
{ident: "f_val", t: TypeText{}},
}
tcc := []struct {
name string
leftAttributes []simpleAttribute
rightAttributes []simpleAttribute
leftOutAttributes []simpleAttribute
rightOutAttributes []simpleAttribute
linkPred LinkPredicate
lIn []simpleRow
fIn []simpleRow
out []simpleRow
f internalFilter
}{
// Basic behavior
{
name: "basic link",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
},
{
name: "basic link multiple right",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 1, "f_val": "f2 v1"},
{"f_pk": 3, "f_fk": 1, "f_val": "f3 v1"},
{"f_pk": 4, "f_fk": 2, "f_val": "f4 v1"},
},
out: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 1, "f_val": "f2 v1"},
{"f_pk": 3, "f_fk": 1, "f_val": "f3 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
{"f_pk": 4, "f_fk": 2, "f_val": "f4 v1"},
},
},
{
name: "basic link omit missing rows",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 9999, "f_val": "f2 v1"},
},
out: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
},
},
{
name: "basic link no rows joined",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 123, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 9999, "f_val": "f2 v1"},
},
out: []simpleRow{},
},
{
name: "basic link empty right",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{},
out: []simpleRow{},
},
{
name: "basic link empty left",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 123, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 9999, "f_val": "f2 v1"},
},
out: []simpleRow{},
},
{
name: "empty input",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{},
fIn: []simpleRow{},
out: []simpleRow{},
},
// Filtering
{
name: "filtering constraints single attr",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_const": "c1", "l_val": "l1 v1"},
{"l_pk": 2, "l_const": "c2", "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{
{"l_pk": 1, "l_const": "c1", "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
},
f: internalFilter{
constraints: map[string][]any{
"l_const": {"c1"},
},
},
},
{
name: "filtering constraints right single attr",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_const": "c1", "l_val": "l1 v1"},
{"l_pk": 2, "l_const": "c2", "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{
{"l_pk": 1, "l_const": "c1", "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
},
f: internalFilter{
constraints: map[string][]any{
"f_val": {"f1 v1"},
},
},
},
{
name: "filtering constraints both single attr",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_const": "c1", "l_val": "l1 v1"},
{"l_pk": 2, "l_const": "c2", "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{},
f: internalFilter{
constraints: map[string][]any{
"l_const": {"c2"},
"f_val": {"f1 v1"},
},
},
},
{
name: "filtering constraints multiple attrs",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_const_a": "cac1", "l_const_b": "cbc1", "l_val": "l1 v1"},
{"l_pk": 2, "l_const_a": "cac1", "l_const_b": "cbc2", "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{
{"l_pk": 1, "l_const_a": "cac1", "l_const_b": "cbc1", "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
},
f: internalFilter{
constraints: map[string][]any{"l_const_a": {"cac1"}, "l_const_b": {"cbc1"}},
},
},
{
name: "filtering constraints single attr multiple options",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_const_a": "cac1", "l_const_b": "cbc1", "l_val": "l1 v1"},
{"l_pk": 2, "l_const_a": "cac1", "l_const_b": "cbc2", "l_val": "l2 v1"},
{"l_pk": 3, "l_const_a": "cac2", "l_const_b": "cbc3", "l_val": "l3 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{
{"l_pk": 1, "l_const_a": "cac1", "l_const_b": "cbc1", "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"l_pk": 2, "l_const_a": "cac1", "l_const_b": "cbc2", "l_val": "l2 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
f: internalFilter{
constraints: map[string][]any{"l_const_b": {"cbc1", "cbc2"}},
},
},
{
name: "filtering expressions constant true",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
f: internalFilter{
expression: "true",
},
},
{
name: "filtering expressions constant false",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{},
f: internalFilter{
expression: "false",
},
},
{
name: "filtering expressions simple",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{
{"l_pk": 2, "l_val": "l2 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
f: internalFilter{
expression: "l_val == 'l2 v1'",
},
},
{
name: "filtering expressions right simple",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 2, "f_val": "f2 v1"},
},
out: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
},
f: internalFilter{
expression: "f_val == 'f1 v1'",
},
},
// Paging
{
name: "paging cut off first entry",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
f: internalFilter{
cursor: crs1,
},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 1, "f_val": "f2 v1"},
{"f_pk": 3, "f_fk": 1, "f_val": "f3 v1"},
{"f_pk": 4, "f_fk": 2, "f_val": "f4 v1"},
},
out: []simpleRow{
{"l_pk": 2, "l_val": "l2 v1"},
{"f_pk": 4, "f_fk": 2, "f_val": "f4 v1"},
},
},
{
name: "paging cut off first entry with constant true",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
f: internalFilter{
cursor: crs1,
expression: "true",
},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 1, "f_val": "f2 v1"},
{"f_pk": 3, "f_fk": 1, "f_val": "f3 v1"},
{"f_pk": 4, "f_fk": 2, "f_val": "f4 v1"},
},
out: []simpleRow{
{"l_pk": 2, "l_val": "l2 v1"},
{"f_pk": 4, "f_fk": 2, "f_val": "f4 v1"},
},
},
{
name: "paging cut off first entry with constant false",
leftAttributes: basicLeftAttrs,
rightAttributes: basicRightAttrs,
leftOutAttributes: basicOutLeftAttrs,
rightOutAttributes: basicOutRightAttrs,
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
f: internalFilter{
cursor: crs1,
expression: "false",
},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 1, "f_val": "f2 v1"},
{"f_pk": 3, "f_fk": 1, "f_val": "f3 v1"},
{"f_pk": 4, "f_fk": 2, "f_val": "f4 v1"},
},
out: []simpleRow{},
},
}
ctx := context.Background()
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
l := InMemoryBuffer()
for _, r := range tc.lIn {
require.NoError(t, l.Add(ctx, r))
}
f := InMemoryBuffer()
for _, r := range tc.fIn {
require.NoError(t, f.Add(ctx, r))
}
def := Link{
Ident: "foo",
On: tc.linkPred,
LeftAttributes: saToMapping(tc.leftAttributes...),
RightAttributes: saToMapping(tc.rightAttributes...),
OutLeftAttributes: saToMapping(tc.leftOutAttributes...),
OutRightAttributes: saToMapping(tc.rightOutAttributes...),
Filter: tc.f,
}
xs, err := def.Initialize(ctx, l, f)
require.NoError(t, err)
i := 0
for xs.Next(ctx) {
require.NoError(t, xs.Err())
out := simpleRow{}
require.NoError(t, xs.Err())
require.NoError(t, xs.Scan(out))
require.Equal(t, tc.out[i], out)
i++
}
require.Equal(t, len(tc.out), i)
})
}
}
func TestStepLinkleft_cursorCollect_forward(t *testing.T) {
tcc := []struct {
name string
ss filter.SortExprSet
in simpleRow
state simpleRow
leftAttrs []simpleAttribute
rightAttrs []simpleAttribute
outleftAttrs []simpleAttribute
outrightAttrs []simpleAttribute
out func() *filter.PagingCursor
err bool
}{
{
name: "one",
in: simpleRow{"f_pk1": 25, "f1": "v25"},
state: simpleRow{"l_pk1": 1, "f1": "v1"},
leftAttrs: []simpleAttribute{{
ident: "l_pk1",
primary: true,
}},
rightAttrs: []simpleAttribute{{
ident: "f_pk1",
primary: true,
}},
outleftAttrs: []simpleAttribute{{
ident: "l_pk1",
primary: true,
}},
outrightAttrs: []simpleAttribute{{
ident: "f_pk1",
primary: true,
}},
out: func() *filter.PagingCursor {
pc := &filter.PagingCursor{}
pc.Set("l_pk1", 1, false)
pc.Set("f_pk1", 25, false)
return pc
},
},
}
for _, c := range tcc {
t.Run(c.name, func(t *testing.T) {
xs := &linkLeft{
def: Link{
Filter: internalFilter{
orderBy: c.ss,
},
LeftAttributes: saToMapping(c.leftAttrs...),
RightAttributes: saToMapping(c.rightAttrs...),
OutLeftAttributes: saToMapping(c.outleftAttrs...),
OutRightAttributes: saToMapping(c.outrightAttrs...),
},
leftRow: simpleToRow(c.state),
}
out, err := xs.ForwardCursor(c.in)
require.NoError(t, err)
require.Equal(t, c.out(), out)
})
}
}
func TestStepLinkleft_cursorCollect_back(t *testing.T) {
tcc := []struct {
name string
ss filter.SortExprSet
in simpleRow
state simpleRow
leftAttrs []simpleAttribute
rightAttrs []simpleAttribute
outleftAttrs []simpleAttribute
outrightAttrs []simpleAttribute
out func() *filter.PagingCursor
err bool
}{
{
name: "one",
in: simpleRow{"f_pk1": 25, "f1": "v25"},
state: simpleRow{"l_pk1": 1, "f1": "v1"},
leftAttrs: []simpleAttribute{{
ident: "l_pk1",
primary: true,
}},
rightAttrs: []simpleAttribute{{
ident: "f_pk1",
primary: true,
}},
outleftAttrs: []simpleAttribute{{
ident: "l_pk1",
primary: true,
}},
outrightAttrs: []simpleAttribute{{
ident: "f_pk1",
primary: true,
}},
out: func() *filter.PagingCursor {
pc := &filter.PagingCursor{}
pc.Set("l_pk1", 1, false)
pc.Set("f_pk1", 25, false)
pc.ROrder = true
return pc
},
},
}
for _, c := range tcc {
t.Run(c.name, func(t *testing.T) {
jj := &linkLeft{
def: Link{
Filter: internalFilter{
orderBy: c.ss,
},
LeftAttributes: saToMapping(c.leftAttrs...),
RightAttributes: saToMapping(c.rightAttrs...),
OutLeftAttributes: saToMapping(c.outleftAttrs...),
OutRightAttributes: saToMapping(c.outrightAttrs...),
},
leftRow: simpleToRow(c.state),
}
out, err := jj.BackCursor(c.in)
require.NoError(t, err)
require.Equal(t, c.out(), out)
})
}
}
func TestStepLinkleft_more(t *testing.T) {
tcc := []struct {
name string
linkPred LinkPredicate
leftAttrs []simpleAttribute
rightAttrs []simpleAttribute
outleftAttrs []simpleAttribute
outrightAttrs []simpleAttribute
lIn []simpleRow
fIn []simpleRow
out1 []simpleRow
out2 []simpleRow
f internalFilter
}{
{
name: "one",
leftAttrs: []simpleAttribute{{
ident: "l_pk",
primary: true,
t: TypeID{},
}},
rightAttrs: []simpleAttribute{{
ident: "f_pk",
primary: true,
t: TypeID{},
}, {
ident: "f_fk",
primary: false,
t: TypeID{},
}},
outleftAttrs: []simpleAttribute{{
ident: "l_pk",
primary: true,
t: TypeID{},
}},
outrightAttrs: []simpleAttribute{{
ident: "f_pk",
primary: true,
t: TypeID{},
}, {
ident: "f_fk",
primary: false,
t: TypeID{},
}},
linkPred: LinkPredicate{Left: "l_pk", Right: "f_fk"},
lIn: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"l_pk": 2, "l_val": "l2 v1"},
},
fIn: []simpleRow{
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
{"f_pk": 2, "f_fk": 1, "f_val": "f2 v1"},
{"f_pk": 3, "f_fk": 1, "f_val": "f3 v1"},
{"f_pk": 4, "f_fk": 2, "f_val": "f4 v1"},
},
out1: []simpleRow{
{"l_pk": 1, "l_val": "l1 v1"},
{"f_pk": 1, "f_fk": 1, "f_val": "f1 v1"},
},
out2: []simpleRow{
{"l_pk": 2, "l_val": "l2 v1"},
{"f_pk": 4, "f_fk": 2, "f_val": "f4 v1"},
},
},
}
ctx := context.Background()
for _, tc := range tcc {
t.Run(tc.name, func(t *testing.T) {
l := InMemoryBuffer()
for _, r := range tc.lIn {
require.NoError(t, l.Add(ctx, r))
}
f := InMemoryBuffer()
for _, r := range tc.fIn {
require.NoError(t, f.Add(ctx, r))
}
def := Link{
Ident: "foo",
On: tc.linkPred,
LeftAttributes: saToMapping(tc.leftAttrs...),
RightAttributes: saToMapping(tc.rightAttrs...),
OutLeftAttributes: saToMapping(tc.outleftAttrs...),
OutRightAttributes: saToMapping(tc.outrightAttrs...),
Filter: tc.f,
}
xs, err := def.Initialize(ctx, l, f)
require.NoError(t, err)
require.True(t, xs.Next(ctx))
out := simpleRow{}
require.NoError(t, xs.Err())
require.NoError(t, xs.Scan(out))
require.Equal(t, tc.out1[0], out)
require.True(t, xs.Next(ctx))
out = simpleRow{}
require.NoError(t, xs.Err())
require.NoError(t, xs.Scan(out))
require.Equal(t, tc.out1[1], out)
l.Seek(ctx, 0)
f.Seek(ctx, 0)
require.NoError(t, xs.More(0, out))
i := 0
for xs.Next(ctx) {
out := simpleRow{}
require.NoError(t, xs.Err())
require.NoError(t, xs.Scan(out))
require.Equal(t, tc.out2[i], out)
i++
}
require.Equal(t, len(tc.out2), i)
})
}
}
func simpleToRow(in simpleRow) (out *row) {
out = &row{}
for k, v := range in {
out.SetValue(k, 0, v)
}
return
}