Commit a47146ea authored by Kirill Smelkov's avatar Kirill Smelkov

go/zodb/btree: {Min,Max}Key

Provide functionality to query for key-range limit for all children
under a tree node.
parent 07869c01
// Copyright (C) 2018 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
//
// Copyright (C) 2018-2019 Nexedi SA and Contributors.
// Kirill Smelkov <kirr@nexedi.com>
// Package btree provides B⁺ Trees for ZODB.
//
......@@ -22,6 +21,8 @@
//
// node.Get(key) performs point-query.
//
// node.{Min,Max}Key() returns key-range limit for all children/values under the node.
//
// node.Entryv() returns [] of (key, child/value).
//
// --------
......
......@@ -93,6 +93,7 @@ type BucketEntry struct {
value interface{}
}
// ---- access []entry ----
// Key returns BTree entry key.
func (e *Entry) Key() KEY { return e.key }
......@@ -132,6 +133,7 @@ func (b *Bucket) Entryv() []BucketEntry {
return ev
}
// ---- point query ----
// Get searches BTree by key.
//
......@@ -208,9 +210,104 @@ func (b *Bucket) get(key KEY) (interface{}, bool) {
return b.values[i], true
}
// TODO Bucket.MinKey
// TODO Bucket.MaxKey
// ---- min/max key ----
// MinKey returns minimum key in BTree.
//
// If the tree is empty, ok=false is returned.
// The tree does not need to be activated beforehand.
func (t *BTree) MinKey(ctx context.Context) (_ KEY, ok bool, err error) {
defer xerr.Contextf(&err, "btree(%s): minkey", t.POid())
err = t.PActivate(ctx)
if err != nil {
return 0, false, err
}
if len(t.data) == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
}
// NOTE -> can also use t.firstbucket
for {
child := t.data[0].child.(zodb.IPersistent)
t.PDeactivate()
err = child.PActivate(ctx)
if err != nil {
return 0, false, err
}
switch child := child.(type) {
case *BTree:
t = child
case *Bucket:
k, ok := child.MinKey()
child.PDeactivate()
return k, ok, nil
}
}
}
// MaxKey returns maximum key in BTree.
//
// If the tree is empty, ok=false is returned.
// The tree does not need to be activated beforehand.
func (t *BTree) MaxKey(ctx context.Context) (_ KEY, _ bool, err error) {
defer xerr.Contextf(&err, "btree(%s): maxkey", t.POid())
err = t.PActivate(ctx)
if err != nil {
return 0, false, err
}
l := len(t.data)
if l == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
}
for {
child := t.data[l-1].child.(zodb.IPersistent)
t.PDeactivate()
err = child.PActivate(ctx)
if err != nil {
return 0, false, err
}
switch child := child.(type) {
case *BTree:
t = child
case *Bucket:
k, ok := child.MaxKey()
child.PDeactivate()
return k, ok, nil
}
}
}
// MinKey returns minimum key in Bucket.
//
// If the bucket is empty, ok=false is returned.
func (b *Bucket) MinKey() (_ KEY, ok bool) {
if len(b.keys) == 0 {
return 0, false
}
return b.keys[0], true
}
// MaxKey returns maximum key in Bucket.
//
// If the bucket is empty, ok=false is returned.
func (b *Bucket) MaxKey() (_ KEY, ok bool) {
l := len(b.keys)
if l == 0 {
return 0, false
}
return b.keys[l-1], true
}
// ---- serialization ----
......
......@@ -26,6 +26,7 @@ import (
"testing"
"lab.nexedi.com/kirr/go123/exc"
"lab.nexedi.com/kirr/go123/xerr"
"lab.nexedi.com/kirr/neo/go/transaction"
"lab.nexedi.com/kirr/neo/go/zodb"
_ "lab.nexedi.com/kirr/neo/go/zodb/wks"
......@@ -53,6 +54,48 @@ type testEntry struct {
// bmapping represents LOBTree or a LOBucket wrapper.
type bmapping interface {
Get(context.Context, int64) (interface{}, bool, error)
MinKey(context.Context) (int64, bool, error)
MaxKey(context.Context) (int64, bool, error)
}
// bucketWrap is syntatic sugar to automatically activate/deactivate a bucket on Get/{Min,Max}Key calls.
type bucketWrap LOBucket
func (b *bucketWrap) bucket() *LOBucket {
return (*LOBucket)(b)
}
// withBucket runs f with b.LOBucket activated.
func (b *bucketWrap) withBucket(ctx context.Context, f func()) error {
err := b.bucket().PActivate(ctx)
if err != nil {
return err
}
defer b.bucket().PDeactivate()
f()
return nil
}
func (b *bucketWrap) Get(ctx context.Context, key int64) (v interface{}, ok bool, err error) {
err = b.withBucket(ctx, func() {
v, ok = b.bucket().get(key) // TODO -> Get
})
return
}
func (b *bucketWrap) MinKey(ctx context.Context) (k int64, ok bool, err error) {
err = b.withBucket(ctx, func() {
k, ok = b.bucket().MinKey()
})
return
}
func (b *bucketWrap) MaxKey(ctx context.Context) (k int64, ok bool, err error) {
err = b.withBucket(ctx, func() {
k, ok = b.bucket().MaxKey()
})
return
}
func TestBTree(t *testing.T) {
......@@ -81,19 +124,18 @@ func TestBTree(t *testing.T) {
t.Fatal(err)
}
obj, ok := xobj.(bmapping)
if !ok {
t.Fatalf("%s: got %T; want LOBucket|LOBTree", tt.oid, xobj)
}
want := ""
switch tt.kind {
case kindBucket:
if _, ok = obj.(*LOBucket); !ok {
bobj, ok := xobj.(*LOBucket)
if !ok {
want = "LOBucket"
} else {
// bucket wrapper that accepts ctx on Get, MinKey etc
xobj = (*bucketWrap)(bobj)
}
case kindBTree:
if _, ok = obj.(*LOBTree); !ok {
if _, ok := xobj.(*LOBTree); !ok {
want = "LOBTree"
}
default:
......@@ -101,9 +143,11 @@ func TestBTree(t *testing.T) {
}
if want != "" {
t.Fatalf("%s: got %T; want %s", tt.oid, obj, want)
t.Fatalf("%s: got %T; want %s", tt.oid, xobj, want)
}
obj := xobj.(bmapping)
for _, kv := range tt.itemv {
value, ok, err := obj.Get(ctx, kv.key)
if err != nil {
......@@ -123,11 +167,32 @@ func TestBTree(t *testing.T) {
// XXX .next == nil
// XXX check keys, values directly (i.e. there is nothing else)
}
// {Min,Max}Key
kmin, okmin, emin := obj.MinKey(ctx)
kmax, okmax, emax := obj.MaxKey(ctx)
if err := xerr.Merge(emin, emax); err != nil {
t.Errorf("%s: min/max key: %s", tt.oid, err)
continue
}
ok := false
ka, kb := int64(0), int64(0)
if l := len(tt.itemv); l != 0 {
ok = true
ka = tt.itemv[0].key
kb = tt.itemv[l-1].key
}
if !(kmin == ka && kmax == kb && okmin == ok && okmax == ok) {
t.Errorf("%s: min/max key wrong: got [%v, %v] (%v, %v); want [%v, %v] (%v, %v)",
tt.oid, kmin, kmax, okmin, okmax, ka, kb, ok, ok)
}
}
// B3 is a large BTree with {i: i} data.
// verify Get(key) and that different bucket links lead to the same in-RAM object.
// verify Get(key), {Min,Max}Key and that different bucket links lead to the same in-RAM object.
xB3, err := conn.Get(ctx, B3_oid)
if err != nil {
t.Fatal(err)
......@@ -150,6 +215,16 @@ func TestBTree(t *testing.T) {
}
}
kmin, okmin, emin := B3.MinKey(ctx)
kmax, okmax, emax := B3.MaxKey(ctx)
if err := xerr.Merge(emin, emax); err != nil {
t.Fatalf("B3: min/max key: %s", err)
}
if !(kmin == 0 && kmax == B3_maxkey && okmin && okmax) {
t.Fatalf("B3: min/max key wrong: got [%v, %v] (%v, %v); want [%v, %v] (%v, %v)",
kmin, kmax, okmin, okmax, 0, B3_maxkey, true, true)
}
// verifyFirstBucket verifies that b.firstbucket is correct and returns it.
var verifyFirstBucket func(b *LOBTree) *LOBucket
verifyFirstBucket = func(b *LOBTree) *LOBucket {
......
......@@ -95,6 +95,7 @@ type IOBucketEntry struct {
value interface{}
}
// ---- access []entry ----
// Key returns IOBTree entry key.
func (e *IOEntry) Key() int32 { return e.key }
......@@ -134,6 +135,7 @@ func (b *IOBucket) Entryv() []IOBucketEntry {
return ev
}
// ---- point query ----
// Get searches IOBTree by key.
//
......@@ -210,9 +212,104 @@ func (b *IOBucket) get(key int32) (interface{}, bool) {
return b.values[i], true
}
// TODO IOBucket.MinKey
// TODO IOBucket.MaxKey
// ---- min/max key ----
// MinKey returns minimum key in IOBTree.
//
// If the tree is empty, ok=false is returned.
// The tree does not need to be activated beforehand.
func (t *IOBTree) MinKey(ctx context.Context) (_ int32, ok bool, err error) {
defer xerr.Contextf(&err, "btree(%s): minkey", t.POid())
err = t.PActivate(ctx)
if err != nil {
return 0, false, err
}
if len(t.data) == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
}
// NOTE -> can also use t.firstbucket
for {
child := t.data[0].child.(zodb.IPersistent)
t.PDeactivate()
err = child.PActivate(ctx)
if err != nil {
return 0, false, err
}
switch child := child.(type) {
case *IOBTree:
t = child
case *IOBucket:
k, ok := child.MinKey()
child.PDeactivate()
return k, ok, nil
}
}
}
// MaxKey returns maximum key in IOBTree.
//
// If the tree is empty, ok=false is returned.
// The tree does not need to be activated beforehand.
func (t *IOBTree) MaxKey(ctx context.Context) (_ int32, _ bool, err error) {
defer xerr.Contextf(&err, "btree(%s): maxkey", t.POid())
err = t.PActivate(ctx)
if err != nil {
return 0, false, err
}
l := len(t.data)
if l == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
}
for {
child := t.data[l-1].child.(zodb.IPersistent)
t.PDeactivate()
err = child.PActivate(ctx)
if err != nil {
return 0, false, err
}
switch child := child.(type) {
case *IOBTree:
t = child
case *IOBucket:
k, ok := child.MaxKey()
child.PDeactivate()
return k, ok, nil
}
}
}
// MinKey returns minimum key in IOBucket.
//
// If the bucket is empty, ok=false is returned.
func (b *IOBucket) MinKey() (_ int32, ok bool) {
if len(b.keys) == 0 {
return 0, false
}
return b.keys[0], true
}
// MaxKey returns maximum key in IOBucket.
//
// If the bucket is empty, ok=false is returned.
func (b *IOBucket) MaxKey() (_ int32, ok bool) {
l := len(b.keys)
if l == 0 {
return 0, false
}
return b.keys[l-1], true
}
// ---- serialization ----
......
......@@ -95,6 +95,7 @@ type LOBucketEntry struct {
value interface{}
}
// ---- access []entry ----
// Key returns LOBTree entry key.
func (e *LOEntry) Key() int64 { return e.key }
......@@ -134,6 +135,7 @@ func (b *LOBucket) Entryv() []LOBucketEntry {
return ev
}
// ---- point query ----
// Get searches LOBTree by key.
//
......@@ -210,9 +212,104 @@ func (b *LOBucket) get(key int64) (interface{}, bool) {
return b.values[i], true
}
// TODO LOBucket.MinKey
// TODO LOBucket.MaxKey
// ---- min/max key ----
// MinKey returns minimum key in LOBTree.
//
// If the tree is empty, ok=false is returned.
// The tree does not need to be activated beforehand.
func (t *LOBTree) MinKey(ctx context.Context) (_ int64, ok bool, err error) {
defer xerr.Contextf(&err, "btree(%s): minkey", t.POid())
err = t.PActivate(ctx)
if err != nil {
return 0, false, err
}
if len(t.data) == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
}
// NOTE -> can also use t.firstbucket
for {
child := t.data[0].child.(zodb.IPersistent)
t.PDeactivate()
err = child.PActivate(ctx)
if err != nil {
return 0, false, err
}
switch child := child.(type) {
case *LOBTree:
t = child
case *LOBucket:
k, ok := child.MinKey()
child.PDeactivate()
return k, ok, nil
}
}
}
// MaxKey returns maximum key in LOBTree.
//
// If the tree is empty, ok=false is returned.
// The tree does not need to be activated beforehand.
func (t *LOBTree) MaxKey(ctx context.Context) (_ int64, _ bool, err error) {
defer xerr.Contextf(&err, "btree(%s): maxkey", t.POid())
err = t.PActivate(ctx)
if err != nil {
return 0, false, err
}
l := len(t.data)
if l == 0 {
// empty btree
t.PDeactivate()
return 0, false, nil
}
for {
child := t.data[l-1].child.(zodb.IPersistent)
t.PDeactivate()
err = child.PActivate(ctx)
if err != nil {
return 0, false, err
}
switch child := child.(type) {
case *LOBTree:
t = child
case *LOBucket:
k, ok := child.MaxKey()
child.PDeactivate()
return k, ok, nil
}
}
}
// MinKey returns minimum key in LOBucket.
//
// If the bucket is empty, ok=false is returned.
func (b *LOBucket) MinKey() (_ int64, ok bool) {
if len(b.keys) == 0 {
return 0, false
}
return b.keys[0], true
}
// MaxKey returns maximum key in LOBucket.
//
// If the bucket is empty, ok=false is returned.
func (b *LOBucket) MaxKey() (_ int64, ok bool) {
l := len(b.keys)
if l == 0 {
return 0, false
}
return b.keys[l-1], true
}
// ---- serialization ----
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment