Files
graph/tok/hnsw/test_helper.go
zeekay f5aaeb712e Hanzo Graph — the engine, on zapdb, ZAP-native
Derived from Dgraph (github.com/dgraph-io/dgraph) at commit 64804bd2, Apache-2.0.
See NOTICE.

WHAT THIS IS. The graph engine as a Go library: posting lists, the DQL parser,
the type and tokenizer system, the index and schema layers, and the wire types.
39,170 lines of the upstream's 166,337.

THE STORE is github.com/luxfi/zapdb, a Badger fork that kept the package name,
the subpackages (options, pb, y) and the API — Open, OpenManaged, DB, Options —
so the 74 import sites are aliased rather than rewritten.

THE CODEC is ZAP. zapdb serializes with ZAP, not protobuf: its pb package builds
the ZAP types by default and the protobuf ones only under `-tags grpc`, a path
that does not compile (table/builder.go calls pb.Marshal, which that tag
removes). One call site in posting needed converting; every other type here is
Hanzo's own protobuf and is untouched.

NO TRANSPORT. An embedded engine dials nothing, so nothing here imports
google.golang.org/grpc. Removed: 20 transport functions in x, the generated
service stubs for Raft, Zero and Worker (1,657 lines), and the JWT helper —
identity belongs to IAM. The core called none of them.

NOT KEPT: worker, conn, raftwal, the cluster commands, the GraphQL layer and the
test harnesses. `query`, the DQL execution engine, went with them because it
depends on worker; parsing DQL is available here, executing it is not.

The history of the upstream project lives at github.com/dgraph-io/dgraph. This
repository begins at the derivation because three quarters of that history
describes code this fork does not carry.
2026-08-20 21:26:27 -07:00

285 lines
7.4 KiB
Go

/*
* SPDX-FileCopyrightText: © 2017-2026 Istari Digital, Inc.
* SPDX-License-Identifier: Apache-2.0
*/
package hnsw
import (
"context"
"encoding/binary"
"math"
"strings"
"sync"
"github.com/hanzoai/graph/tok/index"
"github.com/pkg/errors"
)
// holds an map in memory that is a string (which will be []bytes as string)
// as the key, with an index.Val as the value
type indexStorage struct {
inMemTestDb map[string][]byte
//Two locks allow for lock promotion when writing, so we promote a read lock
//between the start and finish times to a full lock on the finish time
// readMu acquires read locks when accessing values
readMu sync.RWMutex
// writeMu acquires write locks on mutations
writeMu sync.Mutex
}
// datastructure visualization of persistent db over 100 units of time
// within this, we will conduct all testing, i.e. reads at 1 Ts = tsDbs[1],
// writes at 4 Ts = tsDbs[4]
var tsDbs [100]indexStorage
func emptyTsDbs() {
for i := range tsDbs {
tsDbs[i] = indexStorage{inMemTestDb: make(map[string][]byte)}
}
}
type inMemList struct {
key string
startTs uint64
finishTs uint64
}
// creates a new inMem list with the list's corresponding key,
// when it's action was started and when it will conclude.
// for mutations startTs will be txn.StartTs and finishTs will be txn.commitTs
// for reads, they both start and finish at c.ReadTs
// finishTs is unknown in real scenarios, this is for testing purposes
func newInMemList(key string, startTs, finishTs uint64) *inMemList {
return &inMemList{
key: key,
startTs: startTs,
finishTs: finishTs,
}
}
// locks the posting list & invokes ValueWithLockHeld
func (l *inMemList) Value(readTs uint64) (rval []byte, rerr error) {
// reading should only lock the db at current instance in time
tsDbs[readTs].readMu.RLock()
defer tsDbs[readTs].readMu.RUnlock()
return l.ValueWithLockHeld(readTs)
}
// reads value from the database at readTs corresponding to List's key
func (l *inMemList) ValueWithLockHeld(readTs uint64) (rval []byte, rerr error) {
val, ok := tsDbs[readTs].inMemTestDb[l.key]
if !ok {
return nil, errors.New("Could not find data with key " + l.key)
}
return val, nil
}
// locks the posting list and invokes AddMutationWithLockHeld
func (l *inMemList) AddMutation(ctx context.Context, txn index.Txn, t *index.KeyValue) error {
// locks from the txn.StartTs up to txn.CommitTs
l.Lock()
defer l.Unlock()
return l.AddMutationWithLockHeld(ctx, txn, t)
}
// adds mutation to the database at the txn's commitTs
func (l *inMemList) AddMutationWithLockHeld(ctx context.Context, txn index.Txn, t *index.KeyValue) error {
// creates key from directedEdge
//builds value
val := t.Value
// a mutation persists from the moment the txn gets committed until the "rest of time"
for i := l.finishTs; i < uint64(len(tsDbs)); i++ {
tsDbs[i].inMemTestDb[l.key] = val
}
return nil
}
// if youre locking at a certain point in time, the lock should be held for this moment
// and all future moments until your commitTs
func (l *inMemList) Lock() {
if !strings.Contains(l.key, "entry") {
for i := l.startTs; i <= l.finishTs; i++ {
tsDbs[i].readMu.RLock()
}
for i := l.finishTs; i < uint64(len(tsDbs)); i++ {
tsDbs[i].writeMu.Lock()
}
}
}
// undoes lock
func (l *inMemList) Unlock() {
if !strings.Contains(l.key, "entry") {
for i := l.startTs; i <= l.finishTs; i++ {
tsDbs[i].readMu.RUnlock()
}
for i := l.finishTs; i < uint64(len(tsDbs)); i++ {
tsDbs[i].writeMu.Unlock()
}
}
}
// a txn has a startTs (when the txn started) and commitTs (when the txn changes were committed)
type inMemTxn struct {
startTs uint64
commitTs uint64
}
func (t *inMemTxn) Find(prefix []byte, filter func([]byte) bool) (uint64, error) {
tsDbs[t.startTs].readMu.RLock()
defer tsDbs[t.startTs].readMu.RUnlock()
for _, b := range tsDbs[t.startTs].inMemTestDb {
if filter(b) {
return 1, nil
}
}
return 0, nil
}
func (t *inMemTxn) StartTs() uint64 {
return t.startTs
}
// locks the txn and invokes GetWithLockHeld
func (t *inMemTxn) Get(key []byte) (rval []byte, rerr error) {
tsDbs[t.startTs].readMu.RLock()
defer tsDbs[t.startTs].readMu.RUnlock()
return t.GetWithLockHeld(key)
}
// reads value from the database at txn's startTs
func (t *inMemTxn) GetWithLockHeld(key []byte) (rval []byte, rerr error) {
val, ok := tsDbs[t.startTs].inMemTestDb[string(key[:])]
if !ok {
return nil, errors.New("Could not find data with key " + string(key[:]))
}
return val, nil
}
// locks the txn and invokes AddMutationWithLockHeld
func (t *inMemTxn) AddMutation(ctx context.Context, key []byte, t1 *index.KeyValue) error {
tsDbs[t.startTs].writeMu.Lock()
defer tsDbs[t.startTs].writeMu.Unlock()
return t.AddMutationWithLockHeld(ctx, key, t1)
}
// adds mutation to the database at the txn's commitTs
func (t *inMemTxn) AddMutationWithLockHeld(ctx context.Context, key []byte, t1 *index.KeyValue) error {
val := t1.Value
for i := t.commitTs; i < uint64(len(tsDbs)); i++ {
tsDbs[i].inMemTestDb[string(key[:])] = val
}
return nil
}
// locks the txn
func (t *inMemTxn) LockKey(key []byte) {
if !strings.Contains(string(key[:]), "entry") {
// locks from the txn.StartTs up to txn.CommitTs
for i := t.startTs; i <= t.commitTs; i++ {
tsDbs[i].readMu.RLock()
}
for i := t.commitTs; i < uint64(len(tsDbs)); i++ {
tsDbs[i].writeMu.Lock()
}
}
}
// undoes lock
func (t *inMemTxn) UnlockKey(key []byte) {
if !strings.Contains(string(key[:]), "entry") {
// locks from the txn.StartTs up to txn.CommitTs
for i := t.startTs; i <= t.commitTs; i++ {
tsDbs[i].readMu.RUnlock()
}
for i := t.commitTs; i < uint64(len(tsDbs)); i++ {
tsDbs[i].writeMu.Unlock()
}
}
}
type inMemLocalCache struct {
readTs uint64
}
// locks the local cache and invokes GetWithLockHeld
func (c *inMemLocalCache) Get(key []byte) (rval []byte, rerr error) {
tsDbs[c.readTs].readMu.RLock()
defer tsDbs[c.readTs].readMu.RUnlock()
return c.GetWithLockHeld(key)
}
func (c *inMemLocalCache) Find(prefix []byte, filter func([]byte) bool) (uint64, error) {
tsDbs[c.readTs].readMu.RLock()
defer tsDbs[c.readTs].readMu.RUnlock()
for _, b := range tsDbs[c.readTs].inMemTestDb {
if filter(b) {
return 1, nil
}
}
return 0, nil
}
// reads value from the database at c's readTs
func (c *inMemLocalCache) GetWithLockHeld(key []byte) (rval []byte, rerr error) {
val, ok := tsDbs[c.readTs].inMemTestDb[string(key[:])]
if !ok {
return nil, errors.New("Could not find data with key " + string(key[:]))
}
return val, nil
}
func equalFloat64Slice(a, b []float64) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func equalStringSlice(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func equalUint64Slice(a, b []uint64) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// floatArrayAsBytes(v) will create a byte array encoding
// v using LittleEndian format. This is sort of the inverse
// of BytesAsFloatArray, but note that we can always be successful
// converting to bytes, but the inverse is not feasible.
func floatArrayAsBytes(v []float64) []byte {
retVal := make([]byte, 8*len(v))
offset := retVal
for i := range v {
bits := math.Float64bits(v[i])
binary.LittleEndian.PutUint64(offset, bits)
offset = offset[8:]
}
return retVal
}