tauraamui/kvs

View on GitHub
storage/store.go

Summary

Maintainability
A
0 mins
Test Coverage
// Copyright (c) 2023 Adam Prakash Stringer
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted (subject to the limitations in the disclaimer
// below) provided that the following conditions are met:
//
//     * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//
//     * Redistributions in binary form must reproduce the above copyright
//     notice, this list of conditions and the following disclaimer in the
//     documentation and/or other materials provided with the distribution.
//
//     * Neither the name of the copyright holder nor the names of its
//     contributors may be used to endorse or promote products derived from this
//     software without specific prior written permission.
//
// NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
// THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
// IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

package storage

import (
    "fmt"
    "strconv"
    "strings"

    "github.com/dgraph-io/badger/v3"
    "github.com/tauraamui/kvs"
)

type Value interface {
    TableName() string
}

type Store struct {
    db  kvs.KVDB
    pks map[string]*badger.Sequence
}

func New(db kvs.KVDB) Store {
    return Store{db: db, pks: map[string]*badger.Sequence{}}
}

func (s Store) Save(owner kvs.UUID, value Value) error {
    rowID, err := nextRowID(s.db, owner, value.TableName(), s.pks)
    if err != nil {
        return err
    }

    return saveValue(s.db, value.TableName(), owner, rowID, value)
}

func (s Store) Update(owner kvs.UUID, value Value, rowID uint32) error {
    return saveValue(s.db, value.TableName(), owner, rowID, value)
}

func saveValue(db kvs.KVDB, tableName string, ownerID kvs.UUID, rowID uint32, v Value) error {
    if v == nil {
        return nil
    }
    entries := kvs.ConvertToEntries(tableName, ownerID, rowID, v)
    for _, e := range entries {
        if err := kvs.Store(db, e); err != nil {
            return err
        }
    }

    return kvs.LoadID(v, rowID)
}

func (s Store) Delete(owner kvs.UUID, value Value, rowID uint32) error {
    db := s.db

    blankEntries := kvs.ConvertToBlankEntries(value.TableName(), owner, rowID, value)
    for _, ent := range blankEntries {
        db.Update(func(txn *badger.Txn) error {
            return txn.Delete(ent.Key())
        })
    }

    return nil
}

func Load[T Value](s Store, dest T, owner kvs.UUID, rowID uint32) error {
    db := s.db

    blankEntries := kvs.ConvertToBlankEntries(dest.TableName(), owner, rowID, dest)
    for _, ent := range blankEntries {
        db.View(func(txn *badger.Txn) error {
            item, err := txn.Get(ent.Key())
            if err != nil {
                return err
            }

            if err := item.Value(func(val []byte) error {
                ent.Data = val
                return nil
            }); err != nil {
                return err
            }

            return nil
        })

        ent.RowID = rowID

        if err := kvs.LoadEntry(dest, ent); err != nil {
            return err
        }
    }

    return kvs.LoadID(dest, rowID)
}

func extractRowFromKey(k string) (int, error) {
    rowPos := strings.LastIndex(k, ".")
    return strconv.Atoi(k[rowPos+1:])
}

func LoadAll[T Value](s Store, v T, owner kvs.UUID) ([]T, error) {
    db := s.db
    dest := []T{}

    blankEntries := kvs.ConvertToBlankEntries(v.TableName(), owner, 0, v)
    for _, ent := range blankEntries {
        // iterate over all stored values for this entry
        prefix := ent.PrefixKey()
        if err := db.View(func(txn *badger.Txn) error {
            it := txn.NewIterator(badger.DefaultIteratorOptions)
            defer it.Close()

            var structFieldIndex uint32 = 0
            for it.Seek(prefix); it.ValidForPrefix(prefix); it.Next() {
                item := it.Item()

                if len(dest) == 0 || structFieldIndex >= uint32(len(dest)) {
                    key := string(item.Key())
                    rowID, err := extractRowFromKey(string(key))
                    if err != nil {
                        return err
                    }

                    dest = append(dest, *new(T))

                    // for reasons, we have to just keep assigning the current "field we're on" as the full entry's ID
                    if err := kvs.LoadID(&dest[structFieldIndex], uint32(rowID)); err != nil {
                        return err
                    }
                }

                ent.RowID = structFieldIndex
                if err := item.Value(func(val []byte) error {
                    ent.Data = val
                    return nil
                }); err != nil {
                    return err
                }

                if err := kvs.LoadEntry(&dest[structFieldIndex], ent); err != nil {
                    return err
                }
                structFieldIndex++
            }
            return nil
        }); err != nil {
            return nil, err
        }
    }
    return dest, nil
}

func (s Store) Close() (err error) {
    if s.pks == nil {
        return
    }
    for _, seq := range s.pks {
        if seq == nil {
            continue
        }
        // TODO:(tauraamui) should collect all errors into group here rather than immediately escape
        if err = seq.Release(); err != nil {
            return
        }
    }

    s.pks = nil

    return
}

func nextRowID(db kvs.KVDB, owner kvs.UUID, tableName string, pks map[string]*badger.Sequence) (uint32, error) {
    seq, err := resolveSequence(db, fmt.Sprintf("%s.%s", owner, tableName), pks)
    if err != nil {
        return 0, err
    }

    s, err := seq.Next()
    if err != nil {
        return 0, err
    }
    return uint32(s), nil
}

func nextSequence(seq *badger.Sequence) (uint32, error) {
    s, err := seq.Next()
    if err != nil {
        return 0, err
    }
    return uint32(s), nil
}

func resolveSequence(db kvs.KVDB, sequenceKey string, pks map[string]*badger.Sequence) (*badger.Sequence, error) {
    seq, ok := pks[sequenceKey]
    var err error
    if !ok {
        seq, err = db.GetSeq([]byte(sequenceKey), 1)
        if err != nil {
            return nil, err
        }
        pks[sequenceKey] = seq
    }

    return seq, nil
}