Files
upstream-corteza/server/pkg/dal/exec_join_left.go
upstream-mirror 7e385fc126
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
snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
2026-09-18 01:22:23 +05:00

418 lines
10 KiB
Go

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
}