Files
upstream-corteza/server/compose/dalutils/records.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

631 lines
15 KiB
Go

package dalutils
import (
"context"
"fmt"
"math"
"github.com/cortezaproject/corteza/server/compose/types"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/spf13/cast"
)
type (
creator interface {
Create(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, vv ...dal.ValueGetter) error
}
updater interface {
Update(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, rr ...dal.ValueGetter) (err error)
}
searcher interface {
Search(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (dal.Iterator, error)
}
lookuper interface {
Lookup(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, lookup dal.ValueGetter, dst dal.ValueSetter) (err error)
}
deleter interface {
Delete(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, pkv ...dal.ValueGetter) (err error)
}
counter interface {
Count(ctx context.Context, m dal.ModelRef, operations dal.OperationSet, f filter.Filter) (uint, error)
}
)
// ComposeRecordsList iterates over results and collects all available records
func ComposeRecordsList(ctx context.Context, s searcher, mod *types.Module, filter types.RecordFilter) (set types.RecordSet, outFilter types.RecordFilter, err error) {
iter, err := prepIterator(ctx, s, mod, filter)
if err != nil {
return
}
set, _, outFilter, err = drainIterator(ctx, iter, mod, filter)
return
}
func ComposeRecordsListN(ctx context.Context, s searcher, mod *types.Module, filter types.RecordFilter) (set types.RecordSet, summaries map[string]types.RecordSummary, outFilter types.RecordFilter, err error) {
iter, err := prepIterator(ctx, s, mod, filter)
if err != nil {
return
}
set, summaries, outFilter, err = drainIterator(ctx, iter, mod, filter)
return
}
func ComposeRecordsIterator(ctx context.Context, s searcher, mod *types.Module, filter types.RecordFilter) (iter dal.Iterator, outFilter types.RecordFilter, err error) {
iter, err = prepIterator(ctx, s, mod, filter)
if err != nil {
return
}
outFilter = filter
outFilter.Paging = *filter.Paging.Clone()
return
}
func ComposeRecordsFind(ctx context.Context, l lookuper, mod *types.Module, recordID uint64) (out *types.Record, err error) {
out = prepareRecordTarget(mod)
err = l.Lookup(ctx, mod.ModelRef(), recLookupOperations(mod), dal.PKValues{"id": recordID}, out)
if err != nil {
return
}
return
}
func ComposeRecordsCount(ctx context.Context, c counter, mod *types.Module, filter types.RecordFilter) (cnt uint, err error) {
constraints := map[string][]interface{}{
"namespaceID": {mod.NamespaceID},
}
if filter.ModuleID != 0 {
constraints["moduleID"] = []interface{}{filter.ModuleID}
}
dalFilter := filter.ToConstraintedFilter(constraints)
return c.Count(ctx, mod.ModelRef(), recLookupOperations(mod), dalFilter)
}
func ComposeRecordCreate(ctx context.Context, c creator, mod *types.Module, records ...*types.Record) (err error) {
return c.Create(ctx, mod.ModelRef(), recCreateOperations(mod), recToGetters(records...)...)
}
func ComposeRecordUpdate(ctx context.Context, u updater, mod *types.Module, records ...*types.Record) (err error) {
return u.Update(ctx, mod.ModelRef(), recUpdateOperations(mod), recToGetters(records...)...)
}
func ComposeRecordSoftDelete(ctx context.Context, u updater, mod *types.Module, records ...*types.Record) (err error) {
return u.Update(ctx, mod.ModelRef(), recUpdateOperations(mod), recToGetters(records...)...)
}
func ComposeRecordUndelete(ctx context.Context, u updater, mod *types.Module, records ...*types.Record) (err error) {
return u.Update(ctx, mod.ModelRef(), recUpdateOperations(mod), recToGetters(records...)...)
}
func ComposeRecordDelete(ctx context.Context, d deleter, mod *types.Module, records ...*types.Record) (err error) {
return d.Delete(ctx, mod.ModelRef(), recDeleteOperations(mod), recToGetters(records...)...)
}
func WalkIterator(ctx context.Context, iter dal.Iterator, mod *types.Module, f func(r *types.Record) error) (err error) {
for iter.Next(ctx) {
r := prepareRecordTarget(mod)
if err = iter.Scan(r); err != nil {
return
}
if err = f(r); err != nil {
return
}
}
return iter.Err()
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utils
func prepFilter(filter types.RecordFilter, mod *types.Module) filter.Filter {
return filter.ToConstraintedFilter(mod.Config.DAL.Constraints)
}
func prepIterator(ctx context.Context, dal searcher, mod *types.Module, filter types.RecordFilter) (iter dal.Iterator, err error) {
dalFilter := prepFilter(filter, mod)
iter, err = dal.Search(ctx, mod.ModelRef(), recSearchOperations(mod, filter), dalFilter)
return
}
// drains iterator and collects all records
//
// Collection of records is done with respect to check function and limit constraint on record filter
// For any other filter constraint we assume that underlying DAL took care of it
func drainIterator(ctx context.Context, iter dal.Iterator, mod *types.Module, f types.RecordFilter) (set types.RecordSet, summaries map[string]types.RecordSummary, outFilter types.RecordFilter, err error) {
// close iterator after we've drained it
defer iter.Close()
if f.PageCursor != nil {
if f.IncPageNavigation || f.IncTotal {
err = fmt.Errorf("not allowed to fetch page navigation or total item count with page cursor")
return
}
}
var (
ok bool
fetched uint
filtered uint
lastRecord *types.Record
)
// Get the requested number of record
if f.Limit > 0 {
set = make(types.RecordSet, 0, f.Limit)
} else {
set = make(types.RecordSet, 0, 1000)
}
for f.Limit == 0 || uint(len(set)) < f.Limit {
var firstRecord *types.Record
// reset counters every drain
fetched = 0
filtered = 0
add := make(types.RecordSet, 0, 12)
err = WalkIterator(ctx, iter, mod, func(r *types.Record) error {
lastRecord = r
if firstRecord == nil {
firstRecord = r
}
// check fetched record
if f.Check != nil {
if ok, err = f.Check(r); err != nil {
return err
} else if !ok {
filtered++
return nil
}
}
fetched++
add = append(add, r)
return err
})
// if an error occurred inside Next()/WalkIterator,
// we need to stop draining
if err != nil {
return
}
// If it's reverse, we need to add extra fetches to the start
if f.PageCursor != nil && f.PageCursor.ROrder {
set = append(add, set...)
} else {
set = append(set, add...)
}
total := fetched + filtered
if total == 0 || f.Limit == 0 {
// do not re-fetch if:
// 1) nothing was fetch in the previous run
// 2) there was no limit (everything was fetched)
// 3) there are less total (fetched and filtered) items then value of limit
break
}
// Fetch more records
setLen := uint(len(set))
if total > 0 && setLen < f.Limit {
fetchMore := f.Limit - setLen
var crsrRec *types.Record
// request more items
if f.PageCursor == nil || !f.PageCursor.ROrder {
crsrRec = lastRecord
} else {
crsrRec = firstRecord
}
if err = iter.More(fetchMore, crsrRec); err != nil {
return
}
}
}
// Get the page nav/total/next-prev cursors
nav, auxSm, err := generatePageNavigation(ctx, iter, mod, f, set)
if err != nil {
return
}
summaries = auxSm
// Make out filter
outFilter = f
outFilter.Paging = nav.Paging
outFilter.Sorting = nav.Sorting
return
}
// generatePageNavigation generates page navigation for a given record set using an iterator and filter limit.
// If the limit is not defined, the page navigation will consist of only one page without a cursor.
// If the limit is defined and is greater than the total number of records in the set,
// the page navigation will consist of only one page without a cursor.
// If the limit is defined and is less than the total number of records in the set,
// the page navigation will have multiple pages with cursor(s) based on the total number of records and the provided limit.
// @todo revisit and clean up this function properly
func generatePageNavigation(ctx context.Context, iter dal.Iterator, mod *types.Module, p types.RecordFilter, set types.RecordSet) (out types.RecordFilter, summaries map[string]types.RecordSummary, err error) {
const (
howMuchMore = 1000
)
var (
ok bool
total = uint(len(set))
setLen = len(set)
counter = 0
first *types.Record
last *types.Record
page filter.Page
pageNavigation = []*filter.Page{
{
Page: 1,
Count: 0,
Cursor: nil,
},
}
// generatePage generates pageNavigation for given record set
generatePage = func(last *types.Record) (err error) {
if !p.IncPageNavigation || p.Limit == 0 || len(pageNavigation) == 0 {
return
}
lastNavPageNo := len(pageNavigation) - 1
nextPage, err := iter.ForwardCursor(last)
if err != nil {
return
}
if total < p.Limit {
pageNavigation[lastNavPageNo].Count = total
}
// prepare page
if total != 0 && (total%p.Limit) == 0 {
pageNavigation[lastNavPageNo].Count = p.Limit
page = filter.Page{
Page: uint(len(pageNavigation) + 1),
Count: p.Limit,
Cursor: nextPage,
}
}
expectedItemCountUpToPage := uint(lastNavPageNo+1) * p.Limit
if p.Limit == 1 {
expectedItemCountUpToPage = uint(lastNavPageNo) * p.Limit
}
if expectedItemCountUpToPage < total {
// push page when limit is matched with the previous page item size
if pageNavigation[lastNavPageNo].Count == p.Limit {
pageNavigation = append(pageNavigation, &filter.Page{
Page: page.Page,
Count: total % p.Limit,
Cursor: page.Cursor, // prev cursor
})
}
}
return
}
recordChecker = func(i dal.Iterator) (ok bool, err error) {
if p.Check == nil {
return true, err
}
rc := prepareRecordTarget(mod)
err = i.Scan(rc)
if err != nil {
return
}
return p.Check(rc)
}
)
if len(p.Summaries) > 0 {
summaries = make(map[string]types.RecordSummary, len(p.Summaries))
}
if setLen == 0 {
return
}
existing := make(map[any]struct{}, 24)
looped := false
procSummary := func(r *types.Record) (err error) {
for _, smDef := range p.Summaries {
// Get record value
var vv []any
vv, err = r.GetValues(smDef.Field)
if err != nil {
return
}
bit := summaries[fmt.Sprintf("%s %s", smDef.Name, smDef.Field)]
// This will be constant so we're good
bit.Name = smDef.Name
// Skip empty
if len(vv) == 0 {
bit.EmptyCount++
summaries[fmt.Sprintf("%s %s", smDef.Name, smDef.Field)] = bit
continue
}
bit.NotEmptyCount++
for _, v := range vv {
bit.Count++
if _, ok := existing[v]; !ok {
existing[v] = struct{}{}
bit.UniqueCount++
}
}
switch smDef.Name {
case "min":
for _, v := range vv {
if !looped {
bit.Min = cast.ToFloat64(v)
} else {
bit.Min = math.Min(bit.Min, cast.ToFloat64(v))
}
}
case "max":
for _, v := range vv {
if !looped {
bit.Max = cast.ToFloat64(v)
} else {
bit.Max = math.Max(bit.Max, cast.ToFloat64(v))
}
}
case "avg":
for _, v := range vv {
bit.Sum += cast.ToFloat64(v)
}
case "sum":
for _, v := range vv {
bit.Sum += cast.ToFloat64(v)
}
case "earliest":
for _, v := range vv {
if bit.Earliest.IsZero() {
bit.Earliest = cast.ToTime(v)
} else {
aux := cast.ToTime(v)
if !aux.IsZero() && aux.Before(bit.Earliest) {
bit.Earliest = aux
}
}
}
case "latest":
for _, v := range vv {
if bit.Latest.IsZero() {
bit.Latest = cast.ToTime(v)
} else {
aux := cast.ToTime(v)
if !aux.IsZero() && aux.After(bit.Latest) {
bit.Latest = aux
}
}
}
}
looped = true
summaries[fmt.Sprintf("%s %s", smDef.Name, smDef.Field)] = bit
}
return
}
// Firstly sort out the current things
if len(p.Summaries) > 0 {
for _, r := range set {
err = procSummary(r)
if err != nil {
return
}
}
}
first = set[0]
last = set[setLen-1]
// Limit
out.Limit = p.Limit
// Sorting
out.Sort = dal.IteratorSorting(iter)
// No need to generate prev/next cursor
// if limit is not defined and set is empty
if p.Limit > 0 && len(set) > 0 {
// PrevPage
if p.PageCursor != nil {
out.PrevPage, err = dal.PreLoadCursor(ctx, iter, 100, true, first, recordChecker)
if err != nil {
return
}
}
// NextPage
out.NextPage, err = dal.PreLoadCursor(ctx, iter, 100, false, last, recordChecker)
if err != nil {
return
}
}
if p.IncTotal || p.IncPageNavigation || len(p.Summaries) > 0 {
// For the first page nav
err = generatePage(last)
if err != nil {
return
}
for counter == 0 || counter < howMuchMore {
counter++
interLoop := 0
if err = iter.More(howMuchMore, last); err != nil {
return
}
err = WalkIterator(ctx, iter, mod, func(rec *types.Record) error {
// check fetched record
if p.Check != nil {
if ok, err = p.Check(rec); err != nil {
return err
} else if !ok {
return nil
}
}
err = procSummary(rec)
if err != nil {
return err
}
interLoop++
total++
last = rec
return generatePage(rec)
})
if err != nil {
return
}
if interLoop < howMuchMore {
break
}
}
}
// Total
if p.IncTotal {
out.Total = total
}
// Page navigation
if p.IncPageNavigation {
// Ensure that the last page count is correct if it's not equal to the limit.
lastPageCount := pageNavigation[len(pageNavigation)-1].Count
if lastPageCount > 0 && lastPageCount != p.Limit && lastPageCount != total%p.Limit {
pageNavigation[len(pageNavigation)-1].Count = total % p.Limit
}
if p.Limit == 1 {
pageNavigation = pageNavigation[:len(pageNavigation)-1]
}
out.PageNavigation = pageNavigation
}
// Do averages
if len(p.Summaries) > 0 {
for n, s := range summaries {
if s.Name != "avg" {
continue
}
s.Avg = s.Sum / float64(s.Count)
summaries[n] = s
}
}
return
}
func prepareRecordTarget(module *types.Module) *types.Record {
// so we can avoid some code later involving (non)partitioned modules :seenoevil:
r := &types.Record{
ModuleID: module.ID,
NamespaceID: module.NamespaceID,
Values: make(types.RecordValueSet, 0, len(module.Fields)),
}
r.SetModule(module)
return r
}
func recToGetters(rr ...*types.Record) (out []dal.ValueGetter) {
out = make([]dal.ValueGetter, len(rr))
for i := range rr {
out[i] = rr[i]
}
return
}
func recCreateOperations(m *types.Module) (out dal.OperationSet) {
return dal.CreateOperations()
}
func recUpdateOperations(m *types.Module) (out dal.OperationSet) {
return dal.UpdateOperations()
}
func recDeleteOperations(m *types.Module) (out dal.OperationSet) {
return dal.DeleteOperations()
}
func recFilterOperations(f types.RecordFilter) (out dal.OperationSet) {
if f.PageCursor != nil {
out = append(out, dal.Paging)
}
if f.IncPageNavigation {
out = append(out, dal.Paging)
}
if f.Sort != nil {
out = append(out, dal.Sorting)
}
return
}
func recSearchOperations(m *types.Module, f types.RecordFilter) (out dal.OperationSet) {
return dal.SearchOperations().
Union(recFilterOperations(f))
}
func recLookupOperations(m *types.Module) (out dal.OperationSet) {
return dal.LookupOperations()
}