From 0e5fe2321a1a01ac4e13d08078b6bbb02f7ea531 Mon Sep 17 00:00:00 2001 From: gKits Date: Wed, 20 Dec 2023 12:58:06 +0100 Subject: [PATCH 01/51] ref: cleanup for major rewrite --- cmd/pavosql/main.go | 3 +- internal/btree/btree.go | 547 ------------------------- internal/btree/btree_test.go | 82 ---- internal/btree/iterator.go | 42 -- internal/btree/iterator_test.go | 1 - internal/btree/leafNode.go | 160 -------- internal/btree/leafNode_test.go | 495 ---------------------- internal/btree/node.go | 40 -- internal/btree/pointerNode.go | 174 -------- internal/btree/pointerNode_test.go | 1 - internal/cli/cli.go | 51 --- internal/db/cell.go | 142 ------- internal/db/condition.go | 23 -- internal/db/database.go | 145 ------- internal/db/defaults.go | 25 -- internal/db/row.go | 6 - internal/db/table.go | 35 -- internal/db/types.go | 57 --- internal/dbms/dbms.go | 13 - internal/freelist/freelist.go | 109 ----- internal/freelist/freelistNode.go | 65 --- internal/freelist/freelistNode_test.go | 1 - internal/freelist/freelist_test.go | 1 - internal/kvstore/kvreader.go | 45 -- internal/kvstore/kvstore.go | 242 ----------- internal/kvstore/kvwriter.go | 68 --- internal/kvstore/mmap.go | 100 ----- internal/kvstore/mmap_windows.go | 107 ----- internal/parser/lexer.go | 238 ----------- internal/parser/parse.go | 8 - internal/parser/token.go | 99 ----- internal/server/server.go | 71 ---- pkg/vcache/vcache.go | 36 -- 33 files changed, 2 insertions(+), 3230 deletions(-) delete mode 100644 internal/btree/btree.go delete mode 100644 internal/btree/btree_test.go delete mode 100644 internal/btree/iterator.go delete mode 100644 internal/btree/iterator_test.go delete mode 100644 internal/btree/leafNode.go delete mode 100644 internal/btree/leafNode_test.go delete mode 100644 internal/btree/node.go delete mode 100644 internal/btree/pointerNode.go delete mode 100644 internal/btree/pointerNode_test.go delete mode 100644 internal/cli/cli.go delete mode 100644 internal/db/cell.go delete mode 100644 internal/db/condition.go delete mode 100644 internal/db/database.go delete mode 100644 internal/db/defaults.go delete mode 100644 internal/db/row.go delete mode 100644 internal/db/table.go delete mode 100644 internal/db/types.go delete mode 100644 internal/dbms/dbms.go delete mode 100644 internal/freelist/freelist.go delete mode 100644 internal/freelist/freelistNode.go delete mode 100644 internal/freelist/freelistNode_test.go delete mode 100644 internal/freelist/freelist_test.go delete mode 100644 internal/kvstore/kvreader.go delete mode 100644 internal/kvstore/kvstore.go delete mode 100644 internal/kvstore/kvwriter.go delete mode 100644 internal/kvstore/mmap.go delete mode 100644 internal/kvstore/mmap_windows.go delete mode 100644 internal/parser/lexer.go delete mode 100644 internal/parser/parse.go delete mode 100644 internal/parser/token.go delete mode 100644 internal/server/server.go delete mode 100644 pkg/vcache/vcache.go diff --git a/cmd/pavosql/main.go b/cmd/pavosql/main.go index 38dd16d..da29a2c 100644 --- a/cmd/pavosql/main.go +++ b/cmd/pavosql/main.go @@ -1,3 +1,4 @@ package main -func main() {} +func main() { +} diff --git a/internal/btree/btree.go b/internal/btree/btree.go deleted file mode 100644 index 3ce8c35..0000000 --- a/internal/btree/btree.go +++ /dev/null @@ -1,547 +0,0 @@ -package btree - -import ( - "errors" - "fmt" -) - -type getFunc func(uint64) ([]byte, error) -type pullFunc func(uint64) ([]byte, error) -type allocFunc func([]byte) (uint64, error) -type freeFunc func(uint64) error - -type BTree struct { - Root uint64 - pgSize int - get func(uint64) (node, error) - pull func(uint64) (node, error) - alloc func(node) (uint64, error) - free func(uint64) error - readOnly bool -} - -func NewReadOnly(root uint64, pgSize int, get getFunc) BTree { - return BTree{ - Root: root, - pgSize: pgSize, - get: func(ptr uint64) (node, error) { - d, err := get(ptr) - if err != nil { - return nil, err - } - return decodeNode(d) - }, - readOnly: true, - } - -} - -func New( - root uint64, pgSize int, - get getFunc, pull pullFunc, alloc allocFunc, free freeFunc, -) BTree { - return BTree{ - Root: root, - pgSize: pgSize, - get: func(ptr uint64) (node, error) { - d, err := get(ptr) - if err != nil { - return nil, err - } - return decodeNode(d) - }, - pull: func(ptr uint64) (node, error) { - d, err := pull(ptr) - if err != nil { - return nil, err - } - return decodeNode(d) - }, - alloc: func(n node) (uint64, error) { - return alloc(n.Encode()) - }, - free: free, - readOnly: false, - } -} - -func (bt *BTree) Get(k []byte) ([]byte, error) { - errMsg := "btree: cannot get key: %v" - - if bt.Root == 0 { - return nil, fmt.Errorf(errMsg, "tree is empty") - } - - root, err := bt.get(bt.Root) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - - _, v, err := bt.bTreeGet(root, k) - return v, err -} - -func (bt *BTree) Set(k, v []byte) (err error) { - if bt.readOnly { - return fmt.Errorf("btree: set operation not allow on read only tree") - } - errMsg := "btree: cannot set key: %v" - - if bt.Root == 0 { - root := leafNode{} - root, err := root.Insert(0, k, v) - if err != nil { - return fmt.Errorf(errMsg, err) - } - - bt.Root, err = bt.alloc(root) - if err != nil { - return fmt.Errorf(errMsg, err) - } - - return nil - } - - root, err := bt.pull(bt.Root) - if err != nil { - return fmt.Errorf(errMsg, err) - } - - inserted, err := bt.bTreeInsert(root, k, v) - if err != nil { - return fmt.Errorf(errMsg, err) - } - - if inserted.Size() > bt.pgSize { - insertedPtr, err := bt.alloc(inserted) - if err != nil { - return fmt.Errorf(errMsg, err) - } - - k, err := inserted.Key(0) - if err != nil { - return fmt.Errorf(errMsg, err) - } - - t := pointerNode{} - t.Insert(0, k, insertedPtr) - - inserted, err = bt.splitChildPtr(0, t, inserted) - if err != nil { - return fmt.Errorf(errMsg, err) - } - - } - - bt.Root, err = bt.alloc(inserted) - return fmt.Errorf(errMsg, err) -} - -func (bt *BTree) Delete(k []byte) (bool, error) { - if bt.readOnly { - return false, fmt.Errorf("btree: delete operation not allow on read only tree") - } - errMsg := "btree: cannot delete key: %v" - - if bt.Root == 0 { - return false, fmt.Errorf(errMsg, "tree is empty") - } - - root, err := bt.pull(bt.Root) - if err != nil { - return false, fmt.Errorf(errMsg, err) - } - - var deleted bool - root, deleted, err = bt.bTreeDelete(root, k) - if err != nil { - return false, fmt.Errorf(errMsg, err) - } - - if !deleted { - return deleted, nil - } - - if root.Type() == btreePointer && root.Total() == 1 { - pntrRoot := root.(pointerNode) - bt.Root, _ = pntrRoot.Ptr(0) - } else { - bt.Root, err = bt.alloc(root) - if err != nil { - return false, fmt.Errorf(errMsg, err) - } - } - - return true, nil -} - -func (bt *BTree) bTreeGet(n node, k []byte) (node, []byte, error) { - i, exists := n.Search(k) - - switch n.Type() { - case btreeLeaf: - leafN := n.(leafNode) - - if !exists { - return nil, nil, errors.New("key does not exist") - } - - v, err := leafN.Val(i) - if err != nil { - return nil, nil, err - } - return n, v, nil - - case btreePointer: - pntrN := n.(pointerNode) - - ptr, _ := pntrN.Ptr(i) - child, err := bt.get(ptr) - if err != nil { - return nil, nil, err - } - - return bt.bTreeGet(child, k) - default: - return nil, nil, errors.New("invalid node type") - } -} - -func (bt *BTree) bTreeInsert(n node, k, v []byte) (node, error) { - i, exists := n.Search(k) - - var err error - var inserted node - - switch n.Type() { - case btreeLeaf: - leafN := n.(leafNode) - - if !exists { - inserted, err = leafN.Insert(i, k, v) - if err != nil { - return nil, err - } - } else { - inserted, err = leafN.Update(i, k, v) - if err != nil { - return nil, err - } - } - - case btreePointer: - pntrN := n.(pointerNode) - - ptr, _ := pntrN.Ptr(i) - child, err := bt.get(ptr) - if err != nil { - return nil, err - } - - inserted, err = bt.bTreeInsert(child, k, v) - if err != nil { - return nil, err - } - - inserted, err = bt.splitChildPtr(i, pntrN, inserted) - if err != nil { - return nil, err - } - - inPtr, err := bt.alloc(inserted) - if err != nil { - return nil, err - } - - ptrKey, _ := pntrN.Key(i) - inserted, err = pntrN.Update(i, ptrKey, inPtr) - if err != nil { - return nil, err - } - - default: - return nil, errors.New("invalid node type") - } - - return inserted, nil -} - -func (bt *BTree) bTreeDelete(n node, k []byte) (node, bool, error) { - i, exists := n.Search(k) - - var err error - var deleted node - - switch n.Type() { - case btreeLeaf: - if !exists { - return nil, false, errors.New("key does not exist") - } - - leafN := n.(leafNode) - deleted, err = leafN.Delete(i) - if err != nil { - return nil, false, err - } - - case btreePointer: - n := n.(pointerNode) - - ptr, _ := n.Ptr(i) - child, err := bt.pull(ptr) - if err != nil { - return nil, false, err - } - - deleted, _, err = bt.bTreeDelete(child, k) - if err != nil { - return nil, false, err - } - - if deleted.Size() > bt.pgSize/4 { - deleted, err = bt.mergeChildPtr(i, n, deleted) - if err != nil { - return nil, false, err - } - } else { - deletedPtr, err := bt.alloc(deleted) - if err != nil { - return nil, false, err - } - - k, _ := deleted.Key(i) - deleted, err = n.Update(i, k, deletedPtr) - if err != nil { - return nil, false, err - } - } - - default: - return nil, false, errors.New("invalid node type") - } - - return deleted, true, nil -} - -func (bt *BTree) splitChildPtr(i int, par pointerNode, child node) (node, error) { - if !bt.shouldSplit(child) { - return par, nil - } - - var ( - lPtr uint64 - rPtr uint64 - lKey []byte - rKey []byte - err error - errMsg = "cannot split child ptr: %v" - ) - - switch child.Type() { - case btreePointer: - ptrChild := child.(pointerNode) - - l, r := ptrChild.Split() - - lKey = l.keys[0] - rKey = r.keys[0] - - lPtr, err = bt.alloc(l) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - - rPtr, err = bt.alloc(r) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - break - - case btreeLeaf: - leafChild := child.(pointerNode) - - l, r := leafChild.Split() - - lKey = l.keys[0] - rKey = r.keys[0] - - lPtr, err = bt.alloc(l) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - - rPtr, err = bt.alloc(r) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - break - - default: - return nil, fmt.Errorf(errMsg, "child has invalid type") - } - - par, err = par.Update(i, lKey, lPtr) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - - par, err = par.Insert(i+1, rKey, rPtr) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - - return par, nil -} - -func (bt *BTree) mergeChildPtr(i int, par pointerNode, child node) (node, error) { - var ( - leftSib node - rightSib node - merge uint8 = 0 - err error - errMsg = "cannot merge child pointer: %v" - ) - - if i > 0 { - leftSib, err = bt.get(par.ptrs[i-1]) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - if bt.canMerge2(child, leftSib) { - merge++ - } - } - - if i < par.Total()-1 { - rightSib, err = bt.get(par.ptrs[i+1]) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - if bt.canMerge2(child, rightSib) { - merge++ - } - } - - if merge == 2 { - if !bt.canMerge3(child, leftSib, rightSib) { - merge-- - } - } else if merge == 0 { - return par, nil - } - - switch child.Type() { - - case btreePointer: - pntrChild := child.(pointerNode) - - if merge >= 1 { - if rightSib != nil { - pntrRight := rightSib.(pointerNode) - pntrChild, err = pntrChild.Merge(pntrRight) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - } else { - pntrLeft := rightSib.(pointerNode) - pntrChild, err = pntrChild.Merge(pntrLeft) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - } - } - - if merge == 2 { - pntrLeft := rightSib.(pointerNode) - pntrChild, err = pntrChild.Merge(pntrLeft) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - } - - child = pntrChild - - case btreeLeaf: - leafChild := child.(leafNode) - - if merge >= 1 { - if rightSib != nil { - leafRight := rightSib.(leafNode) - leafChild, err = leafChild.Merge(leafRight) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - } else { - leafLeft := rightSib.(leafNode) - leafChild, err = leafChild.Merge(leafLeft) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - } - } - - if merge == 2 { - leafLeft := rightSib.(leafNode) - leafChild, err = leafChild.Merge(leafLeft) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - } - - child = leafChild - - default: - return nil, fmt.Errorf(errMsg, "child has invalid type") - } - - first, err := child.Key(0) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - - ptr, err := bt.alloc(child) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - - var j int - if merge > 1 { - j = i - 1 - par, err = par.Delete(i) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - - if merge == 3 { - par, err = par.Delete(i + 1) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - } - } else { - j = i - par, err = par.Delete(i + 1) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - } - - par, err = par.Update(j, first, ptr) - if err != nil { - return nil, fmt.Errorf(errMsg, err) - } - - return par, nil -} - -func (bt *BTree) canMerge2(a, b node) bool { - return a.Type() == b.Type() && a.Size()+b.Size() <= bt.pgSize -} - -func (bt *BTree) canMerge3(a, b, c node) bool { - return a.Type() == b.Type() && a.Type() == c.Type() && a.Size()+b.Size()+c.Size() <= bt.pgSize -} - -func (bt *BTree) shouldSplit(n node) bool { - return n.Size() > bt.pgSize -} diff --git a/internal/btree/btree_test.go b/internal/btree/btree_test.go deleted file mode 100644 index 0dd6948..0000000 --- a/internal/btree/btree_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package btree - -import ( - "bytes" - "testing" -) - -var mockStore = map[uint64]node{ - 0: nil, -} -var lastPtr uint64 = 0 - -func mockGetPage(ptr uint64) (node, error) { - n, ok := mockStore[ptr] - if !ok { - return nil, errKVBadPtr - } - return n, nil -} - -func mockPullPage(ptr uint64) (node, error) { - n, err := mockGetPage(ptr) - if err != nil { - return nil, err - } - if err := mockFreePage(ptr); err != nil { - return nil, err - } - - return n, nil -} - -func mockAllocPage(n node) (uint64, error) { - lastPtr++ - mockStore[lastPtr] = n - - return lastPtr, nil -} - -func mockFreePage(ptr uint64) error { - if _, ok := mockStore[ptr]; !ok { - return errKVBadPtr - } - - delete(mockStore, ptr) - return nil -} - -func TestBTreeGet(t *testing.T) { - cases := []struct { - name string - bt bTree - input []byte - expected []byte - expectedErr error - }{ - { - name: "Get first key", - bt: bTree{ - root: 0, - get: mockGetPage, - pull: mockPullPage, - alloc: mockAllocPage, - free: mockFreePage, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res, err := c.bt.Get(c.input) - - if err != c.expectedErr { - t.Errorf("") - } - - if !bytes.Equal(c.expected, res) { - t.Errorf("") - } - }) - } -} diff --git a/internal/btree/iterator.go b/internal/btree/iterator.go deleted file mode 100644 index f53ec8f..0000000 --- a/internal/btree/iterator.go +++ /dev/null @@ -1,42 +0,0 @@ -package btree - -type Iterator struct { - bt *BTree - path []int - nodes []node -} - -func (iter *Iterator) Next() bool { - depth := len(iter.path) - 1 - - if iter.path[depth]+1 >= iter.nodes[depth].Total() { - iter.path = iter.path[:depth] - iter.nodes = iter.nodes[:depth] - return iter.Next() - } - - switch iter.nodes[depth].Type() { - case btreePointer: - ptr := iter.nodes[depth].(pointerNode) - - next, err := iter.bt.get(ptr.ptrs[iter.path[depth]+1]) - if err != nil { - return false - } - - iter.nodes = append(iter.nodes, next) - iter.path = append(iter.path, -1) - return iter.Next() - - case btreeLeaf: - iter.path[len(iter.path)-1]++ - return true - } - - return false -} - -func (iter *Iterator) Read() (k []byte, v []byte) { - leaf := iter.nodes[len(iter.nodes)-1].(leafNode) - return leaf.keys[iter.path[len(iter.path)-1]], leaf.vals[iter.path[len(iter.path)-1]] -} diff --git a/internal/btree/iterator_test.go b/internal/btree/iterator_test.go deleted file mode 100644 index 3c38a25..0000000 --- a/internal/btree/iterator_test.go +++ /dev/null @@ -1 +0,0 @@ -package btree diff --git a/internal/btree/leafNode.go b/internal/btree/leafNode.go deleted file mode 100644 index ff29c30..0000000 --- a/internal/btree/leafNode.go +++ /dev/null @@ -1,160 +0,0 @@ -package btree - -import ( - "bytes" - "encoding/binary" - "fmt" - "slices" -) - -type leafNode struct { - keys [][]byte - vals [][]byte -} - -func DecodeLeaf(d []byte) (leafNode, error) { - ln := leafNode{} - - if nodeType(binary.BigEndian.Uint16(d[0:2])) != btreeLeaf { - return leafNode{}, fmt.Errorf("leafNode: cannot decode to leaf, wrong type identifier") - } - - nKeys := binary.BigEndian.Uint16(d[2:4]) - ln.keys = make([][]byte, nKeys) - ln.vals = make([][]byte, nKeys) - - off := uint16(4) - for i := 0; uint16(i) < nKeys; i++ { - kSize := binary.BigEndian.Uint16(d[off : off+2]) - vSize := binary.BigEndian.Uint16(d[off+2 : off+4]) - - ln.keys[i] = d[off+4 : off+4+kSize] - ln.vals[i] = d[off+4+kSize : off+4+kSize+vSize] - - off += 4 + kSize + vSize - } - - return ln, nil - -} - -func (ln leafNode) Type() nodeType { - return btreeLeaf -} - -func (ln leafNode) Total() int { - return len(ln.keys) -} - -func (ln leafNode) Size() int { - size := 4 - for i, k := range ln.keys { - v := ln.vals[i] - size += 4 + len(k) + len(v) - } - return size -} - -func (ln leafNode) Key(i int) ([]byte, error) { - if i < 0 || i >= len(ln.keys) { - return nil, fmt.Errorf("leafNode: key at index '%d' does not exist", i) - } - return ln.keys[i], nil -} - -func (ln *leafNode) Val(i int) ([]byte, error) { - if i < 0 || i >= len(ln.vals) { - return nil, fmt.Errorf("leafNode: val at index '%d' does not exist", i) - } - return ln.vals[i], nil -} - -func (ln leafNode) Insert(i int, k, v []byte) (newLn leafNode, err error) { - if i < 0 || i > len(ln.keys) || i > len(ln.vals) { - return leafNode{}, fmt.Errorf("leafNode: cannot insert at non existing index '%d'", i) - } - - newLn.keys = slices.Insert(ln.keys, i, k) - newLn.vals = slices.Insert(ln.vals, i, k) - - return ln, nil -} - -func (ln leafNode) Update(i int, k, v []byte) (newLn leafNode, err error) { - if i < 0 || i > len(ln.keys) || i > len(ln.vals) { - return leafNode{}, fmt.Errorf("leafNode: cannot update at non existing index '%d'", i) - } - - ln.keys[i] = k - ln.vals[i] = v - - return ln, nil -} - -func (ln leafNode) Delete(i int) (leafNode, error) { - if i < 0 || i > len(ln.keys) || i > len(ln.vals) { - return leafNode{}, fmt.Errorf("leafNode: cannot delete at non existing index '%d'", i) - } - - ln.keys = slices.Delete(ln.keys, i, i) - ln.vals = slices.Delete(ln.vals, i, i) - - return ln, nil -} - -func (ln leafNode) Search(k []byte) (int, bool) { - return slices.BinarySearchFunc(ln.keys, k, bytes.Compare) -} - -func (ln leafNode) Merge(right leafNode) (leafNode, error) { - if bytes.Compare(ln.keys[len(ln.keys)-1], right.keys[0]) >= 0 { - return leafNode{}, fmt.Errorf("leafNode: cannot merge, last key of left is GE first key of right node") - } - - ln.keys = append(ln.keys, right.keys...) - ln.vals = append(ln.vals, right.vals...) - - return ln, nil -} - -func (ln leafNode) Split() (leafNode, leafNode) { - var half int - var size int = 0 - lnSize := ln.Size() - - for i, k := range ln.keys { - v := ln.vals[i] - size += 4 + len(k) + len(v) - if size > lnSize/2 { - half = i - size -= 4 - len(k) - len(v) - break - } - } - - split := leafNode{ - keys: ln.keys[half:], - vals: ln.vals[half:], - } - - ln.keys = ln.keys[:half] - ln.vals = ln.vals[:half] - - return ln, split -} - -func (ln leafNode) Encode() []byte { - var b []byte - - b = binary.BigEndian.AppendUint16(b, uint16(btreeLeaf)) - b = binary.BigEndian.AppendUint16(b, uint16(len(ln.keys))) - for i, k := range ln.keys { - v := ln.vals[i] - b = binary.BigEndian.AppendUint16(b, uint16(len(k))) - b = binary.BigEndian.AppendUint16(b, uint16(len(v))) - b = append(b, k...) - b = append(b, v...) - } - - return b -} diff --git a/internal/btree/leafNode_test.go b/internal/btree/leafNode_test.go deleted file mode 100644 index 5bb3f94..0000000 --- a/internal/btree/leafNode_test.go +++ /dev/null @@ -1,495 +0,0 @@ -package btree - -import ( - "bytes" - "testing" -) - -func TestLeafNodeDecode(t *testing.T) { - cases := []struct { - name string - input []byte - expected *leafNode - expectedErr error - }{ - { - name: "Successful decoding", - input: []byte{ - 0x00, 0x01, // 1 is representation of lfNode nodeType - 0x00, 0x03, // nKeys is equal to 3 - 0x00, 0x04, 0x00, 0x04, 'k', 'e', 'y', '1', 'v', 'a', 'l', '1', // first entry - 0x00, 0x04, 0x00, 0x04, 'k', 'e', 'y', '2', 'v', 'a', 'l', '2', // second entry - 0x00, 0x04, 0x00, 0x04, 'k', 'e', 'y', '3', 'v', 'a', 'l', '3', // third entry - }, - expected: &leafNode{ - keys: [][]byte{{'k', 'e', 'y', '1'}, {'k', 'e', 'y', '2'}, {'k', 'e', 'y', '3'}}, - vals: [][]byte{{'v', 'a', 'l', '1'}, {'v', 'a', 'l', '2'}, {'v', 'a', 'l', '3'}}, - }, - expectedErr: nil, - }, - { - name: "Failed decoding due to wrong nodeType bytes", - input: []byte{ - 0x00, 0x02, // 2 is not the representation of lfNode nodeType - 0x00, 0x03, - 0x00, 0x04, 0x00, 0x04, 'k', 'e', 'y', '1', 'v', 'a', 'l', '1', - 0x00, 0x04, 0x00, 0x04, 'k', 'e', 'y', '2', 'v', 'a', 'l', '2', - 0x00, 0x04, 0x00, 0x04, 'k', 'e', 'y', '3', 'v', 'a', 'l', '3', - }, - expected: &leafNode{ - keys: [][]byte{}, - vals: [][]byte{}, - }, - expectedErr: errNodeDecode, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - ln := &leafNode{} - - err := ln.Decode(c.input) - - if err != c.expectedErr { - t.Errorf("Expected error %v, but got %v", c.expectedErr, err) - } - - if len(ln.keys) != len(c.expected.keys) { - t.Errorf("Expected %v keys, but got %v", len(c.expected.keys), len(ln.keys)) - - for i, exp := range c.expected.keys { - if !bytes.Equal(ln.keys[i], exp) { - t.Errorf("Expected key %v at index %v, but got %v", exp, i, ln.keys[i]) - } - } - } - - if len(ln.vals) != len(c.expected.vals) { - t.Errorf("Expected %v vals, but got %v", len(c.expected.vals), len(ln.vals)) - - for i, exp := range c.expected.vals { - if !bytes.Equal(ln.vals[i], exp) { - t.Errorf("Expected val %v at index %v, but got %v", exp, i, ln.vals[i]) - } - } - } - }) - } -} - -func TestLeafNodeTyp(t *testing.T) { - ln := &leafNode{} - - typ := ln.Type() - if typ != lfNode { - t.Errorf("Expected type %v, but got %v", lfNode, typ) - } -} - -func TestLeafNodeEncode(t *testing.T) { - cases := []struct { - name string - input *leafNode - expected []byte - }{ - { - name: "Successful encoding", - input: &leafNode{ - keys: [][]byte{{'k', 'e', 'y', '1'}, {'k', 'e', 'y', '2'}, {'k', 'e', 'y', '3'}}, - vals: [][]byte{{'v', 'a', 'l', '1'}, {'v', 'a', 'l', '2'}, {'v', 'a', 'l', '3'}}, - }, - expected: []byte{ - 0x00, 0x01, // 1 is representation of lfNode nodeType - 0x00, 0x03, // nKeys is equal to 3 - 0x00, 0x04, 0x00, 0x04, 'k', 'e', 'y', '1', 'v', 'a', 'l', '1', // first entry - 0x00, 0x04, 0x00, 0x04, 'k', 'e', 'y', '2', 'v', 'a', 'l', '2', // second entry - 0x00, 0x04, 0x00, 0x04, 'k', 'e', 'y', '3', 'v', 'a', 'l', '3', // third entry - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := c.input.Encode() - - if !bytes.Equal(res, c.expected) { - t.Errorf("Expected %v, but got %v", c.expected, res) - } - }) - } -} - -func TestLeafNodeSize(t *testing.T) { - cases := []struct { - name string - input *leafNode - expected int - }{ - { - name: "Size calculation", - input: &leafNode{ - keys: [][]byte{{'k', 'e', 'y', '1'}, {'k', 'e', 'y', '2'}, {'k', 'e', 'y', '3'}}, - vals: [][]byte{{'v', 'a', 'l', '1'}, {'v', 'a', 'l', '2'}, {'v', 'a', 'l', '3'}}, - }, - expected: 40, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := c.input.Size() - - if res != c.expected { - t.Errorf("Expected node size %v, but got %v", c.expected, res) - } - }) - } -} - -func TestLeafNodeKey(t *testing.T) { - ln := &leafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}}, - vals: [][]byte{{'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}}, - } - - cases := []struct { - name string - input int - expected []byte - expectedErr error - }{ - { - name: "Key at index 0", - input: 0, - expected: []byte{'a'}, - expectedErr: nil, - }, - { - name: "Key at last index", - input: 5, - expected: []byte{'f'}, - expectedErr: nil, - }, - { - name: "Too large key", - input: 6, - expected: nil, - expectedErr: errNodeIdx, - }, - { - name: "Negative key", - input: -1, - expected: nil, - expectedErr: errNodeIdx, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res, err := ln.Key(c.input) - - if err != c.expectedErr { - t.Errorf("Expected error %v, but got %v", c.expectedErr, err) - } - - if !bytes.Equal(res, c.expected) { - t.Errorf("Expected key %v, but got %v", c.expected, res) - } - }) - } -} - -func TestLeafNodeSearch(t *testing.T) { - cases := []struct { - name string - input []byte - ln *leafNode - expected int - expectedExists bool - }{ - { - name: "Search before first key in odd amount of keys", - input: []byte{'a'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}}, - vals: [][]byte{{}, {}, {}, {}, {}}, - }, - expected: 0, - expectedExists: false, - }, - { - name: "Search before first key in even amount of keys", - input: []byte{'a'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}, {'l'}}, - vals: [][]byte{{}, {}, {}, {}, {}, {}}, - }, - expected: 0, - expectedExists: false, - }, - { - name: "Search after last key in odd amount of keys", - input: []byte{'k'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}}, - vals: [][]byte{{}, {}, {}, {}, {}}, - }, - expected: 5, - expectedExists: false, - }, - { - name: "Search after last key in even amount of keys", - input: []byte{'m'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}, {'l'}}, - vals: [][]byte{{}, {}, {}, {}, {}, {}}, - }, - expected: 6, - expectedExists: false, - }, - { - name: "Search existing key in odd ammount of keys", - input: []byte{'h'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}}, - vals: [][]byte{{}, {}, {}, {}, {}}, - }, - expected: 3, - expectedExists: true, - }, - { - name: "Search existing key in even ammount of keys", - input: []byte{'j'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}, {'l'}}, - vals: [][]byte{{}, {}, {}, {}, {}, {}}, - }, - expected: 4, - expectedExists: true, - }, - { - name: "Search non-existing key in odd ammount of keys", - input: []byte{'c'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}}, - vals: [][]byte{{}, {}, {}, {}, {}}, - }, - expected: 1, - expectedExists: false, - }, - { - name: "Search non-existing key in even ammount of keys", - input: []byte{'c'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}, {'l'}}, - vals: [][]byte{{}, {}, {}, {}, {}, {}}, - }, - expected: 1, - expectedExists: false, - }, - { - name: "Search first key in odd ammount of keys", - input: []byte{'b'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}}, - vals: [][]byte{{}, {}, {}, {}, {}}, - }, - expected: 0, - expectedExists: true, - }, - { - name: "Search first key in even ammount of keys", - input: []byte{'b'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}, {'l'}}, - vals: [][]byte{{}, {}, {}, {}, {}, {}}, - }, - expected: 0, - expectedExists: true, - }, - { - name: "Search last key in odd ammount of keys", - input: []byte{'j'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}}, - vals: [][]byte{{}, {}, {}, {}, {}}, - }, - expected: 4, - expectedExists: true, - }, - { - name: "Search last key in even ammount of keys", - input: []byte{'l'}, - ln: &leafNode{ - keys: [][]byte{{'b'}, {'d'}, {'f'}, {'h'}, {'j'}, {'l'}}, - vals: [][]byte{{}, {}, {}, {}, {}, {}}, - }, - expected: 5, - expectedExists: true, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res, exists := c.ln.Search(c.input) - - if exists != c.expectedExists { - t.Errorf("Expected the key existing %v, but got %v", c.expectedExists, exists) - } - - if res != c.expected { - t.Errorf("Expected index %v, but got %v", c.expected, res) - } - }) - } -} - -func TestLeafNodeMerge(t *testing.T) { - cases := []struct { - name string - left *leafNode - right *leafNode - expected *leafNode - expectedErr error - }{ - { - name: "Successful merge", - left: &leafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}}, - vals: [][]byte{{}, {}, {}}, - }, - right: &leafNode{ - keys: [][]byte{{'d'}, {'e'}, {'f'}}, - vals: [][]byte{{}, {}, {}}, - }, - expected: &leafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}}, - vals: [][]byte{{}, {}, {}, {}, {}, {}}, - }, - expectedErr: nil, - }, - { - name: "Failed merge due to non-ordered keys", - left: &leafNode{ - keys: [][]byte{{'a'}, {'b'}, {'d'}}, - vals: [][]byte{{}, {}, {}}, - }, - right: &leafNode{ - keys: [][]byte{{'c'}, {'e'}, {'f'}}, - vals: [][]byte{{}, {}, {}}, - }, - expected: nil, - expectedErr: errNodeMerge, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res, err := c.left.Merge(c.right) - - if c.expectedErr != err { - t.Errorf("Expected error %v, but got %v", c.expectedErr, err) - } - - if res != nil && c.expected != nil { - ln := res.(*leafNode) - - if len(ln.keys) != len(c.expected.keys) { - t.Errorf("Expected %v keys, but got %v", len(c.expected.keys), len(ln.keys)) - - for i, exp := range c.expected.keys { - if !bytes.Equal(ln.keys[i], exp) { - t.Errorf("Expected key %v at index %v, but got %v", exp, i, ln.keys[i]) - } - } - } - - if len(ln.vals) != len(c.expected.vals) { - t.Errorf("Expected %v vals, but got %v", len(c.expected.vals), len(ln.vals)) - - for i, exp := range c.expected.vals { - if !bytes.Equal(ln.vals[i], exp) { - t.Errorf("Expected val %v at index %v, but got %v", exp, i, ln.vals[i]) - } - } - } - } else if res == nil && c.expected != nil || res != nil && c.expected == nil { - t.Errorf("Expected %v, but got %v", c.expected, res) - } - }) - } -} - -func TestLeafNodeSplit(t *testing.T) { - cases := []struct { - name string - ln *leafNode - left *leafNode - right *leafNode - }{ - { - name: "Split even number of same size", - ln: &leafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}}, - vals: [][]byte{{'1'}, {'2'}, {'3'}, {'4'}, {'5'}, {'6'}}, - }, - left: &leafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}}, - vals: [][]byte{{'1'}, {'2'}, {'3'}}, - }, - right: &leafNode{ - keys: [][]byte{{'d'}, {'e'}, {'f'}}, - vals: [][]byte{{'4'}, {'5'}, {'6'}}, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - resL, resR := c.ln.Split() - - left := resL.(*leafNode) - right := resR.(*leafNode) - - if len(left.keys) != len(c.left.keys) { - t.Errorf("Expected %v keys, but got %v", len(c.left.keys), len(left.keys)) - - for i, exp := range c.left.keys { - if !bytes.Equal(left.keys[i], exp) { - t.Errorf("Expected key %v at index %v, but got %v", exp, i, left.keys[i]) - } - } - } - - if len(left.vals) != len(c.left.vals) { - t.Errorf("left %v vals, but got %v", len(c.left.vals), len(left.vals)) - - for i, exp := range c.left.vals { - if !bytes.Equal(left.vals[i], exp) { - t.Errorf("Expected val %v at index %v, but got %v", exp, i, left.vals[i]) - } - } - } - - if len(right.keys) != len(c.right.keys) { - t.Errorf("Expected %v keys, but got %v", len(c.right.keys), len(right.keys)) - - for i, exp := range c.right.keys { - if !bytes.Equal(right.keys[i], exp) { - t.Errorf("Expected key %v at index %v, but got %v", exp, i, right.keys[i]) - } - } - } - - if len(right.vals) != len(c.right.vals) { - t.Errorf("right %v vals, but got %v", len(c.right.vals), len(right.vals)) - - for i, exp := range c.right.vals { - if !bytes.Equal(right.vals[i], exp) { - t.Errorf("Expected val %v at index %v, but got %v", exp, i, right.vals[i]) - } - } - } - }) - } -} diff --git a/internal/btree/node.go b/internal/btree/node.go deleted file mode 100644 index effbe9c..0000000 --- a/internal/btree/node.go +++ /dev/null @@ -1,40 +0,0 @@ -package btree - -import ( - "fmt" -) - -type nodeType uint16 - -const ( - btreePointer nodeType = iota - btreeLeaf -) - -type node interface { - Type() nodeType - Total() int - Size() int - Key(int) ([]byte, error) - Search([]byte) (int, bool) - Encode() []byte -} - -func decodeNode(d []byte) (node, error) { - var n node - var err error - errMsg := "node: cannot decode node: %v" - - switch nodeType(d[0]) { - case btreePointer: - n, err = DecodePointer(d) - break - case btreeLeaf: - n, err = DecodeLeaf(d) - break - default: - return nil, fmt.Errorf(errMsg, "invalid node type") - } - - return n, err -} diff --git a/internal/btree/pointerNode.go b/internal/btree/pointerNode.go deleted file mode 100644 index fd0a888..0000000 --- a/internal/btree/pointerNode.go +++ /dev/null @@ -1,174 +0,0 @@ -package btree - -import ( - "bytes" - "encoding/binary" - "fmt" - "slices" -) - -type pointerNode struct { - keys [][]byte - ptrs []uint64 -} - -func DecodePointer(d []byte) (pointerNode, error) { - if nodeType(binary.BigEndian.Uint16(d[0:2])) != btreePointer { - return pointerNode{}, fmt.Errorf("ptrNode: cannot decode to pointer, wrong type identifier") - } - - pn := pointerNode{} - - nKeys := binary.BigEndian.Uint16(d[2:4]) - off := uint16(4) - for i := uint16(0); i < nKeys; i++ { - kSize := binary.BigEndian.Uint16(d[off : off+2]) - pn.keys = append(pn.keys, d[off+2:off+2+kSize]) - - ptr := binary.BigEndian.Uint64(d[off+2+kSize : off+2+kSize+8]) - pn.ptrs = append(pn.ptrs, ptr) - - off += 2 + kSize + 8 - } - - return pn, nil -} - -func (pn pointerNode) Type() nodeType { - return btreePointer -} - -func (pn pointerNode) Total() int { - return len(pn.keys) -} - -func (pn pointerNode) Size() int { - size := 4 - for _, k := range pn.keys { - size += 2 + len(k) + 8 - } - return size -} - -func (pn pointerNode) Key(i int) ([]byte, error) { - if i < 0 || i >= len(pn.keys) { - return nil, fmt.Errorf("ptrNode: key at index '%d' does not exist", i) - } - return pn.keys[i], nil -} - -func (pn *pointerNode) Ptr(i int) (uint64, error) { - if i < 0 || i >= len(pn.ptrs) { - return 0, fmt.Errorf("prtNode: ptr at index '%d' does not exist", i) - } - return pn.ptrs[i], nil -} - -func (pn pointerNode) Insert(i int, k []byte, ptr uint64) (newPn pointerNode, err error) { - if i < 0 || i > len(pn.keys) || i > len(pn.ptrs) { - return pointerNode{}, fmt.Errorf("ptrNode: cannot insert at non existing index '%d'", i) - } - - newPn.keys = slices.Insert(pn.keys, i, k) - newPn.ptrs = slices.Insert(pn.ptrs, i, ptr) - - return newPn, nil -} - -func (pn pointerNode) Update(i int, k []byte, ptr uint64) (newPn pointerNode, err error) { - if i < 0 || i > len(pn.keys) || i > len(pn.ptrs) { - return pointerNode{}, fmt.Errorf("ptrNode: cannot update at non existing index '%d'", i) - } - - newPn.keys = append(newPn.keys, pn.keys...) - newPn.ptrs = append(newPn.ptrs, pn.ptrs...) - newPn.keys[i] = k - newPn.ptrs[i] = ptr - - return pn, nil -} - -func (pn pointerNode) Delete(i int) (newPn pointerNode, err error) { - if i < 0 || i > len(pn.keys) || i > len(pn.ptrs) { - return pointerNode{}, fmt.Errorf("ptrNode: cannot delete at non existing index '%d'", i) - } - - newPn.keys = slices.Delete(pn.keys, i, i) - newPn.ptrs = slices.Delete(pn.ptrs, i, i) - - return newPn, nil -} - -func (pn pointerNode) Search(k []byte) (int, bool) { - return slices.BinarySearchFunc(pn.keys, k, bytes.Compare) -} - -func (pn pointerNode) Merge(right pointerNode) (newPn pointerNode, err error) { - if bytes.Compare(pn.keys[len(pn.keys)-1], right.keys[0]) >= 0 { - return pointerNode{}, fmt.Errorf("ptrNode: cannot merge, last key of left is GE first key of right node") - } - - newPn.keys = append(newPn.keys, pn.keys...) - newPn.ptrs = append(newPn.ptrs, pn.ptrs...) - newPn.keys = append(newPn.keys, right.keys...) - newPn.ptrs = append(newPn.ptrs, right.ptrs...) - - return newPn, nil -} - -func (pn pointerNode) Split() (l pointerNode, r pointerNode) { - var half int - var size int = 0 - var pnSize = pn.Size() - - for i, k := range pn.keys { - size += 2 + len(k) + 8 - if size > pnSize/2 { - half = i - size -= 2 - len(k) - 8 - break - } - } - - l = pointerNode{ - keys: pn.keys[:half], - ptrs: pn.ptrs[:half], - } - - r = pointerNode{ - keys: pn.keys[half:], - ptrs: pn.ptrs[half:], - } - - return l, r -} - -func (pn pointerNode) Encode() []byte { - var b []byte - - b = binary.BigEndian.AppendUint16(b, uint16(btreePointer)) - b = binary.BigEndian.AppendUint16(b, uint16(len(pn.keys))) - for i, k := range pn.keys { - b = binary.BigEndian.AppendUint16(b, uint16(len(k))) - b = append(b, k...) - b = binary.BigEndian.AppendUint64(b, pn.ptrs[i]) - } - - return b -} - -func (pn pointerNode) MergePtrs(from, to int, k []byte, ptr uint64) (newPn pointerNode, err error) { - newPn, err = pn.Update(from, k, ptr) - if err != nil { - return pointerNode{}, err - } - - for i := from + 1; i < to; i++ { - newPn, err = newPn.Delete(i) - if err != nil { - return pointerNode{}, err - } - } - - return newPn, nil -} diff --git a/internal/btree/pointerNode_test.go b/internal/btree/pointerNode_test.go deleted file mode 100644 index 3c38a25..0000000 --- a/internal/btree/pointerNode_test.go +++ /dev/null @@ -1 +0,0 @@ -package btree diff --git a/internal/cli/cli.go b/internal/cli/cli.go deleted file mode 100644 index 6a959e8..0000000 --- a/internal/cli/cli.go +++ /dev/null @@ -1,51 +0,0 @@ -package cli - -import ( - "bufio" - "fmt" - "net" - "os" -) - -type Client struct { - Addr string - Port uint16 - conn net.Conn -} - -func NewClient() Client { - return Client{} -} - -func (cli *Client) Run() error { - if err := cli.connect(); err != nil { - return err - } - - reader := bufio.NewScanner(os.Stdin) - for reader.Scan() { - text := reader.Text() - cli.conn.Write([]byte(text)) - } - - if err := cli.disconnect(); err != nil { - return err - } - - return nil -} - -func (cli *Client) connect() error { - var err error - - cli.conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", cli.Addr, cli.Port)) - if err != nil { - return err - } - - return nil -} - -func (cli *Client) disconnect() error { - return cli.conn.Close() -} diff --git a/internal/db/cell.go b/internal/db/cell.go deleted file mode 100644 index 55b94ad..0000000 --- a/internal/db/cell.go +++ /dev/null @@ -1,142 +0,0 @@ -package db - -import ( - "encoding/binary" - "fmt" -) - -const errCellMsg = "cell: cannot interpret '%x' as type '%s'" - -type Cell struct { - typ dbType - b []byte -} - -func (c *Cell) Type() dbType { - return c.typ -} - -func (c *Cell) Raw() []byte { - return c.b -} - -func (c *Cell) Bytes() ([]byte, error) { - if c.typ != dbBytes || int(c.b[0]) != len(c.b[2:]) { - return nil, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - return c.b[2:], nil -} - -func (c *Cell) Bool() (bool, error) { - if c.typ != dbBool || len(c.b) != 1 { - return false, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - n := uint8(c.b[0]) - return n == 1, nil -} - -func (c *Cell) String() (string, error) { - if c.typ != dbString || int(c.b[0]) != len(c.b[2:]) { - return "", fmt.Errorf(errCellMsg, c.b, c.typ) - } - - return string(c.b[2 : 2+int(c.b[0])]), nil -} - -func (c *Cell) RawString() (string, error) { - if c.typ != dbRawString || int(c.b[0]) != len(c.b[2:]) { - return "", fmt.Errorf(errCellMsg, c.b, c.typ) - } - - return string(c.b[2 : 2+int(c.b[0])]), nil -} - -func (c *Cell) Int8() (int8, error) { - if c.typ != dbInt8 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - return int8(c.b[0]), nil -} - -func (c *Cell) Uint8() (uint8, error) { - if c.typ != dbUint8 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - return c.b[0], nil -} - -func (c *Cell) Int16() (int16, error) { - if c.typ != dbInt16 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - n := binary.BigEndian.Uint16(c.b) - return int16(n), nil -} - -func (c *Cell) Uint16() (uint16, error) { - if c.typ != dbUint16 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - n := binary.BigEndian.Uint16(c.b) - return n, nil -} - -func (c *Cell) Int32() (int32, error) { - if c.typ != dbInt32 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - n := binary.BigEndian.Uint32(c.b) - return int32(n), nil -} - -func (c *Cell) Uint32() (uint32, error) { - if c.typ != dbUint32 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - n := binary.BigEndian.Uint32(c.b) - return n, nil -} - -func (c *Cell) Int64() (int64, error) { - if c.typ != dbInt64 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - n := binary.BigEndian.Uint64(c.b) - return int64(n), nil -} - -func (c *Cell) Uint64() (uint64, error) { - if c.typ != dbUint64 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - n := binary.BigEndian.Uint64(c.b) - return n, nil -} - -func (c *Cell) Float32() (float32, error) { - if c.typ != dbFloat32 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - n := binary.BigEndian.Uint32(c.b) - return float32(n), nil -} - -func (c *Cell) Float64() (float64, error) { - if c.typ != dbFloat64 { - return 0, fmt.Errorf(errCellMsg, c.b, c.typ) - } - - n := binary.BigEndian.Uint64(c.b) - return float64(n), nil -} diff --git a/internal/db/condition.go b/internal/db/condition.go deleted file mode 100644 index 904e836..0000000 --- a/internal/db/condition.go +++ /dev/null @@ -1,23 +0,0 @@ -package db - -type cmp uint8 - -const ( - cmpEQ cmp = iota - cmpNEQ - cmpLT - cmpLTE - cmpTween - cmpLike - cmpIn -) - -type Condition struct { - col string - val []byte - cmp cmp -} - -func (cnd *Condition) Check(c Cell) bool { - return false -} diff --git a/internal/db/database.go b/internal/db/database.go deleted file mode 100644 index 9327583..0000000 --- a/internal/db/database.go +++ /dev/null @@ -1,145 +0,0 @@ -package db - -import ( - "encoding/binary" - "encoding/json" - "fmt" - - "github.com/gKits/PavoSQL/internal/kvstore" -) - -type DB struct { - Name string - kv kvstore.KVStore -} - -func (db *DB) Query(tbName string, pk []byte) { - tb, err := db.GetTable(tbName) - if err != nil { - - } - - r, err := db.kv.Read() - if err != nil { - } - - val, err := db.kv.Get(pk) - if err != nil { - - } -} - -func (db *DB) Get(tbName string, r *Row) error { - tb, err := db.GetTable(tbName) - if err != nil { - return fmt.Errorf("table '%s' not found: %v", tbName, err) - } - - tb.checkRow(r) - - return nil -} - -func (db *DB) Insert(tbName string, vals []Row) error { - w, err := db.kv.Write() - if err != nil { - w.Abort() - return err - } - - for _, v := range vals { - - } - - if err := w.Commit(); err != nil { - w.Abort() - return err - } - - return nil -} - -func (db *DB) Update() {} - -func (db *DB) Delete(tbName string) {} - -func (db *DB) GetTable(tbName string) (table, error) { - k := defaultTBDefTable.encodeKey([]byte(tbName)) - - v, err := db.kv.Get(k) - if err != nil { - return table{}, err - } - - tb := table{} - if err := json.Unmarshal(v, &tb); err != nil { - return table{}, err - } - - return tb, nil -} - -func (db *DB) CreateTable(tb table) error { - v, err := json.Marshal(tb) - if err != nil { - return err - } - - pref, err := db.nextPrefix() - if err != nil { - return err - } - - tb.Prefix = pref - - k := defaultTBDefTable.encodeKey([]byte(tb.Name)) - - if err := db.kv.Set(k, v); err != nil { - return err - } - - return nil -} - -func (db *DB) DeleteTable(tbName string) (bool, error) { - k := defaultTBDefTable.encodeKey([]byte(tbName)) - - del, err := db.kv.Del(k) - if err != nil { - return false, err - } - - return del, nil -} - -func (db *DB) prefix() (uint32, error) { - k := defaultMetaTable.encodeKey([]byte("prefix")) - - _, err := db.kv.Get(k) - if err != nil { - return 0, err - } - - var pref uint32 - - return pref, nil -} - -func (db *DB) nextPrefix() (uint32, error) { - k := defaultMetaTable.encodeKey([]byte("prefix")) - - v, err := db.kv.Get(k) - if err != nil { - return 0, err - } - - var pref uint32 - - v = binary.BigEndian.AppendUint32(v, pref+1) - - if err := db.kv.Set(k, v); err != nil { - return 0, err - } - - return pref + 1, nil -} diff --git a/internal/db/defaults.go b/internal/db/defaults.go deleted file mode 100644 index c448465..0000000 --- a/internal/db/defaults.go +++ /dev/null @@ -1,25 +0,0 @@ -package db - -var defaultMetaTable = table{ - Name: "@meta", - Prefix: 0, - Cols: []string{"name", "val"}, - Types: []dbType{dbString, dbBytes}, - PKeys: 1, -} - -var defaultTBDefTable = table{ - Name: "@tbdef", - Prefix: 1, - Cols: []string{"name", "def"}, - Types: []dbType{dbString, dbBytes}, - PKeys: 1, -} - -var defaultUsersTable = table{ - Name: "@users", - Prefix: 2, - Cols: []string{"name", "pass"}, - Types: []dbType{dbString, dbBytes}, - PKeys: 1, -} diff --git a/internal/db/row.go b/internal/db/row.go deleted file mode 100644 index e81ffbe..0000000 --- a/internal/db/row.go +++ /dev/null @@ -1,6 +0,0 @@ -package db - -type Row struct { - Cols []string - Vals []Cell -} diff --git a/internal/db/table.go b/internal/db/table.go deleted file mode 100644 index 5e7e22c..0000000 --- a/internal/db/table.go +++ /dev/null @@ -1,35 +0,0 @@ -package db - -import ( - "encoding/binary" - "fmt" -) - -type table struct { - Name string `json:"name"` - Cols []string `json:"columns"` - Types []dbType `json:"types"` - Null []bool `json:"nullable"` - Pref uint32 `json:"prefix"` -} - -func (tb *table) encodeKey(k []byte) []byte { - var buf [4]byte - binary.BigEndian.PutUint32(buf[:], tb.Pref) - return append(k, buf[:]...) -} - -func (tb *table) CheckRow(row *Row) error { - set := map[string]struct{}{} - for _, col := range tb.Cols { - set[col] = struct{}{} - } - - for _, col := range row.Cols { - if _, ok := set[col]; !ok { - return fmt.Errorf("unknow column '%s' in table '%s'", col, tb.Name) - } - } - - return nil -} diff --git a/internal/db/types.go b/internal/db/types.go deleted file mode 100644 index ea74f3c..0000000 --- a/internal/db/types.go +++ /dev/null @@ -1,57 +0,0 @@ -package db - -import "fmt" - -type dbType uint8 - -const ( - dbBytes dbType = iota - dbBool - dbString - dbRawString - dbInt8 - dbUint8 - dbInt16 - dbUint16 - dbInt32 - dbUint32 - dbInt64 - dbUint64 - dbFloat32 - dbFloat64 -) - -func (dT dbType) String() string { - switch dT { - case dbBytes: - return "Bytes" - case dbBool: - return "Bool" - case dbString: - return "String" - case dbRawString: - return "RawString" - case dbInt8: - return "Int8" - case dbUint8: - return "Uint8" - case dbInt16: - return "Int16" - case dbUint16: - return "Uint16" - case dbInt32: - return "Int32" - case dbUint32: - return "Uint32" - case dbInt64: - return "Int64" - case dbUint64: - return "Uint64" - case dbFloat32: - return "Float32" - case dbFloat64: - return "Float64" - default: - return fmt.Sprintf("%d", dT) - } -} diff --git a/internal/dbms/dbms.go b/internal/dbms/dbms.go deleted file mode 100644 index 4d76fbd..0000000 --- a/internal/dbms/dbms.go +++ /dev/null @@ -1,13 +0,0 @@ -package dbms - -type DBMS struct { - Dir string -} - -func (dbms *DBMS) CreateDatabase() error { - return nil -} - -func (dbms *DBMS) DeleteDatabase() error { - return nil -} diff --git a/internal/freelist/freelist.go b/internal/freelist/freelist.go deleted file mode 100644 index fbcdce2..0000000 --- a/internal/freelist/freelist.go +++ /dev/null @@ -1,109 +0,0 @@ -package freelist - -import () - -type getFunc func(uint64) ([]byte, error) -type pullFunc func(uint64) ([]byte, error) -type allocFunc func([]byte) (uint64, error) -type freeFunc func(uint64) error - -type FreelistData struct { - head uint64 - ptrs []uint64 - nHead int - nDiscard int -} - -type Freelist struct { - FreelistData - version uint64 - minRead uint64 - pgSize int - get func(uint64) (freelistNode, error) - pull func(uint64) (freelistNode, error) - alloc func(freelistNode) (uint64, error) - free func(uint64) error -} - -func New( - head, version uint64, pgSize int, - get getFunc, pull pullFunc, alloc allocFunc, free freeFunc, -) Freelist { - fl := Freelist{ - version: version, - pgSize: pgSize, - get: func(ptr uint64) (freelistNode, error) { - d, err := get(ptr) - if err != nil { - return freelistNode{}, err - } - return decodeFreelistNode(d), nil - }, - pull: func(ptr uint64) (freelistNode, error) { - d, err := pull(ptr) - if err != nil { - return freelistNode{}, err - } - return decodeFreelistNode(d), nil - }, - alloc: func(fn freelistNode) (uint64, error) { - return alloc(fn.Encode()) - }, - free: free, - } - - fl.head = head - - return fl -} - -func (fl *Freelist) Nq(ptr uint64) error { - var head freelistNode - var err error - - if fl.head == 0 { - head = freelistNode{next: fl.head} - head = head.Nq(ptr) - goto alloc - } - - head, err = fl.get(fl.head) - if err != nil { - return err - } - - if head.Size()+8 <= fl.pgSize { - head = head.Nq(ptr) - if err := fl.free(fl.head); err != nil { - return err - } - goto alloc - } - head = freelistNode{next: fl.head} - head = head.Nq(ptr) - -alloc: - fl.head, err = fl.alloc(head) - return err -} - -func (fl *Freelist) Dq() (uint64, error) { - if fl.head == 0 { - return 0, nil - } - - var node freelistNode - var err error - - for next := fl.head; next != 0; next = node.next { - node, err = fl.get(next) - if err != nil { - return 0, err - } - } - - var ptr uint64 - ptr, node = node.Dq() - - return ptr, nil -} diff --git a/internal/freelist/freelistNode.go b/internal/freelist/freelistNode.go deleted file mode 100644 index da11569..0000000 --- a/internal/freelist/freelistNode.go +++ /dev/null @@ -1,65 +0,0 @@ -package freelist - -import ( - "encoding/binary" - "slices" -) - -type freelistNode struct { - next uint64 - ptrs []uint64 -} - -func decodeFreelistNode(d []byte) freelistNode { - fn := freelistNode{} - - nPtrs := binary.BigEndian.Uint16(d[0:2]) - fn.next = binary.BigEndian.Uint64(d[2:10]) - for i := uint16(0); i < nPtrs; i++ { - fn.ptrs = append(fn.ptrs, binary.BigEndian.Uint64(d[10+i*8:])) - } - - return fn -} - -func (fn freelistNode) Encode() []byte { - var b []byte - - binary.BigEndian.AppendUint16(b, uint16(len(fn.ptrs))) - binary.BigEndian.AppendUint64(b, fn.next) - for _, ptr := range fn.ptrs { - binary.BigEndian.AppendUint64(b, ptr) - } - - return b -} - -func (fn freelistNode) Total() int { - return len(fn.ptrs) -} - -func (fn freelistNode) Size() int { - return 12 + len(fn.ptrs)*8 -} - -func (fn freelistNode) Pop() (uint64, freelistNode) { - last := fn.ptrs[fn.Total()-1] - fn.ptrs = fn.ptrs[:fn.Total()-1] - return last, fn -} - -func (fn freelistNode) Push(ptr uint64) freelistNode { - fn.ptrs = append(fn.ptrs, ptr) - return fn -} - -func (fn freelistNode) Nq(ptr uint64) freelistNode { - fn.ptrs = slices.Insert(fn.ptrs, 0, ptr) - return fn -} - -func (fn freelistNode) Dq() (uint64, freelistNode) { - last := fn.ptrs[fn.Total()-1] - fn.ptrs = fn.ptrs[:fn.Total()-1] - return last, fn -} diff --git a/internal/freelist/freelistNode_test.go b/internal/freelist/freelistNode_test.go deleted file mode 100644 index cf8aa0a..0000000 --- a/internal/freelist/freelistNode_test.go +++ /dev/null @@ -1 +0,0 @@ -package freelist diff --git a/internal/freelist/freelist_test.go b/internal/freelist/freelist_test.go deleted file mode 100644 index cf8aa0a..0000000 --- a/internal/freelist/freelist_test.go +++ /dev/null @@ -1 +0,0 @@ -package freelist diff --git a/internal/kvstore/kvreader.go b/internal/kvstore/kvreader.go deleted file mode 100644 index e6f31fc..0000000 --- a/internal/kvstore/kvreader.go +++ /dev/null @@ -1,45 +0,0 @@ -package kvstore - -import ( - "github.com/gKits/PavoSQL/internal/btree" -) - -type KVReader struct { - version uint64 - tree btree.BTree - chunks [][]byte - get func(uint64) ([]byte, error) - close func(*KVReader) - idx int -} - -func newReader(v, root uint64, chunks [][]byte, close func(*KVReader)) *KVReader { - r := &KVReader{version: v, chunks: chunks, close: close} - r.tree = btree.NewReadOnly(root, PAGE_SIZE, r.getPage) - return r -} - -func (r *KVReader) Get(k []byte) ([]byte, error) { - return r.tree.Get(k) -} - -func (r *KVReader) Seek(k []byte) (*btree.Iterator, error) { - return nil, nil -} - -func (r *KVReader) Close() { - r.close(r) -} - -func (r *KVReader) getPage(ptr uint64) ([]byte, error) { - start := uint64(0) - for _, chunk := range r.chunks { - end := start + uint64(len(chunk)/PAGE_SIZE) - if ptr < end { - offset := PAGE_SIZE * (ptr - start) - return chunk[offset : offset+PAGE_SIZE], nil - } - start = end - } - return r.get(ptr) -} diff --git a/internal/kvstore/kvstore.go b/internal/kvstore/kvstore.go deleted file mode 100644 index 93987aa..0000000 --- a/internal/kvstore/kvstore.go +++ /dev/null @@ -1,242 +0,0 @@ -package kvstore - -import ( - "bytes" - "encoding/binary" - "io" - "os" - "path/filepath" - "sync" - - "github.com/gKits/PavoSQL/internal/freelist" - "github.com/gKits/PavoSQL/pkg/vcache" -) - -const PAGE_SIZE = 4096 - -type KVStore struct { - kvOpts // options type embeded - f *os.File // the database file - root uint64 // pointer to the root of the btree - free freelist.Freelist // freelist managing free pages - cache vcache.VCache[uint64, []byte] // cache of freed pages still to be read - version uint64 // latest version of the kv store - wLock sync.Mutex // write lock allowing only a single concurrent writer a time - fLock sync.RWMutex // file lock making sure file is not read and written at the same time -} - -func New(opts ...kvOptFunc) *KVStore { - opt := defaultOpts() - for _, fn := range opts { - fn(&opt) - } - - return &KVStore{ - kvOpts: opt, - cache: vcache.New[uint64, []byte](0), - } -} - -func (kv *KVStore) Open() error { - f, err := os.OpenFile(kv.path, os.O_RDWR|os.O_CREATE, 0644) - if err != nil { - kv.Close() - return err - } - kv.f = f - - if err := kv.loadMaster(); err != nil { - kv.Close() - return err - } - - return nil -} - -func (kv *KVStore) Close() { - kv.f.Close() -} - -func (kv *KVStore) Read() (*KVReader, error) { - r := newReader(kv.version, kv.root, kv.mmap.chunks, kv.endRead) - - return r, nil -} - -func (kv *KVStore) Write() (*KVWriter, error) { - kv.wLock.Lock() - - w := newWriter(kv.root, kv.commitWrite, kv.abortWrite) - - w.version = kv.version - - return w, nil -} - -func (kv *KVStore) endRead(r *KVReader) { -} - -func (kv *KVStore) commitWrite(w *KVWriter) error { - kv.root = w.tree.Root - return nil -} - -func (kv *KVStore) abortWrite(w *KVWriter) { - kv.wLock.Unlock() -} - -func (kv *KVStore) loadMaster() error { - data := kv.mmap.chunks[0] - root := binary.BigEndian.Uint64(data[16:24]) - free := binary.BigEndian.Uint64(data[24:32]) - npages := binary.BigEndian.Uint64(data[32:40]) - version := binary.BigEndian.Uint64(data[40:48]) - - if !bytes.Equal([]byte(kv.sig), data[:16]) { - - } - - if npages < 1 || npages > uint64(kv.mmap.fileSize/PAGE_SIZE) { - } - - kv.root = root - kv.version = version - - return nil -} - -func (kv *KVStore) flushMaster() error { - var data [40]byte - - copy(data[:16], []byte(kv.sig)) - binary.BigEndian.PutUint64(data[16:], kv.root) - binary.BigEndian.PutUint64(data[24:], kv.root) - binary.BigEndian.PutUint64(data[32:], kv.page.flushed) - - _, err := kv.f.WriteAt(data[:], 0) - return err -} - -func (kv *KVStore) writePages(changes map[uint64][]byte) (err error) { - path, name := filepath.Split(kv.path) - - tmp, err := os.CreateTemp(path, name) - if err != nil { - return err - } - defer func() { - tmp.Close() - if err != nil { - os.Remove(tmp.Name()) - } - }() - - tmpName := tmp.Name() - - if _, err := io.Copy(tmp, kv.f); err != nil { - return err - } - - for ptr, page := range changes { - if _, err := tmp.WriteAt(page, int64(ptr)); err != nil { - return err - } - } - - if err := tmp.Sync(); err != nil { - return err - } - - destInfo, err := kv.f.Stat() - if err != nil { - return err - } - - if err := tmp.Chmod(destInfo.Mode()); err != nil { - return err - } - - if err := tmp.Close(); err != nil { - return err - } - - kv.fLock.Lock() - defer kv.fLock.Unlock() - - if err := kv.f.Close(); err != nil { - return err - } - - if err := os.Rename(tmpName, kv.path); err != nil { - return err - } - - kv.f, err = os.Open(kv.path) - if err != nil { - return err - } - - return nil -} - -func (kv *KVStore) getFilePage(ptr uint64) ([]byte, error) { - kv.fLock.RLock() - defer kv.fLock.RUnlock() - - page := make([]byte, PAGE_SIZE) - if _, err := kv.f.ReadAt(page, int64(ptr)); err != nil { - return nil, err - } - return page, nil -} - -func (kv *KVStore) pullPage(ptr uint64) ([]byte, error) { - return nil, nil -} - -func (kv *KVStore) allocPage(page []byte) (uint64, error) { - return 0, nil -} - -func (kv *KVStore) freeFilePage(ptr uint64) error { - page, err := kv.getFilePage(ptr) - if err != nil { - return err - } - - kv.cache.Cache(ptr, page) // store freed page in versioned cache - return nil -} - -/* -kvOpts struct is embeded into KVStore to implement the functional options -pattern for KVStore -*/ -type kvOptFunc func(*kvOpts) - -type kvOpts struct { - path string // path to the database file - sig string // the file signature -} - -func defaultOpts() kvOpts { - return kvOpts{ - path: "/var/lib/pavosql", - sig: "PavoSQL_DB_File:", - } -} - -func WithPath(path string) kvOptFunc { - return func(opts *kvOpts) { - opts.path = path - } -} - -func WithSignature(sig string) kvOptFunc { - s := make([]byte, 16) - copy(s, sig) - - return func(opts *kvOpts) { - opts.sig = string(s) - } -} diff --git a/internal/kvstore/kvwriter.go b/internal/kvstore/kvwriter.go deleted file mode 100644 index c026caa..0000000 --- a/internal/kvstore/kvwriter.go +++ /dev/null @@ -1,68 +0,0 @@ -package kvstore - -import ( - "github.com/gKits/PavoSQL/internal/btree" - "github.com/gKits/PavoSQL/internal/freelist" -) - -type KVWriter struct { - KVReader - kv *KVStore - free freelist.Freelist - nappend int - changes map[uint64][]byte - commit func(*KVWriter) error - abort func(*KVWriter) -} - -func newWriter(root uint64, commit func(*KVWriter) error, abort func(*KVWriter)) *KVWriter { - w := &KVWriter{} - w.tree = btree.New(root, PAGE_SIZE, w.getWriterPage, w.pullPage, w.allocPage, w.freePage) - return w -} - -func (w *KVWriter) Set(k, v []byte) error { - return w.tree.Set(k, v) -} - -func (w *KVWriter) Del(k []byte) (bool, error) { - return w.tree.Delete(k) -} - -func (w *KVWriter) Abort() { - w.abort(w) -} - -func (w *KVWriter) Commit() error { - return w.commit(w) -} - -func (w *KVWriter) getWriterPage(ptr uint64) ([]byte, error) { - page, ok := w.changes[ptr] - if !ok { - return w.getPage(ptr) - } - return page, nil -} - -func (w *KVWriter) pullPage(ptr uint64) ([]byte, error) { - page, err := w.getWriterPage(ptr) - if err != nil { - return nil, err - } - - if err := w.freePage(ptr); err != nil { - return nil, err - } - - return page, nil -} - -func (w *KVWriter) allocPage(d []byte) (uint64, error) { - return 0, nil -} - -func (w *KVWriter) freePage(ptr uint64) error { - w.changes[ptr] = nil - return nil -} diff --git a/internal/kvstore/mmap.go b/internal/kvstore/mmap.go deleted file mode 100644 index 750afe1..0000000 --- a/internal/kvstore/mmap.go +++ /dev/null @@ -1,100 +0,0 @@ -//go:build !windows - -package kvstore - -import ( - "errors" - "os" - "syscall" -) - -var ( - errMmapFileSize = errors.New("mmap: cannot init mmap, file size needs to be multiple of page size") -) - -type mmap struct { - fileSize int - mmapSize int - chunks [][]byte -} - -func (mm *mmap) Init(f *os.File) error { - fStats, err := f.Stat() - if err != nil { - return err - } - - if fStats.Size()%PAGE_SIZE != 0 { - return errMmapFileSize - } - - mmapSize := 64 << 20 - for mmapSize < int(fStats.Size()) { - mmapSize *= 2 - } - - chunk, err := syscall.Mmap( - int(f.Fd()), 0, mmapSize, - syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED, - ) - if err != nil { - return err - } - - mm.mmapSize = mmapSize - mm.chunks = [][]byte{chunk} - mm.fileSize = int(fStats.Size()) - - return nil -} - -func (mm *mmap) Extend(f *os.File, n int) error { - if mm.mmapSize >= n*PAGE_SIZE { - return nil - } - - chunk, err := syscall.Mmap( - int(f.Fd()), int64(mm.mmapSize), mm.mmapSize, - syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED, - ) - if err != nil { - return err - } - - mm.mmapSize *= 2 - mm.chunks = append(mm.chunks, chunk) - - return nil -} - -func (mm *mmap) Close() error { - for _, chunk := range mm.chunks { - if err := syscall.Munmap(chunk); err != nil { - return err - } - } - return nil -} - -func (mm *mmap) ExtendFile(f *os.File, n int) error { - filePages := mm.fileSize / PAGE_SIZE - if filePages >= n { - return nil - } - - for filePages < n { - inc := filePages / 8 - if inc < 1 { - inc = 1 - } - filePages += inc - } - - fileSize := filePages * PAGE_SIZE - if err := syscall.Fallocate(int(f.Fd()), 0, 0, int64(fileSize)); err != nil { - return err - } - - mm.fileSize = fileSize - return nil -} diff --git a/internal/kvstore/mmap_windows.go b/internal/kvstore/mmap_windows.go deleted file mode 100644 index 109fbf3..0000000 --- a/internal/kvstore/mmap_windows.go +++ /dev/null @@ -1,107 +0,0 @@ -//go:build windows - -package kvstore - -import ( - "errors" - "os" - "syscall" -) - -var ( - errMmapFileSize = errors.New("mmap: cannot init mmap, file size needs to be multiple of page size") -) - -type mmap struct { - fileSize int - mmapSize int - chunks [][]byte -} - -func (mm *mmap) Init(f *os.File) error { - fStats, err := f.Stat() - if err != nil { - return err - } - - if fStats.Size()%PageSize != 0 { - return errMmapFileSize - } - - mmapSize := 64 << 20 - for mmapSize < int(fStats.Size()) { - mmapSize *= 2 - } - - fileMap, err := syscall.CreateFileMapping( - syscall.Handle(f.Fd()), - nil, - syscall.PAGE_READWRITE, - 0, uint32(mmapSize), - nil, - ) - defer syscall.CloseHandle(fileMap) - - addr, err := syscall.MapViewOfFile(fileMap, syscall.FILE_MAP_WRITE, 0, 0, uintptr(mmapSize)) - if err != nil { - return err - } - defer syscall.UnmapViewOfFile(addr) - - // data := (([]*byte)(unsafe.Pointer(addr))) - mm.mmapSize = mmapSize - mm.chunks = [][]byte{} - mm.fileSize = int(fStats.Size()) - - return nil -} - -func (mm *mmap) Extend(f *os.File, n int) error { - if mm.mmapSize >= n*PageSize { - return nil - } - - // chunk, err := syscall.Mmap( - // int(f.Fd()), int64(mm.mmapSize), mm.mmapSize, - // syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED, - // ) - // if err != nil { - // return err - // } - - // mm.mmapSize *= 2 - // mm.chunks = append(mm.chunks, chunk) - - return nil -} - -func (mm *mmap) Close() error { - return nil -} - -func (mm *mmap) ExtendFile(f *os.File, n int) error { - filePages := mm.fileSize / PageSize - if filePages >= n { - return nil - } - - for filePages < n { - inc := filePages / 8 - if inc < 1 { - inc = 1 - } - filePages += inc - } - - // if err := syscall.LockFile(); err != nil { - - // } - - // fileSize := filePages * pageSize - // if err := syscall.Fallocate(int(f.Fd()), 0, 0, int64(fileSize)); err != nil { - // return err - // } - - // mm.fileSize = fileSize - return nil -} diff --git a/internal/parser/lexer.go b/internal/parser/lexer.go deleted file mode 100644 index de69c8a..0000000 --- a/internal/parser/lexer.go +++ /dev/null @@ -1,238 +0,0 @@ -package parser - -import ( - "fmt" - "strings" - "unicode" - "unicode/utf8" -) - -type stateFn func(*lexer) stateFn - -type lexer struct { - name string // name of lexer for debugging - input string // input string beeing tokenized - pos int // current position in input string - start int // starting position of current token beeing scanned - width int // width of last rune read - tok token // last emitted token - line int // current line in multt-line statement - startLn int // starting line of current token - opt lexOptions -} - -type lexOptions struct { - emitComments bool - allowComments bool - leftComment string - rightComment string - singleComment string -} - -var defaultLexOptions lexOptions = lexOptions{ - emitComments: false, - allowComments: true, - leftComment: "/*", - rightComment: "*/", - singleComment: "//", -} - -const eof = -1 - -func (l *lexer) next() (r rune) { - if l.pos >= len(l.input) { - l.width = 0 - return eof - } - - r, l.width = utf8.DecodeRuneInString(l.input[l.pos:]) - l.pos += l.width - if r == '\n' { - l.line++ - } - - return r -} - -func (l *lexer) peek() rune { - r := l.next() - l.backup() - return r -} - -func (l *lexer) backup() { - if l.pos > 0 { - r, w := utf8.DecodeLastRuneInString(l.input[:l.pos]) - l.pos -= w - if r == '\n' { - l.line-- - } - } -} - -func (l *lexer) ignore() { - l.line += strings.Count(l.input[l.start:l.pos], "\n") - l.start = l.pos - l.startLn = l.line -} - -func (l *lexer) accept(valid string) bool { - if strings.ContainsRune(valid, l.next()) { - return true - } - l.backup() - return false -} - -func (l *lexer) acceptRun(valid string) { - for strings.ContainsRune(valid, l.next()) { - } - l.backup() -} - -func (l *lexer) curToken(typ tokenType) token { - tok := token{typ, l.input[l.start:l.pos], l.startLn} - l.start = l.pos - l.startLn = l.line - return tok -} - -func (l *lexer) emit(typ tokenType) stateFn { - return l.emitToken(l.curToken(typ)) - -} - -func (l *lexer) emitToken(t token) stateFn { - l.tok = t - return nil -} - -func (l *lexer) run() { - for state := lexDefault; state != nil; { - state = nil - } -} - -func lexDefault(l *lexer) stateFn { - switch l.next() { - case eof: - case '"': - return lexString - case '`': - return lexRawString - case '+', '-': - return lexNumber - default: - return lexIdent - } - return nil -} - -func lexIdent(l *lexer) stateFn { - for { - r := l.next() - - if unicode.IsSpace(r) { - l.backup() - word := l.input[l.start:l.pos] - - typ, ok := key[word] - if !ok { - typ = tokIdent - } - - return l.emit(typ) - } - } -} - -func lexString(l *lexer) stateFn { -loop: - for { - switch l.next() { - case '\\': - if r := l.next(); r != eof && r != '\n' { - break - } - fallthrough - - case eof, '\n': - return l.errorf("unterminated string") - - case '"': - break loop - } - } - - return l.emit(tokString) -} - -func lexRawString(l *lexer) stateFn { -loop: - for { - switch l.next() { - case '`': - break loop - } - } - - return l.emit(tokString) -} - -func lexNumber(l *lexer) stateFn { - l.accept("+-") - digits := "0123456789" - - l.acceptRun(digits) - if l.accept(".") { - l.acceptRun(digits) - } - - if l.accept("eE") { - l.accept("+-") - l.acceptRun(digits) - } - - if unicode.IsLetter(l.peek()) { - l.next() - return l.errorf("invalid number syntax: %q", l.input[l.start:l.pos]) - } - return l.emit(tokNumber) -} - -func lexComment(l *lexer) stateFn { - l.pos += len(l.opt.leftComment) - closeIdx := strings.Index(l.input[l.pos:], l.opt.rightComment) - if closeIdx < 0 { - return l.errorf("unterminated comment") - } - l.pos += closeIdx + len(l.opt.rightComment) - - l.curToken(tokComment) - - return nil -} - -func lexSingleLineComment(l *lexer) stateFn { - l.pos += len(l.opt.singleComment) - newlineIdx := strings.Index(l.input[l.pos:], "\n") - if newlineIdx < 0 { - } - l.pos += newlineIdx - return nil -} - -func lexSpace(l *lexer) stateFn { - for r := l.peek(); unicode.IsSpace(r); r = l.peek() { - l.next() - } - return nil -} - -func (l *lexer) errorf(format string, args ...any) stateFn { - l.tok = token{tokErr, fmt.Sprintf(format, args...), l.line} - l.start = 0 - l.pos = 0 - l.input = l.input[:0] - return nil -} diff --git a/internal/parser/parse.go b/internal/parser/parse.go deleted file mode 100644 index 725d28c..0000000 --- a/internal/parser/parse.go +++ /dev/null @@ -1,8 +0,0 @@ -package parser - -type Parser struct { - name string - lex *lexer -} - -func (p *Parser) Parse(input string) {} diff --git a/internal/parser/token.go b/internal/parser/token.go deleted file mode 100644 index 57b1c80..0000000 --- a/internal/parser/token.go +++ /dev/null @@ -1,99 +0,0 @@ -package parser - -import "fmt" - -type tokenType uint - -const ( - tokErr tokenType = iota - tokEOF - tokComment - tokBool - tokNumber - tokString - tokIdent - - tokKey // Keyword delim - tokAlter - tokCreate - tokDelete - tokDrop - tokInsert - tokSelect - tokTruncate - tokUpdate - tokColumn - tokDatabase - tokIndex - tokTable - tokDistinct - tokTop - tokAs - tokFrom - tokSet - tokLeft - tokRight - tokFull - tokJoin - tokWhere - tokHaving - tokInto - tokValues - tokAnd - tokIs - tokNot - tokNull - tokOr - tokLike - tokIn - tokBetween - tokCount - tokSum - tokAvg - tokMin - tokMax - tokGroup - tokOrder - tokBy - tokDesc -) - -var key = map[string]tokenType{ - "alter": tokAlter, - "create": tokCreate, - "delete": tokDelete, - "drop": tokDrop, - "insert": tokInsert, - "select": tokSelect, - "truncate": tokTruncate, - "update": tokUpdate, - "column": tokColumn, - "database": tokDatabase, - "index": tokIndex, - "table": tokTable, - "distinct": tokDistinct, - "top": tokTop, - "as": tokAs, - "from": tokFrom, - "set": tokSet, -} - -type token struct { - typ tokenType - val string - line int -} - -func (t token) String() string { - switch { - case t.typ == tokEOF: - return "EOF" - case t.typ == tokErr: - return t.val - case t.typ > tokKey: - return fmt.Sprintf("", t.val) - case len(t.val) > 13: - return fmt.Sprintf("%.10q...", t.val) - } - return t.val -} diff --git a/internal/server/server.go b/internal/server/server.go deleted file mode 100644 index 2e682f1..0000000 --- a/internal/server/server.go +++ /dev/null @@ -1,71 +0,0 @@ -package server - -import ( - "bufio" - "fmt" - "net" - _ "net/http" - - "github.com/gKits/PavoSQL/dbms" - "github.com/gKits/PavoSQL/parser" -) - -type Server struct { - Addr string - Port uint16 - listener net.Listener - dbms dbms.DBMS - parser parser.Parser - count int -} - -func NewServer(addr string, port uint16) (Server, error) { - listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", addr, port)) - if err != nil { - return Server{}, err - } - - // TODO: implement sane and correct defaults - return Server{ - Addr: addr, - Port: port, - listener: listener, - dbms: dbms.DBMS{}, - parser: parser.Parser{}, - count: 0, - }, nil -} - -func (s *Server) Run() { - for { - conn, err := s.listener.Accept() - if err != nil { - continue - } - go s.handleConnection(conn) - s.count++ - } -} - -func (s *Server) Stop() { - s.listener.Close() - // TODO: Implement graceful shutdown -} - -func (s *Server) handleConnection(conn net.Conn) { - defer conn.Close() - - scanner := bufio.NewScanner(conn) - for scanner.Scan() { - msg := scanner.Text() - - switch msg { - case "exit": - - case "h", "help": - default: - // TODO: Implement parser, operate, respond loop - } - } - s.count-- -} diff --git a/pkg/vcache/vcache.go b/pkg/vcache/vcache.go deleted file mode 100644 index f86ea5a..0000000 --- a/pkg/vcache/vcache.go +++ /dev/null @@ -1,36 +0,0 @@ -package vcache - -type VCache[I comparable, T any] struct { - cache map[uint64]map[I]T - ver uint64 -} - -func New[I comparable, T any](ver uint64) VCache[I, T] { - return VCache[I, T]{ - cache: map[uint64]map[I]T{}, - ver: ver, - } -} - -func (vc *VCache[I, T]) Get(id I, ver uint64) (T, bool) { - for ; ver <= vc.ver; ver++ { - d, ok := vc.cache[ver][id] - if ok { - return d, true - } - } - return *new(T), false -} - -func (vc *VCache[I, T]) Cache(i I, d T) { - vc.cache[vc.ver][i] = d -} - -func (vc *VCache[I, T]) NewVersion() uint64 { - vc.ver++ - return vc.ver -} - -func (vc *VCache[I, T]) DeleteVersion(ver uint64) { - delete(vc.cache, ver) -} From d655b7a0bfc308025d28f8485fe2e1afa1f6b02d Mon Sep 17 00:00:00 2001 From: gKits Date: Wed, 20 Dec 2023 14:49:38 +0100 Subject: [PATCH 02/51] ref: more cleanup --- scripts/pavod.service | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 scripts/pavod.service diff --git a/scripts/pavod.service b/scripts/pavod.service deleted file mode 100644 index 9a455fe..0000000 --- a/scripts/pavod.service +++ /dev/null @@ -1,14 +0,0 @@ -[Unit] -Description=PavoSQL daemon -After=network-online.target - -[Service] -Type=simple -Restart=always -RestartSec=5 -Environment=PAVO_PORT=1758 -Environment=PAVO_DIR= -ExecStart=pavosql server run - -[Install] -WantedBy=multi-user.target From f101b2dcc978f61a2f78c67819168aa01996bfe8 Mon Sep 17 00:00:00 2001 From: gKits Date: Fri, 22 Dec 2023 17:19:49 +0100 Subject: [PATCH 03/51] ci: add makefile --- Makefile | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 5e33094..f2c7d8d 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,43 @@ build: - @go build -o bin/pavosql ./cmd/pavosql + @go build -o bin/pavosql cmd/pavosql/main.go -run: build - @./bin/pavosql +run: + @go run cmd/pavosql/main.go + +# Testing test: - @go test -v ./... + @go test ./... + +test.cover: + @go test -coverprofile cover.out ./... + +test.cover.show: test.cover + @go tool cover -html cover.out + +## Tree test + +test.tree: + @go test ./internal/tree/... + +test.tree.cover: + @go test -coverprofile tree_cover.out ./internal/tree/... + +test.tree.cover.show: test.tree.cover + @go tool cover -html tree_cover.out + +## Parse test + +test.parse: + @go test ./internal/parse/... + +test.parse.cover: + @go test -coverprofile parse_cover.out ./internal/parse/... + +test.parse.cover.show: test.parse.cover + @go tool cover -html parse_cover.out + +# Cleanup + +cleancover: + @rm *cover.out From c50edf3a539bcfe4f14214c017eb09ae49551f5d Mon Sep 17 00:00:00 2001 From: gKits Date: Fri, 22 Dec 2023 17:21:52 +0100 Subject: [PATCH 04/51] feat: add b+tree implementation in internal tree package --- internal/tree/leafNode.go | 200 +++++++++++ internal/tree/leafNode_test.go | 556 +++++++++++++++++++++++++++++ internal/tree/node.go | 56 +++ internal/tree/node_test.go | 43 +++ internal/tree/pointerNode.go | 176 ++++++++++ internal/tree/pointerNode_test.go | 560 ++++++++++++++++++++++++++++++ internal/tree/tree.go | 403 +++++++++++++++++++++ internal/tree/tree_test.go | 175 ++++++++++ 8 files changed, 2169 insertions(+) create mode 100644 internal/tree/leafNode.go create mode 100644 internal/tree/leafNode_test.go create mode 100644 internal/tree/node.go create mode 100644 internal/tree/node_test.go create mode 100644 internal/tree/pointerNode.go create mode 100644 internal/tree/pointerNode_test.go create mode 100644 internal/tree/tree.go create mode 100644 internal/tree/tree_test.go diff --git a/internal/tree/leafNode.go b/internal/tree/leafNode.go new file mode 100644 index 0000000..8df7b76 --- /dev/null +++ b/internal/tree/leafNode.go @@ -0,0 +1,200 @@ +package tree + +import ( + "bytes" + "encoding/binary" + "fmt" + "slices" +) + +type LeafNode struct { + keys [][]byte + vals [][]byte +} + +func (n *LeafNode) Decode(b []byte) error { + off := uint16(0) + if nodeType(binary.BigEndian.Uint16(b[off:])) != nodeLeaf { + return errInvalNodeType + } + off += 2 + + nKeys := binary.BigEndian.Uint32(b[off:]) + n.keys = make([][]byte, nKeys) + n.vals = make([][]byte, nKeys) + off += 4 + + for i := uint32(0); i < nKeys; i++ { + kSize := binary.BigEndian.Uint16(b[off:]) + off += 2 + vSize := binary.BigEndian.Uint16(b[off:]) + off += 2 + + n.keys[i] = b[off : off+kSize] + off += kSize + n.vals[i] = b[off : off+vSize] + off += vSize + } + + return nil +} + +func (n *LeafNode) Encode() []byte { + b := make([]byte, n.Size()) + off := 0 + + binary.BigEndian.PutUint16(b[off:], uint16(nodeLeaf)) + off += 2 + + binary.BigEndian.PutUint32(b[off:], uint32(len(n.keys))) + off += 4 + + for i := 0; i < len(n.keys); i++ { + binary.BigEndian.PutUint16(b[off:], uint16(len(n.keys[i]))) + off += 2 + + binary.BigEndian.PutUint16(b[off:], uint16(len(n.vals[i]))) + off += 2 + + copy(b[off:], n.keys[i]) + off += len(n.keys[i]) + + copy(b[off:], n.vals[i]) + off += len(n.vals[i]) + } + + return b +} + +func (n *LeafNode) NKeys() int { + return len(n.keys) +} + +func (n *LeafNode) Size() int { + size := NodeHeader + for i := 0; i < len(n.keys); i++ { + size += 4 + len(n.keys[i]) + len(n.vals[i]) + } + return size +} + +func (n *LeafNode) Type() nodeType { + return nodeLeaf +} + +func (n *LeafNode) Key(idx int) ([]byte, error) { + if idx >= len(n.keys) { + return nil, errIdxOutOfRange + } + return n.keys[idx], nil +} + +func (n *LeafNode) ValAt(idx int) ([]byte, error) { + if idx >= len(n.vals) { + return nil, errIdxOutOfRange + } + return n.vals[idx], nil +} + +func (n *LeafNode) Val(k []byte) ([]byte, error) { + idx, exists := n.Find(k) + if !exists { + return nil, errKeyNotExists + } + return n.vals[idx], nil +} + +func (n *LeafNode) Insert(k, v []byte) error { + idx, exists := n.Find(k) + if exists { + return errKeyExists + } + + n.keys = slices.Insert(n.keys, idx, k) + n.vals = slices.Insert(n.vals, idx, v) + + return nil +} + +func (n *LeafNode) Update(k, v []byte) error { + idx, exists := n.Find(k) + if !exists { + return errKeyNotExists + } + + n.vals[idx] = v + return nil +} + +func (n *LeafNode) Delete(k []byte) error { + idx, exists := n.Find(k) + if !exists { + return errKeyNotExists + } + + n.keys = slices.Delete(n.keys, idx, idx+1) + n.vals = slices.Delete(n.vals, idx, idx+1) + + return nil +} + +func (n *LeafNode) Find(k []byte) (int, bool) { + return slices.BinarySearchFunc(n.keys, k, bytes.Compare) +} + +func (n *LeafNode) Split() (Node, Node) { + l := &LeafNode{ + keys: slices.Clone(n.keys[:len(n.keys)/2]), + vals: slices.Clone(n.vals[:len(n.vals)/2]), + } + r := &LeafNode{ + keys: slices.Clone(n.keys[len(n.keys)/2:]), + vals: slices.Clone(n.vals[len(n.vals)/2:]), + } + return l, r +} + +func (n *LeafNode) SplitBy(size int) []Node { + parts := size / n.Size() + nodes := make([]Node, parts) + + var p, accum, prev int + for i := 0; i < len(n.keys) && p < parts; i++ { + sizeOf := NodeHeader + len(n.keys[i]) + len(n.vals[i]) + accum += sizeOf + if accum > size { + nodes[p] = &LeafNode{ + keys: slices.Clone(n.keys[prev:i]), + vals: slices.Clone(n.vals[prev:i]), + } + prev = i + } + } + return nodes +} + +func (n *LeafNode) Merge(m Node) error { + mLeaf, ok := m.(*LeafNode) + if !ok { + return errMergeType + } + + last, err := n.Key(n.NKeys() - 1) + if err != nil { + return err + } + + if next, err := m.Key(0); err != nil { + return err + } else if bytes.Compare(last, next) >= 0 { + return errMergeOrder + } + + n.keys = append(n.keys, mLeaf.keys...) + n.vals = append(n.vals, mLeaf.vals...) + return nil +} + +func (n *LeafNode) String() string { + return fmt.Sprintf("LeafNode{keys: %s vals: %s}", n.keys, n.vals) +} diff --git a/internal/tree/leafNode_test.go b/internal/tree/leafNode_test.go new file mode 100644 index 0000000..6e1eef4 --- /dev/null +++ b/internal/tree/leafNode_test.go @@ -0,0 +1,556 @@ +package tree + +import ( + "bytes" + "slices" + "testing" +) + +func TestLeafDecode(t *testing.T) { + cases := []struct { + name string + in []byte + res *LeafNode + err error + }{ + { + name: "invalid type", + in: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + res: &LeafNode{}, + err: errInvalNodeType, + }, + { + name: "decode leaf node", + in: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x00}, + res: &LeafNode{keys: [][]byte{}, vals: [][]byte{}}, + err: nil, + }, + { + name: "decode leaf node with one k-v pair", + in: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 'k', 'v'}, + res: &LeafNode{keys: [][]byte{{'k'}}, vals: [][]byte{{'v'}}}, + err: nil, + }, + { + name: "decode pointer node with multiple k-v pairs", + in: []byte{ + 0x00, 0x65, + 0x00, 0x00, 0x00, 0x04, + 0x00, 0x01, + 0x00, 0x05, + 'a', + 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x01, + 0x00, 0x0A, + 'b', + 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', + 0x00, 0x02, + 0x00, 0x02, + 'b', 'a', + 'b', 'a', + 0x00, 0x03, + 0x00, 0x04, + 'c', 'b', 'a', + 0x01, 0x02, 0x03, 0x04, + }, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c', 'b', 'a'}}, + vals: [][]byte{ + {0x01, 0x01, 0x01, 0x01, 0x01}, + {'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'}, + {'b', 'a'}, + {0x01, 0x02, 0x03, 0x04}, + }, + }, + err: nil, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := &LeafNode{} + err := res.Decode(c.in) + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } else if res == nil { + if c.res != nil { + t.Errorf("expected result %v, got %v", c.res, res) + } + } else { + if c.res == nil { + t.Errorf("expected result %v, got %v", c.res, res) + } else if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) + } else if !slices.EqualFunc(res.vals, c.res.vals, bytes.Equal) { + t.Errorf("expected vals %v, got %v", c.res.vals, res.vals) + } + } + }) + } +} + +func TestLeafEncode(t *testing.T) { + cases := []struct { + name string + in *LeafNode + res []byte + }{ + { + name: "encode empty leaf node", + in: &LeafNode{keys: [][]byte{}, vals: [][]byte{}}, + res: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x00}, + }, + { + name: "encode leaf node with one k-v pair", + in: &LeafNode{keys: [][]byte{{'k'}}, vals: [][]byte{{'v'}}}, + res: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 'k', 'v'}, + }, + { + name: "encode leaf node with multiple k-v pairs", + in: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c', 'b', 'a'}}, + vals: [][]byte{ + {0x01, 0x01, 0x01, 0x01, 0x01}, + {'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'}, + {'b', 'a'}, + {0x01, 0x02, 0x03, 0x04}, + }, + }, + res: []byte{ + 0x00, 0x65, + 0x00, 0x00, 0x00, 0x04, + 0x00, 0x01, + 0x00, 0x05, + 'a', + 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x01, + 0x00, 0x0A, + 'b', + 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', + 0x00, 0x02, + 0x00, 0x02, + 'b', 'a', + 'b', 'a', + 0x00, 0x03, + 0x00, 0x04, + 'c', 'b', 'a', + 0x01, 0x02, 0x03, 0x04, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := c.in.Encode() + if !bytes.Equal(res, c.res) { + t.Errorf("expected result %v, got %v", c.res, res) + } + }) + } +} + +func TestLeafVal(t *testing.T) { + cases := []struct { + name string + k []byte + res []byte + err error + }{ + { + name: "successfully read key", + k: []byte{'b'}, + res: []byte{'b'}, + }, + { + name: "failed read non existing key", + k: []byte{'e'}, + res: nil, + err: errKeyNotExists, + }, + } + + leaf := LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res, err := leaf.Val(c.k) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } else if !bytes.Equal(res, c.res) { + t.Errorf("expected pointer %v, got %v", c.res, res) + } + }) + } +} + +func TestLeafInsert(t *testing.T) { + cases := []struct { + name string + k []byte + v []byte + res *LeafNode + err error + }{ + { + name: "insert in middle", + k: []byte{'b', 'a'}, + v: []byte{'b', 'a'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c'}, {'d'}}, + }, + }, + { + name: "insert last", + k: []byte{'e'}, + v: []byte{'e'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}}, + }, + }, + { + name: "failed insert existing key", + k: []byte{'a'}, + v: []byte{'a'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + }, + err: errKeyExists, + }, + } + + leaf := &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := *leaf + err := res.Insert(c.k, c.v) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) + } + if !slices.EqualFunc(res.vals, c.res.vals, bytes.Equal) { + t.Errorf("expected vals %v, got %v", c.res.vals, res.vals) + } + }) + } +} + +func TestLeafUpdate(t *testing.T) { + cases := []struct { + name string + k []byte + v []byte + res *LeafNode + err error + }{ + { + name: "update first", + k: []byte{'a'}, + v: []byte{'0', 'a'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'0', 'a'}, {'b'}, {'c'}, {'d'}}, + }, + }, + { + name: "update last", + k: []byte{'d'}, + v: []byte{'3', 'd'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}, {'3', 'd'}}, + }, + }, + { + name: "update middle", + k: []byte{'c'}, + v: []byte{'2', 'c'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'2', 'c'}, {'d'}}, + }, + }, + { + name: "failed update non existing key", + k: []byte{'e'}, + v: []byte{'e'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + }, + err: errKeyNotExists, + }, + } + + leaf := func() *LeafNode { + return &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + } + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := leaf() + err := res.Update(c.k, c.v) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) + } + if !slices.EqualFunc(res.vals, c.res.vals, bytes.Equal) { + t.Errorf("expected vals %v, got %v", c.res.vals, res.vals) + } + }) + } +} + +func TestLeafDelete(t *testing.T) { + cases := []struct { + name string + k []byte + res *LeafNode + err error + }{ + { + name: "delete first", + k: []byte{'a'}, + res: &LeafNode{ + keys: [][]byte{{'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'b'}, {'c'}, {'d'}}, + }, + }, + { + name: "delete last", + k: []byte{'d'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}}, + }, + }, + { + name: "delete middle", + k: []byte{'c'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'d'}}, + }, + }, + { + name: "failed delete non existing key", + k: []byte{'e'}, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + }, + err: errKeyNotExists, + }, + } + + leaf := func() *LeafNode { + return &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + } + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := leaf() + err := res.Delete(c.k) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) + } + if !slices.EqualFunc(res.vals, c.res.vals, bytes.Equal) { + t.Errorf("expected vals %v, got %v", c.res.vals, res.vals) + } + }) + } +} + +func TestLeafSplit(t *testing.T) { + cases := []struct { + name string + in *LeafNode + l *LeafNode + r *LeafNode + }{ + { + name: "split even number", + in: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}}, + vals: [][]byte{{'a'}, {'b'}}, + }, + l: &LeafNode{ + keys: [][]byte{{'a'}}, + vals: [][]byte{{'a'}}, + }, + r: &LeafNode{ + keys: [][]byte{{'b'}}, + vals: [][]byte{{'b'}}, + }, + }, + { + name: "split odd number", + in: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}}, + }, + l: &LeafNode{ + keys: [][]byte{{'a'}}, + vals: [][]byte{{'a'}}, + }, + r: &LeafNode{ + keys: [][]byte{{'b'}, {'c'}}, + vals: [][]byte{{'b'}, {'c'}}, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + outL, outR := c.in.Split() + + if l, ok := outL.(*LeafNode); !ok { + t.Error("expected successful type assertion") + } else { + if !slices.EqualFunc(c.l.keys, l.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.l.keys, l.keys) + } else if !slices.EqualFunc(c.l.vals, l.vals, bytes.Equal) { + t.Errorf("expected vals %v, got %v", c.l.vals, l.vals) + } + } + + if r, ok := outR.(*LeafNode); !ok { + t.Error("expected successful type assertion") + } else { + if !slices.EqualFunc(c.r.keys, r.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.r.keys, r.keys) + } else if !slices.EqualFunc(c.r.vals, r.vals, bytes.Equal) { + t.Errorf("expected vals %v, got %v", c.r.vals, r.vals) + } + } + }) + } +} + +func TestLeafMerge(t *testing.T) { + cases := []struct { + name string + in *LeafNode + merge Node + res *LeafNode + err error + }{ + { + name: "merge same size", + in: &LeafNode{ + keys: [][]byte{{'a'}}, + vals: [][]byte{{'a'}}, + }, + merge: &LeafNode{ + keys: [][]byte{{'b'}}, + vals: [][]byte{{'b'}}, + }, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}}, + vals: [][]byte{{'a'}, {'b'}}, + }, + }, + { + name: "merge not same size", + in: &LeafNode{ + keys: [][]byte{{'a'}}, + vals: [][]byte{{'a'}}, + }, + merge: &LeafNode{ + keys: [][]byte{{'b'}, {'c'}}, + vals: [][]byte{{'b'}, {'c'}}, + }, + res: &LeafNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}}, + vals: [][]byte{{'a'}, {'b'}, {'c'}}, + }, + }, + { + name: "failed merge wrong order", + in: &LeafNode{ + keys: [][]byte{{'b'}}, + vals: [][]byte{{'b'}}, + }, + merge: &LeafNode{ + keys: [][]byte{{'a'}}, + vals: [][]byte{{'a'}}, + }, + res: &LeafNode{ + keys: [][]byte{{'b'}}, + vals: [][]byte{{'b'}}, + }, + err: errMergeOrder, + }, + { + name: "failed merge wrong order equal key", + in: &LeafNode{ + keys: [][]byte{{'b'}}, + vals: [][]byte{{'b'}}, + }, + merge: &LeafNode{ + keys: [][]byte{{'b'}}, + vals: [][]byte{{'a'}}, + }, + res: &LeafNode{ + keys: [][]byte{{'b'}}, + vals: [][]byte{{'b'}}, + }, + err: errMergeOrder, + }, + { + name: "failed merge wrong type", + in: &LeafNode{ + keys: [][]byte{{'a'}}, + vals: [][]byte{{'a'}}, + }, + merge: &PointerNode{}, + res: &LeafNode{ + keys: [][]byte{{'a'}}, + vals: [][]byte{{'a'}}, + }, + err: errMergeType, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := c.in.Merge(c.merge) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + + if !slices.EqualFunc(c.res.keys, c.in.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, c.in.keys) + } else if !slices.EqualFunc(c.res.vals, c.in.vals, bytes.Equal) { + t.Errorf("expected vals %v, got %v", c.res.vals, c.in.vals) + } + }) + } +} diff --git a/internal/tree/node.go b/internal/tree/node.go new file mode 100644 index 0000000..298a01b --- /dev/null +++ b/internal/tree/node.go @@ -0,0 +1,56 @@ +package tree + +import ( + "encoding/binary" + "errors" +) + +const NodeHeader = 6 + +var ( + errKeyExists = errors.New("key already exists") + errKeyNotExists = errors.New("key does not exist") + errInvalNodeType = errors.New("invalid node type") + errIdxOutOfRange = errors.New("index is out of node range") + errLeafHasNoPtr = errors.New("leaf node cannot contain ptr") + errMergeType = errors.New("merging nodes need to have equal type") + errMergeOrder = errors.New("last key of left node needs to greater than first key of right node") + errNodeAssert = errors.New("node assertion failed") +) + +type nodeType uint16 + +const ( + nodePointer nodeType = 100 + nodeLeaf nodeType = 101 +) + +type Node interface { + Type() nodeType // Returns node type. + Key(idx int) ([]byte, error) // Returns key at given index idx. + NKeys() int // Returns the number of keys stored in the node. + Size() int // Returns the encoded size of the node in bytes. + Delete(k []byte) error // Deletes the key and its value from the node. + Find(k []byte) (idx int, exists bool) // Returns the index and existing status of the given key k. + Encode() []byte // Encodes the node and returns the encoded byte stream. + Decode([]byte) error // Decodes the give bytes stream into the node. + Split() (l Node, r Node) // Returns two nodes containing each half of the original node. + Merge(Node) error // Right merges all keys and values onto node. +} + +func NewNode(b []byte) (Node, error) { + var n Node + switch nodeType(binary.BigEndian.Uint16(b)) { + case nodePointer: + n = &PointerNode{} + case nodeLeaf: + n = &LeafNode{} + default: + return nil, errInvalNodeType + } + + if err := n.Decode(b); err != nil { + return nil, err + } + return n, nil +} diff --git a/internal/tree/node_test.go b/internal/tree/node_test.go new file mode 100644 index 0000000..33aa501 --- /dev/null +++ b/internal/tree/node_test.go @@ -0,0 +1,43 @@ +package tree + +import "testing" + +func TestNewNode(t *testing.T) { + cases := []struct { + name string + in []byte + res Node + err error + }{ + { + name: "new pointer node", + in: []byte{0x00, 0x64, 0x00, 0x00, 0x00, 0x00}, + res: &PointerNode{}, + }, + { + name: "new leaf node", + in: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x00}, + res: &LeafNode{}, + }, + { + name: "invalid node type", + in: []byte{0x00, 0x00}, + res: nil, + err: errInvalNodeType, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res, err := NewNode(c.in) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } else if c.res != nil { + if res.Type() != c.res.Type() { + t.Errorf("expected node type %v, got %v", c.res.Type(), res.Type()) + } + } + }) + } +} diff --git a/internal/tree/pointerNode.go b/internal/tree/pointerNode.go new file mode 100644 index 0000000..c840aac --- /dev/null +++ b/internal/tree/pointerNode.go @@ -0,0 +1,176 @@ +package tree + +import ( + "bytes" + "encoding/binary" + "fmt" + "slices" +) + +type PointerNode struct { + keys [][]byte + ptrs []uint64 +} + +func (n *PointerNode) Decode(b []byte) error { + off := uint16(0) + if nodeType(binary.BigEndian.Uint16(b[off:])) != nodePointer { + return errInvalNodeType + } + off += 2 + + nKeys := binary.BigEndian.Uint32(b[off:]) + n.keys = make([][]byte, nKeys) + n.ptrs = make([]uint64, nKeys) + off += 4 + + for i := uint32(0); i < nKeys; i++ { + kSize := binary.BigEndian.Uint16(b[off:]) + off += 2 + + n.keys[i] = b[off : off+kSize] + off += kSize + n.ptrs[i] = binary.BigEndian.Uint64(b[off : off+8]) + off += 8 + } + + return nil +} + +func (n *PointerNode) Encode() []byte { + b := make([]byte, n.Size()) + off := 0 + + binary.BigEndian.PutUint16(b, uint16(nodePointer)) + off += 2 + + binary.BigEndian.PutUint32(b[off:], uint32(len(n.keys))) + off += 4 + + for i := 0; i < len(n.keys); i++ { + binary.BigEndian.PutUint16(b[off:], uint16(len(n.keys[i]))) + off += 2 + + copy(b[off:], n.keys[i]) + off += len(n.keys[i]) + + binary.BigEndian.PutUint64(b[off:], n.ptrs[i]) + off += 8 + } + + return b +} + +func (n *PointerNode) NKeys() int { + return len(n.keys) +} + +func (n *PointerNode) Size() int { + size := NodeHeader + for i := 0; i < len(n.keys); i++ { + size += 2 + len(n.keys[i]) + 8 + } + return size +} + +func (n *PointerNode) Type() nodeType { + return nodePointer +} + +func (n *PointerNode) Key(idx int) ([]byte, error) { + if idx >= len(n.keys) { + return nil, errIdxOutOfRange + } + return n.keys[idx], nil +} + +func (n *PointerNode) PtrAt(idx int) (uint64, error) { + if idx >= len(n.ptrs) { + return 0, errIdxOutOfRange + } + return n.ptrs[idx], nil +} + +func (n *PointerNode) Ptr(k []byte) (uint64, error) { + idx, exists := n.Find(k) + if !exists { + return 0, errKeyNotExists + } + return n.ptrs[idx], nil +} + +func (n *PointerNode) Insert(k []byte, ptr uint64) error { + idx, exists := n.Find(k) + if exists { + return errKeyExists + } + + n.keys = slices.Insert(n.keys, idx, k) + n.ptrs = slices.Insert(n.ptrs, idx, ptr) + + return nil +} + +func (n *PointerNode) Update(k []byte, ptr uint64) error { + idx, exists := n.Find(k) + if !exists { + return errKeyNotExists + } + + n.ptrs[idx] = ptr + return nil +} + +func (n *PointerNode) Delete(k []byte) error { + idx, exists := n.Find(k) + if !exists { + return errKeyNotExists + } + + n.keys = slices.Delete(n.keys, idx, idx+1) + n.ptrs = slices.Delete(n.ptrs, idx, idx+1) + + return nil +} + +func (n *PointerNode) Find(k []byte) (idx int, exists bool) { + return slices.BinarySearchFunc(n.keys, k, bytes.Compare) +} + +func (n *PointerNode) Split() (Node, Node) { + l := &PointerNode{ + keys: slices.Clone(n.keys[:len(n.keys)/2]), + ptrs: slices.Clone(n.ptrs[:len(n.ptrs)/2]), + } + r := &PointerNode{ + keys: slices.Clone(n.keys[len(n.keys)/2:]), + ptrs: slices.Clone(n.ptrs[len(n.ptrs)/2:]), + } + return l, r +} + +func (n *PointerNode) Merge(m Node) error { + mPtr, ok := m.(*PointerNode) + if !ok { + return errMergeType + } + + last, err := n.Key(n.NKeys() - 1) + if err != nil { + return err + } + + if next, err := m.Key(0); err != nil { + return err + } else if bytes.Compare(last, next) >= 0 { + return errMergeOrder + } + + n.keys = append(n.keys, mPtr.keys...) + n.ptrs = append(n.ptrs, mPtr.ptrs...) + return nil +} + +func (n *PointerNode) String() string { + return fmt.Sprintf("PointerNode{keys: %s ptrs: %v}", n.keys, n.ptrs) +} diff --git a/internal/tree/pointerNode_test.go b/internal/tree/pointerNode_test.go new file mode 100644 index 0000000..1360cb5 --- /dev/null +++ b/internal/tree/pointerNode_test.go @@ -0,0 +1,560 @@ +package tree + +import ( + "bytes" + "slices" + "testing" +) + +func TestPointerDecode(t *testing.T) { + cases := []struct { + name string + in []byte + res *PointerNode + err error + }{ + { + name: "invalid type", + in: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + res: &PointerNode{}, + err: errInvalNodeType, + }, + { + name: "decode pointer node", + in: []byte{0x00, 0x64, 0x00, 0x00, 0x00, 0x00}, + res: &PointerNode{keys: [][]byte{}, ptrs: []uint64{}}, + err: nil, + }, + { + name: "decode pointer node with one k-p pair", + in: []byte{ + 0x00, 0x64, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, + 'k', + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + }, + res: &PointerNode{keys: [][]byte{{'k'}}, ptrs: []uint64{72340172838076673}}, + err: nil, + }, + { + name: "decode pointer node with multiple k-p pairs", + in: []byte{ + 0x00, 0x64, + 0x00, 0x00, 0x00, 0x04, + 0x00, 0x01, + 'a', + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x01, + 'b', + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x02, + 'b', 'a', + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x03, + 'c', 'b', 'a', + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c', 'b', 'a'}}, + ptrs: []uint64{ + 72340172838076673, + 255, + 1, + 18446744073709551615, + }, + }, + err: nil, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := &PointerNode{} + err := res.Decode(c.in) + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } else if res == nil { + if c.res != nil { + t.Errorf("expected result %v, got %v", c.res, res) + } + } else { + if c.res == nil { + t.Errorf("expected result %v, got %v", c.res, res) + } else if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) + } else if !slices.Equal(res.ptrs, c.res.ptrs) { + t.Errorf("expected vals %v, got %v", c.res.ptrs, res.ptrs) + } + } + }) + } +} + +func TestPointerEncode(t *testing.T) { + cases := []struct { + name string + in *PointerNode + res []byte + }{ + { + name: "decode pointer node", + in: &PointerNode{keys: [][]byte{}, ptrs: []uint64{}}, + res: []byte{0x00, 0x64, 0x00, 0x00, 0x00, 0x00}, + }, + { + name: "decode pointer node with one k-p pair", + in: &PointerNode{keys: [][]byte{{'k'}}, ptrs: []uint64{72340172838076673}}, + res: []byte{ + 0x00, 0x64, + 0x00, 0x00, 0x00, 0x01, + 0x00, 0x01, + 'k', + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + }, + }, + { + name: "decode pointer node with multiple k-p pairs", + in: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c', 'b', 'a'}}, + ptrs: []uint64{ + 72340172838076673, + 255, + 1, + 18446744073709551615, + }, + }, + res: []byte{ + 0x00, 0x64, + 0x00, 0x00, 0x00, 0x04, + 0x00, 0x01, + 'a', + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, 0x01, + 'b', + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, + 0x00, 0x02, + 'b', 'a', + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x03, + 'c', 'b', 'a', + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := c.in.Encode() + if !bytes.Equal(res, c.res) { + t.Errorf("expected result %v, got %v", c.res, res) + } + }) + } +} + +func TestPointerPtr(t *testing.T) { + cases := []struct { + name string + k []byte + res uint64 + err error + }{ + { + name: "successfully read key", + k: []byte{'b'}, + res: 1, + }, + { + name: "failed read non existing key", + k: []byte{'e'}, + res: 0, + err: errKeyNotExists, + }, + } + + ptr := PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 2, 3}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res, err := ptr.Ptr(c.k) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } else if res != c.res { + t.Errorf("expected pointer %v, got %v", c.res, res) + } + }) + } +} + +func TestPointerInsert(t *testing.T) { + cases := []struct { + name string + k []byte + p uint64 + res *PointerNode + err error + }{ + { + name: "insert in middle", + k: []byte{'b', 'a'}, + p: 4, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 4, 2, 3}, + }, + }, + { + name: "insert last", + k: []byte{'e'}, + p: 4, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}}, + ptrs: []uint64{0, 1, 2, 3, 4}, + }, + }, + { + name: "failed insert existing key", + k: []byte{'a'}, + p: 4, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 2, 3}, + }, + err: errKeyExists, + }, + } + + ptr := &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 2, 3}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := *ptr + err := res.Insert(c.k, c.p) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) + } + if !slices.Equal(res.ptrs, c.res.ptrs) { + t.Errorf("expected ptrs %v, got %v", c.res.ptrs, res.ptrs) + } + }) + } +} + +func TestPointerUpdate(t *testing.T) { + cases := []struct { + name string + k []byte + p uint64 + res *PointerNode + err error + }{ + { + name: "update first", + k: []byte{'a'}, + p: 4, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{4, 1, 2, 3}, + }, + }, + { + name: "update last", + k: []byte{'d'}, + p: 4, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 2, 4}, + }, + }, + { + name: "update middle", + k: []byte{'c'}, + p: 4, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 4, 3}, + }, + }, + { + name: "failed update non existing key", + k: []byte{'e'}, + p: 4, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 2, 3}, + }, + err: errKeyNotExists, + }, + } + + ptr := func() *PointerNode { + return &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 2, 3}, + } + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := ptr() + err := res.Update(c.k, c.p) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) + } + if !slices.Equal(res.ptrs, c.res.ptrs) { + t.Errorf("expected ptrs %v, got %v", c.res.ptrs, res.ptrs) + } + }) + } +} + +func TestPointerDelete(t *testing.T) { + cases := []struct { + name string + k []byte + res *PointerNode + err error + }{ + { + name: "delete first", + k: []byte{'a'}, + res: &PointerNode{ + keys: [][]byte{{'b'}, {'c'}, {'d'}}, + ptrs: []uint64{1, 2, 3}, + }, + }, + { + name: "delete last", + k: []byte{'d'}, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}}, + ptrs: []uint64{0, 1, 2}, + }, + }, + { + name: "delete middle", + k: []byte{'c'}, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'d'}}, + ptrs: []uint64{0, 1, 3}, + }, + }, + { + name: "failed delete non existing key", + k: []byte{'e'}, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 2, 3}, + }, + err: errKeyNotExists, + }, + } + + leaf := func() *PointerNode { + return &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, + ptrs: []uint64{0, 1, 2, 3}, + } + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res := leaf() + err := res.Delete(c.k) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) + } + if !slices.Equal(res.ptrs, c.res.ptrs) { + t.Errorf("expected ptrs %v, got %v", c.res.ptrs, res.ptrs) + } + }) + } +} + +func TestPointerSplit(t *testing.T) { + cases := []struct { + name string + in *PointerNode + l *PointerNode + r *PointerNode + }{ + { + name: "split even number", + in: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}}, + ptrs: []uint64{0, 1}, + }, + l: &PointerNode{ + keys: [][]byte{{'a'}}, + ptrs: []uint64{0}, + }, + r: &PointerNode{ + keys: [][]byte{{'b'}}, + ptrs: []uint64{1}, + }, + }, + { + name: "split odd number", + in: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}}, + ptrs: []uint64{0, 1, 2}, + }, + l: &PointerNode{ + keys: [][]byte{{'a'}}, + ptrs: []uint64{0}, + }, + r: &PointerNode{ + keys: [][]byte{{'b'}, {'c'}}, + ptrs: []uint64{1, 2}, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + outL, outR := c.in.Split() + + if l, ok := outL.(*PointerNode); !ok { + t.Error("expected successful type assertion") + } else { + if !slices.EqualFunc(c.l.keys, l.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.l.keys, l.keys) + } else if !slices.Equal(c.l.ptrs, l.ptrs) { + t.Errorf("expected ptrs %v, got %v", c.l.ptrs, l.ptrs) + } + } + + if r, ok := outR.(*PointerNode); !ok { + t.Error("expected successful type assertion") + } else { + if !slices.EqualFunc(c.r.keys, r.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.r.keys, r.keys) + } else if !slices.Equal(c.r.ptrs, r.ptrs) { + t.Errorf("expected ptrs %v, got %v", c.r.ptrs, r.ptrs) + } + } + }) + } +} + +func TestPointerMerge(t *testing.T) { + cases := []struct { + name string + in *PointerNode + merge Node + res *PointerNode + err error + }{ + { + name: "merge same size", + in: &PointerNode{ + keys: [][]byte{{'a'}}, + ptrs: []uint64{0}, + }, + merge: &PointerNode{ + keys: [][]byte{{'b'}}, + ptrs: []uint64{1}, + }, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}}, + ptrs: []uint64{0, 1}, + }, + }, + { + name: "merge not same size", + in: &PointerNode{ + keys: [][]byte{{'a'}}, + ptrs: []uint64{0}, + }, + merge: &PointerNode{ + keys: [][]byte{{'b'}, {'c'}}, + ptrs: []uint64{1, 2}, + }, + res: &PointerNode{ + keys: [][]byte{{'a'}, {'b'}, {'c'}}, + ptrs: []uint64{0, 1, 2}, + }, + }, + { + name: "failed merge wrong order", + in: &PointerNode{ + keys: [][]byte{{'b'}}, + ptrs: []uint64{1}, + }, + merge: &PointerNode{ + keys: [][]byte{{'a'}}, + ptrs: []uint64{0}, + }, + res: &PointerNode{ + keys: [][]byte{{'b'}}, + ptrs: []uint64{1}, + }, + err: errMergeOrder, + }, + { + name: "failed merge wrong order equal key", + in: &PointerNode{ + keys: [][]byte{{'b'}}, + ptrs: []uint64{1}, + }, + merge: &PointerNode{ + keys: [][]byte{{'b'}}, + ptrs: []uint64{0}, + }, + res: &PointerNode{ + keys: [][]byte{{'b'}}, + ptrs: []uint64{1}, + }, + err: errMergeOrder, + }, + { + name: "failed merge wrong type", + in: &PointerNode{ + keys: [][]byte{{'a'}}, + ptrs: []uint64{0}, + }, + merge: &LeafNode{}, + res: &PointerNode{ + keys: [][]byte{{'a'}}, + ptrs: []uint64{0}, + }, + err: errMergeType, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + err := c.in.Merge(c.merge) + + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + + if !slices.EqualFunc(c.res.keys, c.in.keys, bytes.Equal) { + t.Errorf("expected keys %v, got %v", c.res.keys, c.in.keys) + } else if !slices.Equal(c.res.ptrs, c.in.ptrs) { + t.Errorf("expected ptrs %v, got %v", c.res.ptrs, c.in.ptrs) + } + }) + } +} diff --git a/internal/tree/tree.go b/internal/tree/tree.go new file mode 100644 index 0000000..471d69b --- /dev/null +++ b/internal/tree/tree.go @@ -0,0 +1,403 @@ +package tree + +import ( + "errors" +) + +const PageSize = 4096 +const MaxCell = PageSize - NodeHeader + +var ( + errCellToLarge = errors.New("cell size exceeds maximum") + errMalformedRecurse = errors.New("recursive result is malformed") +) + +type Tree struct { + root uint64 + read func(uint64) (Node, error) // callback to read node from backend + alloc func(Node) (uint64, error) // callback to alloc node in backend + free func(uint64) error // callback to free node in backend + maxNodeSize int +} + +type recurseResult struct { + Key []byte + Ptr uint64 +} + +func (t *Tree) Get(k []byte) ([]byte, error) { + cur, err := t.read(t.root) + if err != nil { + return nil, err + } + + for { + switch cur.Type() { + case nodePointer: + pointer, ok := cur.(*PointerNode) + if !ok { + return nil, errNodeAssert + } + + idx, exists := pointer.Find(k) + if !exists { + idx-- + } + ptr, err := pointer.PtrAt(idx) + if err != nil { + return nil, err + } + + cur, err = t.read(ptr) + if err != nil { + return nil, err + } + continue + + case nodeLeaf: + leaf, ok := cur.(*LeafNode) + if !ok { + return nil, errNodeAssert + } + return leaf.Val(k) + + default: + return nil, errInvalNodeType + } + } +} + +func (t *Tree) Insert(k, v []byte) error { + if !t.cellFits(k, v) { + return errCellToLarge + } + + res, err := t.recursiveInsert(t.root, k, v) + if err != nil { + return err + } + + switch len(res) { + case 1: + t.root = res[0].Ptr + case 2: + root := &PointerNode{ + keys: [][]byte{res[0].Key, res[1].Key}, + ptrs: []uint64{res[0].Ptr, res[1].Ptr}, + } + + t.root, err = t.alloc(root) + if err != nil { + return err + } + default: + return errMalformedRecurse + } + return nil +} + +func (t *Tree) recursiveInsert(ptr uint64, k, v []byte) ([]recurseResult, error) { + cur, err := t.read(ptr) + if err != nil { + return nil, err + } + if err = t.free(ptr); err != nil { + return nil, err + } + + switch cur.Type() { + case nodePointer: + pointer, ok := cur.(*PointerNode) + if !ok { + return nil, errNodeAssert + } + + idx, exists := pointer.Find(k) + if !exists { + idx-- + } + + ptr, err = pointer.PtrAt(idx) + if err != nil { + return nil, err + } + + res, err := t.recursiveInsert(ptr, k, v) + if err != nil { + return nil, err + } + + if err := pointer.Update(res[0].Key, res[0].Ptr); err != nil { + return nil, err + } + + if len(res) > 1 { + if err := pointer.Insert(res[1].Key, res[1].Ptr); err != nil { + return nil, err + } + } + cur = pointer + + case nodeLeaf: + leaf, ok := cur.(*LeafNode) + if !ok { + return nil, errNodeAssert + } + + if err := leaf.Insert(k, v); err != nil { + return nil, err + } + cur = leaf + + default: + return nil, errInvalNodeType + } + + println(cur.Type()) + println(cur.Size()) + if cur.Size() > t.maxNodeSize { + l, r := cur.Split() + + lPtr, err := t.alloc(l) + if err != nil { + return nil, err + } + + rPtr, err := t.alloc(r) + if err != nil { + return nil, err + } + + lK, err := l.Key(0) + if err != nil { + return nil, err + } + + rK, err := r.Key(0) + if err != nil { + return nil, err + } + + return []recurseResult{{Key: lK, Ptr: lPtr}, {Key: rK, Ptr: rPtr}}, nil + } + + ptr, err = t.alloc(cur) + if err != nil { + return nil, err + } + + origin, err := cur.Key(0) + if err != nil { + return nil, err + } + + return []recurseResult{{Key: origin, Ptr: ptr}}, nil +} + +func (t *Tree) Update(k, v []byte) error { + if !t.cellFits(k, v) { + return errCellToLarge + } + + res, err := t.recursiveUpdate(t.root, k, v) + if err != nil { + return err + } + + switch len(res) { + case 1: + t.root = res[0].Ptr + case 2: + root := &PointerNode{ + keys: [][]byte{res[0].Key, res[1].Key}, + ptrs: []uint64{res[0].Ptr, res[1].Ptr}, + } + + t.root, err = t.alloc(root) + if err != nil { + return err + } + default: + return errMalformedRecurse + } + return nil +} + +func (t *Tree) recursiveUpdate(ptr uint64, k, v []byte) ([]recurseResult, error) { + cur, err := t.read(ptr) + if err != nil { + return nil, err + } + if err = t.free(ptr); err != nil { + return nil, err + } + + switch cur.Type() { + case nodePointer: + pointer, ok := cur.(*PointerNode) + if !ok { + return nil, errNodeAssert + } + + idx, _ := pointer.Find(k) + ptr, err = pointer.PtrAt(idx) + if err != nil { + return nil, err + } + + res, err := t.recursiveUpdate(ptr, k, v) + if err != nil { + return nil, err + } + + if err := pointer.Update(res[0].Key, res[0].Ptr); err != nil { + return nil, err + } + + if len(res) > 1 { + if err := pointer.Insert(res[1].Key, res[1].Ptr); err != nil { + return nil, err + } + } + + case nodeLeaf: + leaf, ok := cur.(*LeafNode) + if !ok { + return nil, errNodeAssert + } + + if err := leaf.Update(k, v); err != nil { + return nil, err + } + cur = leaf + + default: + return nil, errInvalNodeType + } + + if cur.Size() > t.maxNodeSize { + l, r := cur.Split() + + lPtr, err := t.alloc(l) + if err != nil { + return nil, err + } + + rPtr, err := t.alloc(r) + if err != nil { + return nil, err + } + + nK, err := r.Key(0) + if err != nil { + return nil, err + } + + return []recurseResult{{Key: k, Ptr: lPtr}, {Key: nK, Ptr: rPtr}}, nil + } + + ptr, err = t.alloc(cur) + if err != nil { + return nil, err + } + + return []recurseResult{{Key: k, Ptr: ptr}}, nil +} + +func (t *Tree) Delete(k []byte) error { + return nil +} + +func (t *Tree) recursiveDelete(ptr uint64, k, v []byte) (Node, error) { + cur, err := t.read(ptr) + if err != nil { + return nil, err + } + if err = t.free(ptr); err != nil { + return nil, err + } + + switch cur.Type() { + case nodePointer: + pointer, ok := cur.(*PointerNode) + if !ok { + return nil, errNodeAssert + } + + idx, _ := pointer.Find(k) + ptr, err = pointer.PtrAt(idx) + if err != nil { + return nil, err + } + + child, err := t.recursiveDelete(ptr, k, v) + if err != nil { + return nil, err + } + + if err = t.tryMergeNeighbors(pointer, child, idx); err != nil { + return nil, err + } + + case nodeLeaf: + if err := cur.Delete(k); err != nil { + return nil, err + } + + default: + return nil, errInvalNodeType + } + + ptr, err = t.alloc(cur) + if err != nil { + return nil, err + } + + return cur, nil +} + +func (t *Tree) tryMergeNeighbors(parent *PointerNode, child Node, idx int) error { + if child.Size() < t.maxNodeSize/3 { + if idx > 0 { + lPtr, err := parent.PtrAt(idx - 1) + if err != nil { + return err + } + + l, err := t.read(lPtr) + if err != nil { + return err + } + + if child.Size()+l.Size() < t.maxNodeSize { + if err := l.Merge(child); err != nil { + return err + } + child = l + } + } + if idx < parent.NKeys()-1 { + rPtr, err := parent.PtrAt(idx + 1) + if err != nil { + return err + } + + r, err := t.read(rPtr) + if err != nil { + return err + } + + if child.Size()+r.Size() < t.maxNodeSize { + if err := child.Merge(r); err != nil { + return err + } + } + } + } + + return nil +} + +func (t *Tree) cellFits(k, v []byte) bool { + return 4+len(k)+len(v) <= t.maxNodeSize-NodeHeader || 2+len(k)+8 <= t.maxNodeSize-NodeHeader +} diff --git a/internal/tree/tree_test.go b/internal/tree/tree_test.go new file mode 100644 index 0000000..35db720 --- /dev/null +++ b/internal/tree/tree_test.go @@ -0,0 +1,175 @@ +package tree + +import ( + "bytes" + "errors" + "maps" + "slices" + "testing" +) + +func mockTree() (Tree, map[uint64]Node) { + mock := map[uint64]Node{ + 0: &PointerNode{keys: [][]byte{{'a'}, {'j'}, {'s'}}, ptrs: []uint64{1, 2, 3}}, + 1: &PointerNode{keys: [][]byte{{'a'}, {'d'}, {'g'}}, ptrs: []uint64{4, 5, 6}}, + 2: &PointerNode{keys: [][]byte{{'j'}, {'m'}, {'p'}}, ptrs: []uint64{7, 8, 9}}, + 3: &PointerNode{keys: [][]byte{{'s'}, {'v'}, {'y'}}, ptrs: []uint64{10, 11, 12}}, + 4: &LeafNode{keys: [][]byte{{'a'}, {'b'}, {'c'}}, vals: [][]byte{{'a'}, {'b'}, {'c'}}}, + 5: &LeafNode{keys: [][]byte{{'d'}, {'e'}, {'f'}}, vals: [][]byte{{'d'}, {'e'}, {'f'}}}, + 6: &LeafNode{keys: [][]byte{{'g'}, {'h'}, {'i'}}, vals: [][]byte{{'g'}, {'h'}, {'i'}}}, + 7: &LeafNode{keys: [][]byte{{'j'}, {'k'}, {'l'}}, vals: [][]byte{{'j'}, {'k'}, {'k'}}}, + 8: &LeafNode{keys: [][]byte{{'m'}, {'n'}, {'o'}}, vals: [][]byte{{'m'}, {'n'}, {'o'}}}, + 9: &LeafNode{keys: [][]byte{{'p'}, {'q'}, {'r'}}, vals: [][]byte{{'p'}, {'q'}, {'r'}}}, + 10: &LeafNode{keys: [][]byte{{'s'}, {'t'}, {'u'}}, vals: [][]byte{{'s'}, {'t'}, {'u'}}}, + 11: &LeafNode{keys: [][]byte{{'v'}, {'w'}, {'x'}}, vals: [][]byte{{'v'}, {'w'}, {'x'}}}, + 12: &LeafNode{keys: [][]byte{{'y'}, {'z'}, {'{'}}, vals: [][]byte{{'y'}, {'z'}, {'{'}}}, + } + + next := uint64(13) + + return Tree{ + root: 0, + read: func(ptr uint64) (Node, error) { + n, ok := (mock)[ptr] + if !ok { + return nil, errors.New("mock get") + } + return n, nil + }, + alloc: func(n Node) (uint64, error) { + next++ + (mock)[next-1] = n + return next - 1, nil + }, + free: func(ptr uint64) error { + delete(mock, ptr) + return nil + }, + maxNodeSize: 39, + }, mock +} + +func TestTreeGet(t *testing.T) { + cases := []struct { + name string + k []byte + res []byte + err error + }{ + { + name: "get first k-v", + k: []byte{'a'}, + res: []byte{'a'}, + }, + { + name: "get last k-v", + k: []byte{'{'}, + res: []byte{'{'}, + }, + { + name: "get middle k-v", k: []byte{'n'}, + res: []byte{'n'}, + }, + { + name: "failed non existing key", + k: []byte{'}'}, + res: nil, + err: errKeyNotExists, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + tree, _ := mockTree() + res, err := tree.Get(c.k) + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + if !bytes.Equal(res, c.res) { + t.Errorf("expected val %v, got %v", c.res, res) + } + }) + } +} + +func TestInsert(t *testing.T) { + cases := []struct { + name string + k []byte + v []byte + res map[uint64]Node + err error + }{ + { + name: "insert without split", + k: []byte{'n', 'a'}, + v: []byte{'n', 'a'}, + res: map[uint64]Node{ + 1: &PointerNode{keys: [][]byte{{'a'}, {'d'}, {'g'}}, ptrs: []uint64{4, 5, 6}}, + 3: &PointerNode{keys: [][]byte{{'s'}, {'v'}, {'y'}}, ptrs: []uint64{10, 11, 12}}, + 4: &LeafNode{keys: [][]byte{{'a'}, {'b'}, {'c'}}, vals: [][]byte{{'a'}, {'b'}, {'c'}}}, + 5: &LeafNode{keys: [][]byte{{'d'}, {'e'}, {'f'}}, vals: [][]byte{{'d'}, {'e'}, {'f'}}}, + 6: &LeafNode{keys: [][]byte{{'g'}, {'h'}, {'i'}}, vals: [][]byte{{'g'}, {'h'}, {'i'}}}, + 7: &LeafNode{keys: [][]byte{{'j'}, {'k'}, {'l'}}, vals: [][]byte{{'j'}, {'k'}, {'k'}}}, + 9: &LeafNode{keys: [][]byte{{'p'}, {'q'}, {'r'}}, vals: [][]byte{{'p'}, {'q'}, {'r'}}}, + 10: &LeafNode{keys: [][]byte{{'s'}, {'t'}, {'u'}}, vals: [][]byte{{'s'}, {'t'}, {'u'}}}, + 11: &LeafNode{keys: [][]byte{{'v'}, {'w'}, {'x'}}, vals: [][]byte{{'v'}, {'w'}, {'x'}}}, + 12: &LeafNode{keys: [][]byte{{'y'}, {'z'}, {'{'}}, vals: [][]byte{{'y'}, {'z'}, {'{'}}}, + 13: &LeafNode{keys: [][]byte{{'m'}, {'n'}, {'n', 'a'}, {'o'}}, vals: [][]byte{{'m'}, {'n'}, {'n', 'a'}, {'o'}}}, + 14: &PointerNode{keys: [][]byte{{'j'}, {'m'}, {'p'}}, ptrs: []uint64{7, 13, 9}}, + 15: &PointerNode{keys: [][]byte{{'a'}, {'j'}, {'s'}}, ptrs: []uint64{1, 14, 3}}, + }, + }, + { + name: "insert with splits", + k: []byte{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, + v: []byte{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, + res: map[uint64]Node{ + 1: &PointerNode{keys: [][]byte{{'a'}, {'d'}, {'g'}}, ptrs: []uint64{4, 5, 6}}, + 3: &PointerNode{keys: [][]byte{{'s'}, {'v'}, {'y'}}, ptrs: []uint64{10, 11, 12}}, + 4: &LeafNode{keys: [][]byte{{'a'}, {'b'}, {'c'}}, vals: [][]byte{{'a'}, {'b'}, {'c'}}}, + 5: &LeafNode{keys: [][]byte{{'d'}, {'e'}, {'f'}}, vals: [][]byte{{'d'}, {'e'}, {'f'}}}, + 6: &LeafNode{keys: [][]byte{{'g'}, {'h'}, {'i'}}, vals: [][]byte{{'g'}, {'h'}, {'i'}}}, + 7: &LeafNode{keys: [][]byte{{'j'}, {'k'}, {'l'}}, vals: [][]byte{{'j'}, {'k'}, {'k'}}}, + 9: &LeafNode{keys: [][]byte{{'p'}, {'q'}, {'r'}}, vals: [][]byte{{'p'}, {'q'}, {'r'}}}, + 10: &LeafNode{keys: [][]byte{{'s'}, {'t'}, {'u'}}, vals: [][]byte{{'s'}, {'t'}, {'u'}}}, + 11: &LeafNode{keys: [][]byte{{'v'}, {'w'}, {'x'}}, vals: [][]byte{{'v'}, {'w'}, {'x'}}}, + 12: &LeafNode{keys: [][]byte{{'y'}, {'z'}, {'{'}}, vals: [][]byte{{'y'}, {'z'}, {'{'}}}, + 13: &LeafNode{keys: [][]byte{{'m'}, {'n'}}, vals: [][]byte{{'m'}, {'n'}}}, + 14: &LeafNode{keys: [][]byte{{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, {'o'}}, vals: [][]byte{{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, {'o'}}}, + 15: &PointerNode{keys: [][]byte{{'j'}, {'m'}}, ptrs: []uint64{7, 13}}, + 16: &PointerNode{keys: [][]byte{{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, {'p'}}, ptrs: []uint64{14, 9}}, + 17: &PointerNode{keys: [][]byte{{'a'}, {'j'}}, ptrs: []uint64{1, 15}}, + 18: &PointerNode{keys: [][]byte{{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, {'s'}}, ptrs: []uint64{16, 3}}, + 19: &PointerNode{keys: [][]byte{{'a'}, {'n', 'a', 'a', 'a', 'a', 'a', 'a'}}, ptrs: []uint64{17, 18}}, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + tree, mock := mockTree() + err := tree.Insert(c.k, c.v) + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } else if !maps.EqualFunc(c.res, mock, func(n1, n2 Node) bool { + if n1.Type() != n2.Type() { + return false + } + switch n1.Type() { + case nodeLeaf: + l1 := n1.(*LeafNode) + l2 := n2.(*LeafNode) + return slices.EqualFunc(l1.keys, l2.keys, bytes.Equal) && slices.EqualFunc(l1.keys, l2.keys, bytes.Equal) + case nodePointer: + p1 := n1.(*PointerNode) + p2 := n2.(*PointerNode) + return slices.EqualFunc(p1.keys, p2.keys, bytes.Equal) && slices.EqualFunc(p1.keys, p2.keys, bytes.Equal) + } + return false + }) { + t.Errorf("expected map %v, got %v", c.res, mock) + } + }) + } +} From d2dfd7dba0e02ca5e58d48f5ffd99019e9384d8d Mon Sep 17 00:00:00 2001 From: gKits Date: Fri, 22 Dec 2023 17:22:33 +0100 Subject: [PATCH 05/51] feat: add parse package containing lexer and parser --- internal/parse/ast.go | 14 ++++++++ internal/parse/lexer.go | 67 +++++++++++++++++++++++++++++++++++ internal/parse/lexer_test.go | 54 ++++++++++++++++++++++++++++ internal/parse/parser.go | 13 +++++++ internal/parse/parser_test.go | 1 + internal/parse/token.go | 62 ++++++++++++++++++++++++++++++++ 6 files changed, 211 insertions(+) create mode 100644 internal/parse/ast.go create mode 100644 internal/parse/lexer.go create mode 100644 internal/parse/lexer_test.go create mode 100644 internal/parse/parser.go create mode 100644 internal/parse/parser_test.go create mode 100644 internal/parse/token.go diff --git a/internal/parse/ast.go b/internal/parse/ast.go new file mode 100644 index 0000000..6714642 --- /dev/null +++ b/internal/parse/ast.go @@ -0,0 +1,14 @@ +package parse + +type AST interface{} + +type Select struct { + Fields []string + Table string +} + +type CreateTable struct { + Name string + Fields []string + Types []string +} diff --git a/internal/parse/lexer.go b/internal/parse/lexer.go new file mode 100644 index 0000000..514f5af --- /dev/null +++ b/internal/parse/lexer.go @@ -0,0 +1,67 @@ +package parse + +import ( + "fmt" + "strings" + "text/scanner" +) + +type Lexer struct { + scan *scanner.Scanner +} + +func NewLexer(text string) *Lexer { + scan := &scanner.Scanner{} + scan.Init(strings.NewReader(text)) + scan.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanComments + scan.Filename = "at" + + return &Lexer{scan: scan} +} + +func (lex *Lexer) Lex() Token { + r := lex.scan.Scan() + + tok := Token{} + text := lex.scan.TokenText() + + switch r { + case scanner.EOF: + tok.Type = tokEOF + + case scanner.Int: + tok.Type = tokInt + tok.Text = text + + case scanner.Float: + tok.Type = tokFloat + tok.Text = text + + case scanner.String, scanner.RawString: + tok.Type = tokString + tok.Text = text[1 : len(text)-1] + + case scanner.Ident: + text = strings.ToLower(text) + + var ok bool + if tok.Type, ok = keywords[text]; !ok { + tok.Type = tokIdent + } + tok.Text = text + + case scanner.Comment: + tok.Text = strings.Join(strings.Fields(text), " ") + tok.Type = tokComment + + default: + tok.Text = text + var ok bool + if tok.Type, ok = keywords[text]; !ok { + tok.Type = tokError + tok.Text = fmt.Sprintf("lex: %s: invalid token '%s'", lex.scan.Position, text) + } + + } + return tok +} diff --git a/internal/parse/lexer_test.go b/internal/parse/lexer_test.go new file mode 100644 index 0000000..9dbf764 --- /dev/null +++ b/internal/parse/lexer_test.go @@ -0,0 +1,54 @@ +package parse + +import "testing" + +func TestLex(t *testing.T) { + q := ` + /* + Multiline + comment + */ + + select test From test + WHERE test = "foo bar" and + // Inline comment + + baz > 1 or baz < 2.0; + ` + + lex := NewLexer(q) + + expected := []Token{ + {Type: tokComment, Text: "/* Multiline comment */"}, + {Type: tokSelect, Text: "select"}, + {Type: tokIdent, Text: "test"}, + {Type: tokFrom, Text: "from"}, + {Type: tokIdent, Text: "test"}, + {Type: tokWhere, Text: "where"}, + {Type: tokIdent, Text: "test"}, + {Type: tokEqual, Text: "="}, + {Type: tokString, Text: "foo bar"}, + {Type: tokAnd, Text: "and"}, + {Type: tokComment, Text: "// Inline comment"}, + {Type: tokIdent, Text: "baz"}, + {Type: tokGreater, Text: ">"}, + {Type: tokInt, Text: "1"}, + {Type: tokOr, Text: "or"}, + {Type: tokIdent, Text: "baz"}, + {Type: tokLess, Text: "<"}, + {Type: tokFloat, Text: "2.0"}, + {Type: tokSemicol, Text: ";"}, + } + + i := 0 + for tok := lex.Lex(); tok.Type != tokEOF; tok = lex.Lex() { + if i >= len(expected) { + t.Fatalf("expected only %d tokens, got more", len(expected)) + } else if tok.Type != expected[i].Type { + t.Fatalf("expected type %v tokens, got %v: %s", expected[i].Type, tok.Type, tok.Text) + } else if tok.Text != expected[i].Text { + t.Fatalf("expected text %v tokens, got %v", expected[i].Text, tok.Text) + } + i++ + } +} diff --git a/internal/parse/parser.go b/internal/parse/parser.go new file mode 100644 index 0000000..6b0c883 --- /dev/null +++ b/internal/parse/parser.go @@ -0,0 +1,13 @@ +package parse + +type Parser struct { + lex *Lexer +} + +func NewParser(query string) *Parser { + return &Parser{lex: NewLexer(query)} +} + +func (p *Parser) Parse() (AST, error) { + return nil, nil +} diff --git a/internal/parse/parser_test.go b/internal/parse/parser_test.go new file mode 100644 index 0000000..fe2554d --- /dev/null +++ b/internal/parse/parser_test.go @@ -0,0 +1 @@ +package parse diff --git a/internal/parse/token.go b/internal/parse/token.go new file mode 100644 index 0000000..157a8d9 --- /dev/null +++ b/internal/parse/token.go @@ -0,0 +1,62 @@ +package parse + +type TokenType uint + +const ( + tokError TokenType = iota + tokEOF + tokComment + tokNumber + tokInt + tokFloat + tokString + tokIdent + tokSelect + tokAsterisk + tokLeftPar + tokRightPar + tokSemicol + tokComma + tokEqual + tokGreater + tokLess + tokGreaterEq + tokLessEq + tokNotEq + tokFrom + tokWhere + tokCreate + tokTable + tokNot + tokAnd + tokOr +) + +var keywords = map[string]TokenType{ + "select": tokSelect, + "from": tokFrom, + "where": tokWhere, + "create": tokCreate, + "table": tokTable, + ",": tokComma, + "*": tokAsterisk, + ";": tokSemicol, + "(": tokLeftPar, + ")": tokRightPar, + "=": tokEqual, + ">": tokGreater, + "<": tokLess, + ">=": tokGreaterEq, + "<=": tokLessEq, + "!=": tokNotEq, + "not": tokNot, + "and": tokAnd, + "or": tokOr, +} + +type Token struct { + Type TokenType + Text string + Line int + Column int +} From 36eb426e05ecf36b47b0e90c11d0caf230bf1ef4 Mon Sep 17 00:00:00 2001 From: gKits Date: Sat, 23 Dec 2023 00:01:31 +0100 Subject: [PATCH 06/51] chore: add docs.go in project root --- docs.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs.go diff --git a/docs.go b/docs.go new file mode 100644 index 0000000..a96caeb --- /dev/null +++ b/docs.go @@ -0,0 +1,25 @@ +package pavosql + +/* +MIT License + +Copyright (c) 2023 Georgios Kitsikoudis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ From 0f0b7926a3fecbe09d4f9528460ece2d09f02e91 Mon Sep 17 00:00:00 2001 From: gKits Date: Wed, 3 Jan 2024 11:09:28 +0100 Subject: [PATCH 07/51] docs: change docs framework from hugo to docsify for simplicity --- .github/workflows/hugo.yaml | 72 ------------- Makefile | 5 + docs/.nojekyll | 0 docs/_coverpage.md | 11 ++ docs/_media/PavoSQL.svg | 95 ++++++++++++++++++ docs/_sidebar.md | 8 ++ docs/archetypes/default.md | 6 -- docs/assets/_custom.css | 2 - docs/content/_index.md | 3 - docs/content/docs/docs/_index.md | 8 -- docs/content/docs/quickstart/_index.md | 6 -- docs/content/docs/quickstart/installation.md | 14 --- docs/content/docs/quickstart/whatis.md | 11 -- docs/content/docs/syntax/_index.md | 7 -- docs/go.mod | 5 - docs/go.sum | 2 - docs/hugo.toml | 34 ------- docs/index.html | 32 ++++++ docs/quickstart.md | 1 + ...s_e129fe35b8d0a70789c8a08429469073.content | 1 - ...scss_e129fe35b8d0a70789c8a08429469073.json | 1 - docs/static/PavoSQL.png | Bin 2340 -> 0 bytes docs/static/android-chrome-192x192.png | Bin 21276 -> 0 bytes docs/static/android-chrome-512x512.png | Bin 91795 -> 0 bytes docs/static/apple-touch-icon.png | Bin 18749 -> 0 bytes docs/static/favicon-16x16.png | Bin 611 -> 0 bytes docs/static/favicon-32x32.png | Bin 1464 -> 0 bytes docs/static/favicon.ico | Bin 15406 -> 0 bytes docs/static/site.webmanifest | 1 - 29 files changed, 152 insertions(+), 173 deletions(-) delete mode 100644 .github/workflows/hugo.yaml create mode 100644 docs/.nojekyll create mode 100644 docs/_coverpage.md create mode 100644 docs/_media/PavoSQL.svg create mode 100644 docs/_sidebar.md delete mode 100644 docs/archetypes/default.md delete mode 100644 docs/assets/_custom.css delete mode 100644 docs/content/_index.md delete mode 100644 docs/content/docs/docs/_index.md delete mode 100644 docs/content/docs/quickstart/_index.md delete mode 100644 docs/content/docs/quickstart/installation.md delete mode 100644 docs/content/docs/quickstart/whatis.md delete mode 100644 docs/content/docs/syntax/_index.md delete mode 100644 docs/go.mod delete mode 100644 docs/go.sum delete mode 100644 docs/hugo.toml create mode 100644 docs/index.html create mode 100644 docs/quickstart.md delete mode 100644 docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content delete mode 100644 docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json delete mode 100644 docs/static/PavoSQL.png delete mode 100644 docs/static/android-chrome-192x192.png delete mode 100644 docs/static/android-chrome-512x512.png delete mode 100644 docs/static/apple-touch-icon.png delete mode 100644 docs/static/favicon-16x16.png delete mode 100644 docs/static/favicon-32x32.png delete mode 100644 docs/static/favicon.ico delete mode 100644 docs/static/site.webmanifest diff --git a/.github/workflows/hugo.yaml b/.github/workflows/hugo.yaml deleted file mode 100644 index f37a325..0000000 --- a/.github/workflows/hugo.yaml +++ /dev/null @@ -1,72 +0,0 @@ -name: Build docs with hugo - -on: - push: - paths: - - docs/* - branches: - - main - workflow_dispatch: - - -permissions: - contents: read - pages: write - id-token: write - -concurrency: - group: "pages" - cancel-in-progress: false - -defaults: - run: - shell: bash - -jobs: - build: - runs-on: ubuntu-latest - env: - HUGO_VERSION: 0.115.4 - steps: - - name: Install Hugo CLI - run: | - wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \ - && sudo dpkg -i ${{ runner.temp }}/hugo.deb - - name: Install Dart Sass - run: sudo snap install dart-sass - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: recursive - fetch-depth: 0 - - name: Setup Pages - id: pages - uses: actions/configure-pages@v3 - - name: Install Node.js dependencies - run: "[[ -f package-lock.json || -f npm-shrinkwrap.json ]] && npm ci || true" - - name: Build with Hugo - env: - HUGO_ENVIRONMENT: production - HUGO_ENV: production - run: | - hugo \ - -s docs\ - -d ../public\ - --gc \ - --minify \ - --baseURL "${{ steps.pages.outputs.base_url }}/" - - name: Upload artifact - uses: actions/upload-pages-artifact@v1 - with: - path: ./public - - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v2 diff --git a/Makefile b/Makefile index f2c7d8d..6fe3434 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,11 @@ build: run: @go run cmd/pavosql/main.go +# Docs + +docs: + @python -m http.server 3000 -d docs + # Testing test: diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docs/_coverpage.md b/docs/_coverpage.md new file mode 100644 index 0000000..461d4f9 --- /dev/null +++ b/docs/_coverpage.md @@ -0,0 +1,11 @@ + +logo + +# PavoSQL + +> A simple SQL Database written in pure Go. + +[Getting started](#pavosql) +[Github](https://github.com/gKits/PavoSQL) + +![color](#000064ff) diff --git a/docs/_media/PavoSQL.svg b/docs/_media/PavoSQL.svg new file mode 100644 index 0000000..e386710 --- /dev/null +++ b/docs/_media/PavoSQL.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_sidebar.md b/docs/_sidebar.md new file mode 100644 index 0000000..9c86b26 --- /dev/null +++ b/docs/_sidebar.md @@ -0,0 +1,8 @@ +- [Home](#pavosql) + +- Getting started + - [Quickstart](quickstart.md) + +- Syntax + +- Docs diff --git a/docs/archetypes/default.md b/docs/archetypes/default.md deleted file mode 100644 index 00e77bd..0000000 --- a/docs/archetypes/default.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "{{ replace .Name "-" " " | title }}" -date: {{ .Date }} -draft: true ---- - diff --git a/docs/assets/_custom.css b/docs/assets/_custom.css deleted file mode 100644 index 4f829af..0000000 --- a/docs/assets/_custom.css +++ /dev/null @@ -1,2 +0,0 @@ -@import "plugins/_numberd.sccs" -@import "plugins/_scrollbar.sccs" diff --git a/docs/content/_index.md b/docs/content/_index.md deleted file mode 100644 index a01f933..0000000 --- a/docs/content/_index.md +++ /dev/null @@ -1,3 +0,0 @@ -+++ -+++ -# PavoSQL diff --git a/docs/content/docs/docs/_index.md b/docs/content/docs/docs/_index.md deleted file mode 100644 index 4582178..0000000 --- a/docs/content/docs/docs/_index.md +++ /dev/null @@ -1,8 +0,0 @@ -+++ -Title = "Docs" -Weight = 2 -bookFlatSection = true -draft = true -+++ - -# Documentation diff --git a/docs/content/docs/quickstart/_index.md b/docs/content/docs/quickstart/_index.md deleted file mode 100644 index 728502b..0000000 --- a/docs/content/docs/quickstart/_index.md +++ /dev/null @@ -1,6 +0,0 @@ -+++ -Title = "Getting started" -Weight = 1 -bookCollapseSection = true -draft = true -+++ diff --git a/docs/content/docs/quickstart/installation.md b/docs/content/docs/quickstart/installation.md deleted file mode 100644 index 017260a..0000000 --- a/docs/content/docs/quickstart/installation.md +++ /dev/null @@ -1,14 +0,0 @@ -+++ -Title = "Installation" -+++ - -# Installation - -{{< tabs "installos" >}} -{{< tab "Linux" >}} -## Linux -{{< /tab >}} -{{< tab "Windows" >}} -## Windows -{{< /tab >}} -{{< /tabs >}} diff --git a/docs/content/docs/quickstart/whatis.md b/docs/content/docs/quickstart/whatis.md deleted file mode 100644 index 807e201..0000000 --- a/docs/content/docs/quickstart/whatis.md +++ /dev/null @@ -1,11 +0,0 @@ -+++ -Title = "What is PavoSQL?" -Weight = 1 -bookToc = false -+++ -# PavoSQL - -[PavoSQLLogo](/PavoSQL.png) - -PavoSQL (or Pavo for short) is a simple Database engine and management system -written completely in vanilla Go. diff --git a/docs/content/docs/syntax/_index.md b/docs/content/docs/syntax/_index.md deleted file mode 100644 index 690cd42..0000000 --- a/docs/content/docs/syntax/_index.md +++ /dev/null @@ -1,7 +0,0 @@ -+++ -Title = "SQL Syntax" -bookFlatSection = true -draft = true -+++ - -# Test diff --git a/docs/go.mod b/docs/go.mod deleted file mode 100644 index 985f690..0000000 --- a/docs/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/gKits/PavoSQL/docs - -go 1.21 - -require github.com/alex-shpak/hugo-book v0.0.0-20230808113920-3f1bcccbfb24 // indirect diff --git a/docs/go.sum b/docs/go.sum deleted file mode 100644 index 6801581..0000000 --- a/docs/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/alex-shpak/hugo-book v0.0.0-20230808113920-3f1bcccbfb24 h1:8NjMYBSFTtBLeT1VmpZAZznPOt1OH8aNCnE86sL4p4k= -github.com/alex-shpak/hugo-book v0.0.0-20230808113920-3f1bcccbfb24/go.mod h1:L4NMyzbn15fpLIpmmtDg9ZFFyTZzw87/lk7M2bMQ7ds= diff --git a/docs/hugo.toml b/docs/hugo.toml deleted file mode 100644 index 05778e0..0000000 --- a/docs/hugo.toml +++ /dev/null @@ -1,34 +0,0 @@ -baseURL = "https://gkits.github.io/PavoSQL" -languageCode = "en-us" -title = "PavoSQL" - -enableGitInfo = true -disablePathToLower = true - -[markup] -[markup.goldmark.renderer] -unsafe = true -[markup.tableOfContents] -startLevel = 1 - -[menu] -[[menu.after]] -name = "Github" -url = "https://github.com/gKits/PavoSQL" -weight = 10 - -[module] -[[module.imports]] -path = "github.com/alex-shpak/hugo-book" - - -[params] -favicon = "favicon.ico" -BookTheme = "dark" -BookToc = true -BookSection = "docs" -BookLogo = "PavoSQL.png" -BookRepo = "github.com/gKits/PavoSQL" -BookCommitPath = "commit" -[params.meta] -favicon = false diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..96b58da --- /dev/null +++ b/docs/index.html @@ -0,0 +1,32 @@ + + + + + + + + + + + + + +
+

Loading...

+
+ + + + + + diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 0000000..acb9843 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1 @@ +# Quickstart diff --git a/docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content b/docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content deleted file mode 100644 index 33ac19a..0000000 --- a/docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.content +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";:root{--gray-100:rgba(255, 255, 255, 0.1);--gray-200:rgba(255, 255, 255, 0.2);--gray-500:rgba(255, 255, 255, 0.5);--color-link:#84b2ff;--color-visited-link:#b88dff;--body-background:#343a40;--body-font-color:#e9ecef;--icon-filter:brightness(0) invert(1);--hint-color-info:#6bf;--hint-color-warning:#fd6;--hint-color-danger:#f66}/*!normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css*/html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}.flex{display:flex}.flex-auto{flex:auto}.flex-even{flex:1 1}.flex-wrap{flex-wrap:wrap}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.align-center{align-items:center}.mx-auto{margin:0 auto}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.hidden{display:none}input.toggle{height:0;width:0;overflow:hidden;opacity:0;position:absolute}.clearfix::after{content:"";display:table;clear:both}html{font-size:16px;scroll-behavior:smooth;touch-action:manipulation}body{min-width:20rem;color:var(--body-font-color);background:var(--body-background);letter-spacing:.33px;font-weight:400;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;box-sizing:border-box}body *{box-sizing:inherit}h1,h2,h3,h4,h5{font-weight:400}a{text-decoration:none;color:var(--color-link)}img{vertical-align:baseline}:focus{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}aside nav ul{padding:0;margin:0;list-style:none}aside nav ul li{margin:1em 0;position:relative}aside nav ul a{display:block}aside nav ul a:hover{opacity:.5}aside nav ul ul{padding-inline-start:1rem}ul.pagination{display:flex;justify-content:center;list-style-type:none;padding-inline-start:0}ul.pagination .page-item a{padding:1rem}.container{max-width:80rem;margin:0 auto}.book-icon{filter:var(--icon-filter)}.book-brand{margin-top:0;margin-bottom:1rem}.book-brand img{height:1.5em;width:1.5em;margin-inline-end:.5rem}.book-menu{flex:0 0 16rem;font-size:.875rem}.book-menu .book-menu-content{width:16rem;padding:1rem;background:var(--body-background);position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-menu a,.book-menu label{color:inherit;cursor:pointer;word-wrap:break-word}.book-menu a.active{color:var(--color-link)}.book-menu input.toggle+label+ul{display:none}.book-menu input.toggle:checked+label+ul{display:block}.book-menu input.toggle+label::after{content:"â–¸"}.book-menu input.toggle:checked+label::after{content:"â–¾"}body[dir=rtl] .book-menu input.toggle+label::after{content:"â—‚"}body[dir=rtl] .book-menu input.toggle:checked+label::after{content:"â–¾"}.book-section-flat{margin:2rem 0}.book-section-flat>a,.book-section-flat>span,.book-section-flat>label{font-weight:bolder}.book-section-flat>ul{padding-inline-start:0}.book-page{min-width:20rem;flex-grow:1;padding:1rem}.book-post{margin-bottom:3rem}.book-header{display:none;margin-bottom:1rem}.book-header label{line-height:0}.book-header img.book-icon{height:1.5em;width:1.5em}.book-search{position:relative;margin:1rem 0;border-bottom:1px solid transparent}.book-search input{width:100%;padding:.5rem;border:0;border-radius:.25rem;background:var(--gray-100);color:var(--body-font-color)}.book-search input:required+.book-search-spinner{display:block}.book-search .book-search-spinner{position:absolute;top:0;margin:.5rem;margin-inline-start:calc(100% - 1.5rem);width:1rem;height:1rem;border:1px solid transparent;border-top-color:var(--body-font-color);border-radius:50%;animation:spin 1s ease infinite}@keyframes spin{100%{transform:rotate(360deg)}}.book-search small{opacity:.5}.book-toc{flex:0 0 16rem;font-size:.75rem}.book-toc .book-toc-content{width:16rem;padding:1rem;position:fixed;top:0;bottom:0;overflow-x:hidden;overflow-y:auto}.book-toc img{height:1em;width:1em}.book-toc nav>ul>li:first-child{margin-top:0}.book-footer{padding-top:1rem;font-size:.875rem}.book-footer img{height:1em;width:1em;margin-inline-end:.5rem}.book-comments{margin-top:1rem}.book-languages{margin-block-end:2rem}.book-languages .book-icon{height:1em;width:1em;margin-inline-end:.5em}.book-languages ul{padding-inline-start:1.5em}.book-menu-content,.book-toc-content,.book-page,.book-header aside,.markdown{transition:.2s ease-in-out;transition-property:transform,margin,opacity,visibility;will-change:transform,margin,opacity}@media screen and (max-width:56rem){#menu-control,#toc-control{display:inline}.book-menu{visibility:hidden;margin-inline-start:-16rem;font-size:16px;z-index:1}.book-toc{display:none}.book-header{display:block}#menu-control:focus~main label[for=menu-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#menu-control:checked~main .book-menu{visibility:initial}#menu-control:checked~main .book-menu .book-menu-content{transform:translateX(16rem);box-shadow:0 0 .5rem rgba(0,0,0,.1)}#menu-control:checked~main .book-page{opacity:.25}#menu-control:checked~main .book-menu-overlay{display:block;position:absolute;top:0;bottom:0;left:0;right:0}#toc-control:focus~main label[for=toc-control]{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}#toc-control:checked~main .book-header aside{display:block}body[dir=rtl] #menu-control:checked~main .book-menu .book-menu-content{transform:translateX(-16rem)}}@media screen and (min-width:80rem){.book-page,.book-menu .book-menu-content,.book-toc .book-toc-content{padding:2rem 1rem}}@font-face{font-family:roboto;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-regular.woff2)format("woff2"),url(fonts/roboto-v27-latin-regular.woff)format("woff")}@font-face{font-family:roboto;font-style:normal;font-weight:700;font-display:swap;src:local(""),url(fonts/roboto-v27-latin-700.woff2)format("woff2"),url(fonts/roboto-v27-latin-700.woff)format("woff")}@font-face{font-family:roboto mono;font-style:normal;font-weight:400;font-display:swap;src:local(""),url(fonts/roboto-mono-v13-latin-regular.woff2)format("woff2"),url(fonts/roboto-mono-v13-latin-regular.woff)format("woff")}body{font-family:roboto,sans-serif}code{font-family:roboto mono,monospace}@media print{.book-menu,.book-footer,.book-toc{display:none}.book-header,.book-header aside{display:block}main{display:block!important}}.markdown{line-height:1.6}.markdown>:first-child{margin-top:0}.markdown h1,.markdown h2,.markdown h3,.markdown h4,.markdown h5,.markdown h6{font-weight:400;line-height:1;margin-top:1.5em;margin-bottom:1rem}.markdown h1 a.anchor,.markdown h2 a.anchor,.markdown h3 a.anchor,.markdown h4 a.anchor,.markdown h5 a.anchor,.markdown h6 a.anchor{opacity:0;font-size:.75em;vertical-align:middle;text-decoration:none}.markdown h1:hover a.anchor,.markdown h1 a.anchor:focus,.markdown h2:hover a.anchor,.markdown h2 a.anchor:focus,.markdown h3:hover a.anchor,.markdown h3 a.anchor:focus,.markdown h4:hover a.anchor,.markdown h4 a.anchor:focus,.markdown h5:hover a.anchor,.markdown h5 a.anchor:focus,.markdown h6:hover a.anchor,.markdown h6 a.anchor:focus{opacity:initial}.markdown h4,.markdown h5,.markdown h6{font-weight:bolder}.markdown h5{font-size:.875em}.markdown h6{font-size:.75em}.markdown b,.markdown optgroup,.markdown strong{font-weight:bolder}.markdown a{text-decoration:none}.markdown a:hover{text-decoration:underline}.markdown a:visited{color:var(--color-visited-link)}.markdown img{max-width:100%;height:auto}.markdown code{padding:0 .25rem;background:var(--gray-200);border-radius:.25rem;font-size:.875em}.markdown pre{padding:1rem;background:var(--gray-100);border-radius:.25rem;overflow-x:auto}.markdown pre code{padding:0;background:0 0}.markdown p{word-wrap:break-word}.markdown blockquote{margin:1rem 0;padding:.5rem 1rem .5rem .75rem;border-inline-start:.25rem solid var(--gray-200);border-radius:.25rem}.markdown blockquote :first-child{margin-top:0}.markdown blockquote :last-child{margin-bottom:0}.markdown table{overflow:auto;display:block;border-spacing:0;border-collapse:collapse;margin-top:1rem;margin-bottom:1rem}.markdown table tr th,.markdown table tr td{padding:.5rem 1rem;border:1px solid var(--gray-200)}.markdown table tr:nth-child(2n){background:var(--gray-100)}.markdown hr{height:1px;border:none;background:var(--gray-200)}.markdown ul,.markdown ol{padding-inline-start:2rem;word-wrap:break-word}.markdown dl dt{font-weight:bolder;margin-top:1rem}.markdown dl dd{margin-inline-start:0;margin-bottom:1rem}.markdown .highlight table tr td:nth-child(1) pre{margin:0;padding-inline-end:0}.markdown .highlight table tr td:nth-child(2) pre{margin:0;padding-inline-start:0}.markdown details{padding:1rem;border:1px solid var(--gray-200);border-radius:.25rem}.markdown details summary{line-height:1;padding:1rem;margin:-1rem;cursor:pointer}.markdown details[open] summary{margin-bottom:0}.markdown figure{margin:1rem 0}.markdown figure figcaption p{margin-top:0}.markdown-inner>:first-child{margin-top:0}.markdown-inner>:last-child{margin-bottom:0}.markdown .book-expand{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden}.markdown .book-expand .book-expand-head{background:var(--gray-100);padding:.5rem 1rem;cursor:pointer}.markdown .book-expand .book-expand-content{display:none;padding:1rem}.markdown .book-expand input[type=checkbox]:checked+.book-expand-content{display:block}.markdown .book-tabs{margin-top:1rem;margin-bottom:1rem;border:1px solid var(--gray-200);border-radius:.25rem;overflow:hidden;display:flex;flex-wrap:wrap}.markdown .book-tabs label{display:inline-block;padding:.5rem 1rem;border-bottom:1px transparent;cursor:pointer}.markdown .book-tabs .book-tabs-content{order:999;width:100%;border-top:1px solid var(--gray-100);padding:1rem;display:none}.markdown .book-tabs input[type=radio]:checked+label{border-bottom:1px solid var(--color-link)}.markdown .book-tabs input[type=radio]:checked+label+.book-tabs-content{display:block}.markdown .book-tabs input[type=radio]:focus+label{outline-style:auto;outline-color:currentColor;outline-color:-webkit-focus-ring-color}.markdown .book-columns{margin-left:-1rem;margin-right:-1rem}.markdown .book-columns>div{margin:1rem 0;min-width:10rem;padding:0 1rem}.markdown a.book-btn{display:inline-block;font-size:.875rem;color:var(--color-link);line-height:2rem;padding:0 1rem;border:1px solid var(--color-link);border-radius:.25rem;cursor:pointer}.markdown a.book-btn:hover{text-decoration:none}.markdown .book-hint.info{border-color:#6bf;background-color:rgba(102,187,255,.1)}.markdown .book-hint.warning{border-color:#fd6;background-color:rgba(255,221,102,.1)}.markdown .book-hint.danger{border-color:#f66;background-color:rgba(255,102,102,.1)} \ No newline at end of file diff --git a/docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json b/docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json deleted file mode 100644 index 8b302b4..0000000 --- a/docs/resources/_gen/assets/scss/book.scss_e129fe35b8d0a70789c8a08429469073.json +++ /dev/null @@ -1 +0,0 @@ -{"Target":"book.min.4f0117e74e5337280f18eb9641eae520cb4b25adcf5dd64fafad4664145a5957.css","MediaType":"text/css","Data":{"Integrity":"sha256-TwEX505TNygPGOuWQerlIMtLJa3PXdZPr61GZBRaWVc="}} \ No newline at end of file diff --git a/docs/static/PavoSQL.png b/docs/static/PavoSQL.png deleted file mode 100644 index b1ad75ab990e2c4e6dd0a64db5d609f966257eb2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2340 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 X*|>|`NQm^P00000NkvXXu0mjfV3(_w diff --git a/docs/static/android-chrome-512x512.png b/docs/static/android-chrome-512x512.png deleted file mode 100644 index 5d8a368afebef10d27e39bbaeeeb7c3327006925..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91795 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 diff --git a/docs/static/favicon-32x32.png b/docs/static/favicon-32x32.png deleted file mode 100644 index 62b6fb270ae2f460d25b0048cc9830c30f24454a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1464 zcmV;p1xNacP)wA9|i;> zYKR(43#PsLaWkTCw*K?4?(4+8SSN7I)H`WKMbvRGsXei zyckM6CIW^iKGifgWX7#`!<_{1E%J?LV7N9wo6G4qhqeq3GCb%-)LW13#Xl#FWB}0r zGAFi-6Pe5^qi>Hb^pR0uWEeR64V`;{ykelJoV{QKSxQia(uJxeT_44kiha%i7JRn} zO0nBgO#Dvq(UA@Bxl=&*As{DYh4%9Az><|f)htHMF}b8rt;K8Q?@hqK>BmUju0(;c z<_Umu0&v+3N5eqhF<@vAaFcPfP!mw#IG>m zwgJ#4@FQ9Hnzlx-0J%(#`S$?T1YtJ%O!?^d**Ae@ETO$*zVnYMFjPlzQ(( zk+&(KUTaDLRUS+#o$q0?`QdgT!vAS>Sa}=p%LO3LDqMHJ6_znz?6S;onPjkwIy%P9 zb;4zQjMn1I96sttZsA)mA5 zT_|P0z@qj^2X{)KtDgdj3oYmNY)M&6f6_Q20QS(}BWVC+y6j>qjr-_Locj4w%z`8i zNCqM-L~U;ZQO14IQuak0b2_Ke{7WcyDFlzNp}`&F^4>mIVlSbPDkvaC2U!FZ;ETaf z?%F~i%!tAwV9h#?Wlgka``DkXijB0=nU9cXKWV-@BMY)b^dRvc6IOx+0d|Q+sN)?V z#=D5be^mo`iaMIHd8dF>2mXjOTPg{5Enu3o@rlLU;i4L(gIfv0OUHTCK>?Q@KLDIM znX)^3F7QA?jLtj=_1$#Sh}M#UgiLwQ3w1B!M_xEk=?FpuCLpr8>?*6^31&f@yH7R= z!@3P*Nd%Iy@sLuTT(lmo*0F5Q6NRkfb11yc{Jzjp6Raoj4zTEaPCI|Q1pBF#jn7ga zX*%Gp)d}3AinnR^vNP=DYaJlQ~bvgUIv0Jc?crdb$riY~p zB*Q0yic$Snu(<9LYVV4%obzh#B28uw?+S-;ad-e*d9BOPCMZ?O**qtZ??W&eKvd__ zrXP{sTPT7cosim?P&u1b9RQs4bYs5%58yA$l6%D0 SdV@Xy0000^?PBZP1+%Fphq+SzC5 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 Date: Thu, 4 Jan 2024 13:19:29 +0100 Subject: [PATCH 08/51] feat: implement iterator type and stack package for b+tree iteration --- internal/tree/iterator.go | 99 ++++++++++++++++++++++++++++++++++ internal/tree/iterator_test.go | 78 +++++++++++++++++++++++++++ internal/tree/tree.go | 2 - internal/tree/tree_test.go | 2 +- pkg/stack/stack.go | 31 +++++++++++ pkg/stack/stack_test.go | 68 +++++++++++++++++++++++ 6 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 internal/tree/iterator.go create mode 100644 internal/tree/iterator_test.go create mode 100644 pkg/stack/stack.go create mode 100644 pkg/stack/stack_test.go diff --git a/internal/tree/iterator.go b/internal/tree/iterator.go new file mode 100644 index 0000000..b424ac1 --- /dev/null +++ b/internal/tree/iterator.go @@ -0,0 +1,99 @@ +package tree + +import ( + "errors" + "fmt" + + "github.com/gKits/PavoSQL/pkg/stack" +) + +type Iterator struct { + t *Tree + stk stack.Stack[Node] + idxStk stack.Stack[int] +} + +var EndOfTree = errors.New("end of tree") + +func NewIterator(t *Tree, k []byte) *Iterator { + return &Iterator{ + t: t, + } +} + +func (it *Iterator) Next() ([]byte, []byte, error) { + for { + var ( + cur Node + err error + ) + + idx, err := it.idxStk.Pop() + if err != nil { + return nil, nil, EndOfTree + } + + cur, err = it.stk.Peek() + if err != nil { + return nil, nil, err + } + + fmt.Println(cur, it.idxStk, idx) + + switch cur.Type() { + case nodeLeaf: + if idx == -1 { + idx++ + } + + leaf := cur.(*LeafNode) + k, err := leaf.Key(idx) + if err != nil { + return nil, nil, err + } + + v, err := leaf.ValAt(idx) + if err != nil { + return nil, nil, err + } + + if idx+1 >= cur.NKeys() { + _, err := it.stk.Pop() + if err != nil { + return nil, nil, err + } + } else { + it.idxStk.Push(idx + 1) + } + + return k, v, nil + + case nodePointer: + if idx+1 >= cur.NKeys() { + it.stk.Pop() + continue + } else { + idx++ + it.idxStk.Push(idx) + } + + pointer := cur.(*PointerNode) + + ptr, err := pointer.PtrAt(idx) + if err != nil { + return nil, nil, err + } + + next, err := it.t.read(ptr) + if err != nil { + return nil, nil, err + } + + it.stk.Push(next) + it.idxStk.Push(-1) + + default: + return nil, nil, errInvalNodeType + } + } +} diff --git a/internal/tree/iterator_test.go b/internal/tree/iterator_test.go new file mode 100644 index 0000000..4ae54e9 --- /dev/null +++ b/internal/tree/iterator_test.go @@ -0,0 +1,78 @@ +package tree + +import ( + "bytes" + "testing" + + "github.com/gKits/PavoSQL/pkg/stack" +) + +func TestIterator(t *testing.T) { + tree, m := mockTree() + + cases := []struct { + name string + it Iterator + num int + res [][]byte + err error + }{ + { + name: "succefully iterate over one adjacent nodes", + it: Iterator{ + stk: stack.Stack[Node]{m[0], m[1], m[4]}, + idxStk: stack.Stack[int]{0, 0, 0}, + t: &tree, + }, + num: 5, + res: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}}, + }, + { + name: "succefully iterate multiple adjacent nodes", + it: Iterator{ + stk: stack.Stack[Node]{m[0], m[1], m[4]}, + idxStk: stack.Stack[int]{0, 0, 0}, + t: &tree, + }, + num: 7, + res: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, {'g'}}, + }, + { + name: "succefully iterate multiple levels", + it: Iterator{ + stk: stack.Stack[Node]{m[0], m[1], m[4]}, + idxStk: stack.Stack[int]{0, 0, 0}, + t: &tree, + }, + num: 10, + res: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, {'g'}, {'h'}, {'i'}, {'j'}}, + }, + { + name: "succefully iterate over whole tree", + it: Iterator{ + stk: stack.Stack[Node]{m[0], m[1], m[4]}, + idxStk: stack.Stack[int]{0, 0, 0}, + t: &tree, + }, + num: 30, + res: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, {'g'}, {'h'}, {'i'}, {'j'}, {'k'}, {'l'}, {'m'}, {'n'}, {'o'}, {'p'}, {'q'}, {'r'}, {'s'}, {'t'}, {'u'}, {'v'}, {'w'}, {'x'}, {'y'}, {'z'}, {'{'}}, + err: EndOfTree, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + for i := 0; i < c.num; i++ { + k, v, err := c.it.Next() + if err == nil { + if !bytes.Equal(k, c.res[i]) || !bytes.Equal(v, c.res[i]) { + t.Log(c.it) + t.Errorf("expected k-v %v, got %v, %v", c.res[i], k, v) + } + } else if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } + } + }) + } +} diff --git a/internal/tree/tree.go b/internal/tree/tree.go index 471d69b..dccfa41 100644 --- a/internal/tree/tree.go +++ b/internal/tree/tree.go @@ -153,8 +153,6 @@ func (t *Tree) recursiveInsert(ptr uint64, k, v []byte) ([]recurseResult, error) return nil, errInvalNodeType } - println(cur.Type()) - println(cur.Size()) if cur.Size() > t.maxNodeSize { l, r := cur.Split() diff --git a/internal/tree/tree_test.go b/internal/tree/tree_test.go index 35db720..ae1da0e 100644 --- a/internal/tree/tree_test.go +++ b/internal/tree/tree_test.go @@ -17,7 +17,7 @@ func mockTree() (Tree, map[uint64]Node) { 4: &LeafNode{keys: [][]byte{{'a'}, {'b'}, {'c'}}, vals: [][]byte{{'a'}, {'b'}, {'c'}}}, 5: &LeafNode{keys: [][]byte{{'d'}, {'e'}, {'f'}}, vals: [][]byte{{'d'}, {'e'}, {'f'}}}, 6: &LeafNode{keys: [][]byte{{'g'}, {'h'}, {'i'}}, vals: [][]byte{{'g'}, {'h'}, {'i'}}}, - 7: &LeafNode{keys: [][]byte{{'j'}, {'k'}, {'l'}}, vals: [][]byte{{'j'}, {'k'}, {'k'}}}, + 7: &LeafNode{keys: [][]byte{{'j'}, {'k'}, {'l'}}, vals: [][]byte{{'j'}, {'k'}, {'l'}}}, 8: &LeafNode{keys: [][]byte{{'m'}, {'n'}, {'o'}}, vals: [][]byte{{'m'}, {'n'}, {'o'}}}, 9: &LeafNode{keys: [][]byte{{'p'}, {'q'}, {'r'}}, vals: [][]byte{{'p'}, {'q'}, {'r'}}}, 10: &LeafNode{keys: [][]byte{{'s'}, {'t'}, {'u'}}, vals: [][]byte{{'s'}, {'t'}, {'u'}}}, diff --git a/pkg/stack/stack.go b/pkg/stack/stack.go new file mode 100644 index 0000000..a682af9 --- /dev/null +++ b/pkg/stack/stack.go @@ -0,0 +1,31 @@ +package stack + +import "errors" + +type Stack[T any] []T + +var ErrStackEmpty = errors.New("cannot pop, stack is empty") + +func (s Stack[T]) Len() int { + return len(s) +} + +func (s *Stack[T]) Push(t T) { + *s = append(*s, t) +} + +func (s *Stack[T]) Pop() (res T, err error) { + if s.Len() < 1 { + return res, ErrStackEmpty + } + res = (*s)[s.Len()-1] + *s = (*s)[:s.Len()-1] + return res, nil +} + +func (s Stack[T]) Peek() (res T, err error) { + if s.Len() < 1 { + return res, ErrStackEmpty + } + return s[s.Len()-1], nil +} diff --git a/pkg/stack/stack_test.go b/pkg/stack/stack_test.go new file mode 100644 index 0000000..8ad6719 --- /dev/null +++ b/pkg/stack/stack_test.go @@ -0,0 +1,68 @@ +package stack + +import ( + "slices" + "testing" +) + +func TestPush(t *testing.T) { + cases := []struct { + name string + s Stack[int] + in int + res Stack[int] + }{ + { + name: "successful push", + s: Stack[int]{0, 1, 2, 3}, + in: 4, + res: Stack[int]{0, 1, 2, 3, 4}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + c.s.Push(c.in) + if !slices.Equal(c.s, c.res) { + t.Errorf("expected stack %v, got %v", c.res, c.s) + } + }) + } +} + +func TestPop(t *testing.T) { + cases := []struct { + name string + in Stack[int] + res int + out Stack[int] + err error + }{ + { + name: "successful pop", + in: Stack[int]{0, 1, 2, 3, 4}, + res: 4, + out: Stack[int]{0, 1, 2, 3}, + }, + { + name: "failed pop", + in: Stack[int]{}, + res: 0, + out: Stack[int]{}, + err: ErrStackEmpty, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + res, err := c.in.Pop() + if err != c.err { + t.Errorf("expected error %v, got %v", c.err, err) + } else if !slices.Equal(c.in, c.out) { + t.Errorf("expected stack %v, got %v", c.out, c.in) + } else if res != c.res { + t.Errorf("expected res %v, got %v", c.res, res) + } + }) + } +} From 4a488e9bc37560dd3e84154ac5e34391fdcdd04d Mon Sep 17 00:00:00 2001 From: gKits Date: Thu, 4 Jan 2024 13:20:46 +0100 Subject: [PATCH 09/51] ci: update Makefile --- Makefile | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6fe3434..200deac 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ run: # Docs -docs: +docsify: @python -m http.server 3000 -d docs # Testing @@ -42,6 +42,17 @@ test.parse.cover: test.parse.cover.show: test.parse.cover @go tool cover -html parse_cover.out +## Stack test + +test.stack: + @go test ./pkg/stack/... + +test.stack.cover: + @go test -coverprofile stack_cover.out ./pkg/stack/... + +test.stack.cover.show: test.stack.cover + @go tool cover -html stack_cover.out + # Cleanup cleancover: From 77381df66e0563515fc508f325b861cc19d2bcdc Mon Sep 17 00:00:00 2001 From: gKits Date: Thu, 4 Jan 2024 13:22:56 +0100 Subject: [PATCH 10/51] docs: remove build tag for hugo docs --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 9b057e1..1625049 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,6 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Build](https://github.com/gKits/PavoSQL/actions/workflows/gobuild.yaml/badge.svg)](https://github.com/gKits/PavoSQL/actions/workflows/gobuild.yaml) [![Test](https://github.com/gKits/PavoSQL/actions/workflows/gotest.yaml/badge.svg)](https://github.com/gKits/PavoSQL/actions/workflows/gotest.yaml) -[![Build Hugo docs and deploy to pages](https://github.com/gKits/PavoSQL/actions/workflows/hugo.yaml/badge.svg)](https://gkits.github.io/PavoSQL) **This is a learning project and is not meant to be run in production environments.** From f1d07f0df404993daa4649412b9d4dbedd8793a4 Mon Sep 17 00:00:00 2001 From: gKits Date: Fri, 19 Jul 2024 07:59:27 +0200 Subject: [PATCH 11/51] refactor: delete old implementation and start new btree --- internal/btree/btree.go | 148 +++++ internal/btree/interface.go | 22 + internal/btree/node/leaf.go | 157 +++++ internal/btree/node/pointer.go | 151 +++++ internal/btree/node/type.go | 26 + internal/parse/ast.go | 14 - internal/parse/lexer.go | 67 --- internal/parse/lexer_test.go | 54 -- internal/parse/parser.go | 13 - internal/parse/token.go | 62 -- internal/tree/iterator.go | 99 ---- internal/tree/iterator_test.go | 78 --- internal/tree/leafNode.go | 200 ------- internal/tree/leafNode_test.go | 556 ----------------- internal/tree/node.go | 56 -- internal/tree/node_test.go | 43 -- internal/tree/pointerNode.go | 176 ------ internal/tree/pointerNode_test.go | 560 ------------------ internal/tree/tree.go | 401 ------------- internal/tree/tree_test.go | 175 ------ pkg/parse/parse.go | 84 +++ .../parser_test.go => pkg/parse/parse_test.go | 0 pkg/parse/statement.go | 7 + pkg/parse/token.go | 47 ++ 24 files changed, 642 insertions(+), 2554 deletions(-) create mode 100644 internal/btree/btree.go create mode 100644 internal/btree/interface.go create mode 100644 internal/btree/node/leaf.go create mode 100644 internal/btree/node/pointer.go create mode 100644 internal/btree/node/type.go delete mode 100644 internal/parse/ast.go delete mode 100644 internal/parse/lexer.go delete mode 100644 internal/parse/lexer_test.go delete mode 100644 internal/parse/parser.go delete mode 100644 internal/parse/token.go delete mode 100644 internal/tree/iterator.go delete mode 100644 internal/tree/iterator_test.go delete mode 100644 internal/tree/leafNode.go delete mode 100644 internal/tree/leafNode_test.go delete mode 100644 internal/tree/node.go delete mode 100644 internal/tree/node_test.go delete mode 100644 internal/tree/pointerNode.go delete mode 100644 internal/tree/pointerNode_test.go delete mode 100644 internal/tree/tree.go delete mode 100644 internal/tree/tree_test.go create mode 100644 pkg/parse/parse.go rename internal/parse/parser_test.go => pkg/parse/parse_test.go (100%) create mode 100644 pkg/parse/statement.go create mode 100644 pkg/parse/token.go diff --git a/internal/btree/btree.go b/internal/btree/btree.go new file mode 100644 index 0000000..abb49de --- /dev/null +++ b/internal/btree/btree.go @@ -0,0 +1,148 @@ +package btree + +import ( + "io" + + "github.com/gKits/PavoSQL/internal/btree/node" +) + +type BTree struct { + root uint64 + nodeSize int + reader io.ReaderAt + writer io.WriterAt +} + +func (bt *BTree) Get(key []byte) ([]byte, error) { + cur, _ := bt.getNode(bt.root) + + for { + i, exists := cur.Search(key) + + switch cur.Type() { + case node.TypeLeaf: + if !exists { + return nil, nil + } + leaf, ok := cur.(*node.Leaf) + if !ok { + return nil, nil + } + val, err := leaf.Val(i) + if err != nil { + return nil, err + } + return val, nil + + case node.TypePointer: + pointer, ok := cur.(*node.Pointer) + if !ok { + return nil, nil + } + ptr, err := pointer.Ptr(i) + if err != nil { + return nil, err + } + cur, err = bt.getNode(ptr) + if err != nil { + return nil, err + } + continue + + default: + return nil, nil + } + } +} + +func (bt *BTree) Insert(key, val []byte) error { + cur, _ := bt.getNode(bt.root) + + for { + i, exists := cur.Search(key) + if exists { + return nil + } + + switch cur.Type() { + case node.TypeLeaf: + leaf, ok := cur.(*node.Leaf) + if !ok { + return nil + } + if err := leaf.Insert(i, key, val); err != nil { + return err + } + + case node.TypePointer: + pointer, ok := cur.(*node.Pointer) + if !ok { + return nil + } + ptr, err := pointer.Ptr(i) + if err != nil { + return err + } + cur, err = bt.getNode(ptr) + if err != nil { + return err + } + continue + + default: + return nil + } + break + } + + if cur.Size() <= bt.nodeSize { + return nil + } + + // TODO: split node and update path + + return nil +} + +func (bt *BTree) getNode(ptr uint64) (noder, error) { + b := make([]byte, bt.nodeSize) + if n, err := bt.reader.ReadAt(b, int64(ptr)); err != nil { + return nil, err + } else if n != bt.nodeSize { + return nil, nil + } + + switch node.TypeOf(b) { + case node.TypePointer: + return node.NewPointer(b) + case node.TypeLeaf: + return node.NewLeaf(b) + default: + return nil, nil + } +} + +func (bt *BTree) writeNode(n noder, ptr uint64) error { + b := make([]byte, bt.nodeSize) + nB, err := n.Bytes() + if err != nil { + return err + } + copy(b, nB) + if n, err := bt.writer.WriteAt(b, int64(ptr)); err != nil { + return err + } else if n != bt.nodeSize { + return nil + } + + return nil +} + +func (bt *BTree) freeNode(ptr uint64) error { + if n, err := bt.writer.WriteAt(make([]byte, bt.nodeSize), int64(ptr)); err != nil { + return err + } else if n != bt.nodeSize { + return nil + } + return nil +} diff --git a/internal/btree/interface.go b/internal/btree/interface.go new file mode 100644 index 0000000..d2088db --- /dev/null +++ b/internal/btree/interface.go @@ -0,0 +1,22 @@ +package btree + +import ( + "io" + + "github.com/gKits/PavoSQL/internal/btree/node" +) + +type noder interface { + Type() node.Type + Len() int + Size() int + Key(i int) ([]byte, error) + Search(key []byte) (int, bool) + Bytes() ([]byte, error) +} + +type Backend interface { + GetNode(off uint64) (noder, error) + NewReader() io.ReaderAt + NewWriter() io.WriterAt +} diff --git a/internal/btree/node/leaf.go b/internal/btree/node/leaf.go new file mode 100644 index 0000000..1b33c7b --- /dev/null +++ b/internal/btree/node/leaf.go @@ -0,0 +1,157 @@ +package node + +import ( + "bytes" + "encoding/binary" + "slices" +) + +type Leaf struct { + keys [][]byte + vals [][]byte +} + +func NewLeaf(d []byte) (*Leaf, error) { + leaf := new(Leaf) + if Type(binary.LittleEndian.Uint16(d[:])) != TypePointer { + return nil, ErrWrongType + } + length := binary.LittleEndian.Uint16(d[2:]) + leaf.keys = make([][]byte, length) + leaf.vals = make([][]byte, length) + off := 4 + for i := range length { + if off >= len(d) { + return nil, ErrNodeDataMalformed + } + + kLen := int(binary.LittleEndian.Uint16(d[off:])) + off += 2 + + leaf.keys[i] = d[off : off+kLen] + off += kLen + + vLen := int(binary.LittleEndian.Uint16(d[off:])) + off += 2 + + leaf.vals[i] = d[off : off+vLen] + off += vLen + } + return leaf, nil +} + +func (leaf *Leaf) Type() Type { + return TypeLeaf +} + +func (leaf *Leaf) Len() int { + return len(leaf.keys) +} + +func (leaf *Leaf) Size() int { + return 2 + // size of type identifier + 2 + // size of number of elements + 2*len(leaf.keys) + // size of all key size prefixes + len(slices.Concat(leaf.keys...)) + // size of all keys + len(leaf.vals) + // size of all value size prefixes + len(slices.Concat(leaf.vals...)) // size of all values +} + +func (leaf *Leaf) Key(i int) ([]byte, error) { + if i >= len(leaf.keys) || i < 0 { + return nil, ErrIndexOutOfBounds + } + return leaf.keys[i], nil +} + +func (leaf *Leaf) Val(i int) ([]byte, error) { + if i >= len(leaf.vals) || i < 0 { + return nil, ErrIndexOutOfBounds + } + return leaf.vals[i], nil +} + +func (leaf *Leaf) KeyVal(i int) ([]byte, []byte, error) { + if i >= len(leaf.keys) || i >= len(leaf.vals) || i < 0 { + return nil, nil, ErrIndexOutOfBounds + } + return leaf.keys[i], leaf.vals[i], nil +} + +func (leaf *Leaf) Search(key []byte) (int, bool) { + return slices.BinarySearchFunc(leaf.keys, key, bytes.Compare) +} + +func (leaf *Leaf) Insert(i int, key, val []byte) error { + if i > len(leaf.keys) || i > len(leaf.vals) || i < 0 { + return ErrIndexOutOfBounds + } + leaf.keys = slices.Insert(leaf.keys, i, key) + leaf.vals = slices.Insert(leaf.vals, i, val) + return nil +} + +func (leaf *Leaf) Update(i int, val []byte) error { + if i >= len(leaf.vals) || i < 0 { + return ErrIndexOutOfBounds + } + leaf.vals[i] = val + return nil +} + +func (leaf *Leaf) Delete(i int) error { + if i >= len(leaf.keys) || i >= len(leaf.vals) || i < 0 { + return ErrIndexOutOfBounds + } + leaf.keys = slices.Delete(leaf.keys, i, i) + leaf.vals = slices.Delete(leaf.vals, i, i) + return nil +} + +func (leaf *Leaf) Split() (Leaf, Leaf, error) { + if len(leaf.keys) <= 1 || len(leaf.vals) <= 1 { + return Leaf{}, Leaf{}, ErrCannotSplit + } + left := Leaf{ + keys: leaf.keys[:len(leaf.keys)/2], + vals: leaf.vals[:len(leaf.vals)/2], + } + right := Leaf{ + keys: leaf.keys[len(leaf.keys)/2:], + vals: leaf.vals[len(leaf.vals)/2:], + } + + return left, right, nil +} + +func (leaf *Leaf) Append(toAdd Leaf) error { + if bytes.Compare(leaf.keys[0], toAdd.keys[0]) >= 0 { + return ErrCannotAppend + } + leaf.keys = append(leaf.keys, toAdd.keys...) + leaf.vals = append(leaf.vals, toAdd.vals...) + return nil +} + +func (leaf *Leaf) Bytes() ([]byte, error) { + b := make([]byte, leaf.Size()) + + binary.LittleEndian.PutUint16(b[:], uint16(TypeLeaf)) + binary.LittleEndian.PutUint16(b[2:], uint16(len(leaf.keys))) + off := 4 + for i, key := range leaf.keys { + val := leaf.vals[i] + + binary.LittleEndian.PutUint16(b[off:], uint16(len(key))) + off += 2 + copy(b[off:], key) + off += len(key) + + binary.LittleEndian.PutUint16(b[off:], uint16(len(val))) + off += 2 + copy(b[off:], val) + off += len(val) + + } + return b, nil +} diff --git a/internal/btree/node/pointer.go b/internal/btree/node/pointer.go new file mode 100644 index 0000000..13bc1e1 --- /dev/null +++ b/internal/btree/node/pointer.go @@ -0,0 +1,151 @@ +package node + +import ( + "bytes" + "encoding/binary" + "slices" +) + +type Pointer struct { + keys [][]byte + ptrs []uint64 +} + +func NewPointer(d []byte) (*Pointer, error) { + pointer := new(Pointer) + if Type(binary.LittleEndian.Uint16(d[:])) != TypePointer { + return nil, ErrWrongType + } + length := binary.LittleEndian.Uint16(d[2:]) + pointer.keys = make([][]byte, length) + pointer.ptrs = make([]uint64, length) + off := 4 + for i := range length { + if off >= len(d) { + return nil, ErrNodeDataMalformed + } + + kLen := int(binary.LittleEndian.Uint16(d[off:])) + off += 2 + + pointer.keys[i] = d[off : off+kLen] + off += kLen + + pointer.ptrs[i] = binary.LittleEndian.Uint64(d[off:]) + off += 8 + } + return pointer, nil +} + +func (pointer *Pointer) Type() Type { + return TypePointer +} + +func (pointer *Pointer) Len() int { + return len(pointer.keys) +} + +func (pointer *Pointer) Size() int { + return 2 + // size of type identifier + 2 + // size of number of elements + 2*len(pointer.keys) + // size of all key size prefixes + len(slices.Concat(pointer.keys...)) + // size of all keys + len(pointer.ptrs)*8 // size of all pointers +} + +func (pointer *Pointer) Key(i int) ([]byte, error) { + if i >= len(pointer.keys) || i < 0 { + return nil, ErrIndexOutOfBounds + } + return pointer.keys[i], nil +} + +func (pointer *Pointer) Ptr(i int) (uint64, error) { + if i >= len(pointer.ptrs) || i < 0 { + return 0, ErrIndexOutOfBounds + } + return pointer.ptrs[i], nil +} + +func (pointer *Pointer) KeyPtr(i int) ([]byte, uint64, error) { + if i >= len(pointer.keys) || i >= len(pointer.ptrs) || i < 0 { + return nil, 0, ErrIndexOutOfBounds + } + return pointer.keys[i], pointer.ptrs[i], nil +} + +func (pointer *Pointer) Search(key []byte) (int, bool) { + return slices.BinarySearchFunc(pointer.keys, key, bytes.Compare) +} + +func (pointer *Pointer) Insert(i int, key []byte, ptr uint64) error { + if i > len(pointer.keys) || i > len(pointer.ptrs) || i < 0 { + return ErrIndexOutOfBounds + } + pointer.keys = slices.Insert(pointer.keys, i, key) + pointer.ptrs = slices.Insert(pointer.ptrs, i, ptr) + return nil +} + +func (pointer *Pointer) Update(i int, ptr uint64) error { + if i >= len(pointer.ptrs) || i < 0 { + return ErrIndexOutOfBounds + } + pointer.ptrs[i] = ptr + return nil +} + +func (pointer *Pointer) Delete(i int) error { + if i >= len(pointer.keys) || i >= len(pointer.ptrs) || i < 0 { + return ErrIndexOutOfBounds + } + pointer.keys = slices.Delete(pointer.keys, i, i) + pointer.ptrs = slices.Delete(pointer.ptrs, i, i) + return nil +} + +func (pointer *Pointer) Split() (Pointer, Pointer, error) { + if len(pointer.keys) <= 1 || len(pointer.ptrs) <= 1 { + return Pointer{}, Pointer{}, ErrCannotSplit + } + left := Pointer{ + keys: pointer.keys[:len(pointer.keys)/2], + ptrs: pointer.ptrs[:len(pointer.ptrs)/2], + } + right := Pointer{ + keys: pointer.keys[len(pointer.keys)/2:], + ptrs: pointer.ptrs[len(pointer.ptrs)/2:], + } + + return left, right, nil +} + +func (pointer *Pointer) Append(toAdd Pointer) error { + if bytes.Compare(pointer.keys[0], toAdd.keys[0]) >= 0 { + return ErrCannotAppend + } + pointer.keys = append(pointer.keys, toAdd.keys...) + pointer.ptrs = append(pointer.ptrs, toAdd.ptrs...) + return nil +} + +func (pointer *Pointer) Bytes() ([]byte, error) { + b := make([]byte, pointer.Size()) + + binary.LittleEndian.PutUint16(b[:], uint16(TypePointer)) + binary.LittleEndian.PutUint16(b[2:], uint16(len(pointer.keys))) + off := 4 + for i, key := range pointer.keys { + ptr := pointer.ptrs[i] + + binary.LittleEndian.PutUint16(b[off:], uint16(len(key))) + off += 2 + copy(b[off:], key) + off += len(key) + + binary.LittleEndian.PutUint64(b[off:], ptr) + off += 8 + + } + return b, nil +} diff --git a/internal/btree/node/type.go b/internal/btree/node/type.go new file mode 100644 index 0000000..745cfb6 --- /dev/null +++ b/internal/btree/node/type.go @@ -0,0 +1,26 @@ +package node + +import ( + "encoding/binary" + "errors" +) + +var ( + ErrIndexOutOfBounds = errors.New("index is out of bounds") + ErrCannotSplit = errors.New("cannot split node with less than 2 entries") + ErrCannotAppend = errors.New("cannot append node with first key gte last key of current node") + ErrWrongType = errors.New("wrong type identifier") + ErrNodeDataMalformed = errors.New("node data is malformed") +) + +type Type uint16 + +const ( + typeInvalid Type = iota + TypePointer + TypeLeaf +) + +func TypeOf(b []byte) Type { + return Type(binary.LittleEndian.Uint16(b)) +} diff --git a/internal/parse/ast.go b/internal/parse/ast.go deleted file mode 100644 index 6714642..0000000 --- a/internal/parse/ast.go +++ /dev/null @@ -1,14 +0,0 @@ -package parse - -type AST interface{} - -type Select struct { - Fields []string - Table string -} - -type CreateTable struct { - Name string - Fields []string - Types []string -} diff --git a/internal/parse/lexer.go b/internal/parse/lexer.go deleted file mode 100644 index 514f5af..0000000 --- a/internal/parse/lexer.go +++ /dev/null @@ -1,67 +0,0 @@ -package parse - -import ( - "fmt" - "strings" - "text/scanner" -) - -type Lexer struct { - scan *scanner.Scanner -} - -func NewLexer(text string) *Lexer { - scan := &scanner.Scanner{} - scan.Init(strings.NewReader(text)) - scan.Mode = scanner.ScanIdents | scanner.ScanInts | scanner.ScanFloats | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanComments - scan.Filename = "at" - - return &Lexer{scan: scan} -} - -func (lex *Lexer) Lex() Token { - r := lex.scan.Scan() - - tok := Token{} - text := lex.scan.TokenText() - - switch r { - case scanner.EOF: - tok.Type = tokEOF - - case scanner.Int: - tok.Type = tokInt - tok.Text = text - - case scanner.Float: - tok.Type = tokFloat - tok.Text = text - - case scanner.String, scanner.RawString: - tok.Type = tokString - tok.Text = text[1 : len(text)-1] - - case scanner.Ident: - text = strings.ToLower(text) - - var ok bool - if tok.Type, ok = keywords[text]; !ok { - tok.Type = tokIdent - } - tok.Text = text - - case scanner.Comment: - tok.Text = strings.Join(strings.Fields(text), " ") - tok.Type = tokComment - - default: - tok.Text = text - var ok bool - if tok.Type, ok = keywords[text]; !ok { - tok.Type = tokError - tok.Text = fmt.Sprintf("lex: %s: invalid token '%s'", lex.scan.Position, text) - } - - } - return tok -} diff --git a/internal/parse/lexer_test.go b/internal/parse/lexer_test.go deleted file mode 100644 index 9dbf764..0000000 --- a/internal/parse/lexer_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package parse - -import "testing" - -func TestLex(t *testing.T) { - q := ` - /* - Multiline - comment - */ - - select test From test - WHERE test = "foo bar" and - // Inline comment - - baz > 1 or baz < 2.0; - ` - - lex := NewLexer(q) - - expected := []Token{ - {Type: tokComment, Text: "/* Multiline comment */"}, - {Type: tokSelect, Text: "select"}, - {Type: tokIdent, Text: "test"}, - {Type: tokFrom, Text: "from"}, - {Type: tokIdent, Text: "test"}, - {Type: tokWhere, Text: "where"}, - {Type: tokIdent, Text: "test"}, - {Type: tokEqual, Text: "="}, - {Type: tokString, Text: "foo bar"}, - {Type: tokAnd, Text: "and"}, - {Type: tokComment, Text: "// Inline comment"}, - {Type: tokIdent, Text: "baz"}, - {Type: tokGreater, Text: ">"}, - {Type: tokInt, Text: "1"}, - {Type: tokOr, Text: "or"}, - {Type: tokIdent, Text: "baz"}, - {Type: tokLess, Text: "<"}, - {Type: tokFloat, Text: "2.0"}, - {Type: tokSemicol, Text: ";"}, - } - - i := 0 - for tok := lex.Lex(); tok.Type != tokEOF; tok = lex.Lex() { - if i >= len(expected) { - t.Fatalf("expected only %d tokens, got more", len(expected)) - } else if tok.Type != expected[i].Type { - t.Fatalf("expected type %v tokens, got %v: %s", expected[i].Type, tok.Type, tok.Text) - } else if tok.Text != expected[i].Text { - t.Fatalf("expected text %v tokens, got %v", expected[i].Text, tok.Text) - } - i++ - } -} diff --git a/internal/parse/parser.go b/internal/parse/parser.go deleted file mode 100644 index 6b0c883..0000000 --- a/internal/parse/parser.go +++ /dev/null @@ -1,13 +0,0 @@ -package parse - -type Parser struct { - lex *Lexer -} - -func NewParser(query string) *Parser { - return &Parser{lex: NewLexer(query)} -} - -func (p *Parser) Parse() (AST, error) { - return nil, nil -} diff --git a/internal/parse/token.go b/internal/parse/token.go deleted file mode 100644 index 157a8d9..0000000 --- a/internal/parse/token.go +++ /dev/null @@ -1,62 +0,0 @@ -package parse - -type TokenType uint - -const ( - tokError TokenType = iota - tokEOF - tokComment - tokNumber - tokInt - tokFloat - tokString - tokIdent - tokSelect - tokAsterisk - tokLeftPar - tokRightPar - tokSemicol - tokComma - tokEqual - tokGreater - tokLess - tokGreaterEq - tokLessEq - tokNotEq - tokFrom - tokWhere - tokCreate - tokTable - tokNot - tokAnd - tokOr -) - -var keywords = map[string]TokenType{ - "select": tokSelect, - "from": tokFrom, - "where": tokWhere, - "create": tokCreate, - "table": tokTable, - ",": tokComma, - "*": tokAsterisk, - ";": tokSemicol, - "(": tokLeftPar, - ")": tokRightPar, - "=": tokEqual, - ">": tokGreater, - "<": tokLess, - ">=": tokGreaterEq, - "<=": tokLessEq, - "!=": tokNotEq, - "not": tokNot, - "and": tokAnd, - "or": tokOr, -} - -type Token struct { - Type TokenType - Text string - Line int - Column int -} diff --git a/internal/tree/iterator.go b/internal/tree/iterator.go deleted file mode 100644 index b424ac1..0000000 --- a/internal/tree/iterator.go +++ /dev/null @@ -1,99 +0,0 @@ -package tree - -import ( - "errors" - "fmt" - - "github.com/gKits/PavoSQL/pkg/stack" -) - -type Iterator struct { - t *Tree - stk stack.Stack[Node] - idxStk stack.Stack[int] -} - -var EndOfTree = errors.New("end of tree") - -func NewIterator(t *Tree, k []byte) *Iterator { - return &Iterator{ - t: t, - } -} - -func (it *Iterator) Next() ([]byte, []byte, error) { - for { - var ( - cur Node - err error - ) - - idx, err := it.idxStk.Pop() - if err != nil { - return nil, nil, EndOfTree - } - - cur, err = it.stk.Peek() - if err != nil { - return nil, nil, err - } - - fmt.Println(cur, it.idxStk, idx) - - switch cur.Type() { - case nodeLeaf: - if idx == -1 { - idx++ - } - - leaf := cur.(*LeafNode) - k, err := leaf.Key(idx) - if err != nil { - return nil, nil, err - } - - v, err := leaf.ValAt(idx) - if err != nil { - return nil, nil, err - } - - if idx+1 >= cur.NKeys() { - _, err := it.stk.Pop() - if err != nil { - return nil, nil, err - } - } else { - it.idxStk.Push(idx + 1) - } - - return k, v, nil - - case nodePointer: - if idx+1 >= cur.NKeys() { - it.stk.Pop() - continue - } else { - idx++ - it.idxStk.Push(idx) - } - - pointer := cur.(*PointerNode) - - ptr, err := pointer.PtrAt(idx) - if err != nil { - return nil, nil, err - } - - next, err := it.t.read(ptr) - if err != nil { - return nil, nil, err - } - - it.stk.Push(next) - it.idxStk.Push(-1) - - default: - return nil, nil, errInvalNodeType - } - } -} diff --git a/internal/tree/iterator_test.go b/internal/tree/iterator_test.go deleted file mode 100644 index 4ae54e9..0000000 --- a/internal/tree/iterator_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package tree - -import ( - "bytes" - "testing" - - "github.com/gKits/PavoSQL/pkg/stack" -) - -func TestIterator(t *testing.T) { - tree, m := mockTree() - - cases := []struct { - name string - it Iterator - num int - res [][]byte - err error - }{ - { - name: "succefully iterate over one adjacent nodes", - it: Iterator{ - stk: stack.Stack[Node]{m[0], m[1], m[4]}, - idxStk: stack.Stack[int]{0, 0, 0}, - t: &tree, - }, - num: 5, - res: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}}, - }, - { - name: "succefully iterate multiple adjacent nodes", - it: Iterator{ - stk: stack.Stack[Node]{m[0], m[1], m[4]}, - idxStk: stack.Stack[int]{0, 0, 0}, - t: &tree, - }, - num: 7, - res: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, {'g'}}, - }, - { - name: "succefully iterate multiple levels", - it: Iterator{ - stk: stack.Stack[Node]{m[0], m[1], m[4]}, - idxStk: stack.Stack[int]{0, 0, 0}, - t: &tree, - }, - num: 10, - res: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, {'g'}, {'h'}, {'i'}, {'j'}}, - }, - { - name: "succefully iterate over whole tree", - it: Iterator{ - stk: stack.Stack[Node]{m[0], m[1], m[4]}, - idxStk: stack.Stack[int]{0, 0, 0}, - t: &tree, - }, - num: 30, - res: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}, {'f'}, {'g'}, {'h'}, {'i'}, {'j'}, {'k'}, {'l'}, {'m'}, {'n'}, {'o'}, {'p'}, {'q'}, {'r'}, {'s'}, {'t'}, {'u'}, {'v'}, {'w'}, {'x'}, {'y'}, {'z'}, {'{'}}, - err: EndOfTree, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - for i := 0; i < c.num; i++ { - k, v, err := c.it.Next() - if err == nil { - if !bytes.Equal(k, c.res[i]) || !bytes.Equal(v, c.res[i]) { - t.Log(c.it) - t.Errorf("expected k-v %v, got %v, %v", c.res[i], k, v) - } - } else if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - } - }) - } -} diff --git a/internal/tree/leafNode.go b/internal/tree/leafNode.go deleted file mode 100644 index 8df7b76..0000000 --- a/internal/tree/leafNode.go +++ /dev/null @@ -1,200 +0,0 @@ -package tree - -import ( - "bytes" - "encoding/binary" - "fmt" - "slices" -) - -type LeafNode struct { - keys [][]byte - vals [][]byte -} - -func (n *LeafNode) Decode(b []byte) error { - off := uint16(0) - if nodeType(binary.BigEndian.Uint16(b[off:])) != nodeLeaf { - return errInvalNodeType - } - off += 2 - - nKeys := binary.BigEndian.Uint32(b[off:]) - n.keys = make([][]byte, nKeys) - n.vals = make([][]byte, nKeys) - off += 4 - - for i := uint32(0); i < nKeys; i++ { - kSize := binary.BigEndian.Uint16(b[off:]) - off += 2 - vSize := binary.BigEndian.Uint16(b[off:]) - off += 2 - - n.keys[i] = b[off : off+kSize] - off += kSize - n.vals[i] = b[off : off+vSize] - off += vSize - } - - return nil -} - -func (n *LeafNode) Encode() []byte { - b := make([]byte, n.Size()) - off := 0 - - binary.BigEndian.PutUint16(b[off:], uint16(nodeLeaf)) - off += 2 - - binary.BigEndian.PutUint32(b[off:], uint32(len(n.keys))) - off += 4 - - for i := 0; i < len(n.keys); i++ { - binary.BigEndian.PutUint16(b[off:], uint16(len(n.keys[i]))) - off += 2 - - binary.BigEndian.PutUint16(b[off:], uint16(len(n.vals[i]))) - off += 2 - - copy(b[off:], n.keys[i]) - off += len(n.keys[i]) - - copy(b[off:], n.vals[i]) - off += len(n.vals[i]) - } - - return b -} - -func (n *LeafNode) NKeys() int { - return len(n.keys) -} - -func (n *LeafNode) Size() int { - size := NodeHeader - for i := 0; i < len(n.keys); i++ { - size += 4 + len(n.keys[i]) + len(n.vals[i]) - } - return size -} - -func (n *LeafNode) Type() nodeType { - return nodeLeaf -} - -func (n *LeafNode) Key(idx int) ([]byte, error) { - if idx >= len(n.keys) { - return nil, errIdxOutOfRange - } - return n.keys[idx], nil -} - -func (n *LeafNode) ValAt(idx int) ([]byte, error) { - if idx >= len(n.vals) { - return nil, errIdxOutOfRange - } - return n.vals[idx], nil -} - -func (n *LeafNode) Val(k []byte) ([]byte, error) { - idx, exists := n.Find(k) - if !exists { - return nil, errKeyNotExists - } - return n.vals[idx], nil -} - -func (n *LeafNode) Insert(k, v []byte) error { - idx, exists := n.Find(k) - if exists { - return errKeyExists - } - - n.keys = slices.Insert(n.keys, idx, k) - n.vals = slices.Insert(n.vals, idx, v) - - return nil -} - -func (n *LeafNode) Update(k, v []byte) error { - idx, exists := n.Find(k) - if !exists { - return errKeyNotExists - } - - n.vals[idx] = v - return nil -} - -func (n *LeafNode) Delete(k []byte) error { - idx, exists := n.Find(k) - if !exists { - return errKeyNotExists - } - - n.keys = slices.Delete(n.keys, idx, idx+1) - n.vals = slices.Delete(n.vals, idx, idx+1) - - return nil -} - -func (n *LeafNode) Find(k []byte) (int, bool) { - return slices.BinarySearchFunc(n.keys, k, bytes.Compare) -} - -func (n *LeafNode) Split() (Node, Node) { - l := &LeafNode{ - keys: slices.Clone(n.keys[:len(n.keys)/2]), - vals: slices.Clone(n.vals[:len(n.vals)/2]), - } - r := &LeafNode{ - keys: slices.Clone(n.keys[len(n.keys)/2:]), - vals: slices.Clone(n.vals[len(n.vals)/2:]), - } - return l, r -} - -func (n *LeafNode) SplitBy(size int) []Node { - parts := size / n.Size() - nodes := make([]Node, parts) - - var p, accum, prev int - for i := 0; i < len(n.keys) && p < parts; i++ { - sizeOf := NodeHeader + len(n.keys[i]) + len(n.vals[i]) - accum += sizeOf - if accum > size { - nodes[p] = &LeafNode{ - keys: slices.Clone(n.keys[prev:i]), - vals: slices.Clone(n.vals[prev:i]), - } - prev = i - } - } - return nodes -} - -func (n *LeafNode) Merge(m Node) error { - mLeaf, ok := m.(*LeafNode) - if !ok { - return errMergeType - } - - last, err := n.Key(n.NKeys() - 1) - if err != nil { - return err - } - - if next, err := m.Key(0); err != nil { - return err - } else if bytes.Compare(last, next) >= 0 { - return errMergeOrder - } - - n.keys = append(n.keys, mLeaf.keys...) - n.vals = append(n.vals, mLeaf.vals...) - return nil -} - -func (n *LeafNode) String() string { - return fmt.Sprintf("LeafNode{keys: %s vals: %s}", n.keys, n.vals) -} diff --git a/internal/tree/leafNode_test.go b/internal/tree/leafNode_test.go deleted file mode 100644 index 6e1eef4..0000000 --- a/internal/tree/leafNode_test.go +++ /dev/null @@ -1,556 +0,0 @@ -package tree - -import ( - "bytes" - "slices" - "testing" -) - -func TestLeafDecode(t *testing.T) { - cases := []struct { - name string - in []byte - res *LeafNode - err error - }{ - { - name: "invalid type", - in: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - res: &LeafNode{}, - err: errInvalNodeType, - }, - { - name: "decode leaf node", - in: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x00}, - res: &LeafNode{keys: [][]byte{}, vals: [][]byte{}}, - err: nil, - }, - { - name: "decode leaf node with one k-v pair", - in: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 'k', 'v'}, - res: &LeafNode{keys: [][]byte{{'k'}}, vals: [][]byte{{'v'}}}, - err: nil, - }, - { - name: "decode pointer node with multiple k-v pairs", - in: []byte{ - 0x00, 0x65, - 0x00, 0x00, 0x00, 0x04, - 0x00, 0x01, - 0x00, 0x05, - 'a', - 0x01, 0x01, 0x01, 0x01, 0x01, - 0x00, 0x01, - 0x00, 0x0A, - 'b', - 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', - 0x00, 0x02, - 0x00, 0x02, - 'b', 'a', - 'b', 'a', - 0x00, 0x03, - 0x00, 0x04, - 'c', 'b', 'a', - 0x01, 0x02, 0x03, 0x04, - }, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c', 'b', 'a'}}, - vals: [][]byte{ - {0x01, 0x01, 0x01, 0x01, 0x01}, - {'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'}, - {'b', 'a'}, - {0x01, 0x02, 0x03, 0x04}, - }, - }, - err: nil, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := &LeafNode{} - err := res.Decode(c.in) - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } else if res == nil { - if c.res != nil { - t.Errorf("expected result %v, got %v", c.res, res) - } - } else { - if c.res == nil { - t.Errorf("expected result %v, got %v", c.res, res) - } else if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) - } else if !slices.EqualFunc(res.vals, c.res.vals, bytes.Equal) { - t.Errorf("expected vals %v, got %v", c.res.vals, res.vals) - } - } - }) - } -} - -func TestLeafEncode(t *testing.T) { - cases := []struct { - name string - in *LeafNode - res []byte - }{ - { - name: "encode empty leaf node", - in: &LeafNode{keys: [][]byte{}, vals: [][]byte{}}, - res: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x00}, - }, - { - name: "encode leaf node with one k-v pair", - in: &LeafNode{keys: [][]byte{{'k'}}, vals: [][]byte{{'v'}}}, - res: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 'k', 'v'}, - }, - { - name: "encode leaf node with multiple k-v pairs", - in: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c', 'b', 'a'}}, - vals: [][]byte{ - {0x01, 0x01, 0x01, 0x01, 0x01}, - {'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a'}, - {'b', 'a'}, - {0x01, 0x02, 0x03, 0x04}, - }, - }, - res: []byte{ - 0x00, 0x65, - 0x00, 0x00, 0x00, 0x04, - 0x00, 0x01, - 0x00, 0x05, - 'a', - 0x01, 0x01, 0x01, 0x01, 0x01, - 0x00, 0x01, - 0x00, 0x0A, - 'b', - 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', - 0x00, 0x02, - 0x00, 0x02, - 'b', 'a', - 'b', 'a', - 0x00, 0x03, - 0x00, 0x04, - 'c', 'b', 'a', - 0x01, 0x02, 0x03, 0x04, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := c.in.Encode() - if !bytes.Equal(res, c.res) { - t.Errorf("expected result %v, got %v", c.res, res) - } - }) - } -} - -func TestLeafVal(t *testing.T) { - cases := []struct { - name string - k []byte - res []byte - err error - }{ - { - name: "successfully read key", - k: []byte{'b'}, - res: []byte{'b'}, - }, - { - name: "failed read non existing key", - k: []byte{'e'}, - res: nil, - err: errKeyNotExists, - }, - } - - leaf := LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res, err := leaf.Val(c.k) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } else if !bytes.Equal(res, c.res) { - t.Errorf("expected pointer %v, got %v", c.res, res) - } - }) - } -} - -func TestLeafInsert(t *testing.T) { - cases := []struct { - name string - k []byte - v []byte - res *LeafNode - err error - }{ - { - name: "insert in middle", - k: []byte{'b', 'a'}, - v: []byte{'b', 'a'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c'}, {'d'}}, - }, - }, - { - name: "insert last", - k: []byte{'e'}, - v: []byte{'e'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}}, - }, - }, - { - name: "failed insert existing key", - k: []byte{'a'}, - v: []byte{'a'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - }, - err: errKeyExists, - }, - } - - leaf := &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := *leaf - err := res.Insert(c.k, c.v) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) - } - if !slices.EqualFunc(res.vals, c.res.vals, bytes.Equal) { - t.Errorf("expected vals %v, got %v", c.res.vals, res.vals) - } - }) - } -} - -func TestLeafUpdate(t *testing.T) { - cases := []struct { - name string - k []byte - v []byte - res *LeafNode - err error - }{ - { - name: "update first", - k: []byte{'a'}, - v: []byte{'0', 'a'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'0', 'a'}, {'b'}, {'c'}, {'d'}}, - }, - }, - { - name: "update last", - k: []byte{'d'}, - v: []byte{'3', 'd'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}, {'3', 'd'}}, - }, - }, - { - name: "update middle", - k: []byte{'c'}, - v: []byte{'2', 'c'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'2', 'c'}, {'d'}}, - }, - }, - { - name: "failed update non existing key", - k: []byte{'e'}, - v: []byte{'e'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - }, - err: errKeyNotExists, - }, - } - - leaf := func() *LeafNode { - return &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - } - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := leaf() - err := res.Update(c.k, c.v) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) - } - if !slices.EqualFunc(res.vals, c.res.vals, bytes.Equal) { - t.Errorf("expected vals %v, got %v", c.res.vals, res.vals) - } - }) - } -} - -func TestLeafDelete(t *testing.T) { - cases := []struct { - name string - k []byte - res *LeafNode - err error - }{ - { - name: "delete first", - k: []byte{'a'}, - res: &LeafNode{ - keys: [][]byte{{'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'b'}, {'c'}, {'d'}}, - }, - }, - { - name: "delete last", - k: []byte{'d'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}}, - }, - }, - { - name: "delete middle", - k: []byte{'c'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'d'}}, - }, - }, - { - name: "failed delete non existing key", - k: []byte{'e'}, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - }, - err: errKeyNotExists, - }, - } - - leaf := func() *LeafNode { - return &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - } - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := leaf() - err := res.Delete(c.k) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) - } - if !slices.EqualFunc(res.vals, c.res.vals, bytes.Equal) { - t.Errorf("expected vals %v, got %v", c.res.vals, res.vals) - } - }) - } -} - -func TestLeafSplit(t *testing.T) { - cases := []struct { - name string - in *LeafNode - l *LeafNode - r *LeafNode - }{ - { - name: "split even number", - in: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}}, - vals: [][]byte{{'a'}, {'b'}}, - }, - l: &LeafNode{ - keys: [][]byte{{'a'}}, - vals: [][]byte{{'a'}}, - }, - r: &LeafNode{ - keys: [][]byte{{'b'}}, - vals: [][]byte{{'b'}}, - }, - }, - { - name: "split odd number", - in: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}}, - }, - l: &LeafNode{ - keys: [][]byte{{'a'}}, - vals: [][]byte{{'a'}}, - }, - r: &LeafNode{ - keys: [][]byte{{'b'}, {'c'}}, - vals: [][]byte{{'b'}, {'c'}}, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - outL, outR := c.in.Split() - - if l, ok := outL.(*LeafNode); !ok { - t.Error("expected successful type assertion") - } else { - if !slices.EqualFunc(c.l.keys, l.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.l.keys, l.keys) - } else if !slices.EqualFunc(c.l.vals, l.vals, bytes.Equal) { - t.Errorf("expected vals %v, got %v", c.l.vals, l.vals) - } - } - - if r, ok := outR.(*LeafNode); !ok { - t.Error("expected successful type assertion") - } else { - if !slices.EqualFunc(c.r.keys, r.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.r.keys, r.keys) - } else if !slices.EqualFunc(c.r.vals, r.vals, bytes.Equal) { - t.Errorf("expected vals %v, got %v", c.r.vals, r.vals) - } - } - }) - } -} - -func TestLeafMerge(t *testing.T) { - cases := []struct { - name string - in *LeafNode - merge Node - res *LeafNode - err error - }{ - { - name: "merge same size", - in: &LeafNode{ - keys: [][]byte{{'a'}}, - vals: [][]byte{{'a'}}, - }, - merge: &LeafNode{ - keys: [][]byte{{'b'}}, - vals: [][]byte{{'b'}}, - }, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}}, - vals: [][]byte{{'a'}, {'b'}}, - }, - }, - { - name: "merge not same size", - in: &LeafNode{ - keys: [][]byte{{'a'}}, - vals: [][]byte{{'a'}}, - }, - merge: &LeafNode{ - keys: [][]byte{{'b'}, {'c'}}, - vals: [][]byte{{'b'}, {'c'}}, - }, - res: &LeafNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}}, - vals: [][]byte{{'a'}, {'b'}, {'c'}}, - }, - }, - { - name: "failed merge wrong order", - in: &LeafNode{ - keys: [][]byte{{'b'}}, - vals: [][]byte{{'b'}}, - }, - merge: &LeafNode{ - keys: [][]byte{{'a'}}, - vals: [][]byte{{'a'}}, - }, - res: &LeafNode{ - keys: [][]byte{{'b'}}, - vals: [][]byte{{'b'}}, - }, - err: errMergeOrder, - }, - { - name: "failed merge wrong order equal key", - in: &LeafNode{ - keys: [][]byte{{'b'}}, - vals: [][]byte{{'b'}}, - }, - merge: &LeafNode{ - keys: [][]byte{{'b'}}, - vals: [][]byte{{'a'}}, - }, - res: &LeafNode{ - keys: [][]byte{{'b'}}, - vals: [][]byte{{'b'}}, - }, - err: errMergeOrder, - }, - { - name: "failed merge wrong type", - in: &LeafNode{ - keys: [][]byte{{'a'}}, - vals: [][]byte{{'a'}}, - }, - merge: &PointerNode{}, - res: &LeafNode{ - keys: [][]byte{{'a'}}, - vals: [][]byte{{'a'}}, - }, - err: errMergeType, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - err := c.in.Merge(c.merge) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - - if !slices.EqualFunc(c.res.keys, c.in.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, c.in.keys) - } else if !slices.EqualFunc(c.res.vals, c.in.vals, bytes.Equal) { - t.Errorf("expected vals %v, got %v", c.res.vals, c.in.vals) - } - }) - } -} diff --git a/internal/tree/node.go b/internal/tree/node.go deleted file mode 100644 index 298a01b..0000000 --- a/internal/tree/node.go +++ /dev/null @@ -1,56 +0,0 @@ -package tree - -import ( - "encoding/binary" - "errors" -) - -const NodeHeader = 6 - -var ( - errKeyExists = errors.New("key already exists") - errKeyNotExists = errors.New("key does not exist") - errInvalNodeType = errors.New("invalid node type") - errIdxOutOfRange = errors.New("index is out of node range") - errLeafHasNoPtr = errors.New("leaf node cannot contain ptr") - errMergeType = errors.New("merging nodes need to have equal type") - errMergeOrder = errors.New("last key of left node needs to greater than first key of right node") - errNodeAssert = errors.New("node assertion failed") -) - -type nodeType uint16 - -const ( - nodePointer nodeType = 100 - nodeLeaf nodeType = 101 -) - -type Node interface { - Type() nodeType // Returns node type. - Key(idx int) ([]byte, error) // Returns key at given index idx. - NKeys() int // Returns the number of keys stored in the node. - Size() int // Returns the encoded size of the node in bytes. - Delete(k []byte) error // Deletes the key and its value from the node. - Find(k []byte) (idx int, exists bool) // Returns the index and existing status of the given key k. - Encode() []byte // Encodes the node and returns the encoded byte stream. - Decode([]byte) error // Decodes the give bytes stream into the node. - Split() (l Node, r Node) // Returns two nodes containing each half of the original node. - Merge(Node) error // Right merges all keys and values onto node. -} - -func NewNode(b []byte) (Node, error) { - var n Node - switch nodeType(binary.BigEndian.Uint16(b)) { - case nodePointer: - n = &PointerNode{} - case nodeLeaf: - n = &LeafNode{} - default: - return nil, errInvalNodeType - } - - if err := n.Decode(b); err != nil { - return nil, err - } - return n, nil -} diff --git a/internal/tree/node_test.go b/internal/tree/node_test.go deleted file mode 100644 index 33aa501..0000000 --- a/internal/tree/node_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package tree - -import "testing" - -func TestNewNode(t *testing.T) { - cases := []struct { - name string - in []byte - res Node - err error - }{ - { - name: "new pointer node", - in: []byte{0x00, 0x64, 0x00, 0x00, 0x00, 0x00}, - res: &PointerNode{}, - }, - { - name: "new leaf node", - in: []byte{0x00, 0x65, 0x00, 0x00, 0x00, 0x00}, - res: &LeafNode{}, - }, - { - name: "invalid node type", - in: []byte{0x00, 0x00}, - res: nil, - err: errInvalNodeType, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res, err := NewNode(c.in) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } else if c.res != nil { - if res.Type() != c.res.Type() { - t.Errorf("expected node type %v, got %v", c.res.Type(), res.Type()) - } - } - }) - } -} diff --git a/internal/tree/pointerNode.go b/internal/tree/pointerNode.go deleted file mode 100644 index c840aac..0000000 --- a/internal/tree/pointerNode.go +++ /dev/null @@ -1,176 +0,0 @@ -package tree - -import ( - "bytes" - "encoding/binary" - "fmt" - "slices" -) - -type PointerNode struct { - keys [][]byte - ptrs []uint64 -} - -func (n *PointerNode) Decode(b []byte) error { - off := uint16(0) - if nodeType(binary.BigEndian.Uint16(b[off:])) != nodePointer { - return errInvalNodeType - } - off += 2 - - nKeys := binary.BigEndian.Uint32(b[off:]) - n.keys = make([][]byte, nKeys) - n.ptrs = make([]uint64, nKeys) - off += 4 - - for i := uint32(0); i < nKeys; i++ { - kSize := binary.BigEndian.Uint16(b[off:]) - off += 2 - - n.keys[i] = b[off : off+kSize] - off += kSize - n.ptrs[i] = binary.BigEndian.Uint64(b[off : off+8]) - off += 8 - } - - return nil -} - -func (n *PointerNode) Encode() []byte { - b := make([]byte, n.Size()) - off := 0 - - binary.BigEndian.PutUint16(b, uint16(nodePointer)) - off += 2 - - binary.BigEndian.PutUint32(b[off:], uint32(len(n.keys))) - off += 4 - - for i := 0; i < len(n.keys); i++ { - binary.BigEndian.PutUint16(b[off:], uint16(len(n.keys[i]))) - off += 2 - - copy(b[off:], n.keys[i]) - off += len(n.keys[i]) - - binary.BigEndian.PutUint64(b[off:], n.ptrs[i]) - off += 8 - } - - return b -} - -func (n *PointerNode) NKeys() int { - return len(n.keys) -} - -func (n *PointerNode) Size() int { - size := NodeHeader - for i := 0; i < len(n.keys); i++ { - size += 2 + len(n.keys[i]) + 8 - } - return size -} - -func (n *PointerNode) Type() nodeType { - return nodePointer -} - -func (n *PointerNode) Key(idx int) ([]byte, error) { - if idx >= len(n.keys) { - return nil, errIdxOutOfRange - } - return n.keys[idx], nil -} - -func (n *PointerNode) PtrAt(idx int) (uint64, error) { - if idx >= len(n.ptrs) { - return 0, errIdxOutOfRange - } - return n.ptrs[idx], nil -} - -func (n *PointerNode) Ptr(k []byte) (uint64, error) { - idx, exists := n.Find(k) - if !exists { - return 0, errKeyNotExists - } - return n.ptrs[idx], nil -} - -func (n *PointerNode) Insert(k []byte, ptr uint64) error { - idx, exists := n.Find(k) - if exists { - return errKeyExists - } - - n.keys = slices.Insert(n.keys, idx, k) - n.ptrs = slices.Insert(n.ptrs, idx, ptr) - - return nil -} - -func (n *PointerNode) Update(k []byte, ptr uint64) error { - idx, exists := n.Find(k) - if !exists { - return errKeyNotExists - } - - n.ptrs[idx] = ptr - return nil -} - -func (n *PointerNode) Delete(k []byte) error { - idx, exists := n.Find(k) - if !exists { - return errKeyNotExists - } - - n.keys = slices.Delete(n.keys, idx, idx+1) - n.ptrs = slices.Delete(n.ptrs, idx, idx+1) - - return nil -} - -func (n *PointerNode) Find(k []byte) (idx int, exists bool) { - return slices.BinarySearchFunc(n.keys, k, bytes.Compare) -} - -func (n *PointerNode) Split() (Node, Node) { - l := &PointerNode{ - keys: slices.Clone(n.keys[:len(n.keys)/2]), - ptrs: slices.Clone(n.ptrs[:len(n.ptrs)/2]), - } - r := &PointerNode{ - keys: slices.Clone(n.keys[len(n.keys)/2:]), - ptrs: slices.Clone(n.ptrs[len(n.ptrs)/2:]), - } - return l, r -} - -func (n *PointerNode) Merge(m Node) error { - mPtr, ok := m.(*PointerNode) - if !ok { - return errMergeType - } - - last, err := n.Key(n.NKeys() - 1) - if err != nil { - return err - } - - if next, err := m.Key(0); err != nil { - return err - } else if bytes.Compare(last, next) >= 0 { - return errMergeOrder - } - - n.keys = append(n.keys, mPtr.keys...) - n.ptrs = append(n.ptrs, mPtr.ptrs...) - return nil -} - -func (n *PointerNode) String() string { - return fmt.Sprintf("PointerNode{keys: %s ptrs: %v}", n.keys, n.ptrs) -} diff --git a/internal/tree/pointerNode_test.go b/internal/tree/pointerNode_test.go deleted file mode 100644 index 1360cb5..0000000 --- a/internal/tree/pointerNode_test.go +++ /dev/null @@ -1,560 +0,0 @@ -package tree - -import ( - "bytes" - "slices" - "testing" -) - -func TestPointerDecode(t *testing.T) { - cases := []struct { - name string - in []byte - res *PointerNode - err error - }{ - { - name: "invalid type", - in: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, - res: &PointerNode{}, - err: errInvalNodeType, - }, - { - name: "decode pointer node", - in: []byte{0x00, 0x64, 0x00, 0x00, 0x00, 0x00}, - res: &PointerNode{keys: [][]byte{}, ptrs: []uint64{}}, - err: nil, - }, - { - name: "decode pointer node with one k-p pair", - in: []byte{ - 0x00, 0x64, - 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, - 'k', - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - }, - res: &PointerNode{keys: [][]byte{{'k'}}, ptrs: []uint64{72340172838076673}}, - err: nil, - }, - { - name: "decode pointer node with multiple k-p pairs", - in: []byte{ - 0x00, 0x64, - 0x00, 0x00, 0x00, 0x04, - 0x00, 0x01, - 'a', - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x00, 0x01, - 'b', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, - 0x00, 0x02, - 'b', 'a', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x03, - 'c', 'b', 'a', - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - }, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c', 'b', 'a'}}, - ptrs: []uint64{ - 72340172838076673, - 255, - 1, - 18446744073709551615, - }, - }, - err: nil, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := &PointerNode{} - err := res.Decode(c.in) - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } else if res == nil { - if c.res != nil { - t.Errorf("expected result %v, got %v", c.res, res) - } - } else { - if c.res == nil { - t.Errorf("expected result %v, got %v", c.res, res) - } else if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) - } else if !slices.Equal(res.ptrs, c.res.ptrs) { - t.Errorf("expected vals %v, got %v", c.res.ptrs, res.ptrs) - } - } - }) - } -} - -func TestPointerEncode(t *testing.T) { - cases := []struct { - name string - in *PointerNode - res []byte - }{ - { - name: "decode pointer node", - in: &PointerNode{keys: [][]byte{}, ptrs: []uint64{}}, - res: []byte{0x00, 0x64, 0x00, 0x00, 0x00, 0x00}, - }, - { - name: "decode pointer node with one k-p pair", - in: &PointerNode{keys: [][]byte{{'k'}}, ptrs: []uint64{72340172838076673}}, - res: []byte{ - 0x00, 0x64, - 0x00, 0x00, 0x00, 0x01, - 0x00, 0x01, - 'k', - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - }, - }, - { - name: "decode pointer node with multiple k-p pairs", - in: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c', 'b', 'a'}}, - ptrs: []uint64{ - 72340172838076673, - 255, - 1, - 18446744073709551615, - }, - }, - res: []byte{ - 0x00, 0x64, - 0x00, 0x00, 0x00, 0x04, - 0x00, 0x01, - 'a', - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, - 0x00, 0x01, - 'b', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, - 0x00, 0x02, - 'b', 'a', - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, - 0x00, 0x03, - 'c', 'b', 'a', - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := c.in.Encode() - if !bytes.Equal(res, c.res) { - t.Errorf("expected result %v, got %v", c.res, res) - } - }) - } -} - -func TestPointerPtr(t *testing.T) { - cases := []struct { - name string - k []byte - res uint64 - err error - }{ - { - name: "successfully read key", - k: []byte{'b'}, - res: 1, - }, - { - name: "failed read non existing key", - k: []byte{'e'}, - res: 0, - err: errKeyNotExists, - }, - } - - ptr := PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 2, 3}, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res, err := ptr.Ptr(c.k) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } else if res != c.res { - t.Errorf("expected pointer %v, got %v", c.res, res) - } - }) - } -} - -func TestPointerInsert(t *testing.T) { - cases := []struct { - name string - k []byte - p uint64 - res *PointerNode - err error - }{ - { - name: "insert in middle", - k: []byte{'b', 'a'}, - p: 4, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'b', 'a'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 4, 2, 3}, - }, - }, - { - name: "insert last", - k: []byte{'e'}, - p: 4, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}, {'e'}}, - ptrs: []uint64{0, 1, 2, 3, 4}, - }, - }, - { - name: "failed insert existing key", - k: []byte{'a'}, - p: 4, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 2, 3}, - }, - err: errKeyExists, - }, - } - - ptr := &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 2, 3}, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := *ptr - err := res.Insert(c.k, c.p) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) - } - if !slices.Equal(res.ptrs, c.res.ptrs) { - t.Errorf("expected ptrs %v, got %v", c.res.ptrs, res.ptrs) - } - }) - } -} - -func TestPointerUpdate(t *testing.T) { - cases := []struct { - name string - k []byte - p uint64 - res *PointerNode - err error - }{ - { - name: "update first", - k: []byte{'a'}, - p: 4, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{4, 1, 2, 3}, - }, - }, - { - name: "update last", - k: []byte{'d'}, - p: 4, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 2, 4}, - }, - }, - { - name: "update middle", - k: []byte{'c'}, - p: 4, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 4, 3}, - }, - }, - { - name: "failed update non existing key", - k: []byte{'e'}, - p: 4, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 2, 3}, - }, - err: errKeyNotExists, - }, - } - - ptr := func() *PointerNode { - return &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 2, 3}, - } - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := ptr() - err := res.Update(c.k, c.p) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) - } - if !slices.Equal(res.ptrs, c.res.ptrs) { - t.Errorf("expected ptrs %v, got %v", c.res.ptrs, res.ptrs) - } - }) - } -} - -func TestPointerDelete(t *testing.T) { - cases := []struct { - name string - k []byte - res *PointerNode - err error - }{ - { - name: "delete first", - k: []byte{'a'}, - res: &PointerNode{ - keys: [][]byte{{'b'}, {'c'}, {'d'}}, - ptrs: []uint64{1, 2, 3}, - }, - }, - { - name: "delete last", - k: []byte{'d'}, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}}, - ptrs: []uint64{0, 1, 2}, - }, - }, - { - name: "delete middle", - k: []byte{'c'}, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'d'}}, - ptrs: []uint64{0, 1, 3}, - }, - }, - { - name: "failed delete non existing key", - k: []byte{'e'}, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 2, 3}, - }, - err: errKeyNotExists, - }, - } - - leaf := func() *PointerNode { - return &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}, {'d'}}, - ptrs: []uint64{0, 1, 2, 3}, - } - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res := leaf() - err := res.Delete(c.k) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - if !slices.EqualFunc(res.keys, c.res.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, res.keys) - } - if !slices.Equal(res.ptrs, c.res.ptrs) { - t.Errorf("expected ptrs %v, got %v", c.res.ptrs, res.ptrs) - } - }) - } -} - -func TestPointerSplit(t *testing.T) { - cases := []struct { - name string - in *PointerNode - l *PointerNode - r *PointerNode - }{ - { - name: "split even number", - in: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}}, - ptrs: []uint64{0, 1}, - }, - l: &PointerNode{ - keys: [][]byte{{'a'}}, - ptrs: []uint64{0}, - }, - r: &PointerNode{ - keys: [][]byte{{'b'}}, - ptrs: []uint64{1}, - }, - }, - { - name: "split odd number", - in: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}}, - ptrs: []uint64{0, 1, 2}, - }, - l: &PointerNode{ - keys: [][]byte{{'a'}}, - ptrs: []uint64{0}, - }, - r: &PointerNode{ - keys: [][]byte{{'b'}, {'c'}}, - ptrs: []uint64{1, 2}, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - outL, outR := c.in.Split() - - if l, ok := outL.(*PointerNode); !ok { - t.Error("expected successful type assertion") - } else { - if !slices.EqualFunc(c.l.keys, l.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.l.keys, l.keys) - } else if !slices.Equal(c.l.ptrs, l.ptrs) { - t.Errorf("expected ptrs %v, got %v", c.l.ptrs, l.ptrs) - } - } - - if r, ok := outR.(*PointerNode); !ok { - t.Error("expected successful type assertion") - } else { - if !slices.EqualFunc(c.r.keys, r.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.r.keys, r.keys) - } else if !slices.Equal(c.r.ptrs, r.ptrs) { - t.Errorf("expected ptrs %v, got %v", c.r.ptrs, r.ptrs) - } - } - }) - } -} - -func TestPointerMerge(t *testing.T) { - cases := []struct { - name string - in *PointerNode - merge Node - res *PointerNode - err error - }{ - { - name: "merge same size", - in: &PointerNode{ - keys: [][]byte{{'a'}}, - ptrs: []uint64{0}, - }, - merge: &PointerNode{ - keys: [][]byte{{'b'}}, - ptrs: []uint64{1}, - }, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}}, - ptrs: []uint64{0, 1}, - }, - }, - { - name: "merge not same size", - in: &PointerNode{ - keys: [][]byte{{'a'}}, - ptrs: []uint64{0}, - }, - merge: &PointerNode{ - keys: [][]byte{{'b'}, {'c'}}, - ptrs: []uint64{1, 2}, - }, - res: &PointerNode{ - keys: [][]byte{{'a'}, {'b'}, {'c'}}, - ptrs: []uint64{0, 1, 2}, - }, - }, - { - name: "failed merge wrong order", - in: &PointerNode{ - keys: [][]byte{{'b'}}, - ptrs: []uint64{1}, - }, - merge: &PointerNode{ - keys: [][]byte{{'a'}}, - ptrs: []uint64{0}, - }, - res: &PointerNode{ - keys: [][]byte{{'b'}}, - ptrs: []uint64{1}, - }, - err: errMergeOrder, - }, - { - name: "failed merge wrong order equal key", - in: &PointerNode{ - keys: [][]byte{{'b'}}, - ptrs: []uint64{1}, - }, - merge: &PointerNode{ - keys: [][]byte{{'b'}}, - ptrs: []uint64{0}, - }, - res: &PointerNode{ - keys: [][]byte{{'b'}}, - ptrs: []uint64{1}, - }, - err: errMergeOrder, - }, - { - name: "failed merge wrong type", - in: &PointerNode{ - keys: [][]byte{{'a'}}, - ptrs: []uint64{0}, - }, - merge: &LeafNode{}, - res: &PointerNode{ - keys: [][]byte{{'a'}}, - ptrs: []uint64{0}, - }, - err: errMergeType, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - err := c.in.Merge(c.merge) - - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - - if !slices.EqualFunc(c.res.keys, c.in.keys, bytes.Equal) { - t.Errorf("expected keys %v, got %v", c.res.keys, c.in.keys) - } else if !slices.Equal(c.res.ptrs, c.in.ptrs) { - t.Errorf("expected ptrs %v, got %v", c.res.ptrs, c.in.ptrs) - } - }) - } -} diff --git a/internal/tree/tree.go b/internal/tree/tree.go deleted file mode 100644 index dccfa41..0000000 --- a/internal/tree/tree.go +++ /dev/null @@ -1,401 +0,0 @@ -package tree - -import ( - "errors" -) - -const PageSize = 4096 -const MaxCell = PageSize - NodeHeader - -var ( - errCellToLarge = errors.New("cell size exceeds maximum") - errMalformedRecurse = errors.New("recursive result is malformed") -) - -type Tree struct { - root uint64 - read func(uint64) (Node, error) // callback to read node from backend - alloc func(Node) (uint64, error) // callback to alloc node in backend - free func(uint64) error // callback to free node in backend - maxNodeSize int -} - -type recurseResult struct { - Key []byte - Ptr uint64 -} - -func (t *Tree) Get(k []byte) ([]byte, error) { - cur, err := t.read(t.root) - if err != nil { - return nil, err - } - - for { - switch cur.Type() { - case nodePointer: - pointer, ok := cur.(*PointerNode) - if !ok { - return nil, errNodeAssert - } - - idx, exists := pointer.Find(k) - if !exists { - idx-- - } - ptr, err := pointer.PtrAt(idx) - if err != nil { - return nil, err - } - - cur, err = t.read(ptr) - if err != nil { - return nil, err - } - continue - - case nodeLeaf: - leaf, ok := cur.(*LeafNode) - if !ok { - return nil, errNodeAssert - } - return leaf.Val(k) - - default: - return nil, errInvalNodeType - } - } -} - -func (t *Tree) Insert(k, v []byte) error { - if !t.cellFits(k, v) { - return errCellToLarge - } - - res, err := t.recursiveInsert(t.root, k, v) - if err != nil { - return err - } - - switch len(res) { - case 1: - t.root = res[0].Ptr - case 2: - root := &PointerNode{ - keys: [][]byte{res[0].Key, res[1].Key}, - ptrs: []uint64{res[0].Ptr, res[1].Ptr}, - } - - t.root, err = t.alloc(root) - if err != nil { - return err - } - default: - return errMalformedRecurse - } - return nil -} - -func (t *Tree) recursiveInsert(ptr uint64, k, v []byte) ([]recurseResult, error) { - cur, err := t.read(ptr) - if err != nil { - return nil, err - } - if err = t.free(ptr); err != nil { - return nil, err - } - - switch cur.Type() { - case nodePointer: - pointer, ok := cur.(*PointerNode) - if !ok { - return nil, errNodeAssert - } - - idx, exists := pointer.Find(k) - if !exists { - idx-- - } - - ptr, err = pointer.PtrAt(idx) - if err != nil { - return nil, err - } - - res, err := t.recursiveInsert(ptr, k, v) - if err != nil { - return nil, err - } - - if err := pointer.Update(res[0].Key, res[0].Ptr); err != nil { - return nil, err - } - - if len(res) > 1 { - if err := pointer.Insert(res[1].Key, res[1].Ptr); err != nil { - return nil, err - } - } - cur = pointer - - case nodeLeaf: - leaf, ok := cur.(*LeafNode) - if !ok { - return nil, errNodeAssert - } - - if err := leaf.Insert(k, v); err != nil { - return nil, err - } - cur = leaf - - default: - return nil, errInvalNodeType - } - - if cur.Size() > t.maxNodeSize { - l, r := cur.Split() - - lPtr, err := t.alloc(l) - if err != nil { - return nil, err - } - - rPtr, err := t.alloc(r) - if err != nil { - return nil, err - } - - lK, err := l.Key(0) - if err != nil { - return nil, err - } - - rK, err := r.Key(0) - if err != nil { - return nil, err - } - - return []recurseResult{{Key: lK, Ptr: lPtr}, {Key: rK, Ptr: rPtr}}, nil - } - - ptr, err = t.alloc(cur) - if err != nil { - return nil, err - } - - origin, err := cur.Key(0) - if err != nil { - return nil, err - } - - return []recurseResult{{Key: origin, Ptr: ptr}}, nil -} - -func (t *Tree) Update(k, v []byte) error { - if !t.cellFits(k, v) { - return errCellToLarge - } - - res, err := t.recursiveUpdate(t.root, k, v) - if err != nil { - return err - } - - switch len(res) { - case 1: - t.root = res[0].Ptr - case 2: - root := &PointerNode{ - keys: [][]byte{res[0].Key, res[1].Key}, - ptrs: []uint64{res[0].Ptr, res[1].Ptr}, - } - - t.root, err = t.alloc(root) - if err != nil { - return err - } - default: - return errMalformedRecurse - } - return nil -} - -func (t *Tree) recursiveUpdate(ptr uint64, k, v []byte) ([]recurseResult, error) { - cur, err := t.read(ptr) - if err != nil { - return nil, err - } - if err = t.free(ptr); err != nil { - return nil, err - } - - switch cur.Type() { - case nodePointer: - pointer, ok := cur.(*PointerNode) - if !ok { - return nil, errNodeAssert - } - - idx, _ := pointer.Find(k) - ptr, err = pointer.PtrAt(idx) - if err != nil { - return nil, err - } - - res, err := t.recursiveUpdate(ptr, k, v) - if err != nil { - return nil, err - } - - if err := pointer.Update(res[0].Key, res[0].Ptr); err != nil { - return nil, err - } - - if len(res) > 1 { - if err := pointer.Insert(res[1].Key, res[1].Ptr); err != nil { - return nil, err - } - } - - case nodeLeaf: - leaf, ok := cur.(*LeafNode) - if !ok { - return nil, errNodeAssert - } - - if err := leaf.Update(k, v); err != nil { - return nil, err - } - cur = leaf - - default: - return nil, errInvalNodeType - } - - if cur.Size() > t.maxNodeSize { - l, r := cur.Split() - - lPtr, err := t.alloc(l) - if err != nil { - return nil, err - } - - rPtr, err := t.alloc(r) - if err != nil { - return nil, err - } - - nK, err := r.Key(0) - if err != nil { - return nil, err - } - - return []recurseResult{{Key: k, Ptr: lPtr}, {Key: nK, Ptr: rPtr}}, nil - } - - ptr, err = t.alloc(cur) - if err != nil { - return nil, err - } - - return []recurseResult{{Key: k, Ptr: ptr}}, nil -} - -func (t *Tree) Delete(k []byte) error { - return nil -} - -func (t *Tree) recursiveDelete(ptr uint64, k, v []byte) (Node, error) { - cur, err := t.read(ptr) - if err != nil { - return nil, err - } - if err = t.free(ptr); err != nil { - return nil, err - } - - switch cur.Type() { - case nodePointer: - pointer, ok := cur.(*PointerNode) - if !ok { - return nil, errNodeAssert - } - - idx, _ := pointer.Find(k) - ptr, err = pointer.PtrAt(idx) - if err != nil { - return nil, err - } - - child, err := t.recursiveDelete(ptr, k, v) - if err != nil { - return nil, err - } - - if err = t.tryMergeNeighbors(pointer, child, idx); err != nil { - return nil, err - } - - case nodeLeaf: - if err := cur.Delete(k); err != nil { - return nil, err - } - - default: - return nil, errInvalNodeType - } - - ptr, err = t.alloc(cur) - if err != nil { - return nil, err - } - - return cur, nil -} - -func (t *Tree) tryMergeNeighbors(parent *PointerNode, child Node, idx int) error { - if child.Size() < t.maxNodeSize/3 { - if idx > 0 { - lPtr, err := parent.PtrAt(idx - 1) - if err != nil { - return err - } - - l, err := t.read(lPtr) - if err != nil { - return err - } - - if child.Size()+l.Size() < t.maxNodeSize { - if err := l.Merge(child); err != nil { - return err - } - child = l - } - } - if idx < parent.NKeys()-1 { - rPtr, err := parent.PtrAt(idx + 1) - if err != nil { - return err - } - - r, err := t.read(rPtr) - if err != nil { - return err - } - - if child.Size()+r.Size() < t.maxNodeSize { - if err := child.Merge(r); err != nil { - return err - } - } - } - } - - return nil -} - -func (t *Tree) cellFits(k, v []byte) bool { - return 4+len(k)+len(v) <= t.maxNodeSize-NodeHeader || 2+len(k)+8 <= t.maxNodeSize-NodeHeader -} diff --git a/internal/tree/tree_test.go b/internal/tree/tree_test.go deleted file mode 100644 index ae1da0e..0000000 --- a/internal/tree/tree_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package tree - -import ( - "bytes" - "errors" - "maps" - "slices" - "testing" -) - -func mockTree() (Tree, map[uint64]Node) { - mock := map[uint64]Node{ - 0: &PointerNode{keys: [][]byte{{'a'}, {'j'}, {'s'}}, ptrs: []uint64{1, 2, 3}}, - 1: &PointerNode{keys: [][]byte{{'a'}, {'d'}, {'g'}}, ptrs: []uint64{4, 5, 6}}, - 2: &PointerNode{keys: [][]byte{{'j'}, {'m'}, {'p'}}, ptrs: []uint64{7, 8, 9}}, - 3: &PointerNode{keys: [][]byte{{'s'}, {'v'}, {'y'}}, ptrs: []uint64{10, 11, 12}}, - 4: &LeafNode{keys: [][]byte{{'a'}, {'b'}, {'c'}}, vals: [][]byte{{'a'}, {'b'}, {'c'}}}, - 5: &LeafNode{keys: [][]byte{{'d'}, {'e'}, {'f'}}, vals: [][]byte{{'d'}, {'e'}, {'f'}}}, - 6: &LeafNode{keys: [][]byte{{'g'}, {'h'}, {'i'}}, vals: [][]byte{{'g'}, {'h'}, {'i'}}}, - 7: &LeafNode{keys: [][]byte{{'j'}, {'k'}, {'l'}}, vals: [][]byte{{'j'}, {'k'}, {'l'}}}, - 8: &LeafNode{keys: [][]byte{{'m'}, {'n'}, {'o'}}, vals: [][]byte{{'m'}, {'n'}, {'o'}}}, - 9: &LeafNode{keys: [][]byte{{'p'}, {'q'}, {'r'}}, vals: [][]byte{{'p'}, {'q'}, {'r'}}}, - 10: &LeafNode{keys: [][]byte{{'s'}, {'t'}, {'u'}}, vals: [][]byte{{'s'}, {'t'}, {'u'}}}, - 11: &LeafNode{keys: [][]byte{{'v'}, {'w'}, {'x'}}, vals: [][]byte{{'v'}, {'w'}, {'x'}}}, - 12: &LeafNode{keys: [][]byte{{'y'}, {'z'}, {'{'}}, vals: [][]byte{{'y'}, {'z'}, {'{'}}}, - } - - next := uint64(13) - - return Tree{ - root: 0, - read: func(ptr uint64) (Node, error) { - n, ok := (mock)[ptr] - if !ok { - return nil, errors.New("mock get") - } - return n, nil - }, - alloc: func(n Node) (uint64, error) { - next++ - (mock)[next-1] = n - return next - 1, nil - }, - free: func(ptr uint64) error { - delete(mock, ptr) - return nil - }, - maxNodeSize: 39, - }, mock -} - -func TestTreeGet(t *testing.T) { - cases := []struct { - name string - k []byte - res []byte - err error - }{ - { - name: "get first k-v", - k: []byte{'a'}, - res: []byte{'a'}, - }, - { - name: "get last k-v", - k: []byte{'{'}, - res: []byte{'{'}, - }, - { - name: "get middle k-v", k: []byte{'n'}, - res: []byte{'n'}, - }, - { - name: "failed non existing key", - k: []byte{'}'}, - res: nil, - err: errKeyNotExists, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - tree, _ := mockTree() - res, err := tree.Get(c.k) - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } - if !bytes.Equal(res, c.res) { - t.Errorf("expected val %v, got %v", c.res, res) - } - }) - } -} - -func TestInsert(t *testing.T) { - cases := []struct { - name string - k []byte - v []byte - res map[uint64]Node - err error - }{ - { - name: "insert without split", - k: []byte{'n', 'a'}, - v: []byte{'n', 'a'}, - res: map[uint64]Node{ - 1: &PointerNode{keys: [][]byte{{'a'}, {'d'}, {'g'}}, ptrs: []uint64{4, 5, 6}}, - 3: &PointerNode{keys: [][]byte{{'s'}, {'v'}, {'y'}}, ptrs: []uint64{10, 11, 12}}, - 4: &LeafNode{keys: [][]byte{{'a'}, {'b'}, {'c'}}, vals: [][]byte{{'a'}, {'b'}, {'c'}}}, - 5: &LeafNode{keys: [][]byte{{'d'}, {'e'}, {'f'}}, vals: [][]byte{{'d'}, {'e'}, {'f'}}}, - 6: &LeafNode{keys: [][]byte{{'g'}, {'h'}, {'i'}}, vals: [][]byte{{'g'}, {'h'}, {'i'}}}, - 7: &LeafNode{keys: [][]byte{{'j'}, {'k'}, {'l'}}, vals: [][]byte{{'j'}, {'k'}, {'k'}}}, - 9: &LeafNode{keys: [][]byte{{'p'}, {'q'}, {'r'}}, vals: [][]byte{{'p'}, {'q'}, {'r'}}}, - 10: &LeafNode{keys: [][]byte{{'s'}, {'t'}, {'u'}}, vals: [][]byte{{'s'}, {'t'}, {'u'}}}, - 11: &LeafNode{keys: [][]byte{{'v'}, {'w'}, {'x'}}, vals: [][]byte{{'v'}, {'w'}, {'x'}}}, - 12: &LeafNode{keys: [][]byte{{'y'}, {'z'}, {'{'}}, vals: [][]byte{{'y'}, {'z'}, {'{'}}}, - 13: &LeafNode{keys: [][]byte{{'m'}, {'n'}, {'n', 'a'}, {'o'}}, vals: [][]byte{{'m'}, {'n'}, {'n', 'a'}, {'o'}}}, - 14: &PointerNode{keys: [][]byte{{'j'}, {'m'}, {'p'}}, ptrs: []uint64{7, 13, 9}}, - 15: &PointerNode{keys: [][]byte{{'a'}, {'j'}, {'s'}}, ptrs: []uint64{1, 14, 3}}, - }, - }, - { - name: "insert with splits", - k: []byte{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, - v: []byte{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, - res: map[uint64]Node{ - 1: &PointerNode{keys: [][]byte{{'a'}, {'d'}, {'g'}}, ptrs: []uint64{4, 5, 6}}, - 3: &PointerNode{keys: [][]byte{{'s'}, {'v'}, {'y'}}, ptrs: []uint64{10, 11, 12}}, - 4: &LeafNode{keys: [][]byte{{'a'}, {'b'}, {'c'}}, vals: [][]byte{{'a'}, {'b'}, {'c'}}}, - 5: &LeafNode{keys: [][]byte{{'d'}, {'e'}, {'f'}}, vals: [][]byte{{'d'}, {'e'}, {'f'}}}, - 6: &LeafNode{keys: [][]byte{{'g'}, {'h'}, {'i'}}, vals: [][]byte{{'g'}, {'h'}, {'i'}}}, - 7: &LeafNode{keys: [][]byte{{'j'}, {'k'}, {'l'}}, vals: [][]byte{{'j'}, {'k'}, {'k'}}}, - 9: &LeafNode{keys: [][]byte{{'p'}, {'q'}, {'r'}}, vals: [][]byte{{'p'}, {'q'}, {'r'}}}, - 10: &LeafNode{keys: [][]byte{{'s'}, {'t'}, {'u'}}, vals: [][]byte{{'s'}, {'t'}, {'u'}}}, - 11: &LeafNode{keys: [][]byte{{'v'}, {'w'}, {'x'}}, vals: [][]byte{{'v'}, {'w'}, {'x'}}}, - 12: &LeafNode{keys: [][]byte{{'y'}, {'z'}, {'{'}}, vals: [][]byte{{'y'}, {'z'}, {'{'}}}, - 13: &LeafNode{keys: [][]byte{{'m'}, {'n'}}, vals: [][]byte{{'m'}, {'n'}}}, - 14: &LeafNode{keys: [][]byte{{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, {'o'}}, vals: [][]byte{{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, {'o'}}}, - 15: &PointerNode{keys: [][]byte{{'j'}, {'m'}}, ptrs: []uint64{7, 13}}, - 16: &PointerNode{keys: [][]byte{{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, {'p'}}, ptrs: []uint64{14, 9}}, - 17: &PointerNode{keys: [][]byte{{'a'}, {'j'}}, ptrs: []uint64{1, 15}}, - 18: &PointerNode{keys: [][]byte{{'n', 'a', 'a', 'a', 'a', 'a', 'a'}, {'s'}}, ptrs: []uint64{16, 3}}, - 19: &PointerNode{keys: [][]byte{{'a'}, {'n', 'a', 'a', 'a', 'a', 'a', 'a'}}, ptrs: []uint64{17, 18}}, - }, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - tree, mock := mockTree() - err := tree.Insert(c.k, c.v) - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } else if !maps.EqualFunc(c.res, mock, func(n1, n2 Node) bool { - if n1.Type() != n2.Type() { - return false - } - switch n1.Type() { - case nodeLeaf: - l1 := n1.(*LeafNode) - l2 := n2.(*LeafNode) - return slices.EqualFunc(l1.keys, l2.keys, bytes.Equal) && slices.EqualFunc(l1.keys, l2.keys, bytes.Equal) - case nodePointer: - p1 := n1.(*PointerNode) - p2 := n2.(*PointerNode) - return slices.EqualFunc(p1.keys, p2.keys, bytes.Equal) && slices.EqualFunc(p1.keys, p2.keys, bytes.Equal) - } - return false - }) { - t.Errorf("expected map %v, got %v", c.res, mock) - } - }) - } -} diff --git a/pkg/parse/parse.go b/pkg/parse/parse.go new file mode 100644 index 0000000..b927dc0 --- /dev/null +++ b/pkg/parse/parse.go @@ -0,0 +1,84 @@ +package parse + +import ( + "fmt" + "io" + "text/scanner" +) + +type parseState func(p *parser) (parseState, error) + +type parser struct { + q Statement + scan *scanner.Scanner +} + +func Parse(query io.Reader) ([]Statement, error) { + p := parser{ + scan: new(scanner.Scanner).Init(query), + } + + var ( + state = parseOperator + err error + ) + for state != nil { + if state, err = state(&p); err != nil { + return nil, err + } + } + + return nil, nil +} + +func parseOperator(p *parser) (parseState, error) { + tok := p.scan.Scan() + if tok == scanner.Ident { + if state, ok := operators[p.scan.TokenText()]; ok { + return state, nil + } + } + return nil, fmt.Errorf("expected operator, got %s", p.scan.TokenText()) +} + +func parseGet(p *parser) (parseState, error) { + tok := p.scan.Scan() + if tok != '(' { + return nil, fmt.Errorf("expected '(', got %s", p.scan.TokenText()) + } + + tok = p.scan.Scan() + if tok != scanner.Ident { + return nil, fmt.Errorf("expected table name identifier, got %s", p.scan.TokenText()) + } + + tok = p.scan.Scan() + if tok != ',' { + return nil, fmt.Errorf("expected ',', got %s", p.scan.TokenText()) + } + + return nil, nil +} + +func parseInsert(p *parser) (parseState, error) { + return nil, nil +} + +func parseUpdate(p *parser) (parseState, error) { + return nil, nil +} + +func parseDelete(p *parser) (parseState, error) { + return nil, nil +} + +func parseCreate(p *parser) (parseState, error) { + return nil, nil +} + +func parseCondition(p *parser) (parseState, error) { + for tok := p.scan.Scan(); tok != scanner.EOF; tok = p.scan.Scan() { + break + } + return nil, nil +} diff --git a/internal/parse/parser_test.go b/pkg/parse/parse_test.go similarity index 100% rename from internal/parse/parser_test.go rename to pkg/parse/parse_test.go diff --git a/pkg/parse/statement.go b/pkg/parse/statement.go new file mode 100644 index 0000000..03fed50 --- /dev/null +++ b/pkg/parse/statement.go @@ -0,0 +1,7 @@ +package parse + +type Statement interface{} + +type GetStatement struct { + Table string +} diff --git a/pkg/parse/token.go b/pkg/parse/token.go new file mode 100644 index 0000000..fa2868e --- /dev/null +++ b/pkg/parse/token.go @@ -0,0 +1,47 @@ +package parse + +type tokenType uint + +const ( + tokError tokenType = iota + tokEOF + tokNumber + tokString + tokComment + tokIdent + // keywords + tokDot // . + tokComma // , + tokLeftPar // ( + tokRightPar // ) + // operator keyword + tokOperator // only used for separation of operators + tokGet // get + tokDelete // delete + tokUpdate // update + tokInsert // insert +) + +var operators map[string]parseState = map[string]parseState{ + "get": parseGet, + "delete": parseDelete, + "update": parseUpdate, + "insert": parseInsert, + "create": parseCreate, +} + +var keywords map[string]tokenType = map[string]tokenType{ + ".": tokDot, + ",": tokComma, + "(": tokLeftPar, + ")": tokRightPar, + "get": tokGet, + "delete": tokDelete, + "update": tokUpdate, + "insert": tokInsert, +} + +type token struct { + typ tokenType + val string +} From 0bdce2856383b1f41d544651c112b13b628175e1 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Fri, 24 Jan 2025 22:45:21 +0100 Subject: [PATCH 12/51] chore: cleanup --- .dockerignore | 7 -- Dockerfile | 12 --- Makefile | 59 ------------- docs/.nojekyll | 0 docs/_coverpage.md | 11 --- docs/_media/PavoSQL.svg | 95 -------------------- docs/_sidebar.md | 8 -- docs/index.html | 32 ------- docs/quickstart.md | 1 - internal/btree/btree.go | 148 ------------------------------- internal/btree/interface.go | 22 ----- internal/btree/node/leaf.go | 157 --------------------------------- internal/btree/node/pointer.go | 151 ------------------------------- internal/btree/node/type.go | 26 ------ pkg/driver/driver.go | 1 - pkg/parse/parse_test.go | 1 - pkg/parse/statement.go | 7 -- pkg/parse/token.go | 47 ---------- pkg/stack/stack.go | 31 ------- pkg/stack/stack_test.go | 68 -------------- 20 files changed, 884 deletions(-) delete mode 100644 .dockerignore delete mode 100644 Dockerfile delete mode 100644 Makefile delete mode 100644 docs/.nojekyll delete mode 100644 docs/_coverpage.md delete mode 100644 docs/_media/PavoSQL.svg delete mode 100644 docs/_sidebar.md delete mode 100644 docs/index.html delete mode 100644 docs/quickstart.md delete mode 100644 internal/btree/btree.go delete mode 100644 internal/btree/interface.go delete mode 100644 internal/btree/node/leaf.go delete mode 100644 internal/btree/node/pointer.go delete mode 100644 internal/btree/node/type.go delete mode 100644 pkg/driver/driver.go delete mode 100644 pkg/parse/parse_test.go delete mode 100644 pkg/parse/statement.go delete mode 100644 pkg/parse/token.go delete mode 100644 pkg/stack/stack.go delete mode 100644 pkg/stack/stack_test.go diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 4b30371..0000000 --- a/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -.git -LICENSE -README.md -docs -pavod -pavosql -*_test.go diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 50cf86e..0000000 --- a/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM golang:1.21.0 - -WORKDIR /pavosql - -COPY ./ ./ -RUN go mod download - -RUN CGO_ENABLED=0 GOOS=linux go build ./cmd/pavosql - -EXPOSE 1758 - -CMD [ "./pavosql" ] diff --git a/Makefile b/Makefile deleted file mode 100644 index 200deac..0000000 --- a/Makefile +++ /dev/null @@ -1,59 +0,0 @@ -build: - @go build -o bin/pavosql cmd/pavosql/main.go - -run: - @go run cmd/pavosql/main.go - -# Docs - -docsify: - @python -m http.server 3000 -d docs - -# Testing - -test: - @go test ./... - -test.cover: - @go test -coverprofile cover.out ./... - -test.cover.show: test.cover - @go tool cover -html cover.out - -## Tree test - -test.tree: - @go test ./internal/tree/... - -test.tree.cover: - @go test -coverprofile tree_cover.out ./internal/tree/... - -test.tree.cover.show: test.tree.cover - @go tool cover -html tree_cover.out - -## Parse test - -test.parse: - @go test ./internal/parse/... - -test.parse.cover: - @go test -coverprofile parse_cover.out ./internal/parse/... - -test.parse.cover.show: test.parse.cover - @go tool cover -html parse_cover.out - -## Stack test - -test.stack: - @go test ./pkg/stack/... - -test.stack.cover: - @go test -coverprofile stack_cover.out ./pkg/stack/... - -test.stack.cover.show: test.stack.cover - @go tool cover -html stack_cover.out - -# Cleanup - -cleancover: - @rm *cover.out diff --git a/docs/.nojekyll b/docs/.nojekyll deleted file mode 100644 index e69de29..0000000 diff --git a/docs/_coverpage.md b/docs/_coverpage.md deleted file mode 100644 index 461d4f9..0000000 --- a/docs/_coverpage.md +++ /dev/null @@ -1,11 +0,0 @@ - -logo - -# PavoSQL - -> A simple SQL Database written in pure Go. - -[Getting started](#pavosql) -[Github](https://github.com/gKits/PavoSQL) - -![color](#000064ff) diff --git a/docs/_media/PavoSQL.svg b/docs/_media/PavoSQL.svg deleted file mode 100644 index e386710..0000000 --- a/docs/_media/PavoSQL.svg +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/_sidebar.md b/docs/_sidebar.md deleted file mode 100644 index 9c86b26..0000000 --- a/docs/_sidebar.md +++ /dev/null @@ -1,8 +0,0 @@ -- [Home](#pavosql) - -- Getting started - - [Quickstart](quickstart.md) - -- Syntax - -- Docs diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 96b58da..0000000 --- a/docs/index.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - -
-

Loading...

-
- - - - - - diff --git a/docs/quickstart.md b/docs/quickstart.md deleted file mode 100644 index acb9843..0000000 --- a/docs/quickstart.md +++ /dev/null @@ -1 +0,0 @@ -# Quickstart diff --git a/internal/btree/btree.go b/internal/btree/btree.go deleted file mode 100644 index abb49de..0000000 --- a/internal/btree/btree.go +++ /dev/null @@ -1,148 +0,0 @@ -package btree - -import ( - "io" - - "github.com/gKits/PavoSQL/internal/btree/node" -) - -type BTree struct { - root uint64 - nodeSize int - reader io.ReaderAt - writer io.WriterAt -} - -func (bt *BTree) Get(key []byte) ([]byte, error) { - cur, _ := bt.getNode(bt.root) - - for { - i, exists := cur.Search(key) - - switch cur.Type() { - case node.TypeLeaf: - if !exists { - return nil, nil - } - leaf, ok := cur.(*node.Leaf) - if !ok { - return nil, nil - } - val, err := leaf.Val(i) - if err != nil { - return nil, err - } - return val, nil - - case node.TypePointer: - pointer, ok := cur.(*node.Pointer) - if !ok { - return nil, nil - } - ptr, err := pointer.Ptr(i) - if err != nil { - return nil, err - } - cur, err = bt.getNode(ptr) - if err != nil { - return nil, err - } - continue - - default: - return nil, nil - } - } -} - -func (bt *BTree) Insert(key, val []byte) error { - cur, _ := bt.getNode(bt.root) - - for { - i, exists := cur.Search(key) - if exists { - return nil - } - - switch cur.Type() { - case node.TypeLeaf: - leaf, ok := cur.(*node.Leaf) - if !ok { - return nil - } - if err := leaf.Insert(i, key, val); err != nil { - return err - } - - case node.TypePointer: - pointer, ok := cur.(*node.Pointer) - if !ok { - return nil - } - ptr, err := pointer.Ptr(i) - if err != nil { - return err - } - cur, err = bt.getNode(ptr) - if err != nil { - return err - } - continue - - default: - return nil - } - break - } - - if cur.Size() <= bt.nodeSize { - return nil - } - - // TODO: split node and update path - - return nil -} - -func (bt *BTree) getNode(ptr uint64) (noder, error) { - b := make([]byte, bt.nodeSize) - if n, err := bt.reader.ReadAt(b, int64(ptr)); err != nil { - return nil, err - } else if n != bt.nodeSize { - return nil, nil - } - - switch node.TypeOf(b) { - case node.TypePointer: - return node.NewPointer(b) - case node.TypeLeaf: - return node.NewLeaf(b) - default: - return nil, nil - } -} - -func (bt *BTree) writeNode(n noder, ptr uint64) error { - b := make([]byte, bt.nodeSize) - nB, err := n.Bytes() - if err != nil { - return err - } - copy(b, nB) - if n, err := bt.writer.WriteAt(b, int64(ptr)); err != nil { - return err - } else if n != bt.nodeSize { - return nil - } - - return nil -} - -func (bt *BTree) freeNode(ptr uint64) error { - if n, err := bt.writer.WriteAt(make([]byte, bt.nodeSize), int64(ptr)); err != nil { - return err - } else if n != bt.nodeSize { - return nil - } - return nil -} diff --git a/internal/btree/interface.go b/internal/btree/interface.go deleted file mode 100644 index d2088db..0000000 --- a/internal/btree/interface.go +++ /dev/null @@ -1,22 +0,0 @@ -package btree - -import ( - "io" - - "github.com/gKits/PavoSQL/internal/btree/node" -) - -type noder interface { - Type() node.Type - Len() int - Size() int - Key(i int) ([]byte, error) - Search(key []byte) (int, bool) - Bytes() ([]byte, error) -} - -type Backend interface { - GetNode(off uint64) (noder, error) - NewReader() io.ReaderAt - NewWriter() io.WriterAt -} diff --git a/internal/btree/node/leaf.go b/internal/btree/node/leaf.go deleted file mode 100644 index 1b33c7b..0000000 --- a/internal/btree/node/leaf.go +++ /dev/null @@ -1,157 +0,0 @@ -package node - -import ( - "bytes" - "encoding/binary" - "slices" -) - -type Leaf struct { - keys [][]byte - vals [][]byte -} - -func NewLeaf(d []byte) (*Leaf, error) { - leaf := new(Leaf) - if Type(binary.LittleEndian.Uint16(d[:])) != TypePointer { - return nil, ErrWrongType - } - length := binary.LittleEndian.Uint16(d[2:]) - leaf.keys = make([][]byte, length) - leaf.vals = make([][]byte, length) - off := 4 - for i := range length { - if off >= len(d) { - return nil, ErrNodeDataMalformed - } - - kLen := int(binary.LittleEndian.Uint16(d[off:])) - off += 2 - - leaf.keys[i] = d[off : off+kLen] - off += kLen - - vLen := int(binary.LittleEndian.Uint16(d[off:])) - off += 2 - - leaf.vals[i] = d[off : off+vLen] - off += vLen - } - return leaf, nil -} - -func (leaf *Leaf) Type() Type { - return TypeLeaf -} - -func (leaf *Leaf) Len() int { - return len(leaf.keys) -} - -func (leaf *Leaf) Size() int { - return 2 + // size of type identifier - 2 + // size of number of elements - 2*len(leaf.keys) + // size of all key size prefixes - len(slices.Concat(leaf.keys...)) + // size of all keys - len(leaf.vals) + // size of all value size prefixes - len(slices.Concat(leaf.vals...)) // size of all values -} - -func (leaf *Leaf) Key(i int) ([]byte, error) { - if i >= len(leaf.keys) || i < 0 { - return nil, ErrIndexOutOfBounds - } - return leaf.keys[i], nil -} - -func (leaf *Leaf) Val(i int) ([]byte, error) { - if i >= len(leaf.vals) || i < 0 { - return nil, ErrIndexOutOfBounds - } - return leaf.vals[i], nil -} - -func (leaf *Leaf) KeyVal(i int) ([]byte, []byte, error) { - if i >= len(leaf.keys) || i >= len(leaf.vals) || i < 0 { - return nil, nil, ErrIndexOutOfBounds - } - return leaf.keys[i], leaf.vals[i], nil -} - -func (leaf *Leaf) Search(key []byte) (int, bool) { - return slices.BinarySearchFunc(leaf.keys, key, bytes.Compare) -} - -func (leaf *Leaf) Insert(i int, key, val []byte) error { - if i > len(leaf.keys) || i > len(leaf.vals) || i < 0 { - return ErrIndexOutOfBounds - } - leaf.keys = slices.Insert(leaf.keys, i, key) - leaf.vals = slices.Insert(leaf.vals, i, val) - return nil -} - -func (leaf *Leaf) Update(i int, val []byte) error { - if i >= len(leaf.vals) || i < 0 { - return ErrIndexOutOfBounds - } - leaf.vals[i] = val - return nil -} - -func (leaf *Leaf) Delete(i int) error { - if i >= len(leaf.keys) || i >= len(leaf.vals) || i < 0 { - return ErrIndexOutOfBounds - } - leaf.keys = slices.Delete(leaf.keys, i, i) - leaf.vals = slices.Delete(leaf.vals, i, i) - return nil -} - -func (leaf *Leaf) Split() (Leaf, Leaf, error) { - if len(leaf.keys) <= 1 || len(leaf.vals) <= 1 { - return Leaf{}, Leaf{}, ErrCannotSplit - } - left := Leaf{ - keys: leaf.keys[:len(leaf.keys)/2], - vals: leaf.vals[:len(leaf.vals)/2], - } - right := Leaf{ - keys: leaf.keys[len(leaf.keys)/2:], - vals: leaf.vals[len(leaf.vals)/2:], - } - - return left, right, nil -} - -func (leaf *Leaf) Append(toAdd Leaf) error { - if bytes.Compare(leaf.keys[0], toAdd.keys[0]) >= 0 { - return ErrCannotAppend - } - leaf.keys = append(leaf.keys, toAdd.keys...) - leaf.vals = append(leaf.vals, toAdd.vals...) - return nil -} - -func (leaf *Leaf) Bytes() ([]byte, error) { - b := make([]byte, leaf.Size()) - - binary.LittleEndian.PutUint16(b[:], uint16(TypeLeaf)) - binary.LittleEndian.PutUint16(b[2:], uint16(len(leaf.keys))) - off := 4 - for i, key := range leaf.keys { - val := leaf.vals[i] - - binary.LittleEndian.PutUint16(b[off:], uint16(len(key))) - off += 2 - copy(b[off:], key) - off += len(key) - - binary.LittleEndian.PutUint16(b[off:], uint16(len(val))) - off += 2 - copy(b[off:], val) - off += len(val) - - } - return b, nil -} diff --git a/internal/btree/node/pointer.go b/internal/btree/node/pointer.go deleted file mode 100644 index 13bc1e1..0000000 --- a/internal/btree/node/pointer.go +++ /dev/null @@ -1,151 +0,0 @@ -package node - -import ( - "bytes" - "encoding/binary" - "slices" -) - -type Pointer struct { - keys [][]byte - ptrs []uint64 -} - -func NewPointer(d []byte) (*Pointer, error) { - pointer := new(Pointer) - if Type(binary.LittleEndian.Uint16(d[:])) != TypePointer { - return nil, ErrWrongType - } - length := binary.LittleEndian.Uint16(d[2:]) - pointer.keys = make([][]byte, length) - pointer.ptrs = make([]uint64, length) - off := 4 - for i := range length { - if off >= len(d) { - return nil, ErrNodeDataMalformed - } - - kLen := int(binary.LittleEndian.Uint16(d[off:])) - off += 2 - - pointer.keys[i] = d[off : off+kLen] - off += kLen - - pointer.ptrs[i] = binary.LittleEndian.Uint64(d[off:]) - off += 8 - } - return pointer, nil -} - -func (pointer *Pointer) Type() Type { - return TypePointer -} - -func (pointer *Pointer) Len() int { - return len(pointer.keys) -} - -func (pointer *Pointer) Size() int { - return 2 + // size of type identifier - 2 + // size of number of elements - 2*len(pointer.keys) + // size of all key size prefixes - len(slices.Concat(pointer.keys...)) + // size of all keys - len(pointer.ptrs)*8 // size of all pointers -} - -func (pointer *Pointer) Key(i int) ([]byte, error) { - if i >= len(pointer.keys) || i < 0 { - return nil, ErrIndexOutOfBounds - } - return pointer.keys[i], nil -} - -func (pointer *Pointer) Ptr(i int) (uint64, error) { - if i >= len(pointer.ptrs) || i < 0 { - return 0, ErrIndexOutOfBounds - } - return pointer.ptrs[i], nil -} - -func (pointer *Pointer) KeyPtr(i int) ([]byte, uint64, error) { - if i >= len(pointer.keys) || i >= len(pointer.ptrs) || i < 0 { - return nil, 0, ErrIndexOutOfBounds - } - return pointer.keys[i], pointer.ptrs[i], nil -} - -func (pointer *Pointer) Search(key []byte) (int, bool) { - return slices.BinarySearchFunc(pointer.keys, key, bytes.Compare) -} - -func (pointer *Pointer) Insert(i int, key []byte, ptr uint64) error { - if i > len(pointer.keys) || i > len(pointer.ptrs) || i < 0 { - return ErrIndexOutOfBounds - } - pointer.keys = slices.Insert(pointer.keys, i, key) - pointer.ptrs = slices.Insert(pointer.ptrs, i, ptr) - return nil -} - -func (pointer *Pointer) Update(i int, ptr uint64) error { - if i >= len(pointer.ptrs) || i < 0 { - return ErrIndexOutOfBounds - } - pointer.ptrs[i] = ptr - return nil -} - -func (pointer *Pointer) Delete(i int) error { - if i >= len(pointer.keys) || i >= len(pointer.ptrs) || i < 0 { - return ErrIndexOutOfBounds - } - pointer.keys = slices.Delete(pointer.keys, i, i) - pointer.ptrs = slices.Delete(pointer.ptrs, i, i) - return nil -} - -func (pointer *Pointer) Split() (Pointer, Pointer, error) { - if len(pointer.keys) <= 1 || len(pointer.ptrs) <= 1 { - return Pointer{}, Pointer{}, ErrCannotSplit - } - left := Pointer{ - keys: pointer.keys[:len(pointer.keys)/2], - ptrs: pointer.ptrs[:len(pointer.ptrs)/2], - } - right := Pointer{ - keys: pointer.keys[len(pointer.keys)/2:], - ptrs: pointer.ptrs[len(pointer.ptrs)/2:], - } - - return left, right, nil -} - -func (pointer *Pointer) Append(toAdd Pointer) error { - if bytes.Compare(pointer.keys[0], toAdd.keys[0]) >= 0 { - return ErrCannotAppend - } - pointer.keys = append(pointer.keys, toAdd.keys...) - pointer.ptrs = append(pointer.ptrs, toAdd.ptrs...) - return nil -} - -func (pointer *Pointer) Bytes() ([]byte, error) { - b := make([]byte, pointer.Size()) - - binary.LittleEndian.PutUint16(b[:], uint16(TypePointer)) - binary.LittleEndian.PutUint16(b[2:], uint16(len(pointer.keys))) - off := 4 - for i, key := range pointer.keys { - ptr := pointer.ptrs[i] - - binary.LittleEndian.PutUint16(b[off:], uint16(len(key))) - off += 2 - copy(b[off:], key) - off += len(key) - - binary.LittleEndian.PutUint64(b[off:], ptr) - off += 8 - - } - return b, nil -} diff --git a/internal/btree/node/type.go b/internal/btree/node/type.go deleted file mode 100644 index 745cfb6..0000000 --- a/internal/btree/node/type.go +++ /dev/null @@ -1,26 +0,0 @@ -package node - -import ( - "encoding/binary" - "errors" -) - -var ( - ErrIndexOutOfBounds = errors.New("index is out of bounds") - ErrCannotSplit = errors.New("cannot split node with less than 2 entries") - ErrCannotAppend = errors.New("cannot append node with first key gte last key of current node") - ErrWrongType = errors.New("wrong type identifier") - ErrNodeDataMalformed = errors.New("node data is malformed") -) - -type Type uint16 - -const ( - typeInvalid Type = iota - TypePointer - TypeLeaf -) - -func TypeOf(b []byte) Type { - return Type(binary.LittleEndian.Uint16(b)) -} diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go deleted file mode 100644 index bce7c46..0000000 --- a/pkg/driver/driver.go +++ /dev/null @@ -1 +0,0 @@ -package driver diff --git a/pkg/parse/parse_test.go b/pkg/parse/parse_test.go deleted file mode 100644 index fe2554d..0000000 --- a/pkg/parse/parse_test.go +++ /dev/null @@ -1 +0,0 @@ -package parse diff --git a/pkg/parse/statement.go b/pkg/parse/statement.go deleted file mode 100644 index 03fed50..0000000 --- a/pkg/parse/statement.go +++ /dev/null @@ -1,7 +0,0 @@ -package parse - -type Statement interface{} - -type GetStatement struct { - Table string -} diff --git a/pkg/parse/token.go b/pkg/parse/token.go deleted file mode 100644 index fa2868e..0000000 --- a/pkg/parse/token.go +++ /dev/null @@ -1,47 +0,0 @@ -package parse - -type tokenType uint - -const ( - tokError tokenType = iota - tokEOF - tokNumber - tokString - tokComment - tokIdent - // keywords - tokDot // . - tokComma // , - tokLeftPar // ( - tokRightPar // ) - // operator keyword - tokOperator // only used for separation of operators - tokGet // get - tokDelete // delete - tokUpdate // update - tokInsert // insert -) - -var operators map[string]parseState = map[string]parseState{ - "get": parseGet, - "delete": parseDelete, - "update": parseUpdate, - "insert": parseInsert, - "create": parseCreate, -} - -var keywords map[string]tokenType = map[string]tokenType{ - ".": tokDot, - ",": tokComma, - "(": tokLeftPar, - ")": tokRightPar, - "get": tokGet, - "delete": tokDelete, - "update": tokUpdate, - "insert": tokInsert, -} - -type token struct { - typ tokenType - val string -} diff --git a/pkg/stack/stack.go b/pkg/stack/stack.go deleted file mode 100644 index a682af9..0000000 --- a/pkg/stack/stack.go +++ /dev/null @@ -1,31 +0,0 @@ -package stack - -import "errors" - -type Stack[T any] []T - -var ErrStackEmpty = errors.New("cannot pop, stack is empty") - -func (s Stack[T]) Len() int { - return len(s) -} - -func (s *Stack[T]) Push(t T) { - *s = append(*s, t) -} - -func (s *Stack[T]) Pop() (res T, err error) { - if s.Len() < 1 { - return res, ErrStackEmpty - } - res = (*s)[s.Len()-1] - *s = (*s)[:s.Len()-1] - return res, nil -} - -func (s Stack[T]) Peek() (res T, err error) { - if s.Len() < 1 { - return res, ErrStackEmpty - } - return s[s.Len()-1], nil -} diff --git a/pkg/stack/stack_test.go b/pkg/stack/stack_test.go deleted file mode 100644 index 8ad6719..0000000 --- a/pkg/stack/stack_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package stack - -import ( - "slices" - "testing" -) - -func TestPush(t *testing.T) { - cases := []struct { - name string - s Stack[int] - in int - res Stack[int] - }{ - { - name: "successful push", - s: Stack[int]{0, 1, 2, 3}, - in: 4, - res: Stack[int]{0, 1, 2, 3, 4}, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - c.s.Push(c.in) - if !slices.Equal(c.s, c.res) { - t.Errorf("expected stack %v, got %v", c.res, c.s) - } - }) - } -} - -func TestPop(t *testing.T) { - cases := []struct { - name string - in Stack[int] - res int - out Stack[int] - err error - }{ - { - name: "successful pop", - in: Stack[int]{0, 1, 2, 3, 4}, - res: 4, - out: Stack[int]{0, 1, 2, 3}, - }, - { - name: "failed pop", - in: Stack[int]{}, - res: 0, - out: Stack[int]{}, - err: ErrStackEmpty, - }, - } - - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - res, err := c.in.Pop() - if err != c.err { - t.Errorf("expected error %v, got %v", c.err, err) - } else if !slices.Equal(c.in, c.out) { - t.Errorf("expected stack %v, got %v", c.out, c.in) - } else if res != c.res { - t.Errorf("expected res %v, got %v", c.res, res) - } - }) - } -} From 2e776ec228019719d77016cd61108754a96d58c3 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:40:44 +0100 Subject: [PATCH 13/51] chore: add test dependencies --- go.mod | 12 ++++++++++-- go.sum | 10 ++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 go.sum diff --git a/go.mod b/go.mod index dd1a8b6..44b339e 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ -module github.com/gKits/PavoSQL +module github.com/pavosql/pavosql -go 1.21 +go 1.23 + +require github.com/stretchr/testify v1.10.0 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..713a0b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 519f573c2ccf1ca6a218be7c37761edbcad221c9 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:51:29 +0100 Subject: [PATCH 14/51] feat(node): add bpt/node package --- internal/bpt/node/node.go | 265 +++++++++++++++ internal/bpt/node/node_test.go | 593 +++++++++++++++++++++++++++++++++ 2 files changed, 858 insertions(+) create mode 100644 internal/bpt/node/node.go create mode 100644 internal/bpt/node/node_test.go diff --git a/internal/bpt/node/node.go b/internal/bpt/node/node.go new file mode 100644 index 0000000..0c5cb8b --- /dev/null +++ b/internal/bpt/node/node.go @@ -0,0 +1,265 @@ +// The node package provides all basic functionalities of a singular node inside a B+Tree. A single +// node can be of two either two types [TypeLeaf] or [TypePointer]. Both node types contain +// key-value pairs with the primary difference beeing that [TypeLeaf] nodes store actual data as +// their values while [TypePointer] store pointers to other nodes that are called their children. +// +// Nodes are stored in a custom byte format that is structured as follows: +// +// offset | size in B | description +// --------+-----------+------------- +// 0 | 1 | Type identifier +// 1 | 2 | Number of stored cells N +// 3 | 2xN | Offset list to each cell +// 3+2xN | ... | Cells +// +// The cells that are stored on a [TypeLeaf] node consist of a key and a value. They are also stored +// in the form of a custom byte format with following structure. +// +// offset | size in B | description +// --------+-----------+------------- +// 0 | 2 | Length of key K +// 2 | K | Key +// 2+K | 2 | Length of value V +// 4+K | V | Value +// +// Since a [TypePointer] node only stores uint64 pointers to other nodes their cells structure looks +// slightly different. +// +// offset | size in B | description +// --------+-----------+------------- +// 0 | 2 | Length of key K +// 2 | K | Key +// 2+K | 8 | Pointer +package node + +import ( + "bytes" + "encoding/binary" + "log" + "slices" +) + +type Type uint8 + +const ( + TypeLeaf Type = 0x01 + TypePointer Type = 0x10 +) + +func TypeOf(node []byte) Type { + t := Type(node[0]) + if t != TypeLeaf && t != TypePointer { + panic("invalid node type") + } + return t +} + +func LenOf(node []byte) uint16 { + return binary.LittleEndian.Uint16(node[1:]) +} + +func Key(node []byte, i uint16) []byte { + if i >= LenOf(node) { + panic("index out of bounds") + } + off := offset(node, i) + kLen := binary.LittleEndian.Uint16(node[off:]) + return node[off+2 : off+2+kLen] +} + +func Value(node []byte, i uint16) []byte { + if i >= LenOf(node) { + panic("index out of bounds") + } + off := offset(node, i) + kLen := binary.LittleEndian.Uint16(node[off:]) + vLen := binary.LittleEndian.Uint16(node[off+2+kLen:]) + return node[off+4+kLen : off+4+kLen+vLen] +} + +func Search(node []byte, target []byte) (uint16, bool) { + n := LenOf(node) + left, right := uint16(0), n + + for left < right { + cur := uint16(uint(left+right) >> 1) + if cmp := bytes.Compare(Key(node, cur), target); cmp == -1 { + left = cur + 1 + } else if cmp == 1 { + right = cur + } else { + return cur, true + } + } + + return left, left < n && bytes.Equal(Key(node, left), target) +} + +func Insert(node []byte, i uint16, k, v []byte) []byte { + n := LenOf(node) + off := offset(node, i) + cell := makeCell(k, v) + + binary.LittleEndian.PutUint16(node[1:], n+1) + + log.Println(len(node), off) + node = slices.Insert(node, int(off), cell...) + + node = slices.Insert(node, int(3+i*2), 0, 0) + binary.LittleEndian.PutUint16(node[3+i*2:], off) + + for a := uint16(1); a <= n+1; a++ { + currentOffset := offset(node, a) + shift := 2 + if a > i { + shift += len(cell) + } + setOffset(node, a, uint16(int(currentOffset)+shift)) + } + return node +} + +func Update(node []byte, i uint16, k, v []byte) []byte { + n := LenOf(node) + off := offset(node, i) + cell := makeCell(k, v) + + nextOff := offset(node, i+1) + node = slices.Replace(node, int(off), int(nextOff), cell...) + lenDiff := len(cell) - int(nextOff-off) + + for a := i + 1; a <= n; a++ { + currentOffset := offset(node, a) + setOffset(node, a, uint16(int(currentOffset)+lenDiff)) + } + + return node +} + +func Delete(node []byte, i uint16) []byte { + n := LenOf(node) + off := offset(node, i) + nextOff := offset(node, i+1) + + binary.LittleEndian.PutUint16(node[1:], n-1) + node = slices.Delete(node, int(off), int(nextOff)) + + node = slices.Delete(node, int(3+i*2), int(3+i*2+2)) + + for a := uint16(1); a <= n-1; a++ { + currentOffset := offset(node, a) + shift := uint16(2) + if a > i { + shift += nextOff - off + } + log.Println(a, currentOffset, shift) + setOffset(node, a, uint16(currentOffset-shift)) + } + + return node +} + +func Merge(left, right []byte) []byte { + if TypeOf(left) != TypeOf(right) { + panic("node types do not match") + } + + ln := LenOf(left) + rn := LenOf(right) + + merged := slices.Insert(left, int(offset(left, 0)), right[3:3+2*rn]...) + merged = slices.Insert(merged, len(merged), right[offset(right, 0):]...) + + binary.LittleEndian.PutUint16(merged[1:], ln+rn) + + for i := range LenOf(merged) + 1 { + curOff := offset(merged, i) + shift := rn * 2 + if i > ln { + shift = offset(left, ln) - 3 + } + log.Println(curOff, shift) + setOffset(merged, i, curOff+shift) + } + + lastOff := offset(left, LenOf(left)) + + _ = lastOff + + return merged +} + +func Split(node []byte) ([]byte, []byte) { + center := func(node []byte) uint16 { + target := ((uint16(len(node)) - offset(node, 0)) >> 1) + offset(node, 0) + n := LenOf(node) + left, right := uint16(0), n + + for left < right { + cur := uint16(uint(left+right) >> 1) + off := offset(node, cur) + if off < target { + left = cur + 1 + } else if off > target { + right = cur + } else { + return cur + } + } + return left + }(node) + + n := LenOf(node) + typ := TypeOf(node) + off0 := offset(node, 0) + offC := offset(node, center) + + left := []byte{byte(typ)} + rigth := []byte{byte(typ)} + + left = binary.LittleEndian.AppendUint16(left, center) + rigth = binary.LittleEndian.AppendUint16(rigth, n-center) + + left = append(left, node[3:3+2*center]...) + rigth = append(rigth, node[3+2*center:off0]...) + + left = append(left, node[off0:offC]...) + rigth = append(rigth, node[offC:]...) + + for i := range LenOf(left) + 1 { + curOff := offset(left, i) + setOffset(left, i, curOff-(n-center)*2) + } + for i := range LenOf(rigth) + 1 { + curOff := offset(rigth, i) + setOffset(rigth, i, curOff-offC+3+2*(n-center)) + } + + return left, rigth +} + +func offset(node []byte, i uint16) uint16 { + if i == 0 { + return 3 + LenOf(node)*2 + } else if i > LenOf(node) { + panic("node index out of bounds") + } + return binary.LittleEndian.Uint16(node[3+(i-1)*2:]) +} + +func setOffset(node []byte, i uint16, off uint16) { + if i > LenOf(node) { + panic("node index out of bounds") + } else if i != 0 { + binary.LittleEndian.PutUint16(node[3+(i-1)*2:], off) + } +} + +func makeCell(k, v []byte) []byte { + cell := make([]byte, 4+len(k)+len(v)) + binary.LittleEndian.PutUint16(cell[0:], uint16(len(k))) + copy(cell[2:], k) + binary.LittleEndian.PutUint16(cell[2+len(k):], uint16(len(v))) + copy(cell[4+len(k):], v) + return cell +} diff --git a/internal/bpt/node/node_test.go b/internal/bpt/node/node_test.go new file mode 100644 index 0000000..1272745 --- /dev/null +++ b/internal/bpt/node/node_test.go @@ -0,0 +1,593 @@ +package node + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTypeOf(t *testing.T) { + cases := []struct { + name string + node []byte + want Type + assertPanic assert.PanicAssertionFunc + }{ + { + name: "get leaf node type", + node: []byte{byte(TypeLeaf)}, + want: TypeLeaf, + assertPanic: assert.NotPanics, + }, + { + name: "get pointer node type", + node: []byte{byte(TypePointer)}, + want: TypePointer, + assertPanic: assert.NotPanics, + }, + { + name: "panic on invalid node type 0x00", + node: []byte{0x00}, + assertPanic: assert.Panics, + }, + { + name: "panic on invalid node type 0x11", + node: []byte{0x11}, + assertPanic: assert.Panics, + }, + { + name: "panic on invalid node type 0xff", + node: []byte{0xff}, + assertPanic: assert.Panics, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + c.assertPanic(t, func() { + got := TypeOf(c.node) + assert.Equal(t, c.want, got) + }) + }) + } +} + +func TestKey(t *testing.T) { + cases := []struct { + name string + node []byte + input uint16 + want []byte + assertPanic assert.PanicAssertionFunc + }{ + { + name: "0th key", + node: fixtureNodeEvenNumberOfKeys(), + input: 0, + want: []byte("aaa"), + assertPanic: assert.NotPanics, + }, + { + name: "1st key", + node: fixtureNodeEvenNumberOfKeys(), + input: 1, + want: []byte("bbb"), + assertPanic: assert.NotPanics, + }, + { + name: "9th key", + node: fixtureNodeEvenNumberOfKeys(), + input: 9, + want: []byte("jjj"), + assertPanic: assert.NotPanics, + }, + { + name: "10th key", + node: fixtureNodeEvenNumberOfKeys(), + input: 10, + assertPanic: assert.Panics, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + c.assertPanic(t, func() { + got := Key(c.node, c.input) + assert.Equal(t, c.want, got) + }) + }) + } +} + +func TestValue(t *testing.T) { + cases := []struct { + name string + node []byte + input uint16 + want []byte + assertPanic assert.PanicAssertionFunc + }{ + { + name: "0th value", + node: fixtureNodeEvenNumberOfKeys(), + input: 0, + want: []byte("aaa"), + assertPanic: assert.NotPanics, + }, + { + name: "1st value", + node: fixtureNodeEvenNumberOfKeys(), + input: 1, + want: []byte("bbb"), + assertPanic: assert.NotPanics, + }, + { + name: "9th value", + node: fixtureNodeEvenNumberOfKeys(), + input: 9, + want: []byte("jjj"), + assertPanic: assert.NotPanics, + }, + { + name: "10th value", + node: fixtureNodeEvenNumberOfKeys(), + input: 10, + assertPanic: assert.Panics, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + c.assertPanic(t, func() { + got := Value(c.node, c.input) + assert.Equal(t, c.want, got) + }) + }) + } +} + +func TestSearch(t *testing.T) { + cases := []struct { + name string + node, input []byte + want uint16 + assertExists assert.BoolAssertionFunc + }{ + { + name: "search existing target in odd length node", + node: fixtureNodeOddNumberOfKeys(), + input: []byte("ddd"), + want: 3, + assertExists: assert.True, + }, + { + name: "search non existing target in odd length node", + node: fixtureNodeOddNumberOfKeys(), + input: []byte("iij"), + want: 9, + assertExists: assert.False, + }, + { + name: "search existing target in even length node", + node: fixtureNodeEvenNumberOfKeys(), + input: []byte("hhh"), + want: 7, + assertExists: assert.True, + }, + { + name: "search non existing target in even length node", + node: fixtureNodeEvenNumberOfKeys(), + input: []byte("bbc"), + want: 2, + assertExists: assert.False, + }, + { + name: "search before first key in odd length node", + node: fixtureNodeOddNumberOfKeys(), + input: []byte("aa"), + want: 0, + assertExists: assert.False, + }, + { + name: "search after last key in odd length node", + node: fixtureNodeOddNumberOfKeys(), + input: []byte("zzzzz"), + want: 11, + assertExists: assert.False, + }, + { + name: "search before first key in even length node", + node: fixtureNodeEvenNumberOfKeys(), + input: []byte("aa"), + want: 0, + assertExists: assert.False, + }, + { + name: "search after last key in even length node", + node: fixtureNodeEvenNumberOfKeys(), + input: []byte("zzzzz"), + want: 10, + assertExists: assert.False, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got, exists := Search(c.node, c.input) + assert.Equal(t, c.want, got) + c.assertExists(t, exists) + }) + } +} + +func TestInsert(t *testing.T) { + cases := []struct { + name string + node []byte + i uint16 + k, v []byte + want []byte + + wantLen uint16 + wantKAfter []byte + }{ + { + name: "insert new cell", + i: 1, k: []byte("new"), v: []byte("new"), + node: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + want: []byte{ + 1, 4, 0, 17, 0, 27, 0, 33, 0, 39, 0, + 1, 0, 'a', 1, 0, 'a', + 3, 0, 'n', 'e', 'w', 3, 0, 'n', 'e', 'w', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + }, + { + name: "insert new cell at the start", + i: 0, k: []byte("key"), v: []byte("value"), + node: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + want: []byte{ + 1, 4, 0, 23, 0, 29, 0, 35, 0, 41, 0, + 3, 0, 'k', 'e', 'y', 5, 0, 'v', 'a', 'l', 'u', 'e', + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + }, + { + name: "insert new cell at the end", + i: 2, k: []byte("at"), v: []byte("the end"), + node: []byte{ + 1, 2, 0, 13, 0, 19, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + }, + want: []byte{ + 1, 3, 0, 15, 0, 21, 0, 34, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 2, 0, 'a', 't', 7, 0, 't', 'h', 'e', ' ', 'e', 'n', 'd', + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := Insert(c.node, c.i, c.k, c.v) + assert.Equal(t, c.want, got) + }) + } +} + +func TestUpdate(t *testing.T) { + cases := []struct { + name string + node []byte + i uint16 + k, v []byte + want []byte + }{ + { + name: "update a single cell node", + i: 0, k: []byte("a"), v: []byte("this is a new value"), + node: []byte{ + 1, 1, 0, 11, 0, + 1, 0, 'a', 1, 0, 'a', + }, + want: []byte{ + 1, 1, 0, 29, 0, + 1, 0, 'a', 19, 0, 't', 'h', 'i', 's', ' ', 'i', 's', ' ', 'a', ' ', 'n', 'e', 'w', ' ', 'v', 'a', 'l', 'u', 'e', + }, + }, + { + name: "update first cell of a multi cell node", + i: 0, k: []byte("a"), v: []byte("new"), + node: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + want: []byte{ + 1, 3, 0, 17, 0, 23, 0, 29, 0, + 1, 0, 'a', 3, 0, 'n', 'e', 'w', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + }, + { + name: "update last cell of a multi cell node", + i: 2, k: []byte("ccc"), v: []byte("new"), + node: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + want: []byte{ + 1, 3, 0, 15, 0, 21, 0, 31, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 3, 0, 'c', 'c', 'c', 3, 0, 'n', 'e', 'w', + }, + }, + { + name: "update cell with shorter cell", + i: 2, k: []byte("c"), v: []byte("c"), + node: []byte{ + 1, 3, 0, 15, 0, 21, 0, 29, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 2, 0, 'c', 'c', 2, 0, 'c', 'c', + }, + want: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := Update(c.node, c.i, c.k, c.v) + assert.Equal(t, c.want, got) + }) + } +} + +func TestDelete(t *testing.T) { + cases := []struct { + name string + node []byte + i uint16 + want []byte + }{ + { + name: "delete last cell of single cell node", + i: 0, + node: []byte{ + 1, 1, 0, 11, 0, + 1, 0, 'a', 1, 0, 'a', + }, + want: []byte{ + 1, 0, 0, + }, + }, + { + name: "delete first cell of multi cell node", + i: 0, + node: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + want: []byte{ + 1, 2, 0, 13, 0, 19, 0, + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + }, + { + name: "delete last cell of multi cell node", + i: 2, + node: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + want: []byte{ + 1, 2, 0, 13, 0, 19, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + }, + }, + { + name: "delete cell of multi cell node", + i: 1, + node: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', + }, + want: []byte{ + 1, 2, 0, 13, 0, 19, 0, + 1, 0, 'a', 1, 0, 'a', + 1, 0, 'c', 1, 0, 'c', + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := Delete(c.node, c.i) + assert.Equal(t, c.want, got) + }) + } +} + +func TestMerge(t *testing.T) { + cases := []struct { + name string + left, right []byte + want []byte + }{ + { + name: "merge two equally sized nodes", + left: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', + }, + right: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + }, + want: []byte{ + 1, 6, 0, 21, 0, 27, 0, 33, 0, 39, 0, 45, 0, 51, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + }, + }, + { + name: "merge two differently sized nodes", + left: []byte{ + 1, 4, 0, 15, 0, 21, 0, 27, 0, 33, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + }, + right: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', 1, 0, 'g', 1, 0, 'g', + }, + want: []byte{ + 1, 7, 0, 21, 0, 27, 0, 33, 0, 39, 0, 45, 0, 51, 0, 57, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + 1, 0, 'g', 1, 0, 'g', + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + got := Merge(c.left, c.right) + assert.Equal(t, c.want, got) + }) + } +} + +func TestSplit(t *testing.T) { + cases := []struct { + name string + node []byte + wantL, wantR []byte + }{ + { + name: "split symmetrical node", + node: []byte{ + 1, 6, 0, 21, 0, 27, 0, 33, 0, 39, 0, 45, 0, 51, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + }, + wantL: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', + }, + wantR: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + gotL, gotR := Split(c.node) + t.Log(gotL) + t.Log(c.wantL) + t.Log(gotR) + t.Log(c.wantR) + assert.Equal(t, c.wantL, gotL) + assert.Equal(t, c.wantR, gotR) + }) + } +} + +// Fixtures + +func fixtureNodeEvenNumberOfKeys() []byte { + return []byte{ + 1, + 10, 0, + // Offsets + 33, 0, + 43, 0, + 53, 0, + 63, 0, + 73, 0, + 83, 0, + 93, 0, + 103, 0, + 113, 0, + 123, 0, + // K-V Pairs + 3, 0, 'a', 'a', 'a', 3, 0, 'a', 'a', 'a', + 3, 0, 'b', 'b', 'b', 3, 0, 'b', 'b', 'b', + 3, 0, 'c', 'c', 'c', 3, 0, 'c', 'c', 'c', + 3, 0, 'd', 'd', 'd', 3, 0, 'd', 'd', 'd', + 3, 0, 'e', 'e', 'e', 3, 0, 'e', 'e', 'e', + 3, 0, 'f', 'f', 'f', 3, 0, 'f', 'f', 'f', + 3, 0, 'g', 'g', 'g', 3, 0, 'g', 'g', 'g', + 3, 0, 'h', 'h', 'h', 3, 0, 'h', 'h', 'h', + 3, 0, 'i', 'i', 'i', 3, 0, 'i', 'i', 'i', + 3, 0, 'j', 'j', 'j', 3, 0, 'j', 'j', 'j', + } +} + +func fixtureNodeOddNumberOfKeys() []byte { + return []byte{ + 1, + 11, 0, + // Offsets + 35, 0, + 45, 0, + 55, 0, + 65, 0, + 75, 0, + 85, 0, + 95, 0, + 105, 0, + 115, 0, + 125, 0, + 135, 0, + // K-V Pairs + 3, 0, 'a', 'a', 'a', 3, 0, 'a', 'a', 'a', + 3, 0, 'b', 'b', 'b', 3, 0, 'b', 'b', 'b', + 3, 0, 'c', 'c', 'c', 3, 0, 'c', 'c', 'c', + 3, 0, 'd', 'd', 'd', 3, 0, 'd', 'd', 'd', + 3, 0, 'e', 'e', 'e', 3, 0, 'e', 'e', 'e', + 3, 0, 'f', 'f', 'f', 3, 0, 'f', 'f', 'f', + 3, 0, 'g', 'g', 'g', 3, 0, 'g', 'g', 'g', + 3, 0, 'h', 'h', 'h', 3, 0, 'h', 'h', 'h', + 3, 0, 'i', 'i', 'i', 3, 0, 'i', 'i', 'i', + 3, 0, 'j', 'j', 'j', 3, 0, 'j', 'j', 'j', + 3, 0, 'k', 'k', 'k', 3, 0, 'k', 'k', 'k', + } +} From dc3e9d0493bda89c9d4e613c48209afba71dcbe7 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:54:35 +0100 Subject: [PATCH 15/51] docs: update README --- README.md | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 1625049..8ec0612 100644 --- a/README.md +++ b/README.md @@ -4,33 +4,28 @@ [![Build](https://github.com/gKits/PavoSQL/actions/workflows/gobuild.yaml/badge.svg)](https://github.com/gKits/PavoSQL/actions/workflows/gobuild.yaml) [![Test](https://github.com/gKits/PavoSQL/actions/workflows/gotest.yaml/badge.svg)](https://github.com/gKits/PavoSQL/actions/workflows/gotest.yaml) -**This is a learning project and is not meant to be run in production environments.** - -**This project is stil w.i.p.** - -PavoSQL is a SQL relational Database written in pure Go, meaning only using Go's standard library. +PavoSQL is a SQL database written purely in Go. ## Roadmap -- [x] Atomic backend store on single file -- [ ] Relational model build on KV Store - - [ ] Point queries - - [ ] Range queries - - [ ] Insert - - [ ] Delete - - [ ] Sorting - - [ ] Group By - - [ ] Joins -- [ ] Lexer and Parser for SQL queries -- [ ] Database server and client to use PavoSQL over the network -- [ ] User and privilege system +- [ ] Database engine + - [ ] Single file backend + - [ ] B+tree structure + - [ ] Concurrent r/w + - [ ] Atomic i/o + - [ ] SQL + - [ ] Relational model + - [ ] Tables + - [ ] Indexes + - [ ] Metadata + - [ ] Lexer, parser and AST + - [ ] Query functionality +- [ ] Network + - [ ] Server + client + - [ ] Authentication + Authorization - [ ] Implement [database/sql](https://pkg.go.dev/database/sql) driver interface -- [ ] Database Management System in single directory -- [ ] Windows compatibilty of backend store (remain atomic) - [ ] Documentation -- [ ] Installable as service/daemon (e.g. systemd) -- [ ] Create and release Docker image -- [ ] 80% Test coverage (not needed but nice to have) +- [ ] Docker image ## Reference material From 2b1b3a65b0e4eb99b1b30144ff6530f5a1e6a2e2 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:55:43 +0100 Subject: [PATCH 16/51] feat(parse): implement simple tokenizer --- pkg/parse/tokenize.go | 132 +++++++++++++++++++++++++++++++++++++ pkg/parse/tokenize_test.go | 77 ++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 pkg/parse/tokenize.go create mode 100644 pkg/parse/tokenize_test.go diff --git a/pkg/parse/tokenize.go b/pkg/parse/tokenize.go new file mode 100644 index 0000000..2ff46ab --- /dev/null +++ b/pkg/parse/tokenize.go @@ -0,0 +1,132 @@ +package parse + +import ( + "io" + "iter" + "strings" + "text/scanner" +) + +type TokenType int + +const ( + LexError TokenType = iota - 1 + String + RawString + Int + Float + Ident + + SpecialChar // This is a separator => if Token.Type > SpecialChar && Token.Type < Keyword {...} + Semicolon + Period + Equal + LParen + RParen + LBracket + RBracket + LBrace + RBrace + Plus + Hyphen + Asterisk + Greater + Less + + Keyword // This is a separator => if Token.Type > Keyword {...} + Select + Delete + Create + Update + Insert + From + Into + Table + Set + Values + Where + If + Exists + Not + And + Or +) + +var keywords map[string]TokenType = map[string]TokenType{ + "select": Select, + "delete": Delete, + "create": Create, + "update": Update, + "insert": Insert, + "from": From, + "into": Into, + "table": Table, + "set": Set, + "values": Values, + "where": Where, + "if": If, + "exists": Exists, + "not": Not, + "and": And, + "or": Or, +} + +var specialChars map[string]TokenType = map[string]TokenType{ + ";": Semicolon, + ".": Period, + "=": Equal, + "(": LParen, + ")": RParen, + "[": LBracket, + "]": RBracket, + "{": LBrace, + "}": RBrace, +} + +type Token struct { + Val string + Type TokenType + Line, Column int +} + +func tokenize(r io.Reader) iter.Seq[Token] { + scan := new(scanner.Scanner) + scan.Init(r) + + return func(yield func(Token) bool) { + for r := scan.Scan(); r != scanner.EOF; r = scan.Scan() { + tok := Token{ + Val: scan.TokenText(), + Line: scan.Pos().Line, + Column: scan.Pos().Column - len(scan.TokenText()), + } + + switch r { + case scanner.Int: + tok.Type = Int + case scanner.Float: + tok.Type = Float + case scanner.String, scanner.Char: + tok.Type = String + case scanner.RawString: + tok.Type = RawString + case scanner.Ident: + if keyword, ok := keywords[strings.ToLower(tok.Val)]; ok { + tok.Type = keyword + break + } + tok.Type = Ident + default: + if specCh, ok := specialChars[tok.Val]; ok { + tok.Type = specCh + break + } + tok.Type = LexError + } + + if !yield(tok) { + return + } + } + } +} diff --git a/pkg/parse/tokenize_test.go b/pkg/parse/tokenize_test.go new file mode 100644 index 0000000..56b37f8 --- /dev/null +++ b/pkg/parse/tokenize_test.go @@ -0,0 +1,77 @@ +package parse + +import ( + "strings" + "testing" +) + +func Test_tokenize(t *testing.T) { + cases := []struct { + name string + in string + want []Token + }{ + { + name: "tokenize with keywords and special chars #1", + in: `SeLECt name frOM users wHERE name == "john doe"`, + want: []Token{ + {"SeLECt", Select, 1, 1}, + {"name", Ident, 1, 8}, + {"frOM", From, 1, 13}, + {"users", Ident, 1, 18}, + {"wHERE", Where, 1, 24}, + {"name", Ident, 1, 30}, + {"=", Equal, 1, 35}, + {"=", Equal, 1, 36}, + {"\"john doe\"", String, 1, 38}, + }, + }, + { + name: "tokenize with keywords and special chars #2", + in: ` creaTe TABlE iF exists users ( )`, + want: []Token{ + {"creaTe", Create, 1, 3}, + {"TABlE", Table, 1, 10}, + {"iF", If, 1, 16}, + {"exists", Exists, 1, 19}, + {"users", Ident, 1, 26}, + {"(", LParen, 1, 32}, + {")", RParen, 1, 34}, + }, + }, + { + name: "tokenize with comments #1", + in: `"hello" // this is an inline comment + /* This + is a multiline comment + */ + . = [] {} 'xxxx' + `, + want: []Token{ + {"\"hello\"", String, 1, 1}, + {".", Period, 5, 17}, + {"=", Equal, 5, 19}, + {"[", LBracket, 5, 21}, + {"]", RBracket, 5, 22}, + {"{", LBrace, 5, 24}, + {"}", RBrace, 5, 25}, + {"'xxxx'", String, 5, 27}, + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + var i int + for got := range tokenize(strings.NewReader(c.in)) { + if i >= len(c.want) { + t.Fatalf("want %d tokens, got at least %d", len(c.want), i+1) + } + if got != c.want[i] { + t.Fatalf("want token %v, got %v", c.want[i], got) + } + i++ + } + }) + } +} From 1be262f7c7e5f8279756ffb994dd03cabe333528 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 4 Feb 2025 01:56:24 +0100 Subject: [PATCH 17/51] feat(parse): add blueprint for basic parsing loop --- pkg/parse/parse.go | 91 ++++++++++------------------------------------ 1 file changed, 20 insertions(+), 71 deletions(-) diff --git a/pkg/parse/parse.go b/pkg/parse/parse.go index b927dc0..1ee0fa6 100644 --- a/pkg/parse/parse.go +++ b/pkg/parse/parse.go @@ -1,84 +1,33 @@ package parse import ( - "fmt" "io" - "text/scanner" -) - -type parseState func(p *parser) (parseState, error) + "sync" -type parser struct { - q Statement - scan *scanner.Scanner -} + "github.com/pavosql/pavosql/pkg/ast" +) -func Parse(query io.Reader) ([]Statement, error) { - p := parser{ - scan: new(scanner.Scanner).Init(query), - } +func Parse(r io.Reader) []ast.Stmnt { + var wg sync.WaitGroup + wg.Add(2) - var ( - state = parseOperator - err error - ) - for state != nil { - if state, err = state(&p); err != nil { - return nil, err + toks := make(chan Token) + go func() { + defer wg.Done() + for tok := range tokenize(r) { + toks <- tok } - } - - return nil, nil -} + }() -func parseOperator(p *parser) (parseState, error) { - tok := p.scan.Scan() - if tok == scanner.Ident { - if state, ok := operators[p.scan.TokenText()]; ok { - return state, nil + go func() { + defer wg.Done() + for tok := range toks { + _ = tok + // TODO: Implement parsing logic } - } - return nil, fmt.Errorf("expected operator, got %s", p.scan.TokenText()) -} - -func parseGet(p *parser) (parseState, error) { - tok := p.scan.Scan() - if tok != '(' { - return nil, fmt.Errorf("expected '(', got %s", p.scan.TokenText()) - } - - tok = p.scan.Scan() - if tok != scanner.Ident { - return nil, fmt.Errorf("expected table name identifier, got %s", p.scan.TokenText()) - } - - tok = p.scan.Scan() - if tok != ',' { - return nil, fmt.Errorf("expected ',', got %s", p.scan.TokenText()) - } + }() - return nil, nil -} - -func parseInsert(p *parser) (parseState, error) { - return nil, nil -} - -func parseUpdate(p *parser) (parseState, error) { - return nil, nil -} - -func parseDelete(p *parser) (parseState, error) { - return nil, nil -} - -func parseCreate(p *parser) (parseState, error) { - return nil, nil -} + wg.Wait() -func parseCondition(p *parser) (parseState, error) { - for tok := p.scan.Scan(); tok != scanner.EOF; tok = p.scan.Scan() { - break - } - return nil, nil + return nil } From 8165314455f53bed0c7211c3e999dd2158c5a623 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 4 Feb 2025 02:00:58 +0100 Subject: [PATCH 18/51] chore: remove hugo ignores --- .gitignore | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index 39a63e9..3d4d92b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,9 +21,4 @@ bin # Go workspace file go.work -# Hugo Docs -docs/.hugo_build.lock -public -docs/public - dist/ From 13c6034dd67d32a61f7b5f04485b4567b24503f8 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 4 Feb 2025 02:01:59 +0100 Subject: [PATCH 19/51] ci: update go version --- .github/workflows/gobuild.yaml | 4 +--- .github/workflows/gotest.yaml | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gobuild.yaml b/.github/workflows/gobuild.yaml index 0618a9d..d3c19df 100644 --- a/.github/workflows/gobuild.yaml +++ b/.github/workflows/gobuild.yaml @@ -10,8 +10,6 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: '1.21.x' - - name: Install dependencies - run: go get . + go-version: 1.23.x - name: Build run: go build -v ./cmd/pavosql diff --git a/.github/workflows/gotest.yaml b/.github/workflows/gotest.yaml index a87c526..27fe37e 100644 --- a/.github/workflows/gotest.yaml +++ b/.github/workflows/gotest.yaml @@ -10,8 +10,6 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: '1.21.x' - - name: Install dependencies - run: go get . + go-version: 1.23.x - name: Test with the Go CLI run: go test -v ./... From 086737fe0ed88806548b833cd95c8159733e66d4 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 4 Feb 2025 02:08:05 +0100 Subject: [PATCH 20/51] ci: adjust pipeline parameters --- .github/workflows/{gobuild.yaml => build.yaml} | 6 ++++-- .github/workflows/{gotest.yaml => test.yaml} | 6 ++++-- README.md | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) rename .github/workflows/{gobuild.yaml => build.yaml} (69%) rename .github/workflows/{gotest.yaml => test.yaml} (70%) diff --git a/.github/workflows/gobuild.yaml b/.github/workflows/build.yaml similarity index 69% rename from .github/workflows/gobuild.yaml rename to .github/workflows/build.yaml index d3c19df..2ef136f 100644 --- a/.github/workflows/gobuild.yaml +++ b/.github/workflows/build.yaml @@ -1,4 +1,4 @@ -name: Go build +name: Build on: [push] jobs: @@ -10,6 +10,8 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: 1.23.x + go-version-file: go.mod + cache-dependency-path: | + go.sum - name: Build run: go build -v ./cmd/pavosql diff --git a/.github/workflows/gotest.yaml b/.github/workflows/test.yaml similarity index 70% rename from .github/workflows/gotest.yaml rename to .github/workflows/test.yaml index 27fe37e..0ad850c 100644 --- a/.github/workflows/gotest.yaml +++ b/.github/workflows/test.yaml @@ -1,4 +1,4 @@ -name: Go test +name: Test on: [push] jobs: @@ -10,6 +10,8 @@ jobs: - name: Setup Go uses: actions/setup-go@v4 with: - go-version: 1.23.x + go-version-file: go.mod + cache-dependency-path: | + go.sum - name: Test with the Go CLI run: go test -v ./... diff --git a/README.md b/README.md index 8ec0612..b141fdd 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # PavoSQL [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Build](https://github.com/gKits/PavoSQL/actions/workflows/gobuild.yaml/badge.svg)](https://github.com/gKits/PavoSQL/actions/workflows/gobuild.yaml) -[![Test](https://github.com/gKits/PavoSQL/actions/workflows/gotest.yaml/badge.svg)](https://github.com/gKits/PavoSQL/actions/workflows/gotest.yaml) +[![Build](https://github.com/pavosql/pavosql/actions/workflows/build.yaml/badge.svg)](https://github.com/pavosql/pavosql/actions/workflows/build.yaml) +[![Test](https://github.com/pavosql/pavosql/actions/workflows/test.yaml/badge.svg)](https://github.com/pavosql/pavosql/actions/workflows/test.yaml) PavoSQL is a SQL database written purely in Go. From 12d3f30816680d340f43b009e8b2771673f2c544 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Tue, 4 Feb 2025 02:10:15 +0100 Subject: [PATCH 21/51] feat: add blueprint files for ast and bpt --- internal/bpt/bpt.go | 14 ++++++++++++++ pkg/ast/ast.go | 3 +++ 2 files changed, 17 insertions(+) create mode 100644 internal/bpt/bpt.go create mode 100644 pkg/ast/ast.go diff --git a/internal/bpt/bpt.go b/internal/bpt/bpt.go new file mode 100644 index 0000000..72e9fa0 --- /dev/null +++ b/internal/bpt/bpt.go @@ -0,0 +1,14 @@ +package bpt + +import "io" + +const pageSize = 4096 + +type pager interface { + io.ReaderAt + io.WriterAt +} + +type Tree struct { + pager pager +} diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go new file mode 100644 index 0000000..ce64e7d --- /dev/null +++ b/pkg/ast/ast.go @@ -0,0 +1,3 @@ +package ast + +type Stmnt interface{} From 304d1a03dd44ab7e8470f7d36b8862ff7cfffd70 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Sat, 8 Feb 2025 20:02:14 +0100 Subject: [PATCH 22/51] feat(bpt): add commit and rollback to pager interface --- internal/bpt/bpt.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/bpt/bpt.go b/internal/bpt/bpt.go index 72e9fa0..2af2040 100644 --- a/internal/bpt/bpt.go +++ b/internal/bpt/bpt.go @@ -7,6 +7,8 @@ const pageSize = 4096 type pager interface { io.ReaderAt io.WriterAt + Commit() error + Rollback() error } type Tree struct { From 72285bf50c410fc0d32596c9398f3ddb39bfd39e Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Sat, 8 Feb 2025 20:46:43 +0100 Subject: [PATCH 23/51] feat(node): add sizeof function --- internal/bpt/node/node.go | 14 +- internal/bpt/node/node_test.go | 280 +++++++++++++++++++-------------- 2 files changed, 169 insertions(+), 125 deletions(-) diff --git a/internal/bpt/node/node.go b/internal/bpt/node/node.go index 0c5cb8b..7f42389 100644 --- a/internal/bpt/node/node.go +++ b/internal/bpt/node/node.go @@ -58,6 +58,10 @@ func LenOf(node []byte) uint16 { return binary.LittleEndian.Uint16(node[1:]) } +func SizeOf(node []byte) uint16 { + return offset(node, LenOf(node)) +} + func Key(node []byte, i uint16) []byte { if i >= LenOf(node) { panic("index out of bounds") @@ -166,10 +170,12 @@ func Merge(left, right []byte) []byte { ln := LenOf(left) rn := LenOf(right) + lsz := SizeOf(left) + rsz := SizeOf(right) - merged := slices.Insert(left, int(offset(left, 0)), right[3:3+2*rn]...) - merged = slices.Insert(merged, len(merged), right[offset(right, 0):]...) - + merged := slices.Insert(left, int(lsz), right[offset(right, 0):rsz]...) + log.Println(merged) + merged = slices.Insert(merged, int(offset(left, 0)), right[3:3+2*rn]...) binary.LittleEndian.PutUint16(merged[1:], ln+rn) for i := range LenOf(merged) + 1 { @@ -186,7 +192,7 @@ func Merge(left, right []byte) []byte { _ = lastOff - return merged + return merged[:SizeOf(merged)] } func Split(node []byte) ([]byte, []byte) { diff --git a/internal/bpt/node/node_test.go b/internal/bpt/node/node_test.go index 1272745..61ca8ec 100644 --- a/internal/bpt/node/node_test.go +++ b/internal/bpt/node/node_test.go @@ -7,43 +7,37 @@ import ( ) func TestTypeOf(t *testing.T) { - cases := []struct { - name string + cases := map[string]struct { node []byte want Type assertPanic assert.PanicAssertionFunc }{ - { - name: "get leaf node type", + "get leaf node type": { node: []byte{byte(TypeLeaf)}, want: TypeLeaf, assertPanic: assert.NotPanics, }, - { - name: "get pointer node type", + "get pointer node type": { node: []byte{byte(TypePointer)}, want: TypePointer, assertPanic: assert.NotPanics, }, - { - name: "panic on invalid node type 0x00", + "panic on invalid node type 0x00": { node: []byte{0x00}, assertPanic: assert.Panics, }, - { - name: "panic on invalid node type 0x11", + "panic on invalid node type 0x11": { node: []byte{0x11}, assertPanic: assert.Panics, }, - { - name: "panic on invalid node type 0xff", + "panic on invalid node type 0xff": { node: []byte{0xff}, assertPanic: assert.Panics, }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { c.assertPanic(t, func() { got := TypeOf(c.node) assert.Equal(t, c.want, got) @@ -53,44 +47,39 @@ func TestTypeOf(t *testing.T) { } func TestKey(t *testing.T) { - cases := []struct { - name string + cases := map[string]struct { node []byte input uint16 want []byte assertPanic assert.PanicAssertionFunc }{ - { - name: "0th key", + "0th key": { node: fixtureNodeEvenNumberOfKeys(), input: 0, want: []byte("aaa"), assertPanic: assert.NotPanics, }, - { - name: "1st key", + "1st key": { node: fixtureNodeEvenNumberOfKeys(), input: 1, want: []byte("bbb"), assertPanic: assert.NotPanics, }, - { - name: "9th key", + "9th key": { node: fixtureNodeEvenNumberOfKeys(), input: 9, want: []byte("jjj"), assertPanic: assert.NotPanics, }, - { - name: "10th key", + "10th key": { node: fixtureNodeEvenNumberOfKeys(), input: 10, assertPanic: assert.Panics, }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { c.assertPanic(t, func() { got := Key(c.node, c.input) assert.Equal(t, c.want, got) @@ -100,44 +89,39 @@ func TestKey(t *testing.T) { } func TestValue(t *testing.T) { - cases := []struct { - name string + cases := map[string]struct { node []byte input uint16 want []byte assertPanic assert.PanicAssertionFunc }{ - { - name: "0th value", + "0th value": { node: fixtureNodeEvenNumberOfKeys(), input: 0, want: []byte("aaa"), assertPanic: assert.NotPanics, }, - { - name: "1st value", + "1st value": { node: fixtureNodeEvenNumberOfKeys(), input: 1, want: []byte("bbb"), assertPanic: assert.NotPanics, }, - { - name: "9th value", + "9th value": { node: fixtureNodeEvenNumberOfKeys(), input: 9, want: []byte("jjj"), assertPanic: assert.NotPanics, }, - { - name: "10th value", + "10th value": { node: fixtureNodeEvenNumberOfKeys(), input: 10, assertPanic: assert.Panics, }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { c.assertPanic(t, func() { got := Value(c.node, c.input) assert.Equal(t, c.want, got) @@ -147,63 +131,54 @@ func TestValue(t *testing.T) { } func TestSearch(t *testing.T) { - cases := []struct { - name string + cases := map[string]struct { node, input []byte want uint16 assertExists assert.BoolAssertionFunc }{ - { - name: "search existing target in odd length node", + "search existing target in odd length node": { node: fixtureNodeOddNumberOfKeys(), input: []byte("ddd"), want: 3, assertExists: assert.True, }, - { - name: "search non existing target in odd length node", + "search non existing target in odd length node": { node: fixtureNodeOddNumberOfKeys(), input: []byte("iij"), want: 9, assertExists: assert.False, }, - { - name: "search existing target in even length node", + "search existing target in even length node": { node: fixtureNodeEvenNumberOfKeys(), input: []byte("hhh"), want: 7, assertExists: assert.True, }, - { - name: "search non existing target in even length node", + "search non existing target in even length node": { node: fixtureNodeEvenNumberOfKeys(), input: []byte("bbc"), want: 2, assertExists: assert.False, }, - { - name: "search before first key in odd length node", + "search before first key in odd length node": { node: fixtureNodeOddNumberOfKeys(), input: []byte("aa"), want: 0, assertExists: assert.False, }, - { - name: "search after last key in odd length node", + "search after last key in odd length node": { node: fixtureNodeOddNumberOfKeys(), input: []byte("zzzzz"), want: 11, assertExists: assert.False, }, - { - name: "search before first key in even length node", + "search before first key in even length node": { node: fixtureNodeEvenNumberOfKeys(), input: []byte("aa"), want: 0, assertExists: assert.False, }, - { - name: "search after last key in even length node", + "search after last key in even length node": { node: fixtureNodeEvenNumberOfKeys(), input: []byte("zzzzz"), want: 10, @@ -211,8 +186,8 @@ func TestSearch(t *testing.T) { }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { got, exists := Search(c.node, c.input) assert.Equal(t, c.want, got) c.assertExists(t, exists) @@ -221,8 +196,7 @@ func TestSearch(t *testing.T) { } func TestInsert(t *testing.T) { - cases := []struct { - name string + cases := map[string]struct { node []byte i uint16 k, v []byte @@ -231,9 +205,8 @@ func TestInsert(t *testing.T) { wantLen uint16 wantKAfter []byte }{ - { - name: "insert new cell", - i: 1, k: []byte("new"), v: []byte("new"), + "insert new cell": { + i: 1, k: []byte("new"), v: []byte("new"), node: []byte{ 1, 3, 0, 15, 0, 21, 0, 27, 0, 1, 0, 'a', 1, 0, 'a', @@ -248,9 +221,8 @@ func TestInsert(t *testing.T) { 1, 0, 'c', 1, 0, 'c', }, }, - { - name: "insert new cell at the start", - i: 0, k: []byte("key"), v: []byte("value"), + "insert new cell at the start": { + i: 0, k: []byte("key"), v: []byte("value"), node: []byte{ 1, 3, 0, 15, 0, 21, 0, 27, 0, 1, 0, 'a', 1, 0, 'a', @@ -265,9 +237,8 @@ func TestInsert(t *testing.T) { 1, 0, 'c', 1, 0, 'c', }, }, - { - name: "insert new cell at the end", - i: 2, k: []byte("at"), v: []byte("the end"), + "insert new cell at the end": { + i: 2, k: []byte("at"), v: []byte("the end"), node: []byte{ 1, 2, 0, 13, 0, 19, 0, 1, 0, 'a', 1, 0, 'a', @@ -282,8 +253,8 @@ func TestInsert(t *testing.T) { }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { got := Insert(c.node, c.i, c.k, c.v) assert.Equal(t, c.want, got) }) @@ -291,16 +262,14 @@ func TestInsert(t *testing.T) { } func TestUpdate(t *testing.T) { - cases := []struct { - name string + cases := map[string]struct { node []byte i uint16 k, v []byte want []byte }{ - { - name: "update a single cell node", - i: 0, k: []byte("a"), v: []byte("this is a new value"), + "update a single cell node": { + i: 0, k: []byte("a"), v: []byte("this is a new value"), node: []byte{ 1, 1, 0, 11, 0, 1, 0, 'a', 1, 0, 'a', @@ -310,9 +279,8 @@ func TestUpdate(t *testing.T) { 1, 0, 'a', 19, 0, 't', 'h', 'i', 's', ' ', 'i', 's', ' ', 'a', ' ', 'n', 'e', 'w', ' ', 'v', 'a', 'l', 'u', 'e', }, }, - { - name: "update first cell of a multi cell node", - i: 0, k: []byte("a"), v: []byte("new"), + "update first cell of a multi cell node": { + i: 0, k: []byte("a"), v: []byte("new"), node: []byte{ 1, 3, 0, 15, 0, 21, 0, 27, 0, 1, 0, 'a', 1, 0, 'a', @@ -326,9 +294,8 @@ func TestUpdate(t *testing.T) { 1, 0, 'c', 1, 0, 'c', }, }, - { - name: "update last cell of a multi cell node", - i: 2, k: []byte("ccc"), v: []byte("new"), + "update last cell of a multi cell node": { + i: 2, k: []byte("ccc"), v: []byte("new"), node: []byte{ 1, 3, 0, 15, 0, 21, 0, 27, 0, 1, 0, 'a', 1, 0, 'a', @@ -342,9 +309,8 @@ func TestUpdate(t *testing.T) { 3, 0, 'c', 'c', 'c', 3, 0, 'n', 'e', 'w', }, }, - { - name: "update cell with shorter cell", - i: 2, k: []byte("c"), v: []byte("c"), + "update cell with shorter cell": { + i: 2, k: []byte("c"), v: []byte("c"), node: []byte{ 1, 3, 0, 15, 0, 21, 0, 29, 0, 1, 0, 'a', 1, 0, 'a', @@ -360,8 +326,8 @@ func TestUpdate(t *testing.T) { }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { got := Update(c.node, c.i, c.k, c.v) assert.Equal(t, c.want, got) }) @@ -369,15 +335,13 @@ func TestUpdate(t *testing.T) { } func TestDelete(t *testing.T) { - cases := []struct { - name string + cases := map[string]struct { node []byte i uint16 want []byte }{ - { - name: "delete last cell of single cell node", - i: 0, + "delete last cell of single cell node": { + i: 0, node: []byte{ 1, 1, 0, 11, 0, 1, 0, 'a', 1, 0, 'a', @@ -386,9 +350,8 @@ func TestDelete(t *testing.T) { 1, 0, 0, }, }, - { - name: "delete first cell of multi cell node", - i: 0, + "delete first cell of multi cell node": { + i: 0, node: []byte{ 1, 3, 0, 15, 0, 21, 0, 27, 0, 1, 0, 'a', 1, 0, 'a', @@ -401,9 +364,8 @@ func TestDelete(t *testing.T) { 1, 0, 'c', 1, 0, 'c', }, }, - { - name: "delete last cell of multi cell node", - i: 2, + "delete last cell of multi cell node": { + i: 2, node: []byte{ 1, 3, 0, 15, 0, 21, 0, 27, 0, 1, 0, 'a', 1, 0, 'a', @@ -416,9 +378,8 @@ func TestDelete(t *testing.T) { 1, 0, 'b', 1, 0, 'b', }, }, - { - name: "delete cell of multi cell node", - i: 1, + "delete cell of multi cell node": { + i: 1, node: []byte{ 1, 3, 0, 15, 0, 21, 0, 27, 0, 1, 0, 'a', 1, 0, 'a', @@ -433,8 +394,8 @@ func TestDelete(t *testing.T) { }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { got := Delete(c.node, c.i) assert.Equal(t, c.want, got) }) @@ -442,13 +403,11 @@ func TestDelete(t *testing.T) { } func TestMerge(t *testing.T) { - cases := []struct { - name string + cases := map[string]struct { left, right []byte want []byte }{ - { - name: "merge two equally sized nodes", + "merge two equally sized nodes": { left: []byte{ 1, 3, 0, 15, 0, 21, 0, 27, 0, 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', @@ -464,8 +423,7 @@ func TestMerge(t *testing.T) { 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', }, }, - { - name: "merge two differently sized nodes", + "merge two differently sized nodes": { left: []byte{ 1, 4, 0, 15, 0, 21, 0, 27, 0, 33, 0, 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', @@ -484,8 +442,8 @@ func TestMerge(t *testing.T) { }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { got := Merge(c.left, c.right) assert.Equal(t, c.want, got) }) @@ -493,13 +451,11 @@ func TestMerge(t *testing.T) { } func TestSplit(t *testing.T) { - cases := []struct { - name string + cases := map[string]struct { node []byte wantL, wantR []byte }{ - { - name: "split symmetrical node", + "split symmetrical node with even number of cells": { node: []byte{ 1, 6, 0, 21, 0, 27, 0, 33, 0, 39, 0, 45, 0, 51, 0, 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', @@ -515,15 +471,97 @@ func TestSplit(t *testing.T) { 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', }, }, + "split symmetrical node with odd number of cells": { + node: []byte{ + 1, 7, 0, 23, 0, 29, 0, 35, 0, 41, 0, 47, 0, 53, 0, 59, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + 1, 0, 'g', 1, 0, 'g', + }, + wantL: []byte{ + 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + }, + wantR: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', 1, 0, 'g', 1, 0, 'g', + }, + }, + "split asymmetrical node with even number of cells": { + node: []byte{ + 1, 6, 0, 22, 0, 28, 0, 36, 0, 42, 0, 48, 0, 54, 0, + 2, 0, 'a', 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', + 2, 0, 'c', 'c', 2, 0, 'c', 'c', 1, 0, 'd', 1, 0, 'd', + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + }, + wantL: []byte{ + 1, 3, 0, 16, 0, 22, 0, 30, 0, + 2, 0, 'a', 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 2, 0, 'c', 'c', 2, 0, 'c', 'c', + }, + wantR: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + }, + }, + "split asymmetrical node with odd number of cells": { + node: []byte{ + 1, 7, 0, 24, 0, 30, 0, 38, 0, 44, 0, 50, 0, 56, 0, 64, 0, + 2, 0, 'a', 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', + 2, 0, 'c', 'c', 2, 0, 'c', 'c', 1, 0, 'd', 1, 0, 'd', + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + 1, 0, 'g', 3, 0, 'g', 'g', 'g', + }, + wantL: []byte{ + 1, 3, 0, 16, 0, 22, 0, 30, 0, + 2, 0, 'a', 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 2, 0, 'c', 'c', 2, 0, 'c', 'c', + }, + wantR: []byte{ + 1, 4, 0, 17, 0, 23, 0, 29, 0, 37, 0, + 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', 1, 0, 'g', 3, 0, 'g', 'g', 'g', + }, + }, + "split asymmetrical node with large last cell": { + node: []byte{ + 1, 5, 0, 19, 0, 25, 0, 31, 0, 37, 0, 61, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + 10, 0, 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', + 10, 0, 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', + }, + wantL: []byte{ + 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + }, + wantR: []byte{ + 1, 1, 0, 29, 0, + 10, 0, 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', + 10, 0, 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', + }, + }, + "split asymmetrical node with large first cell": { + node: []byte{ + 1, 5, 0, 37, 0, 43, 0, 49, 0, 55, 0, 61, 0, + 10, 0, 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', + 10, 0, 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', + 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', + 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', + }, + wantL: []byte{ + 1, 1, 0, 29, 0, + 10, 0, 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', + 10, 0, 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', + }, + wantR: []byte{ + 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, + 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', + }, + }, } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { + for name, c := range cases { + t.Run(name, func(t *testing.T) { gotL, gotR := Split(c.node) - t.Log(gotL) - t.Log(c.wantL) - t.Log(gotR) - t.Log(c.wantR) assert.Equal(t, c.wantL, gotL) assert.Equal(t, c.wantR, gotR) }) From 29311a30f05c87cbe0cb4dd999a569ab76d2fa7b Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Sat, 8 Feb 2025 20:47:42 +0100 Subject: [PATCH 24/51] feat(node): change center calc of split func --- internal/bpt/node/node.go | 55 ++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/internal/bpt/node/node.go b/internal/bpt/node/node.go index 7f42389..972b147 100644 --- a/internal/bpt/node/node.go +++ b/internal/bpt/node/node.go @@ -35,7 +35,6 @@ package node import ( "bytes" "encoding/binary" - "log" "slices" ) @@ -86,7 +85,7 @@ func Search(node []byte, target []byte) (uint16, bool) { left, right := uint16(0), n for left < right { - cur := uint16(uint(left+right) >> 1) + cur := uint16(uint(left+right) >> 1) // #nosec G115 // right shift stops overflow if cmp := bytes.Compare(Key(node, cur), target); cmp == -1 { left = cur + 1 } else if cmp == 1 { @@ -106,7 +105,6 @@ func Insert(node []byte, i uint16, k, v []byte) []byte { binary.LittleEndian.PutUint16(node[1:], n+1) - log.Println(len(node), off) node = slices.Insert(node, int(off), cell...) node = slices.Insert(node, int(3+i*2), 0, 0) @@ -114,11 +112,11 @@ func Insert(node []byte, i uint16, k, v []byte) []byte { for a := uint16(1); a <= n+1; a++ { currentOffset := offset(node, a) - shift := 2 + var shift uint16 = 2 if a > i { - shift += len(cell) + shift += uint16(len(cell)) } - setOffset(node, a, uint16(int(currentOffset)+shift)) + setOffset(node, a, uint16(currentOffset)+shift) } return node } @@ -156,7 +154,6 @@ func Delete(node []byte, i uint16) []byte { if a > i { shift += nextOff - off } - log.Println(a, currentOffset, shift) setOffset(node, a, uint16(currentOffset-shift)) } @@ -174,7 +171,6 @@ func Merge(left, right []byte) []byte { rsz := SizeOf(right) merged := slices.Insert(left, int(lsz), right[offset(right, 0):rsz]...) - log.Println(merged) merged = slices.Insert(merged, int(offset(left, 0)), right[3:3+2*rn]...) binary.LittleEndian.PutUint16(merged[1:], ln+rn) @@ -184,7 +180,6 @@ func Merge(left, right []byte) []byte { if i > ln { shift = offset(left, ln) - 3 } - log.Println(curOff, shift) setOffset(merged, i, curOff+shift) } @@ -197,22 +192,40 @@ func Merge(left, right []byte) []byte { func Split(node []byte) ([]byte, []byte) { center := func(node []byte) uint16 { - target := ((uint16(len(node)) - offset(node, 0)) >> 1) + offset(node, 0) n := LenOf(node) - left, right := uint16(0), n - - for left < right { - cur := uint16(uint(left+right) >> 1) - off := offset(node, cur) - if off < target { - left = cur + 1 - } else if off > target { - right = cur - } else { + target := ((uint16(SizeOf(node)) - offset(node, 0)) >> 1) + offset(node, 0) + + cur := n / 2 + for cur > 0 && cur < n { + curOff := offset(node, cur) + prevOff := offset(node, cur-1) + nextOff := offset(node, cur+1) + + switch { + case curOff == target: return cur + case nextOff == target: + return cur + 1 + case prevOff == target: + return cur - 1 + case curOff < target && nextOff < target: + cur = cur + 1 + continue + case curOff > target && prevOff > target: + cur = cur - 1 + case curOff < target && nextOff > target: + if target-curOff < nextOff-target { + return cur + } + return cur + 1 + case curOff > target && prevOff < target: + if curOff-target < target-prevOff { + return cur + } + return cur - 1 } } - return left + return cur }(node) n := LenOf(node) From faab4b9103b3ae337a62d37095fded91e26d1032 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Sat, 8 Feb 2025 20:48:03 +0100 Subject: [PATCH 25/51] test(node): add and adjust tests --- internal/bpt/node/node_test.go | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/internal/bpt/node/node_test.go b/internal/bpt/node/node_test.go index 61ca8ec..6218a9a 100644 --- a/internal/bpt/node/node_test.go +++ b/internal/bpt/node/node_test.go @@ -425,15 +425,34 @@ func TestMerge(t *testing.T) { }, "merge two differently sized nodes": { left: []byte{ - 1, 4, 0, 15, 0, 21, 0, 27, 0, 33, 0, + 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + }, + right: []byte{ + 1, 3, 0, 15, 0, 21, 0, 27, 0, + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', 1, 0, 'g', 1, 0, 'g', + }, + want: []byte{ + 1, 7, 0, 23, 0, 29, 0, 35, 0, 41, 0, 47, 0, 53, 0, 59, 0, + 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', + 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', + 1, 0, 'g', 1, 0, 'g', + }, + }, + "merge two differently sized nodes with trailing zeros": { + left: []byte{ + 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', + 0, 0, 0, 0, 0, }, right: []byte{ 1, 3, 0, 15, 0, 21, 0, 27, 0, 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', 1, 0, 'g', 1, 0, 'g', + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, want: []byte{ - 1, 7, 0, 21, 0, 27, 0, 33, 0, 39, 0, 45, 0, 51, 0, 57, 0, + 1, 7, 0, 23, 0, 29, 0, 35, 0, 41, 0, 47, 0, 53, 0, 59, 0, 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', From 671a71ca64e3a77b79f0c8829b1fdebbc54b20f4 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Sat, 8 Feb 2025 20:48:54 +0100 Subject: [PATCH 26/51] chore: add initial linter config --- .golangci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .golangci.yml diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..e960468 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,19 @@ +linters: + disable-all: true + + enable: + - errcheck + - lll + - gofmt + - gosec + - govet + - gosimple + - ineffassign + - staticcheck + - unused + + fast: true + +linter-settings: + +issues: From 9388d5d954b2e9bd12b6017499324b3f2b14ba38 Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Sat, 8 Feb 2025 21:32:26 +0100 Subject: [PATCH 27/51] feat(node): add pointer function --- internal/bpt/node/node.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/internal/bpt/node/node.go b/internal/bpt/node/node.go index 972b147..fbe3706 100644 --- a/internal/bpt/node/node.go +++ b/internal/bpt/node/node.go @@ -73,6 +73,8 @@ func Key(node []byte, i uint16) []byte { func Value(node []byte, i uint16) []byte { if i >= LenOf(node) { panic("index out of bounds") + } else if TypeOf(node) != TypeLeaf { + panic("non leaf node does not have values") } off := offset(node, i) kLen := binary.LittleEndian.Uint16(node[off:]) @@ -80,6 +82,17 @@ func Value(node []byte, i uint16) []byte { return node[off+4+kLen : off+4+kLen+vLen] } +func Pointer(node []byte, i uint16) uint64 { + if i >= LenOf(node) { + panic("index out of bounds") + } else if TypeOf(node) != TypePointer { + panic("non pointer node does not hold pointers") + } + off := offset(node, i) + kLen := binary.LittleEndian.Uint16(node[off:]) + return binary.LittleEndian.Uint64(node[off+2+kLen:]) +} + func Search(node []byte, target []byte) (uint16, bool) { n := LenOf(node) left, right := uint16(0), n From fa4e5f959a4b1b5a9bf1f1ee37b5ce1d86740d3a Mon Sep 17 00:00:00 2001 From: gkits <56302678+gKits@users.noreply.github.com> Date: Sat, 8 Feb 2025 21:32:36 +0100 Subject: [PATCH 28/51] feat(bpt): add basic get function --- internal/bpt/bpt.go | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/internal/bpt/bpt.go b/internal/bpt/bpt.go index 2af2040..72f2f72 100644 --- a/internal/bpt/bpt.go +++ b/internal/bpt/bpt.go @@ -1,16 +1,53 @@ package bpt -import "io" +import "github.com/pavosql/pavosql/internal/bpt/node" const pageSize = 4096 type pager interface { - io.ReaderAt - io.WriterAt + ReadPage(off uint64) ([]byte, error) Commit() error Rollback() error } type Tree struct { + root uint64 pager pager } + +func (tree *Tree) Get(k []byte) ([]byte, error) { + cur, err := tree.pager.ReadPage(tree.root) + if err != nil { + return nil, err + } + // TODO: add defered recover and custom errors + for { + i, exists := node.Search(cur, k) + switch node.TypeOf(cur) { + case node.TypePointer: + cur, err = tree.pager.ReadPage(node.Pointer(cur, i)) + if err != nil { + return nil, err + } + case node.TypeLeaf: + if !exists { + return nil, nil + } + return node.Value(cur, i), nil + default: + return nil, nil + } + } +} + +func (tree *Tree) Insert(k []byte, v []byte) error { + return nil +} + +func (tree *Tree) Update(k []byte, v []byte) error { + return nil +} + +func (tree *Tree) Delete(k []byte) error { + return nil +} From cb390b727d96d330b1349577f31dceca322405f7 Mon Sep 17 00:00:00 2001 From: gkits Date: Mon, 23 Jun 2025 10:34:51 +0200 Subject: [PATCH 29/51] feat: rewrite a lot of stuff --- .gitmodules | 0 docs.go | 5 +- go.mod | 10 +- go.sum | 14 +- internal/bpt/bpt.go | 53 --- internal/bpt/node/node.go | 297 --------------- internal/bpt/node/node_test.go | 650 -------------------------------- internal/common/common.go | 3 + internal/tree/node/errors.go | 7 + internal/tree/node/node.go | 269 +++++++++++++ internal/tree/node/node_test.go | 37 ++ internal/tree/tree.go | 28 ++ pkg/ast/ast.go | 2 + pkg/driver/driver.go | 1 + public/categories/index.xml | 11 + public/index.xml | 11 + public/sitemap.xml | 11 + public/tags/index.xml | 11 + 18 files changed, 399 insertions(+), 1021 deletions(-) create mode 100644 .gitmodules delete mode 100644 internal/bpt/bpt.go delete mode 100644 internal/bpt/node/node.go delete mode 100644 internal/bpt/node/node_test.go create mode 100644 internal/common/common.go create mode 100644 internal/tree/node/errors.go create mode 100644 internal/tree/node/node.go create mode 100644 internal/tree/node/node_test.go create mode 100644 internal/tree/tree.go create mode 100644 pkg/driver/driver.go create mode 100644 public/categories/index.xml create mode 100644 public/index.xml create mode 100644 public/sitemap.xml create mode 100644 public/tags/index.xml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..e69de29 diff --git a/docs.go b/docs.go index a96caeb..631787c 100644 --- a/docs.go +++ b/docs.go @@ -1,9 +1,7 @@ -package pavosql - /* MIT License -Copyright (c) 2023 Georgios Kitsikoudis +# Copyright (c) 2023 Georgios Kitsikoudis Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -23,3 +21,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +package pavosql diff --git a/go.mod b/go.mod index 44b339e..4096e5c 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,5 @@ module github.com/pavosql/pavosql -go 1.23 +go 1.24 -require github.com/stretchr/testify v1.10.0 - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) +require github.com/google/docsy v0.12.0 // indirect diff --git a/go.sum b/go.sum index 713a0b4..3ed3add 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,4 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/FortAwesome/Font-Awesome v0.0.0-20241216213156-af620534bfc3/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo= +github.com/google/docsy v0.12.0 h1:CddZKL39YyJzawr8GTVaakvcUTCJRAAYdz7W0qfZ2P4= +github.com/google/docsy v0.12.0/go.mod h1:1bioDqA493neyFesaTvQ9reV0V2vYy+xUAnlnz7+miM= +github.com/twbs/bootstrap v5.3.6+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0= diff --git a/internal/bpt/bpt.go b/internal/bpt/bpt.go deleted file mode 100644 index 72f2f72..0000000 --- a/internal/bpt/bpt.go +++ /dev/null @@ -1,53 +0,0 @@ -package bpt - -import "github.com/pavosql/pavosql/internal/bpt/node" - -const pageSize = 4096 - -type pager interface { - ReadPage(off uint64) ([]byte, error) - Commit() error - Rollback() error -} - -type Tree struct { - root uint64 - pager pager -} - -func (tree *Tree) Get(k []byte) ([]byte, error) { - cur, err := tree.pager.ReadPage(tree.root) - if err != nil { - return nil, err - } - // TODO: add defered recover and custom errors - for { - i, exists := node.Search(cur, k) - switch node.TypeOf(cur) { - case node.TypePointer: - cur, err = tree.pager.ReadPage(node.Pointer(cur, i)) - if err != nil { - return nil, err - } - case node.TypeLeaf: - if !exists { - return nil, nil - } - return node.Value(cur, i), nil - default: - return nil, nil - } - } -} - -func (tree *Tree) Insert(k []byte, v []byte) error { - return nil -} - -func (tree *Tree) Update(k []byte, v []byte) error { - return nil -} - -func (tree *Tree) Delete(k []byte) error { - return nil -} diff --git a/internal/bpt/node/node.go b/internal/bpt/node/node.go deleted file mode 100644 index fbe3706..0000000 --- a/internal/bpt/node/node.go +++ /dev/null @@ -1,297 +0,0 @@ -// The node package provides all basic functionalities of a singular node inside a B+Tree. A single -// node can be of two either two types [TypeLeaf] or [TypePointer]. Both node types contain -// key-value pairs with the primary difference beeing that [TypeLeaf] nodes store actual data as -// their values while [TypePointer] store pointers to other nodes that are called their children. -// -// Nodes are stored in a custom byte format that is structured as follows: -// -// offset | size in B | description -// --------+-----------+------------- -// 0 | 1 | Type identifier -// 1 | 2 | Number of stored cells N -// 3 | 2xN | Offset list to each cell -// 3+2xN | ... | Cells -// -// The cells that are stored on a [TypeLeaf] node consist of a key and a value. They are also stored -// in the form of a custom byte format with following structure. -// -// offset | size in B | description -// --------+-----------+------------- -// 0 | 2 | Length of key K -// 2 | K | Key -// 2+K | 2 | Length of value V -// 4+K | V | Value -// -// Since a [TypePointer] node only stores uint64 pointers to other nodes their cells structure looks -// slightly different. -// -// offset | size in B | description -// --------+-----------+------------- -// 0 | 2 | Length of key K -// 2 | K | Key -// 2+K | 8 | Pointer -package node - -import ( - "bytes" - "encoding/binary" - "slices" -) - -type Type uint8 - -const ( - TypeLeaf Type = 0x01 - TypePointer Type = 0x10 -) - -func TypeOf(node []byte) Type { - t := Type(node[0]) - if t != TypeLeaf && t != TypePointer { - panic("invalid node type") - } - return t -} - -func LenOf(node []byte) uint16 { - return binary.LittleEndian.Uint16(node[1:]) -} - -func SizeOf(node []byte) uint16 { - return offset(node, LenOf(node)) -} - -func Key(node []byte, i uint16) []byte { - if i >= LenOf(node) { - panic("index out of bounds") - } - off := offset(node, i) - kLen := binary.LittleEndian.Uint16(node[off:]) - return node[off+2 : off+2+kLen] -} - -func Value(node []byte, i uint16) []byte { - if i >= LenOf(node) { - panic("index out of bounds") - } else if TypeOf(node) != TypeLeaf { - panic("non leaf node does not have values") - } - off := offset(node, i) - kLen := binary.LittleEndian.Uint16(node[off:]) - vLen := binary.LittleEndian.Uint16(node[off+2+kLen:]) - return node[off+4+kLen : off+4+kLen+vLen] -} - -func Pointer(node []byte, i uint16) uint64 { - if i >= LenOf(node) { - panic("index out of bounds") - } else if TypeOf(node) != TypePointer { - panic("non pointer node does not hold pointers") - } - off := offset(node, i) - kLen := binary.LittleEndian.Uint16(node[off:]) - return binary.LittleEndian.Uint64(node[off+2+kLen:]) -} - -func Search(node []byte, target []byte) (uint16, bool) { - n := LenOf(node) - left, right := uint16(0), n - - for left < right { - cur := uint16(uint(left+right) >> 1) // #nosec G115 // right shift stops overflow - if cmp := bytes.Compare(Key(node, cur), target); cmp == -1 { - left = cur + 1 - } else if cmp == 1 { - right = cur - } else { - return cur, true - } - } - - return left, left < n && bytes.Equal(Key(node, left), target) -} - -func Insert(node []byte, i uint16, k, v []byte) []byte { - n := LenOf(node) - off := offset(node, i) - cell := makeCell(k, v) - - binary.LittleEndian.PutUint16(node[1:], n+1) - - node = slices.Insert(node, int(off), cell...) - - node = slices.Insert(node, int(3+i*2), 0, 0) - binary.LittleEndian.PutUint16(node[3+i*2:], off) - - for a := uint16(1); a <= n+1; a++ { - currentOffset := offset(node, a) - var shift uint16 = 2 - if a > i { - shift += uint16(len(cell)) - } - setOffset(node, a, uint16(currentOffset)+shift) - } - return node -} - -func Update(node []byte, i uint16, k, v []byte) []byte { - n := LenOf(node) - off := offset(node, i) - cell := makeCell(k, v) - - nextOff := offset(node, i+1) - node = slices.Replace(node, int(off), int(nextOff), cell...) - lenDiff := len(cell) - int(nextOff-off) - - for a := i + 1; a <= n; a++ { - currentOffset := offset(node, a) - setOffset(node, a, uint16(int(currentOffset)+lenDiff)) - } - - return node -} - -func Delete(node []byte, i uint16) []byte { - n := LenOf(node) - off := offset(node, i) - nextOff := offset(node, i+1) - - binary.LittleEndian.PutUint16(node[1:], n-1) - node = slices.Delete(node, int(off), int(nextOff)) - - node = slices.Delete(node, int(3+i*2), int(3+i*2+2)) - - for a := uint16(1); a <= n-1; a++ { - currentOffset := offset(node, a) - shift := uint16(2) - if a > i { - shift += nextOff - off - } - setOffset(node, a, uint16(currentOffset-shift)) - } - - return node -} - -func Merge(left, right []byte) []byte { - if TypeOf(left) != TypeOf(right) { - panic("node types do not match") - } - - ln := LenOf(left) - rn := LenOf(right) - lsz := SizeOf(left) - rsz := SizeOf(right) - - merged := slices.Insert(left, int(lsz), right[offset(right, 0):rsz]...) - merged = slices.Insert(merged, int(offset(left, 0)), right[3:3+2*rn]...) - binary.LittleEndian.PutUint16(merged[1:], ln+rn) - - for i := range LenOf(merged) + 1 { - curOff := offset(merged, i) - shift := rn * 2 - if i > ln { - shift = offset(left, ln) - 3 - } - setOffset(merged, i, curOff+shift) - } - - lastOff := offset(left, LenOf(left)) - - _ = lastOff - - return merged[:SizeOf(merged)] -} - -func Split(node []byte) ([]byte, []byte) { - center := func(node []byte) uint16 { - n := LenOf(node) - target := ((uint16(SizeOf(node)) - offset(node, 0)) >> 1) + offset(node, 0) - - cur := n / 2 - for cur > 0 && cur < n { - curOff := offset(node, cur) - prevOff := offset(node, cur-1) - nextOff := offset(node, cur+1) - - switch { - case curOff == target: - return cur - case nextOff == target: - return cur + 1 - case prevOff == target: - return cur - 1 - case curOff < target && nextOff < target: - cur = cur + 1 - continue - case curOff > target && prevOff > target: - cur = cur - 1 - case curOff < target && nextOff > target: - if target-curOff < nextOff-target { - return cur - } - return cur + 1 - case curOff > target && prevOff < target: - if curOff-target < target-prevOff { - return cur - } - return cur - 1 - } - } - return cur - }(node) - - n := LenOf(node) - typ := TypeOf(node) - off0 := offset(node, 0) - offC := offset(node, center) - - left := []byte{byte(typ)} - rigth := []byte{byte(typ)} - - left = binary.LittleEndian.AppendUint16(left, center) - rigth = binary.LittleEndian.AppendUint16(rigth, n-center) - - left = append(left, node[3:3+2*center]...) - rigth = append(rigth, node[3+2*center:off0]...) - - left = append(left, node[off0:offC]...) - rigth = append(rigth, node[offC:]...) - - for i := range LenOf(left) + 1 { - curOff := offset(left, i) - setOffset(left, i, curOff-(n-center)*2) - } - for i := range LenOf(rigth) + 1 { - curOff := offset(rigth, i) - setOffset(rigth, i, curOff-offC+3+2*(n-center)) - } - - return left, rigth -} - -func offset(node []byte, i uint16) uint16 { - if i == 0 { - return 3 + LenOf(node)*2 - } else if i > LenOf(node) { - panic("node index out of bounds") - } - return binary.LittleEndian.Uint16(node[3+(i-1)*2:]) -} - -func setOffset(node []byte, i uint16, off uint16) { - if i > LenOf(node) { - panic("node index out of bounds") - } else if i != 0 { - binary.LittleEndian.PutUint16(node[3+(i-1)*2:], off) - } -} - -func makeCell(k, v []byte) []byte { - cell := make([]byte, 4+len(k)+len(v)) - binary.LittleEndian.PutUint16(cell[0:], uint16(len(k))) - copy(cell[2:], k) - binary.LittleEndian.PutUint16(cell[2+len(k):], uint16(len(v))) - copy(cell[4+len(k):], v) - return cell -} diff --git a/internal/bpt/node/node_test.go b/internal/bpt/node/node_test.go deleted file mode 100644 index 6218a9a..0000000 --- a/internal/bpt/node/node_test.go +++ /dev/null @@ -1,650 +0,0 @@ -package node - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestTypeOf(t *testing.T) { - cases := map[string]struct { - node []byte - want Type - assertPanic assert.PanicAssertionFunc - }{ - "get leaf node type": { - node: []byte{byte(TypeLeaf)}, - want: TypeLeaf, - assertPanic: assert.NotPanics, - }, - "get pointer node type": { - node: []byte{byte(TypePointer)}, - want: TypePointer, - assertPanic: assert.NotPanics, - }, - "panic on invalid node type 0x00": { - node: []byte{0x00}, - assertPanic: assert.Panics, - }, - "panic on invalid node type 0x11": { - node: []byte{0x11}, - assertPanic: assert.Panics, - }, - "panic on invalid node type 0xff": { - node: []byte{0xff}, - assertPanic: assert.Panics, - }, - } - - for name, c := range cases { - t.Run(name, func(t *testing.T) { - c.assertPanic(t, func() { - got := TypeOf(c.node) - assert.Equal(t, c.want, got) - }) - }) - } -} - -func TestKey(t *testing.T) { - cases := map[string]struct { - node []byte - input uint16 - want []byte - assertPanic assert.PanicAssertionFunc - }{ - "0th key": { - node: fixtureNodeEvenNumberOfKeys(), - input: 0, - want: []byte("aaa"), - assertPanic: assert.NotPanics, - }, - "1st key": { - node: fixtureNodeEvenNumberOfKeys(), - input: 1, - want: []byte("bbb"), - assertPanic: assert.NotPanics, - }, - "9th key": { - node: fixtureNodeEvenNumberOfKeys(), - input: 9, - want: []byte("jjj"), - assertPanic: assert.NotPanics, - }, - "10th key": { - node: fixtureNodeEvenNumberOfKeys(), - input: 10, - assertPanic: assert.Panics, - }, - } - - for name, c := range cases { - t.Run(name, func(t *testing.T) { - c.assertPanic(t, func() { - got := Key(c.node, c.input) - assert.Equal(t, c.want, got) - }) - }) - } -} - -func TestValue(t *testing.T) { - cases := map[string]struct { - node []byte - input uint16 - want []byte - assertPanic assert.PanicAssertionFunc - }{ - "0th value": { - node: fixtureNodeEvenNumberOfKeys(), - input: 0, - want: []byte("aaa"), - assertPanic: assert.NotPanics, - }, - "1st value": { - node: fixtureNodeEvenNumberOfKeys(), - input: 1, - want: []byte("bbb"), - assertPanic: assert.NotPanics, - }, - "9th value": { - node: fixtureNodeEvenNumberOfKeys(), - input: 9, - want: []byte("jjj"), - assertPanic: assert.NotPanics, - }, - "10th value": { - node: fixtureNodeEvenNumberOfKeys(), - input: 10, - assertPanic: assert.Panics, - }, - } - - for name, c := range cases { - t.Run(name, func(t *testing.T) { - c.assertPanic(t, func() { - got := Value(c.node, c.input) - assert.Equal(t, c.want, got) - }) - }) - } -} - -func TestSearch(t *testing.T) { - cases := map[string]struct { - node, input []byte - want uint16 - assertExists assert.BoolAssertionFunc - }{ - "search existing target in odd length node": { - node: fixtureNodeOddNumberOfKeys(), - input: []byte("ddd"), - want: 3, - assertExists: assert.True, - }, - "search non existing target in odd length node": { - node: fixtureNodeOddNumberOfKeys(), - input: []byte("iij"), - want: 9, - assertExists: assert.False, - }, - "search existing target in even length node": { - node: fixtureNodeEvenNumberOfKeys(), - input: []byte("hhh"), - want: 7, - assertExists: assert.True, - }, - "search non existing target in even length node": { - node: fixtureNodeEvenNumberOfKeys(), - input: []byte("bbc"), - want: 2, - assertExists: assert.False, - }, - "search before first key in odd length node": { - node: fixtureNodeOddNumberOfKeys(), - input: []byte("aa"), - want: 0, - assertExists: assert.False, - }, - "search after last key in odd length node": { - node: fixtureNodeOddNumberOfKeys(), - input: []byte("zzzzz"), - want: 11, - assertExists: assert.False, - }, - "search before first key in even length node": { - node: fixtureNodeEvenNumberOfKeys(), - input: []byte("aa"), - want: 0, - assertExists: assert.False, - }, - "search after last key in even length node": { - node: fixtureNodeEvenNumberOfKeys(), - input: []byte("zzzzz"), - want: 10, - assertExists: assert.False, - }, - } - - for name, c := range cases { - t.Run(name, func(t *testing.T) { - got, exists := Search(c.node, c.input) - assert.Equal(t, c.want, got) - c.assertExists(t, exists) - }) - } -} - -func TestInsert(t *testing.T) { - cases := map[string]struct { - node []byte - i uint16 - k, v []byte - want []byte - - wantLen uint16 - wantKAfter []byte - }{ - "insert new cell": { - i: 1, k: []byte("new"), v: []byte("new"), - node: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - want: []byte{ - 1, 4, 0, 17, 0, 27, 0, 33, 0, 39, 0, - 1, 0, 'a', 1, 0, 'a', - 3, 0, 'n', 'e', 'w', 3, 0, 'n', 'e', 'w', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - }, - "insert new cell at the start": { - i: 0, k: []byte("key"), v: []byte("value"), - node: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - want: []byte{ - 1, 4, 0, 23, 0, 29, 0, 35, 0, 41, 0, - 3, 0, 'k', 'e', 'y', 5, 0, 'v', 'a', 'l', 'u', 'e', - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - }, - "insert new cell at the end": { - i: 2, k: []byte("at"), v: []byte("the end"), - node: []byte{ - 1, 2, 0, 13, 0, 19, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - }, - want: []byte{ - 1, 3, 0, 15, 0, 21, 0, 34, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 2, 0, 'a', 't', 7, 0, 't', 'h', 'e', ' ', 'e', 'n', 'd', - }, - }, - } - - for name, c := range cases { - t.Run(name, func(t *testing.T) { - got := Insert(c.node, c.i, c.k, c.v) - assert.Equal(t, c.want, got) - }) - } -} - -func TestUpdate(t *testing.T) { - cases := map[string]struct { - node []byte - i uint16 - k, v []byte - want []byte - }{ - "update a single cell node": { - i: 0, k: []byte("a"), v: []byte("this is a new value"), - node: []byte{ - 1, 1, 0, 11, 0, - 1, 0, 'a', 1, 0, 'a', - }, - want: []byte{ - 1, 1, 0, 29, 0, - 1, 0, 'a', 19, 0, 't', 'h', 'i', 's', ' ', 'i', 's', ' ', 'a', ' ', 'n', 'e', 'w', ' ', 'v', 'a', 'l', 'u', 'e', - }, - }, - "update first cell of a multi cell node": { - i: 0, k: []byte("a"), v: []byte("new"), - node: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - want: []byte{ - 1, 3, 0, 17, 0, 23, 0, 29, 0, - 1, 0, 'a', 3, 0, 'n', 'e', 'w', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - }, - "update last cell of a multi cell node": { - i: 2, k: []byte("ccc"), v: []byte("new"), - node: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - want: []byte{ - 1, 3, 0, 15, 0, 21, 0, 31, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 3, 0, 'c', 'c', 'c', 3, 0, 'n', 'e', 'w', - }, - }, - "update cell with shorter cell": { - i: 2, k: []byte("c"), v: []byte("c"), - node: []byte{ - 1, 3, 0, 15, 0, 21, 0, 29, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 2, 0, 'c', 'c', 2, 0, 'c', 'c', - }, - want: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - }, - } - - for name, c := range cases { - t.Run(name, func(t *testing.T) { - got := Update(c.node, c.i, c.k, c.v) - assert.Equal(t, c.want, got) - }) - } -} - -func TestDelete(t *testing.T) { - cases := map[string]struct { - node []byte - i uint16 - want []byte - }{ - "delete last cell of single cell node": { - i: 0, - node: []byte{ - 1, 1, 0, 11, 0, - 1, 0, 'a', 1, 0, 'a', - }, - want: []byte{ - 1, 0, 0, - }, - }, - "delete first cell of multi cell node": { - i: 0, - node: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - want: []byte{ - 1, 2, 0, 13, 0, 19, 0, - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - }, - "delete last cell of multi cell node": { - i: 2, - node: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - want: []byte{ - 1, 2, 0, 13, 0, 19, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - }, - }, - "delete cell of multi cell node": { - i: 1, - node: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', - }, - want: []byte{ - 1, 2, 0, 13, 0, 19, 0, - 1, 0, 'a', 1, 0, 'a', - 1, 0, 'c', 1, 0, 'c', - }, - }, - } - - for name, c := range cases { - t.Run(name, func(t *testing.T) { - got := Delete(c.node, c.i) - assert.Equal(t, c.want, got) - }) - } -} - -func TestMerge(t *testing.T) { - cases := map[string]struct { - left, right []byte - want []byte - }{ - "merge two equally sized nodes": { - left: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', - }, - right: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - }, - want: []byte{ - 1, 6, 0, 21, 0, 27, 0, 33, 0, 39, 0, 45, 0, 51, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - }, - }, - "merge two differently sized nodes": { - left: []byte{ - 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - }, - right: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', 1, 0, 'g', 1, 0, 'g', - }, - want: []byte{ - 1, 7, 0, 23, 0, 29, 0, 35, 0, 41, 0, 47, 0, 53, 0, 59, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - 1, 0, 'g', 1, 0, 'g', - }, - }, - "merge two differently sized nodes with trailing zeros": { - left: []byte{ - 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - 0, 0, 0, 0, 0, - }, - right: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', 1, 0, 'g', 1, 0, 'g', - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - }, - want: []byte{ - 1, 7, 0, 23, 0, 29, 0, 35, 0, 41, 0, 47, 0, 53, 0, 59, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - 1, 0, 'g', 1, 0, 'g', - }, - }, - } - - for name, c := range cases { - t.Run(name, func(t *testing.T) { - got := Merge(c.left, c.right) - assert.Equal(t, c.want, got) - }) - } -} - -func TestSplit(t *testing.T) { - cases := map[string]struct { - node []byte - wantL, wantR []byte - }{ - "split symmetrical node with even number of cells": { - node: []byte{ - 1, 6, 0, 21, 0, 27, 0, 33, 0, 39, 0, 45, 0, 51, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - }, - wantL: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', - }, - wantR: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - }, - }, - "split symmetrical node with odd number of cells": { - node: []byte{ - 1, 7, 0, 23, 0, 29, 0, 35, 0, 41, 0, 47, 0, 53, 0, 59, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - 1, 0, 'g', 1, 0, 'g', - }, - wantL: []byte{ - 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - }, - wantR: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', 1, 0, 'g', 1, 0, 'g', - }, - }, - "split asymmetrical node with even number of cells": { - node: []byte{ - 1, 6, 0, 22, 0, 28, 0, 36, 0, 42, 0, 48, 0, 54, 0, - 2, 0, 'a', 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', - 2, 0, 'c', 'c', 2, 0, 'c', 'c', 1, 0, 'd', 1, 0, 'd', - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - }, - wantL: []byte{ - 1, 3, 0, 16, 0, 22, 0, 30, 0, - 2, 0, 'a', 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 2, 0, 'c', 'c', 2, 0, 'c', 'c', - }, - wantR: []byte{ - 1, 3, 0, 15, 0, 21, 0, 27, 0, - 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - }, - }, - "split asymmetrical node with odd number of cells": { - node: []byte{ - 1, 7, 0, 24, 0, 30, 0, 38, 0, 44, 0, 50, 0, 56, 0, 64, 0, - 2, 0, 'a', 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', - 2, 0, 'c', 'c', 2, 0, 'c', 'c', 1, 0, 'd', 1, 0, 'd', - 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', - 1, 0, 'g', 3, 0, 'g', 'g', 'g', - }, - wantL: []byte{ - 1, 3, 0, 16, 0, 22, 0, 30, 0, - 2, 0, 'a', 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 2, 0, 'c', 'c', 2, 0, 'c', 'c', - }, - wantR: []byte{ - 1, 4, 0, 17, 0, 23, 0, 29, 0, 37, 0, - 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', 1, 0, 'f', 1, 0, 'f', 1, 0, 'g', 3, 0, 'g', 'g', 'g', - }, - }, - "split asymmetrical node with large last cell": { - node: []byte{ - 1, 5, 0, 19, 0, 25, 0, 31, 0, 37, 0, 61, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', - 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - 10, 0, 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', - 10, 0, 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', - }, - wantL: []byte{ - 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, - 1, 0, 'a', 1, 0, 'a', 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', - }, - wantR: []byte{ - 1, 1, 0, 29, 0, - 10, 0, 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', - 10, 0, 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', 'e', - }, - }, - "split asymmetrical node with large first cell": { - node: []byte{ - 1, 5, 0, 37, 0, 43, 0, 49, 0, 55, 0, 61, 0, - 10, 0, 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', - 10, 0, 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', - 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', - 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', - }, - wantL: []byte{ - 1, 1, 0, 29, 0, - 10, 0, 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', - 10, 0, 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', - }, - wantR: []byte{ - 1, 4, 0, 17, 0, 23, 0, 29, 0, 35, 0, - 1, 0, 'b', 1, 0, 'b', 1, 0, 'c', 1, 0, 'c', 1, 0, 'd', 1, 0, 'd', 1, 0, 'e', 1, 0, 'e', - }, - }, - } - - for name, c := range cases { - t.Run(name, func(t *testing.T) { - gotL, gotR := Split(c.node) - assert.Equal(t, c.wantL, gotL) - assert.Equal(t, c.wantR, gotR) - }) - } -} - -// Fixtures - -func fixtureNodeEvenNumberOfKeys() []byte { - return []byte{ - 1, - 10, 0, - // Offsets - 33, 0, - 43, 0, - 53, 0, - 63, 0, - 73, 0, - 83, 0, - 93, 0, - 103, 0, - 113, 0, - 123, 0, - // K-V Pairs - 3, 0, 'a', 'a', 'a', 3, 0, 'a', 'a', 'a', - 3, 0, 'b', 'b', 'b', 3, 0, 'b', 'b', 'b', - 3, 0, 'c', 'c', 'c', 3, 0, 'c', 'c', 'c', - 3, 0, 'd', 'd', 'd', 3, 0, 'd', 'd', 'd', - 3, 0, 'e', 'e', 'e', 3, 0, 'e', 'e', 'e', - 3, 0, 'f', 'f', 'f', 3, 0, 'f', 'f', 'f', - 3, 0, 'g', 'g', 'g', 3, 0, 'g', 'g', 'g', - 3, 0, 'h', 'h', 'h', 3, 0, 'h', 'h', 'h', - 3, 0, 'i', 'i', 'i', 3, 0, 'i', 'i', 'i', - 3, 0, 'j', 'j', 'j', 3, 0, 'j', 'j', 'j', - } -} - -func fixtureNodeOddNumberOfKeys() []byte { - return []byte{ - 1, - 11, 0, - // Offsets - 35, 0, - 45, 0, - 55, 0, - 65, 0, - 75, 0, - 85, 0, - 95, 0, - 105, 0, - 115, 0, - 125, 0, - 135, 0, - // K-V Pairs - 3, 0, 'a', 'a', 'a', 3, 0, 'a', 'a', 'a', - 3, 0, 'b', 'b', 'b', 3, 0, 'b', 'b', 'b', - 3, 0, 'c', 'c', 'c', 3, 0, 'c', 'c', 'c', - 3, 0, 'd', 'd', 'd', 3, 0, 'd', 'd', 'd', - 3, 0, 'e', 'e', 'e', 3, 0, 'e', 'e', 'e', - 3, 0, 'f', 'f', 'f', 3, 0, 'f', 'f', 'f', - 3, 0, 'g', 'g', 'g', 3, 0, 'g', 'g', 'g', - 3, 0, 'h', 'h', 'h', 3, 0, 'h', 'h', 'h', - 3, 0, 'i', 'i', 'i', 3, 0, 'i', 'i', 'i', - 3, 0, 'j', 'j', 'j', 3, 0, 'j', 'j', 'j', - 3, 0, 'k', 'k', 'k', 3, 0, 'k', 'k', 'k', - } -} diff --git a/internal/common/common.go b/internal/common/common.go new file mode 100644 index 0000000..1a1d308 --- /dev/null +++ b/internal/common/common.go @@ -0,0 +1,3 @@ +package common + +const PageSize = 8192 diff --git a/internal/tree/node/errors.go b/internal/tree/node/errors.go new file mode 100644 index 0000000..4293cfd --- /dev/null +++ b/internal/tree/node/errors.go @@ -0,0 +1,7 @@ +package node + +import "errors" + +var ( + ErrIndexOutOfBounds = errors.New("index is out of bounds") +) diff --git a/internal/tree/node/node.go b/internal/tree/node/node.go new file mode 100644 index 0000000..b4b0730 --- /dev/null +++ b/internal/tree/node/node.go @@ -0,0 +1,269 @@ +package node + +import ( + "bytes" + "encoding/binary" + "iter" + + "github.com/pavosql/pavosql/internal/common" +) + +const ( + nOff = 1 + wCurOff = nOff + 2 + dataOff = wCurOff + 2 +) + +/* +A Nodes is an array of bytes representing the data stored in a single node of a B+Tree. +Nodes are stored in a custom byte format that is structured as follows: + + Description | Header | Data area + ------------+--------+---------- + Size in B | 5 | ? + +The Header contains specific meta data about the current state of the node and is structured as +follows: + + Description | Type | N | W-Cursor + ------------+------+---+------------- + Size in B | 1 | 2 | 2 + +N is the number of cells currently stored in the node and W-Cursor the write cursor represented +by an uint16 offset to the next position data can be written to. + +The Data area is dynamically sized an contains the actual data in form of cells as well as the +offset references to those cells. Those two parts are separated by an empty space called the +void. The Data area is structured as follows: + + Description | Offsets | Void | Cells + ------------+---------+---------------------------+------ + Size in B | N * 2 | W-Cursor - (N*2 + Header) | ? + +The offsets are just an ordered list of uint16 pointing to the position of cell they are +referencing inside this node. The void is the empty space between the end of the offset list and +the W-Cursor which always points to the beginning of the cells. + +The data stored in the cells is formatted as follows: + + Description | KeyLen | ValLen | Key | Val + ------------+--------+--------+--------+------- + Size in B | 2 | 2 | KeyLen | ValLen +*/ +type Node [common.PageSize]byte + +func New(typ byte) Node { + var n Node + n[0] = typ + n.setWCursor(common.PageSize) + return n +} + +// Returns the type of n. +func (n *Node) Type() byte { + return n[0] +} + +// Returns the number of cells currently stored on n. +func (n *Node) N() uint16 { + return binary.LittleEndian.Uint16(n[nOff:]) +} + +// Returns the key of the i'th cell stored in n. +// +// Panics if i is greater or equal than the length of n. +func (n *Node) Key(i uint16) []byte { + if !n.indexInBounds(i) { + panic(ErrIndexOutOfBounds) + } + off := n.offset(i) + kLen := binary.LittleEndian.Uint16(n[off:]) + return n[off+4 : off+4+kLen] +} + +// Returns the value of the i'th cell stored in n. +// +// Panics if i is greater or equal than the length of n. +func (n *Node) Val(i uint16) []byte { + if !n.indexInBounds(i) { + panic(ErrIndexOutOfBounds) + } + off := n.offset(i) + kLen := binary.LittleEndian.Uint16(n[off:]) + vLen := binary.LittleEndian.Uint16(n[off+2:]) + return n[off+4+kLen : off+4+kLen+vLen] +} + +// Binary searches the target key inside n and returns its position and weither it exists. +func (n *Node) Search(target []byte) (uint16, bool) { + l := n.N() + left, right := uint16(0), l + + for left < right { + cur := uint16(uint(left+right) >> 1) // #nosec G115 // right shift stops overflow + if cmp := bytes.Compare(n.Key(cur), target); cmp == -1 { + left = cur + 1 + } else if cmp == 1 { + right = cur + } else { + return cur, true + } + } + return left, left < l && bytes.Equal(n.Key(left), target) +} + +// Returns a copy of n with k-v set at position i. If the key at i is equal to k it will be +// overwritten an n.N will stay the same otherwise k-v will be inserted as a new cell and n.N will +// be increased by 1. +// +// Overwriting a k-v pair does not overwrite the data stored in the original cell, it mereley +// overwrites the reference to it. To free up the space taken up by unreferenced cells use Vacuum. +// +// WARNING: No additional check is performed weither i is the correct position for k. Meaning it is +// the callers responsibility to ensure that k belongs at position i to ensure the order of the keys +// will not break. Always use Search before using Set to get the correct value for i. +func (n *Node) Set(i uint16, k, v []byte) Node { + l := n.N() + cell := makeCell(k, v) + + wCur := n.wCursor() + off := wCur - uint16(len(cell)) + + var res Node + copy(res[:], n[:]) + copy(res[off:wCur], cell) + + res.setWCursor(off) + + if ogK := n.Key(i); bytes.Equal(k, ogK) { + res.setOffset(i, off) + return res + } + + res.setN(l + 1) + + return res +} + +// Returns true if n has enough space left in its void to add the given k-v pair. CanSet always +// assumes that k does not exist. +func (n *Node) CanSet(k, v []byte) bool { + return n.voidSize() >= uint16(6+len(k)+len(v)) +} + +// Returns a copy of n with the given k-v pair set into it. If k already exists its value is +// overwritten otherwise a new cell for k-v is inserted. +func (n *Node) Delete(k []byte) Node { + return Node{} +} + +// Splits n into two separate nodes. +func (n *Node) Split() (left Node, right Node) { + var addToRight bool + var i uint16 + var wc uint16 = common.PageSize + + left, right = New(n.Type()), New(n.Type()) + + addToNode := func(addTo *Node, i uint16, cell []byte, wCursor *uint16) { + *wCursor -= uint16(len(cell)) + addTo.setOffset(i, *wCursor) + copy(addTo[*wCursor:], cell) + addTo.setWCursor(*wCursor) + } + + for k, v := range n.All() { + cell := makeCell(k, v) + + if addToRight { + addToNode(&right, i, cell, &wc) + i++ + continue + } + + addToNode(&left, i, cell, &wc) + i++ + } + return left, right +} + +// Returns a resorted and reduced copy of n by freeing up space used by unreferenced cells. +func (n *Node) Vacuum() Node { + var vacuumed Node + vacuumed[0] = n.Type() + vacuumed.setN(n.N()) + + var wc uint16 = common.PageSize + var i uint16 + for k, v := range n.All() { + cell := makeCell(k, v) + wc -= uint16(len(cell)) + vacuumed.setOffset(i, wc) + copy(vacuumed[wc:], cell) + + i++ + } + vacuumed.setWCursor(wc) + + return vacuumed +} + +// An iterator over all key-value pairs of n. +func (n *Node) All() iter.Seq2[[]byte, []byte] { + return n.AllFrom(0) +} + +// An iterator over all key-value pairs of n starting from position i. +func (n *Node) AllFrom(i uint16) iter.Seq2[[]byte, []byte] { + return func(yield func([]byte, []byte) bool) { + for ; i < n.N(); i++ { + k, v := n.Key(i), n.Val(i) + if !yield(k, v) { + return + } + } + } +} + +func (n *Node) setN(nc uint16) { + binary.LittleEndian.PutUint16(n[nOff:], nc) +} + +func (n *Node) offset(i uint16) uint16 { + if n.indexInBounds(i) { + panic(ErrIndexOutOfBounds) + } + return binary.LittleEndian.Uint16(n[2*i+dataOff:]) +} + +func (n *Node) setOffset(i, off uint16) { + if n.indexInBounds(i) { + panic(ErrIndexOutOfBounds) + } + binary.LittleEndian.PutUint16(n[2*i+dataOff:], off) +} + +func (n *Node) indexInBounds(i uint16) bool { + return i >= n.N() +} + +func (n *Node) wCursor() uint16 { + return binary.LittleEndian.Uint16(n[wCurOff:]) +} + +func (n *Node) setWCursor(wc uint16) { + binary.LittleEndian.PutUint16(n[wCurOff:], wc) +} + +func (n *Node) voidSize() uint16 { + return n.wCursor() - (dataOff + n.N()*2) +} + +func makeCell(k, v []byte) []byte { + cell := make([]byte, 4+len(k)+len(v)) + binary.LittleEndian.PutUint16(cell[0:], uint16(len(k))) + binary.LittleEndian.PutUint16(cell[2:], uint16(len(v))) + copy(cell[4:], k) + copy(cell[4+len(k):], v) + return cell +} diff --git a/internal/tree/node/node_test.go b/internal/tree/node/node_test.go new file mode 100644 index 0000000..4599b5e --- /dev/null +++ b/internal/tree/node/node_test.go @@ -0,0 +1,37 @@ +package node + +import ( + "testing" +) + +func TestKey(t *testing.T) { + t.Error("not tested") +} + +func TestVal(t *testing.T) { + t.Error("not tested") +} + +func TestSearch(t *testing.T) { + t.Error("not tested") +} + +func TestSet(t *testing.T) { + t.Error("not tested") +} + +func TestCanSet(t *testing.T) { + t.Error("not tested") +} + +func TestDelete(t *testing.T) { + t.Error("not tested") +} + +func TestSplit(t *testing.T) { + t.Error("not tested") +} + +func TestVacuum(t *testing.T) { + t.Error("not tested") +} diff --git a/internal/tree/tree.go b/internal/tree/tree.go new file mode 100644 index 0000000..97cdbbf --- /dev/null +++ b/internal/tree/tree.go @@ -0,0 +1,28 @@ +package tree + +type pager interface { + ReadPage(off uint64) ([]byte, error) + Commit() error + Rollback() error +} + +type Tree struct { + root uint64 + pager pager +} + +func New() *Tree { + return &Tree{} +} + +func (t *Tree) Get(k []byte) ([]byte, error) { + return nil, nil +} + +func (t *Tree) Set(k []byte, v []byte) error { + return nil +} + +func (t *Tree) Delete(k []byte) error { + return nil +} diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go index ce64e7d..1fd22f1 100644 --- a/pkg/ast/ast.go +++ b/pkg/ast/ast.go @@ -1,3 +1,5 @@ package ast type Stmnt interface{} + +// TODO: Define AST types. diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go new file mode 100644 index 0000000..bce7c46 --- /dev/null +++ b/pkg/driver/driver.go @@ -0,0 +1 @@ +package driver diff --git a/public/categories/index.xml b/public/categories/index.xml new file mode 100644 index 0000000..97e554c --- /dev/null +++ b/public/categories/index.xml @@ -0,0 +1,11 @@ + + + + Categories on + //localhost:1313/categories/ + Recent content in Categories on + Hugo + en + + + diff --git a/public/index.xml b/public/index.xml new file mode 100644 index 0000000..ddfcdb6 --- /dev/null +++ b/public/index.xml @@ -0,0 +1,11 @@ + + + + + //localhost:1313/ + Recent content on + Hugo + en + + + diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..7501792 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,11 @@ + + + + //localhost:1313/ + + //localhost:1313/categories/ + + //localhost:1313/tags/ + + diff --git a/public/tags/index.xml b/public/tags/index.xml new file mode 100644 index 0000000..b21383f --- /dev/null +++ b/public/tags/index.xml @@ -0,0 +1,11 @@ + + + + Tags on + //localhost:1313/tags/ + Recent content in Tags on + Hugo + en + + + From 4349c276855c833f7cd589199fc17b770943bbd1 Mon Sep 17 00:00:00 2001 From: "georgios.kitsikoudis" Date: Mon, 23 Jun 2025 10:55:06 +0200 Subject: [PATCH 30/51] remove generated public dir --- public/categories/index.xml | 11 ----------- public/index.xml | 11 ----------- public/sitemap.xml | 11 ----------- public/tags/index.xml | 11 ----------- 4 files changed, 44 deletions(-) delete mode 100644 public/categories/index.xml delete mode 100644 public/index.xml delete mode 100644 public/sitemap.xml delete mode 100644 public/tags/index.xml diff --git a/public/categories/index.xml b/public/categories/index.xml deleted file mode 100644 index 97e554c..0000000 --- a/public/categories/index.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Categories on - //localhost:1313/categories/ - Recent content in Categories on - Hugo - en - - - diff --git a/public/index.xml b/public/index.xml deleted file mode 100644 index ddfcdb6..0000000 --- a/public/index.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - //localhost:1313/ - Recent content on - Hugo - en - - - diff --git a/public/sitemap.xml b/public/sitemap.xml deleted file mode 100644 index 7501792..0000000 --- a/public/sitemap.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - //localhost:1313/ - - //localhost:1313/categories/ - - //localhost:1313/tags/ - - diff --git a/public/tags/index.xml b/public/tags/index.xml deleted file mode 100644 index b21383f..0000000 --- a/public/tags/index.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - Tags on - //localhost:1313/tags/ - Recent content in Tags on - Hugo - en - - - From 11607a01c817228d322bceb1e372c2903934438b Mon Sep 17 00:00:00 2001 From: "georgios.kitsikoudis" Date: Mon, 23 Jun 2025 10:57:43 +0200 Subject: [PATCH 31/51] ci: migrate from golangcilint v1 to v2 --- .golangci.toml | 40 ++++++++++++++++++++++++++++++++++++++++ .golangci.yml | 19 ------------------- 2 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 .golangci.toml delete mode 100644 .golangci.yml diff --git a/.golangci.toml b/.golangci.toml new file mode 100644 index 0000000..d2f36af --- /dev/null +++ b/.golangci.toml @@ -0,0 +1,40 @@ +version = '2' + +[linters] +default = 'none' +enable = [ + 'errcheck', + 'gosec', + 'govet', + 'ineffassign', + 'lll', + 'staticcheck', + 'unused' +] + +[linters.exclusions] +generated = 'lax' +presets = [ + 'comments', + 'common-false-positives', + 'legacy', + 'std-error-handling' +] +paths = [ + 'third_party$', + 'builtin$', + 'examples$' +] + +[formatters] +enable = [ + 'gofmt' +] + +[formatters.exclusions] +generated = 'lax' +paths = [ + 'third_party$', + 'builtin$', + 'examples$' +] diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index e960468..0000000 --- a/.golangci.yml +++ /dev/null @@ -1,19 +0,0 @@ -linters: - disable-all: true - - enable: - - errcheck - - lll - - gofmt - - gosec - - govet - - gosimple - - ineffassign - - staticcheck - - unused - - fast: true - -linter-settings: - -issues: From d42f8140ed0f6ecf929d2b211396b6e8c694f7c6 Mon Sep 17 00:00:00 2001 From: "georgios.kitsikoudis" Date: Mon, 23 Jun 2025 10:58:04 +0200 Subject: [PATCH 32/51] style: move custom errors into node.go --- internal/tree/node/errors.go | 7 ------- internal/tree/node/node.go | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 internal/tree/node/errors.go diff --git a/internal/tree/node/errors.go b/internal/tree/node/errors.go deleted file mode 100644 index 4293cfd..0000000 --- a/internal/tree/node/errors.go +++ /dev/null @@ -1,7 +0,0 @@ -package node - -import "errors" - -var ( - ErrIndexOutOfBounds = errors.New("index is out of bounds") -) diff --git a/internal/tree/node/node.go b/internal/tree/node/node.go index b4b0730..8f76cd2 100644 --- a/internal/tree/node/node.go +++ b/internal/tree/node/node.go @@ -3,6 +3,7 @@ package node import ( "bytes" "encoding/binary" + "errors" "iter" "github.com/pavosql/pavosql/internal/common" @@ -14,6 +15,10 @@ const ( dataOff = wCurOff + 2 ) +var ( + ErrIndexOutOfBounds = errors.New("index is out of bounds") +) + /* A Nodes is an array of bytes representing the data stored in a single node of a B+Tree. Nodes are stored in a custom byte format that is structured as follows: From 4411ce368d74c62e58e3573d8669ffb89ff24aa4 Mon Sep 17 00:00:00 2001 From: "georgios.kitsikoudis" Date: Mon, 23 Jun 2025 13:49:43 +0200 Subject: [PATCH 33/51] implement node type --- internal/tree/node/node.go | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/internal/tree/node/node.go b/internal/tree/node/node.go index 8f76cd2..f9334aa 100644 --- a/internal/tree/node/node.go +++ b/internal/tree/node/node.go @@ -126,7 +126,8 @@ func (n *Node) Search(target []byte) (uint16, bool) { // // WARNING: No additional check is performed weither i is the correct position for k. Meaning it is // the callers responsibility to ensure that k belongs at position i to ensure the order of the keys -// will not break. Always use Search before using Set to get the correct value for i. +// will not break. Always use Search and CanSet before using Set to ensure that n has enough space +// for the k-v pair and that the value of i is correct. func (n *Node) Set(i uint16, k, v []byte) Node { l := n.N() cell := makeCell(k, v) @@ -140,11 +141,14 @@ func (n *Node) Set(i uint16, k, v []byte) Node { res.setWCursor(off) - if ogK := n.Key(i); bytes.Equal(k, ogK) { + if bytes.Equal(k, n.Key(i)) { res.setOffset(i, off) return res } + trailingOffs := n[offPos(i) : offPos(l)+2] + copy(res[offPos(i+1):], trailingOffs) + res.setOffset(i, off) res.setN(l + 1) return res @@ -153,7 +157,7 @@ func (n *Node) Set(i uint16, k, v []byte) Node { // Returns true if n has enough space left in its void to add the given k-v pair. CanSet always // assumes that k does not exist. func (n *Node) CanSet(k, v []byte) bool { - return n.voidSize() >= uint16(6+len(k)+len(v)) + return n.voidSize() >= 6+len(k)+len(v) } // Returns a copy of n with the given k-v pair set into it. If k already exists its value is @@ -170,6 +174,8 @@ func (n *Node) Split() (left Node, right Node) { left, right = New(n.Type()), New(n.Type()) + thresh := (common.PageSize - wc) / 2 + addToNode := func(addTo *Node, i uint16, cell []byte, wCursor *uint16) { *wCursor -= uint16(len(cell)) addTo.setOffset(i, *wCursor) @@ -188,6 +194,10 @@ func (n *Node) Split() (left Node, right Node) { addToNode(&left, i, cell, &wc) i++ + + if wc < common.PageSize-thresh { + addToRight = true + } } return left, right } @@ -238,14 +248,14 @@ func (n *Node) offset(i uint16) uint16 { if n.indexInBounds(i) { panic(ErrIndexOutOfBounds) } - return binary.LittleEndian.Uint16(n[2*i+dataOff:]) + return binary.LittleEndian.Uint16(n[offPos(i):]) } func (n *Node) setOffset(i, off uint16) { if n.indexInBounds(i) { panic(ErrIndexOutOfBounds) } - binary.LittleEndian.PutUint16(n[2*i+dataOff:], off) + binary.LittleEndian.PutUint16(n[offPos(i):], off) } func (n *Node) indexInBounds(i uint16) bool { @@ -260,10 +270,14 @@ func (n *Node) setWCursor(wc uint16) { binary.LittleEndian.PutUint16(n[wCurOff:], wc) } -func (n *Node) voidSize() uint16 { - return n.wCursor() - (dataOff + n.N()*2) +func (n *Node) voidSize() int { + return int(n.wCursor()) - int(offPos(n.N())+2) } +// Returns the calculated position to the offset inside the offset list. This does NOT return the +// offset itself only the reference to the offset. +func offPos(i uint16) uint16 { return dataOff + 2*i } + func makeCell(k, v []byte) []byte { cell := make([]byte, 4+len(k)+len(v)) binary.LittleEndian.PutUint16(cell[0:], uint16(len(k))) From d788282dc1e3e0c030c385073267a15820c8c7d4 Mon Sep 17 00:00:00 2001 From: gkits Date: Thu, 3 Jul 2025 18:54:45 +0200 Subject: [PATCH 34/51] docs: add logo --- README.md | 9 +++- assets/pavosql-gopher.png | Bin 0 -> 119954 bytes assets/pavosql-gopher.svg | 95 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) mode change 100644 => 100755 README.md create mode 100644 assets/pavosql-gopher.png create mode 100644 assets/pavosql-gopher.svg diff --git a/README.md b/README.md old mode 100644 new mode 100755 index b141fdd..bf3777f --- a/README.md +++ b/README.md @@ -1,4 +1,11 @@ -# PavoSQL +
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![Build](https://github.com/pavosql/pavosql/actions/workflows/build.yaml/badge.svg)](https://github.com/pavosql/pavosql/actions/workflows/build.yaml) diff --git a/assets/pavosql-gopher.png b/assets/pavosql-gopher.png new file mode 100644 index 0000000000000000000000000000000000000000..2879d57e062e4a2476bde54ee4b0522905fff61d GIT binary patch literal 119954 zcmXtfWmHw~)ApgeyE~*irMslNq$LiGG}6+bv~)L!boW788tLxt65h@4|2`jBti{^w z``(k+%v=+x_CXE}nHU)a0--6$OKX5Y&^y4-3nD!57i4C;V&E5&v%J0=2!!7E_5(HO zRB8$Qk;q*}&t22W+TGL4)e7Y4>B(m6XzylW=4{30~`kojBq68^OOK5pz9R&U-& zVd-eicwl`q#DwjK!Ni0$NAFd>Q>GuHb+fegX>IH55~H8QbamJEuPFE3_3koh|D2lj zIG37!T3(Xv(>akEiup07HZmq+rcAShyxext9E5;EWsOX=81V@?5FVreA`TP<(MZ>8 zBYs2p1GkO_szIijOHwS-_4V_F;qii@x3mykqWv@9_HX9dTCr z6%qS7Ae|XEq@D?=cHz_!aHJ14aO2+Povb#%c1;VB|wxL_Nxp{fp6ADcIYo(u(wn z4?CkjldojNNU+dXAsLy?ZP@D;A#sT;IAb0bIC z1w1O_zth4T_?}E9BSVILtrJ?isfHKYEf z#qc*RS{fJ{?g{rku)==-1<^9pE@V@d04FVMaeN$TkU72|LKg_pcw+br(#sy%5wb)d z27H3>&7P^h4Otd5mS3{K8d$~JlG{>y%W}<(GyM|qM~6QnlzylCUGNxX!2BXDJ3Ct< zJ%kHFm{1t(I=nL8sxNKNHPETPu!otp`o1y{=UY2%ozo(!n_FLp!cw-}#6w zVmQm*#2yA|Jh4?fw-G4462_GC3;X>GL1<4iRK~HgX(4zwxaKm4ZmHdoRmr~SImZ*T~Dzm+X!Mxv= zT>L#fwE=@vS!$j#WHY1p=)XNDZT96&vr_QVD3XUPMd>Ukf#+Ra9*`|qoc}s}4x-5v z!+y51RiG9hcUJ!Y5K%oPBJI^+y6NiXL8~*Qfq`_ zu3eKPz=&^N_lE)k?iN@O->eLc$>X7JZRqImH-DifvZz#a0fHAw5_pLMHZA5+!4XktNn2_riui5$$06j!@r+>eis#e#9%Fx{vizCc(kUPLM_Y9IK)0I}w7J(8+}6&sQV2 zoI2SRV}6bh64*83RjtQDJ~chf&1h?Y1C8~~N?#7D8M7M>REeo-3Vja3T3MZ%a=qi8 zc&~|iu^dwy_?AZ!0nc{=dDY}_ZZh;^i3zgV%5Kg7>5wAyw(CFR$N?fDMIvX;1@P1m zs2cj598^c0jSg~$Zq`>%G87UL@+rthwHOsfAO71sySQkFLKWvqYJ-k723dfv@Bz6{ zM?nQ*nkPgcOGb!NAL2{8L+@isR!;MrOgOb)$cpy9hW~Y&=ge8IMg{d){2s}*xB*%GmU5UecRmJ+%)N6%lri4 zP!pXQTjs*Q-yMYhF1R6{kIX0plBB7C4<+yv{jk=bbEke}35-@F>ABAi*bo?RYi(`i zlM}&$=vloTWILWIxEn_%yec6*4{1m02mPJUzN$!S&x-d5D`5P29l^{Sur`fqfk{3?_FU? z0|zYx0gh>>=;r38IDcag<%cLvfB0}*jJi$^G9BJ=Rh1D*XIyCXCkxCF!N`R|X5g8taS{+^Bc(G!$dkR30L9vHvt+QKgTcF4=y zA<$UwQmi^)y1NQmgD*O~`vJ`4?%^Rqek0vGwYpjjWbP`MKZhPM_rn6446aRhU9#E4 zJ%2hn=N>xJCiV!BsYvzR%0H)AvBICDQ5z877zI;@-s|Y<&Oe1>c6w%-$H0Yzxa!;h zX~sr4&J7{ZN_}DgjjG?GynJ851MqP+`EdkUs4E^tt_=pc~qZN$V2ez2R&s%@uKFGI^8l8GcKC>{-mI;iT{*YPm7FvWc}Va) zo=i)1LBUpMDFbyhcu5sn@ATj!^kH>X$lRfK4pk$@PBzzL*CNJ-S0_a5+sIB_qF_ns z-$s89tba9_<|Ye!^jW!oiM0&F77Hb9wZw;NQ?V?u`y&Dgu>r7@)T+b?X31ODgb!|$ zd8N*o$YBUQ*WT+VOau^~j`oj?3`2M0FQ9HL^$EI}S7I`C!Ih|M$P8UQ8PKoBgKRZ>#QC@;r=N+Y{5lDrfO9I+GJ z2a>`E5P^@79u%?YZ&u8ZZ3oYv|HPW&d5^Dm2>_RJh}1-086Q{4fpgYe-_t-`bU=e( z!N>HWCcLX*Sv&CHxST;mp1^xPU4EpOzFq|omy-fX0_QFf!cHX<5cK!T{6hIbOvT0SfB7NCjxU5jy^al56RAaqFmep~0)&cWt;M&8* zUuwjWzV@f;_m;^1`aN{v7oquxd9M4yk%tH_*Y09R6T+RdV_jc z%W~*n$KY${j9EbEgq=2o18AL}omZ?h%d~{dr;AlG-ayI~fC%x+{!8#lYu$sz+)jB= z5GwfVbfJ18;u-5%ZPpS(EXKPebpu^V7h|k}SQ)XJ*OdiX>9YeM$l5a6T$$}zOOR|_ zNDLcUQhP($4E1rjzhcT}pJ%dC5E2m$Ij*!enC@l<&nzzbG;ayqLH&m3>`a-(6y}5X zuSVCPJ|=w^xG@;cdAT1EQ~4kTls7Yx^|rVcvu_tm|1aa-lhMv5Fx$e_Zkclsq~|ub zlN2!`#&q&TP1?u}LSF}VaY;t713_y=YfShI@B=K6YQ{){V(F{6$o%)Ozlogx+!cvN zwu4RJy_tEr(zKFTgq(4sBv-{wVO!W&- z>WIGtB$LqM6o{JC64F1c^f90XChzo+d)_ZfKlckT;ycU@A9$$UTR_otQ3MepUAJ8^ z%c!#*@9*!UUDBHTZTtrtXwgn;&%%@7W2kr;vk-*(&lQoT>n4SjA^!%j;aoe@yN~*F z-qP1FAPxRObrS9V4^cfvALCBmtA%BXBEU4E<+cya=aeF@|2AT~%Ph3zgcz;oX3F$7 zp431!ubv4gh6HafegCu6J7yG#71T?CLKxsmXik}ShKW_hknaW{gcs7^Nbl{y;H)!& zag=Oy`rMcB$J+1I_)Io!A)T+sS4O+y(l+j&Kd&--8%$Z&{k0FnxHXEpKl&*iM6|Hmk5od3Yq}IZ z03^nx>5q#wn-cpVdD%`o^X~F&K{6s8uz9fl+`|;YU7itJSKm?XBWV`QWdtJ{^0idQ z7u`_Ivu^@qWh^6)X_%WC33x&{n1;ks|Gpk!377LhyMl5BsQk5w7nMyjTv+!{PhaHp z-OPataPP2a2@`zo1c{M&VPcwM$@y8e`iDHVf=>gNe6%G`_bhUyLm^ikdq=mR|vxB!oIA%LO$?~#6MQ{LR-ZZhbq144i$ zyh5Gn=t3WIOZ?@FhZCr3&6Vzisb0ADSDC&Jn;gc~BMNzo68(2U;lxZ{qt}P0a?mQP zi%=(EIB@c)zZ=qt4_8ffr=bKnZt2-QtdI{aC%)L@h^w$1TRFVn$jyAKU*dbVHpM}d zAqk!D(Y7KE(T?Tm(v*~W{7Ru}cQ_N6*aaB!?ZHc(u@Qb98MjjENlG2$B*H>E#uyqn zTE~p0_LOTxJ9|!^e;sbSK=RCW5v=iVLRNJ3TgFc=44$!iC)O5K4HW*k>430L*6y1h zf0m~XS)$`eKG)4}4bc|Xe2@|=<>S(`#KtaRNa*BaZMez7+;koJ6(BC1r`s}~!9Rg1 znYpv0$Att5KJ-ufArBOyIVpK5R{AAFnehaT;4dbKR5nsHX!0fr(wo0@-C~enfm93^7lA9I-ig+2s8tLqi`M*8tSu8fjyE=^_QhW$=0O8ys%fe5 zU&l`+{YJ+XAM8LM_Q69p4>2NPdaMK3x|M6ccHmZ#(=MzTV*$|JDqIi^4dlZ$V+^-% zcWZJD+`A+Jt8t?fmRtlwnHUxhYXI&G8nNU$3!eXlM?7Las~GO*^qaPjO_9pwGjzhW zQ7yN{3oA7;>y$R`j#14Z1YZG6GUmq>&m(dstsrglAxG5z`}RBS{>GC#6si{$h~A+k z-hutJn&IFVYJpCn)0WQ62*;%NWRL|>AO%A6dK}J_NpE|9ZN87Zs6`Wo1OfTvaE;l6 zVZ;;oEIj>?lZkQN1sh*MovcDh`-+Yx(yeqMg)q^s^8(QI7Iy@cy+*gg9vOc3t!JeMKt`Ny?2|<(?@X}YzN#PFCibC8%cIEhyk8zuC zrTIs;x1+Nn8XD+_P`k+X zx`EFeBO*FU&Y5>-P0%$AwZuBU>nHHs>jzc}#cdsMZ@fVd60CpN;=^ZpwH6lg*~z4- z7>~*M0#pI*B~9^KY^h=_zM?U;{6HfLF2A9{1E>grh|b2rFj^g7TVNq?{OvNac6k7F zUgyBqCf-}WTG-Gq3(qkzQoFmcWg*h-WyN;;az~XfZb)NY_m^H<`wZsKrf227f(z}3 z8zW7T>NL4@wZrAL$H@Ksta)gNp7@(~oJj{Y(WvPVFQy^7WDi062nVHYtsi7RnlOYm z&;P1C;M#47-V&%-=ggekn{}Mej1;$@eQqDpBb$+xblV-}48!z<66l{7ho$|XLK!gC z-&%Z(aS(cstlEXq=R&Hw6A@E8447S^y{)Y+jB|FX1x^WlZXx~ZpvG9aMf60w)8VgZ z8R@&G#zwy3wD}L&!?^YMXot9L#$}%&)Duoc#N6ql>RfxjM*jTy(*#9&WRD4rZ9|JG zoU-I{I;6Ml4O3Rpw@MLI8yWhP#CgD@(k6kpAw)d=Yq5E?e@mRlP->;^@Xr@}v3~Ro zn^GyV!IrkFs*`Or626#wqKF8dkzem;%a7db#gsmP43e?xClL*jpnsGhCJu8bFn_{3Y(jU5g{Nk+ViuW!73V9Tn&>Yh;1k`}ahL zX_J$ahEPVEHkc#7QlQT3)nhoD_~zj-VeuTIKr!Q0pEHaSTc~_uv+& zC26pcNisd0VgYo?{Pf<5JBT8Ymtd)J0_t49YpY*1$}RjEG)kiq^PZRMw#UQFMvfI| zf}nu9!p$`fS_7y8>yNgqkEVP)=Y>4VqpsVg)TTPSvIz`RZlJ_q5UJiLS2Nxq&y+PJ z^E&2YkI(UMRn+n}PcBDnHGuIDw!0UuEHk)bMq(ZPSYL=&!P|c-W8nGCBK=q@MUCc& z;g=c-pqpVt&y)aTH%xo(yl3i+JpBOH0qWTk z-wOsT|H)sKTYjWx?-=JX=Il4CzCs0Q)|>cIHR7+ubVWtqsHElN`o~vDtkZ%?ImE?%{zgiE*Y`)FEb-Ti>e<3>VCL614V^) zB6Gq2_X^i8G3E3>c4fp`roaOZz762Z3#q788@6jLns41quldNzP&5Ph34a_4jH~XA zW$ZC&(C2HeZxBleNn~huVCcIgQTg#TlIs$A&$KbBUKHpP4V0K4)fy}j4?isD3p~8z zznrC`OC0he?tT{l`D1M-pFIfGVrZE#-$21o-_$h!qUZ=IM@86CMyA$?{C$%z2{h(& zhURboaqbdF4pZevYq+3l~KN48(PGcGePmrmn}xceb&qA zBk%9VldTQ2J(2P6y7Y~subaHot$Z0{CI_2)s^_lc{-19|EUl8%{pcIXOY9A)yY{Q* znl}Bd5pbKp?9jy%=P#aaq=O!fetQA%O?`3!8B?Lx!p}7wX}|0F&=jYpn|kM$&>#zj zGX7u8w?Z*Dshb8FZQ;V(qN0#k12jl?HNN;ShheVOgP#IDHmjY(a<1mzk!9t0Fm z`u5E2X@=s?M%3*nR_(XT51}hnu1s8ML!I%Gnwpx9qVkuKa(j*h-|&aLlx=D8n)wM; zST4oxf$#{>towd38}rau*Ld#NG?*T(`~usWB*4AVt%Iy^#t~$Mhyc^SVAk~ z07^gI#dlJw`~ff+oJ~?l6gwFuu+*BnDwfjA?&9_9iFb01`#o}FQX<}#Gy>& z^*$3-SWs=Eg$0;D9ruxi4A=9Yin^SvtdZvu0LDlVc1D|ATl|WbEZ!te^X0i+O#^u< z_0eSE7JaFaeRbmXOIWHzg&7PZ8M>&QWB)J{^5?rfrUDeVq`cA+D9|64voB(O9O!AR zIeg$BMT}83Lar9sLH920H9=ZYO?<`D_3s1!a`2hCX!ZNiCUUq2c$q9P{GK=}0%TD5DG zu6PYQ7k<#CvtzI8deI|n=F`y-9%B32aW}OG`5fEBP)Shm)lRB%Jz)!7@#oH$72sKyHCH@osc^d3D|PW1i5O9-TsVa_Em6 zJj5K}TQVL;P^fggh~(4r?^pb1xZ%3R(gayFp9<0gVF&G(IM_E)yMz>OGz{BEu$~sx zAipXtp!}wex(hKi2#RxgE?AivaT(_Akwa->Iy*bzpj0Ajk-pYAkFw9#u*W`R>V1=i zn@YVuoqis8r&(MG6&QhNax_G99O}}!8e%bX@-5309{T))2nNmY%g%O1DBax3iiqdL z7eV{EN+0S+QNPMyj(C2NWL*x=_UE`TOe5X#G+JOjGZ7&VwQi@&AB7bYBFLDkV5^q! zeo5EA#flKZ0Lm>Y+Tj3ccA(}POeR&c%s#qgui^}~SdB0i=v9Fu-O)vbeu^X3dQE)M zWyEz1AHi-~v7P4WSKSWFGW%29_JtCsyoujFrnU*d;od1@)_c7>yA(k2*3^d{=n@L$ z7F6?ii2=Zy5L_Ak4G)lq-TjvZS*A^55&r7Ae^~`i5Oz zzUMbB?OHZ$+13HOQAfSbn1``opha*RiUkf9VAyqlVb?ENG&earW7{y~ez+thB#GPd z8#tm?1*v89p$+4K=1H>TWeB_BsU)m2i@tSv6Hl#QxowP`ACaT5pOch1ZB=*Sq!WB%CotrB_sNHIH|V(j|;H0v~-hYkK+i( zF?94kCJcU*rrE=8p*`v6ryrlaK$>O_y+FCpK z_DTGC#<@+qRI}F?u~dqPp#sb5n^aOUc8c_j=ZdPBGt>n;X6c^)-Ax58K1ONA{ z$Dt%Cud^Cy7s<`h&^tHoS1pXSz3+D^sWG)(0Q&acVPY&H3?pvlq^uQcUzfE--P6by z|5((>n&hS(A~i$ysZe8o?*=dFDM5f2if*i_sIkUW)G6``?y`K<*uo|K?2uNtUcPOTSvPHOJ7KAm`UU>RIQT*`Xd zHBqbQ5dNIb!B-2w=60c(F$~|9-`ua}%oagnsOn^4jZ`yHNJ(^SR%MF>kAkzs&J_6y z429z=7a_jM=f8@(T{4L-ZF>JRJGZKqmKHeif((oH%6*#`Th<<%k-p*bxxARZL0i+y zf9VzIRAir#qLCSIHde@iakW1yo0V%>qKJsqM8bPC2t8>hGS@t6pK~%D0?3N#%S=AD znCQX>7PDCj_Swf=>L2()>ePrVItY?tPI?%I zf8V1MIin;l9?}X`fvYd4`Bbc)w%v^jO)R)T#e<57C(_$3#1I$z0kz1WdO#h@jxu6( zm?AjxH@dTIj0z&@J5F{ZtKoMaa=p;jav?$Y7cQuk81$t@t?RFi@UXi~J*m}*Q10!M z!MY}PeMd*!zdb(mG_LZ5e>GRrMu^FBV&XhL4 zA*60zn6mdbR(bgTOEP*!R*XBeGC|EiPz)UxwcI0W)dtT)(J_y5Yq+# z)^+SUJ|UORT9YL^m#xN5*RwWz%=A)>_5r=J<`=$n2@Ol0ANcPqn4ad)M6V4pH7W(;Lo}iz%5t!-5POGdFpVxX` zV2L>FS&3tA*M0ZTMrfHfjb+JA<2%ApZd8Kcww*+6kSO@px}Rb`tfk*`c-@<*{@}Jj zuUW7fMF5B}x(-W?p_tWd?RGS&HCiwlh_y5pgsr;fbNVujM{w2()n#nBsD2Musq3?; zHu7Y5t8MWT??t1yC!S(wT!Lzq1tpjHBS+U?sxP=Vzp^w zJ=b?*-Ie~M@e5l;t6yaoQtqR7Pl7@B8n+eNgq>dN0=WzMOD*4KSESJ+fA4!cD z@ed{D$j-PFHmTbwRy&a;L$8AT4*BJB^^%@a7-Qff+kT@DC;PzFTI`5mo~r&(yl-y7 za+>$e)$}-n&d^3~9^bEQ9ib&UwJBso2B<|3K)!Mdi&(ZACMiGB=@SoZk^hhw%E7hc z@m-N`uBM^Z%-Aaf9sMN2Y>c^@7>`UX1l#-}YWrTy#TB~nYb~P};VYJ5w0HUQZH|Pk z?vqQ@8x~*fS5g~KcZECyX}bkTJQzMO*t@Z{3kjF-G)cLnuK3=f|F$ zO#Hi-R?j|I7aQ4XP-7I1V}PWUEdLfyY;!O6tV7@vdb2UHz) zVvam(rIF<8zBnNvp2QEOB^UgK&9A;lR05jgX&9uOoJ~dzNx8IulK_e7bwufzj?i?BSHdt1#(+3d+wZKAjLoDVVzJM+UvX4qM@sGnK#&B69A#!*9O_!Di9r zRL88t*TERXB5b!A<$G=wT^^VJzDtyJtifn-gR-nd2CDL9#nuNib`mvd)&gAT@N~>3HG&Vw829bFX)e1Ix z2I{@$2RRWu#JilP^_PVh6%gPyO@O6IAud{$6=1W?@o#v6g>mEcn<_QBWrpe7RnB@e zi+q-iGlMfrSD%tZb?@+!zJ4QMp7TmJW}$bwFcB>L90#I=e-;Dv>-0l2f2ReV@w%pw znrHm}nOW^r7L}Wc8|>`DN}GQ;F3q8{WB24ZC@CWio0{&*4sOVIE?kU%jj0+yTI8F6 zdN|cWBrO3C5^UMKv`jWk#Q4s{w7j(bWVAa^O)%SdCB2YtHk>&#Pm|dXZ_k}Xs?BJa zK0D(Ecxge_L%Q~cwen<$4$q8a<;UU~T%$e(=(#c)qmX*VR(3$uwAgDNwV%*vR zk@Sd0*6v#{yCgvlX4T^B?qKFlQqvHiP}Xjeh|s7JZ_u6?M4`KPjS}5>4Kv~5gf_nb7)kc>(s+Th8F&{z7hshH%{JK86+au<L7nsT`;$%6a(7IFsdp~l5S+*#kF zVN7gq%PJc)FgnsVTC0Il!=#iH*=m`fwo&JsU>*E6`m{7C-PR5z!vz9LIbu|gq# z^$fz#+X4et9RzB_ubX+VfR9*_mYUoCqkpJJWmlF*TOFkV<(k~6+*7U- zG~=d5-pks|i0CBX`WR#CHxQDGY4w(!eWdhE|LtW zoj0MjVwRSPx&k*}u%#)LF7utuD8k#Pg`WSR6+Hap#sF}kLRNO_48^MLJsd zWV@(CsFhVE6XLF>b#t|9A`ifOAdn{2MWq)XwC?teL2x7tiogTk3`}RH7fO;1*xBNK z4rJI>WGlvIZa4=Q>Sl%3*RpkPQjzjGh16esCWt3+-eNfIzUFH&)&+eGDMUzA^gqpG z{RoNwu%w{JKi_qM$L4Y;P>b7lr9DVCwdWLhSn6gXgm}FxE-QkWMJ*bl0m?_MpXgcV zEzRa77}_~DJw%@dDYGJ$jWkbs8eS!1x3l_vWv-w2Cog03;suWA`}4ssfjfg;lveEn zN+ts(Vt20`U>|2E;g{p)MdkTAja9>yU~gq-{~=wwC1|2He9KQf-?NWDO#b85MC5eQ z+2Fzl{ij4stL93c>w9FaA6xa{G(#zoSUkjKOkbMsi=ONfE!?=PedA{%9vXwwH`vuJ zr))+y*}e(NJ|nVxswV?aPS2dHuiu2fDZ`~sE>pX^3vM{1=nc2TS0vR#U!8y4^(<|L zNoO)b!N!M!#+Nx?J7aB+E1nClb)hai*;o+J2#lUkNs?$mM6OSf3>IC; zf#!qIPQ2c)W4r|H=d*SFAq?85EjmVig7lywuat^OCw__sRJ@;9Ju^pViA~kDZ&)4% z!17fApyV#O_@90|JpRnY6{Y|3NT!ppj0!!sfHu>g@iHDJ%0x=!WHI^3ymRtaJ`|Xm zHtgvqi4kNuJd`Xo8$wp3)Q;X(;9tuPhPXZkECd^|I#MsVx{&RyYuO4 z@@n5L%k%+t&)3gv3}=^AA)q>uG?o_VAUcEQwo{#gAuOZ8_nmNW6l)5ggHVk*sY4|_ z@GYPFSUmQl;RJ57OI#z}_0_QZk48N9q-n3(Oys>#*iS@g1M|!nAV5ZII~$jU;=z~3 z@-EK%%V<*sL65cignBqr)T%arBwFTIf)&Bx@I~*^cpl6SF%P}Oh?RAy5r;jU)jIi4 zPEH&aB3*QZLF8}fk2sUGRQ-56?D&QuFkJRZqIgJb3bd zG&oi)Nm5m56ev>obxKfce*1%j7Gmw%t8anqOGQcFnmz$1)iZT&$iDxvuD7>b{dokwXO^rgqFva`GvCT0HT0DHyioo3JmuR0cLiJ zN(P@wO!OM;0Dzcf5I2|>W2qUPbP+hh+N(6mWh)v?-fP%Ni-w+(*g>q+OfW5$!a^4Y z4>nwJ#&Hc3sxSDdlD$foBYF~6zk^P>cOYi)zY*y_DyR`nCp%-;4CIHGSRw#6 zsTm5DXo2mb7|`k>!Sd%z9qGWjDNK9Y+R%;obR}vsastfSnHgdeSsvJ!V5|SMoj>%( ztWWTIIM}FeXaUoRa8kyy^ZZM*y!E|-cg@QYjtDaxs7QEX?585)sM16Jdw5$B04Max0+k~7C8>W<-1sqBVo&;li~zzpE6|l}t*Eefn@Kdkbk1?zjl!hf+uiFpM%|Z# zM&Fe=9~S|K88Y)kxULxq<4QgrhzdjJoDL9k*lV zmxo*my6vU`iau!si^)qLMeeUb%_xd@3d3}JMCDfkl7<01uE(KhL~=0Qbyue(gM!To z<*wmqK;O{92I-zrG zL=-kzB>)JF_XDrw@m=jR@U?^Cq9HIysOYrkkKw2RSNqiaK1g!pS2menCJWMd6juFq z8KnfISAuj$dK{aot+mz%hQEl8n(Vasc4k6wir`5rVr#~%WF_u(dctw$Qmsoig;FIR zHhMbCJ|Q_9M^pl}zhqB$Q>Tw;E+UKYp_T2{DfXjK-}k1k;6ayiCPpV)+jM~W{1wUw zi88Y@e^Rfs{E)j5yp}ey;~3||^~T*!XD-qx)0{VMf5#lpdaHHVTb5)%Dac5{x__T@ zmL11HUg3A5T3fmi33Q&gyTovHjjxnggbhqCHk=&~E}r@k=A372O$6Km$}2&| z#4EIRPL5;^f2br^J8HM9ucE%|DYHO#(X{<%q+qBR{GchX=K_JBV=@#yO$<0PYO=8;SiNKw{?(V2i>* zoClnf&uMiTCiTf(sAB^b9IKVI*yZEJ7(U_usHh9|kUjY2s%v6!BET=dUXWD-Awe5? z3bI7fB;mb4r`Se<%haj!2~DJeGzlR}#fzmMKcX#Zl@W@qQwZ{&=1-F;GVCGrzRc^R z4~p1hj%Hzen-U<0+p|n`Iaf?$#0MSY)4S`eRmRYWdO8h&oNfmAqx7{dRX=Z<0Mx@8 zB31Rl;o;*&yZ063Eecx-5_aS5e- z-n1Fqh&#(dCQz}zjRa+u!Y>(zPi>cHMD0fx`&p&xX2G5kW)UBdT6&ew1TIChe#`WA zuWGBT$2$Kfs$zA(`K7FIbIDYgV8`_*-r~{x;{BG>o+6nWZ9iOB;GWOr&96Q6GGGj0 zz0~NWgS*2EI)mnzBoNJR11~k^jHPok+>5p35+v25nz5y(a&?JKyF4u~I_o@SSqGXbiXCNLT6 zTcG|hj{dqrBqb&FI+CSt_m{V~XcZ(LDr%GmYbYF3x#j>W-rtM+zUde4 zPe8LNB>Cf#Xe-ii7I?Py>8h%L=sB-`k8N)D>_tPfkWK*DH(R9Akr&fLc-J$ojgfnH z=ijBr=0EEHaAZII(>t-9tMG(+nF*EMi9eFDUfJ8%w+$G|-LzKMz?`##G|aNvAeoc1 zvo|8tn|d{}%FRB^yE&NV9U15>W^g-53M3BCYgw0e-FMewYa(c0$Nk9;guY1JOd3#_ z5TQBtln6Ck<{F_!)V|EAUxSxoVS07t3@;>nV4|h=4^C4-8@H4A@70i-ZZ@(NNtB$g zW+ySwMK6jK9qkvha$-)aoxe=fJX^sRE3eR7Ri04kT6hg5;ixmF19UeXtVvxEDKX^X zR?b!}_k&Iz&&hL6A}%8*ux3YB4T7hX9TXjb4dy-ujW2}Tn@fwMB1K?9*;ItMsuJyt z?bI57-)Ws`JQ)LtGWvd`rvSvwE2BQw@^`kby#aM20`r&sOUNawNaD}kSzB!DI!~B? z3eU%b4C}+5uT%2^BQpWZ$jm?Ej;n)R<#LyG!xsr%wBl-Cc1+on`+Q?a<0>|FBK;-% zbWy6j{vtIdexNgIkkkVCe?I9zGrjuJu2HYJBjO-m&n`l}o$FWeWM8%=5Cp#ijOBqD z{{t6R7S^`+tpZ%V4){3dfVkfF$5B>HGIA7-;sds94#mKJbb*Qe)0!mW1=LdG+rq+H zG8ZKZKy4W19drSD{_4bSzbiC@(H7m2s19?oMfO?!e_Q!JMm{v3Pnl9>pLcRJ#3}w} zgxk*gICoL1shuE+`N677v(^}-tubW`zmZX(?|=nAKI>av%|2o}tl$Uci3U?47(8hh z=z1SS@kzl91i!|{DOOh=FX2BI8~PFxf1jV@BKULX=OiuBP2^62de}-X{}2UULW)T) z^#f7O#{it-$DYRpu{w_gqGub^I-XA{;rE9EvN>b}e`>-ZtFn)GvA~`}D!|hw<}%Glva#Cb2Ht8r%JVqe6&tk zr84wIQgNWx83wE?-8O;VrF3wsV8Fy9IIm=y23Zr~N#9zF{fC)pboDXwHc_SGZ{ zekJ0zwscr}i3BL%)G>iZBujaG{xZagqM}Gcb>A&AEVO&<`M3~)y}r`6Ms&r*vjlKT zv90NMPImm5Lj`cH!4-C3REAgE5X4*iyk+XAKzV_uTS(#GV^iJL*ZjJWnU5i^;PUWI zrDUt8a$ZJ+K$E;Jl&}N6>e~(T0z6%y zKi;V5(smAwAFy84# zBXk2C!4#_%=yl63i<4AjLN~Pjl!4r_(l`gNB7ntOBX2QiEUe}=cjXxZ^2>_AN1+Oy zNa56dyDsLpiqQz}{qDbqzrFr7s``tHjrU;_Sd+=;t&1@-AS&l``M?~SRW@jeR!{TK zZ8FEm1P0*dt)~tQdF3vI(#(niZM_W+kFQBWoxn8fZb#_3d?q6HgI5k+jkJ1hk{ec% zD$3pGZ=TAQbZ|R==g8k}Y1vJF*ERcbI=6B>Vo&9{|IV`oV>k>mJLGcZ*OnwJzBhu z&vJwKC8(Os)=Y;k^H*kuqzE1i%5btQTp^BhZLm=dD02*bW^R9pS|=a2H*p6ZXdsKz z;MJ;;^)L5=&^5kt3abCz%Vk77k|C{0O7zOh?5+4f{fkVngV7^p-FizI?9<#7otfZS zwUFvq@_$@_pLH4E6fw-4DXGAt{{&wO_X=aC8S2$2&iezK#+jq?z1}Dg*}&47C+5e` zMrFE~!b0$Fk@`0ZAyHTf@41mUnK&LZ_r7+0$>y>e250a)Rllf^(PO@i(aW?B2lDdr zpaY}MGK=PwI0z63k~|-d)&Bg!Tn%>bOS%I6k*9;5BTa)C(CD--S(KG%aZ5a$`h>;F zDJD8Q1zk4P@XKe7yc97!exFu0FtBlV?CzUjrJssD zIgh1wB2a{Mf#=en0-;`8V8P@`;Uoy@wU2$k?c)%IU7!AL1B@1cdH&^m*Gt34rP~}3 zsr~_Hbub76zF)6fOLcWTQ5nr*cm}M|G`LEjU({-sH0SVheQ#bf8GuHx?H77J}j z?$+OSMnk+HdaSNIIpeGV*mhC~e;R(RiCeC`Ewg#Sj~DsHc+S;u#s1t-Uz^Puw!~Xn z-!pyFubtxeczuwlSTw~C6m#qh!kBbLl%M@jcQtSd=TD{F=;Wz@mFT?J-GHPf@79-q zPr%crX!%)mSH^I?3x;M0410n+CPLev!)lnaJkTo@!K;`fXYLC_$cnLN0G+C%CEit z_BUb#{TnMz3O0w;^oYBy@p*xOZp`xa_}KdY(R7tTaWzeMafii%ySoH;CqaU{2X}XO zx3EZX*C2u5Zh=5>4esvleD`^)zN&@d2NXSbrlSe4AknYAYQTu{YZa2y?$Zr)wd~p>%Wc40mBerQ{k`YELHYtA3Z|d z`2$#U?q?_af7WTSrU&ldW#ormbwgg=8TlUS7EB^1D>jo_YvljkkedT^Mg+v$(MOP=aY+{m63@5ZPol5Q%*b5#Y~Ebz$RYFk0X5{7f6^JVWOw8`~&7;&tf-#T&4NZdPXh zjgu5)_W?c^qutHCy3F=E%}=CzLJOnGK2KU|qIYnqNUl6ZqqtUq%3TNf2y)pHGb0>> z`IV`oIvwbkVDlFEO9ePRo77E9R-R|u?KdNtwCe;kuLucQS5Mj3sp5pKr|vY@)Hvb~hz9Oy{uS&qdc80H9pHDP z)@*V|yf7wvcf6@uRNSe+w9i2wC>PaS3TR%H+|LB8|g?RdBtLFKZ0T~h3b)^`yiKR$NT*% zlhx=ux%BS1MOi&sfyZ?H)EezP1Y3K5fJ2@jO@xO5eO%}s>4)|sbFP;gg`uyzfQuuS zsnzhng)(XgEY>Bo>Y)i2*Ij>o6Hv^8hc5b+gq9a!M*6Hv6+1$@g@(Won?@pvx3S@$ zPK^r@$XW^aNF5jGAW9Y`o=0{~j!u>Z%0C6D3R6M3h!&bZR9_e@yMQSzwaxbJFk-eP zLve5JIG>YIPf}Y#f1WJC#dNowO1Z%D`d@jhT-~}}{yoRI<(?mz3+yS;TE(FBrol3? z?>?oa$Af|Xk_S`L1wS($(<5~dt9|6uYMn&#YQOdN6}yIaH!bRfHHWm%+EO%pW;{TC zi&s5VV$i>&baFyZ_xRAnM@>s>!4)(KG`j)~ve%%R&|)^s!>~mpj)BD3Ye9d!Pjnfw znfPz1$B~OEP%rWzwXdufvN?Y)%CpEmz(AMO7=NrxOeZX!HT(Uw=s&gQH_f>h=x4?= zTHx19=>LfT0k{6j)bKl?3ke;E_7?vF>Q>}>TLC>0sFsRnEn0?ruNw?TrPO2p^F=+B zGv+UY%ELM?5Mj#^GOz?+lRa5cY z^zMZJ2_mHJL|{V0yMJ#}XuSVad2@s-|43CZuz6^b21y&V)yIN1%S!gj%P`B)z7Qvd z>C2;o2HmA=W6z&iMxPjhhM@kYag$wt!oEGPPW-Vw;gw&EYvrmR=*d09m5BURpNXiJ&fjAZVm2R znx5KLFV3z;sp^QUsfk+r{cC#ma;rW2MyPs7!LYxYMJIRbB*EM`Sx+eQuKF)4By$X+ z^>Dl*-w>Ti5d*#JfEO!NmG5SrUM7FbC%qga3EAk za}*znIMJ$BRSE(6q5%(w1$4Ix-JG~t3JhJ`?ES=h<4B7FNX$D6uK%ggQ9(F25_ zp*=wvYtl1%tG#J2!P{;3R@5?A|A_I?Yt94_3nt*Js^EGL1+G^*{yp7B$1IkGJismE<&%Votb> zDAMysp#!gAOt9_NxMR3v3vOEK!e}P;#96-p_-T;5zp?Yn*F?ge8P(KP6Z{b$?vQ=E zr<@&4f}^Clgad?rrA^FsiajKkP!lBm-$w;x)d94T47Td1(1dCzoF>t{z} zRHpAF@nu-keKCV*Q(2mam3^x*9cxE+z0_D6ceG_1Bv^af;U8;` z!x>C|abvoeBFU42(}itMWUVKFb}9U=1BJ%`%|9F~EVRsx2rOZWHu&p!w0wn_s$Cz- zWP0c8`iTUpasmx;Q3RyDO-`$Xk^BDJy{XmJ9O^KuqcbGroq639mNL*8Hnz5<3Y|jH zfu{Eg+u$iWOi+_u>#rdL8sWJ{eKkCTrcAiYR>^}dmh`Rh6@1xtW)<*@TFpQp z@;0#wiqZ_uh~Y8ux0av8Ul)eBglBzm(6PVfn~q8t*xOgT=Nt;5_dSPs;UkyaI9Hpx z*t(`Mkifxu#+cSAqUpHyKx2wu5IM))RySh=r?t75Cyv@-#&!TDRx`>3;cY({dO$bvAACgBZWcJqI+eUJ zDkSfq8$Jz*wn_JnDdhSNj*%txDqQJT_VM+N1ieTex2*WiuTbB86yty4<0U0y8aF6; zZqSbOw5y?d=dv?+Ge=V|SjwDRROqS?Zc&De=G2#d$#ocooYQ^Qd66RyT}MVcNI&(i z1O7j0vdv|+9kyZ0C`k&*dg4-*gf&kfE)%cQy%I?x^ht7C49BO555meF=R|5OZanu`!1xhjhZ8bJyDk3lFR}1`UCx->b+&RU6jgVdGaq z!#P2@3j9GoWrjAB$x{&zM)*jjO5RGM=$&UN@$OWc?Z1{{4 zMDuX{L@4yL6>DGNWau%)2NQ$nm~UE^|A(qvojz;71Gn+PN~v-0@28C|@(`44jhc^{ zQ~y#IT6yy+hyHVb2A^9S2GT@P3B*TUmTi|R7sS69LG1I$(8wE~D+p~N^j&AR;=-o( z3}XS`ApMRmXp7|n&BgGOi~n2`QRyeQLB=RyJ9K}eN7Y+bDhpC9JKG&mzK&6Izpmw2iZWuf?)4o8Nd<| z^d*uY0Vq3)FuOhPfSDSDcKDOnHGD*hKv*Yiu1|RGV&NT%@S@*&+3Nc1)X=t``=s>u zjt8)67N&nZ3ebf34#GZ_3wO7FXyI77XwMu9#7L#z>8$t74Bzo?Qaz$n`~p>?)=xXp zBEMFJnS>Cmzh4m8RY4b~C`=(`p8vHl&N(g!_D?}XR1_!YSLL45Wv-+`<_P*&@sR?> z+oJfVsmdo3Evg#0896OPu!tz?yS}AkE?N8m&aPqU3hj#TtvljJi@z6WsHns!#-t@T zW+2&Tzb@y>N-USKuGJeg9(0ImRAD(SRx}zYO-(L+-=($WnMbKD251BxiPDJiJ>>$S%i!~;2rcKh!+43O)31F8GAgcB$c8~>h#%DmGA_s%9q6bDH({yG! z0i{({Oy(Ou6~a}YWF?|#Raos)^W7MYZBKCFPb)DzV{g6WD*L4Uc=udFF9_(f5EvrU zcaS^iHN>7N+g_baTRWs%fAt$EhF$eZ8whiIUj1+_qt}9iF1Q`8r6TT0Z{5XQv6P`q-< zVurQ@X~JF#fF|d7Tm&}Q?)CRD`j~Dw<=lI6WV3V&%+_fF1K#Bdvj5Ed6t~UifOx$q zPCQuJa|Aw!k-HGLgq8P9|B8zj8rbHW-rLGS>~g-}j8oH8nGZde_48+%->8=UEZ*nX zYh<;-7LOE554tNK?JJELl|w<=8Xi9RFF_a5CJz?;wI(S;bO$GYZC}2{$QpVT3Ss7mW)z#gg@_Lj>3YWf=fcvpL_~h#0PZv_49IGtTa@?6b;; zs#JPSrYWkfCQJTcfxgb95wRu$#CTo`F%)AaZFUuS8%|O8qYo;%_c}+EDm8!5r%G;j zBtD8=PiCVcJRHfSlOupO{d-&XlU}m~_Pub?YsA)NmqsDW1N8a>qlth8aHI8E3?=K4 z*RYSl3h)=kLZn9NpQy*11v8WY;h~M7$5wYJnn+5~zBdP%=r@I9M4y9a8WISE*b_fv z9N)4^6R`W(MN==hCYV|5Ey@SQio1(5yw}=0v%Zmw)+Qs48?$1wt*<2^FOfSM6HET1 zCL#Af-xpGOWB6~6m8h}aVK1~mw;n@9!PcN$V2jy?ww=q|x7*~eqx}E5H20^HED0)+ zLPID#quP@s!_}C`s2VZ>0F!3HM{4 zipQ|5rZd(B-J13#&jj4ch2*YvJg?Yc?hS$!sYw$96TnxX6?=qVfYb&w?!Bb}x61QD znLnO=KG2)~=q1#JB36{B5juGeupxrZd)gmIYEt}o&B&rRzKg?txrA+9&W~e7+ITW#a2Uki z1tC%ZcE4wUuc%te>4r_BROJH65{D*itgKp^fF7fXabx-tIm+Y*;h^_<9`JD29x>eq z;RnWh_h_L3ca8-)@b{zAFI;&&b57(Y(3#`sg>3fD!St3_c5@XirVaV4w>vgAEfg8Q z6hk_+BBnieysXklr`i%LEW|7QHfS%B+8#UB>NmXhyNzUu+~|{+PH{}RMQP(g?`9xg
    tVu_O;mLSQHn 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%0AOW5MAN>XnVv#O0wQ0-%hsSmd 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 JDsB|?e*mzF$Zr4u literal 0 HcmV?d00001 diff --git a/assets/pavosql-gopher.svg b/assets/pavosql-gopher.svg new file mode 100644 index 0000000..7961487 --- /dev/null +++ b/assets/pavosql-gopher.svg @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + From 06965de387fd7790bf2883ec9951214f3b42eb3f Mon Sep 17 00:00:00 2001 From: "georgios.kitsikoudis" Date: Fri, 4 Jul 2025 23:28:11 +0200 Subject: [PATCH 35/51] feat: add cli blueprint with cobra --- cmd/pavosql/cmd/root.go | 37 +++++++++++++++++++++++++++++ cmd/pavosql/cmd/serve/serve.go | 24 +++++++++++++++++++ cmd/pavosql/cmd/version/version.go | 18 ++++++++++++++ pkg/fmt/fmt.go | 7 ++++++ pkg/fmt/fmt_test.go | 38 ++++++++++++++++++++++++++++++ 5 files changed, 124 insertions(+) create mode 100644 cmd/pavosql/cmd/root.go create mode 100644 cmd/pavosql/cmd/serve/serve.go create mode 100644 cmd/pavosql/cmd/version/version.go create mode 100644 pkg/fmt/fmt.go create mode 100644 pkg/fmt/fmt_test.go diff --git a/cmd/pavosql/cmd/root.go b/cmd/pavosql/cmd/root.go new file mode 100644 index 0000000..3a2d0b6 --- /dev/null +++ b/cmd/pavosql/cmd/root.go @@ -0,0 +1,37 @@ +package cmd + +import ( + "os" + + "github.com/pavosql/pavosql/cmd/pavosql/cmd/serve" + "github.com/pavosql/pavosql/cmd/pavosql/cmd/version" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "pavosql", + Short: "A brief description of your application", + Long: `A longer description that spans multiple lines and likely contains +examples and usage of using your application. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + +func init() { + rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + + rootCmd.AddCommand(version.Command()) + rootCmd.AddCommand(serve.Command()) +} diff --git a/cmd/pavosql/cmd/serve/serve.go b/cmd/pavosql/cmd/serve/serve.go new file mode 100644 index 0000000..9232af3 --- /dev/null +++ b/cmd/pavosql/cmd/serve/serve.go @@ -0,0 +1,24 @@ +package serve + +import ( + "github.com/spf13/cobra" +) + +var ( + port uint16 +) + +func Command() *cobra.Command { + var serveCmd = &cobra.Command{ + Use: "serve", + Short: "", + Long: "", + Run: func(cmd *cobra.Command, args []string) { + // TODO: start server + }, + } + + serveCmd.Flags().Uint16VarP(&port, "port", "p", 6677, "") + + return serveCmd +} diff --git a/cmd/pavosql/cmd/version/version.go b/cmd/pavosql/cmd/version/version.go new file mode 100644 index 0000000..bec728a --- /dev/null +++ b/cmd/pavosql/cmd/version/version.go @@ -0,0 +1,18 @@ +package version + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +func Command() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "", + Long: "", + Run: func(cmd *cobra.Command, args []string) { + fmt.Println("v0.0.0") + }, + } +} diff --git a/pkg/fmt/fmt.go b/pkg/fmt/fmt.go new file mode 100644 index 0000000..a95fed3 --- /dev/null +++ b/pkg/fmt/fmt.go @@ -0,0 +1,7 @@ +package fmt + +import "io" + +func Format(r io.Reader) ([]byte, error) { + return nil, nil +} diff --git a/pkg/fmt/fmt_test.go b/pkg/fmt/fmt_test.go new file mode 100644 index 0000000..7e91461 --- /dev/null +++ b/pkg/fmt/fmt_test.go @@ -0,0 +1,38 @@ +package fmt_test + +import ( + "io" + "testing" + + "github.com/pavosql/pavosql/pkg/fmt" +) + +func TestFormat(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + r io.Reader + want []byte + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := fmt.Format(tt.r) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("Format() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("Format() succeeded unexpectedly") + } + // TODO: update the condition below to compare got with tt.want. + if true { + t.Errorf("Format() = %v, want %v", got, tt.want) + } + }) + } +} From 277b4ed26d5ca57063e641dd7db88d67593a07dc Mon Sep 17 00:00:00 2001 From: "georgios.kitsikoudis" Date: Fri, 4 Jul 2025 23:28:42 +0200 Subject: [PATCH 36/51] feat: add pavofmt scaffold --- cmd/pavofmt/main.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 cmd/pavofmt/main.go diff --git a/cmd/pavofmt/main.go b/cmd/pavofmt/main.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/cmd/pavofmt/main.go @@ -0,0 +1 @@ +package main From 3914ac2617f8ff353bc4da621843768c45883b23 Mon Sep 17 00:00:00 2001 From: "georgios.kitsikoudis" Date: Fri, 4 Jul 2025 23:29:37 +0200 Subject: [PATCH 37/51] feat: cli scaffold --- cmd/pavosql/main.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cmd/pavosql/main.go b/cmd/pavosql/main.go index da29a2c..b78a2d0 100644 --- a/cmd/pavosql/main.go +++ b/cmd/pavosql/main.go @@ -1,4 +1,30 @@ +/* +MIT License + +# Copyright (c) 2023 Georgios Kitsikoudis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ package main +import "github.com/pavosql/pavosql/cmd/pavosql/cmd" + func main() { + cmd.Execute() } From a2373a74c44ca928243bd20a92d9c2a2d01a123b Mon Sep 17 00:00:00 2001 From: "georgios.kitsikoudis" Date: Fri, 4 Jul 2025 23:30:48 +0200 Subject: [PATCH 38/51] refactor(tree): move node package into tree package --- internal/tree/{node => }/node.go | 58 +++++++++++++-------------- internal/tree/{node => }/node_test.go | 2 +- 2 files changed, 30 insertions(+), 30 deletions(-) rename internal/tree/{node => }/node.go (85%) rename internal/tree/{node => }/node_test.go (97%) diff --git a/internal/tree/node/node.go b/internal/tree/node.go similarity index 85% rename from internal/tree/node/node.go rename to internal/tree/node.go index f9334aa..509a26c 100644 --- a/internal/tree/node/node.go +++ b/internal/tree/node.go @@ -1,4 +1,4 @@ -package node +package tree import ( "bytes" @@ -20,7 +20,7 @@ var ( ) /* -A Nodes is an array of bytes representing the data stored in a single node of a B+Tree. +A node is an array of bytes representing the data stored in a single node of a B+Tree. Nodes are stored in a custom byte format that is structured as follows: Description | Header | Data area @@ -55,29 +55,29 @@ The data stored in the cells is formatted as follows: ------------+--------+--------+--------+------- Size in B | 2 | 2 | KeyLen | ValLen */ -type Node [common.PageSize]byte +type node [common.PageSize]byte -func New(typ byte) Node { - var n Node +func newNode(typ byte) node { + var n node n[0] = typ n.setWCursor(common.PageSize) return n } // Returns the type of n. -func (n *Node) Type() byte { +func (n *node) Type() byte { return n[0] } // Returns the number of cells currently stored on n. -func (n *Node) N() uint16 { +func (n *node) N() uint16 { return binary.LittleEndian.Uint16(n[nOff:]) } // Returns the key of the i'th cell stored in n. // // Panics if i is greater or equal than the length of n. -func (n *Node) Key(i uint16) []byte { +func (n *node) Key(i uint16) []byte { if !n.indexInBounds(i) { panic(ErrIndexOutOfBounds) } @@ -89,7 +89,7 @@ func (n *Node) Key(i uint16) []byte { // Returns the value of the i'th cell stored in n. // // Panics if i is greater or equal than the length of n. -func (n *Node) Val(i uint16) []byte { +func (n *node) Val(i uint16) []byte { if !n.indexInBounds(i) { panic(ErrIndexOutOfBounds) } @@ -100,7 +100,7 @@ func (n *Node) Val(i uint16) []byte { } // Binary searches the target key inside n and returns its position and weither it exists. -func (n *Node) Search(target []byte) (uint16, bool) { +func (n *node) Search(target []byte) (uint16, bool) { l := n.N() left, right := uint16(0), l @@ -128,14 +128,14 @@ func (n *Node) Search(target []byte) (uint16, bool) { // the callers responsibility to ensure that k belongs at position i to ensure the order of the keys // will not break. Always use Search and CanSet before using Set to ensure that n has enough space // for the k-v pair and that the value of i is correct. -func (n *Node) Set(i uint16, k, v []byte) Node { +func (n *node) Set(i uint16, k, v []byte) node { l := n.N() cell := makeCell(k, v) wCur := n.wCursor() off := wCur - uint16(len(cell)) - var res Node + var res node copy(res[:], n[:]) copy(res[off:wCur], cell) @@ -156,27 +156,27 @@ func (n *Node) Set(i uint16, k, v []byte) Node { // Returns true if n has enough space left in its void to add the given k-v pair. CanSet always // assumes that k does not exist. -func (n *Node) CanSet(k, v []byte) bool { +func (n *node) CanSet(k, v []byte) bool { return n.voidSize() >= 6+len(k)+len(v) } // Returns a copy of n with the given k-v pair set into it. If k already exists its value is // overwritten otherwise a new cell for k-v is inserted. -func (n *Node) Delete(k []byte) Node { - return Node{} +func (n *node) Delete(k []byte) node { + return node{} } // Splits n into two separate nodes. -func (n *Node) Split() (left Node, right Node) { +func (n *node) Split() (left node, right node) { var addToRight bool var i uint16 var wc uint16 = common.PageSize - left, right = New(n.Type()), New(n.Type()) + left, right = newNode(n.Type()), newNode(n.Type()) thresh := (common.PageSize - wc) / 2 - addToNode := func(addTo *Node, i uint16, cell []byte, wCursor *uint16) { + addToNode := func(addTo *node, i uint16, cell []byte, wCursor *uint16) { *wCursor -= uint16(len(cell)) addTo.setOffset(i, *wCursor) copy(addTo[*wCursor:], cell) @@ -203,8 +203,8 @@ func (n *Node) Split() (left Node, right Node) { } // Returns a resorted and reduced copy of n by freeing up space used by unreferenced cells. -func (n *Node) Vacuum() Node { - var vacuumed Node +func (n *node) Vacuum() node { + var vacuumed node vacuumed[0] = n.Type() vacuumed.setN(n.N()) @@ -224,12 +224,12 @@ func (n *Node) Vacuum() Node { } // An iterator over all key-value pairs of n. -func (n *Node) All() iter.Seq2[[]byte, []byte] { +func (n *node) All() iter.Seq2[[]byte, []byte] { return n.AllFrom(0) } // An iterator over all key-value pairs of n starting from position i. -func (n *Node) AllFrom(i uint16) iter.Seq2[[]byte, []byte] { +func (n *node) AllFrom(i uint16) iter.Seq2[[]byte, []byte] { return func(yield func([]byte, []byte) bool) { for ; i < n.N(); i++ { k, v := n.Key(i), n.Val(i) @@ -240,37 +240,37 @@ func (n *Node) AllFrom(i uint16) iter.Seq2[[]byte, []byte] { } } -func (n *Node) setN(nc uint16) { +func (n *node) setN(nc uint16) { binary.LittleEndian.PutUint16(n[nOff:], nc) } -func (n *Node) offset(i uint16) uint16 { +func (n *node) offset(i uint16) uint16 { if n.indexInBounds(i) { panic(ErrIndexOutOfBounds) } return binary.LittleEndian.Uint16(n[offPos(i):]) } -func (n *Node) setOffset(i, off uint16) { +func (n *node) setOffset(i, off uint16) { if n.indexInBounds(i) { panic(ErrIndexOutOfBounds) } binary.LittleEndian.PutUint16(n[offPos(i):], off) } -func (n *Node) indexInBounds(i uint16) bool { +func (n *node) indexInBounds(i uint16) bool { return i >= n.N() } -func (n *Node) wCursor() uint16 { +func (n *node) wCursor() uint16 { return binary.LittleEndian.Uint16(n[wCurOff:]) } -func (n *Node) setWCursor(wc uint16) { +func (n *node) setWCursor(wc uint16) { binary.LittleEndian.PutUint16(n[wCurOff:], wc) } -func (n *Node) voidSize() int { +func (n *node) voidSize() int { return int(n.wCursor()) - int(offPos(n.N())+2) } diff --git a/internal/tree/node/node_test.go b/internal/tree/node_test.go similarity index 97% rename from internal/tree/node/node_test.go rename to internal/tree/node_test.go index 4599b5e..a3c64f3 100644 --- a/internal/tree/node/node_test.go +++ b/internal/tree/node_test.go @@ -1,4 +1,4 @@ -package node +package tree import ( "testing" From ed1c72c99e4db7262231b070d1ffd313b935cd23 Mon Sep 17 00:00:00 2001 From: "georgios.kitsikoudis" Date: Fri, 4 Jul 2025 23:32:03 +0200 Subject: [PATCH 39/51] feat(ast/parse): add basic ast statement types --- go.mod | 6 +++- go.sum | 14 +++++--- pkg/ast/ast.go | 10 ++++++ pkg/parse/parse.go | 71 ++++++++++++++++++++++++++++---------- pkg/parse/parse_test.go | 39 +++++++++++++++++++++ pkg/parse/tokenize.go | 8 +++-- pkg/parse/tokenize_test.go | 3 +- 7 files changed, 123 insertions(+), 28 deletions(-) create mode 100644 pkg/parse/parse_test.go diff --git a/go.mod b/go.mod index 4096e5c..a994f97 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module github.com/pavosql/pavosql go 1.24 -require github.com/google/docsy v0.12.0 // indirect +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect +) diff --git a/go.sum b/go.sum index 3ed3add..ffae55e 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,10 @@ -github.com/FortAwesome/Font-Awesome v0.0.0-20241216213156-af620534bfc3/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo= -github.com/google/docsy v0.12.0 h1:CddZKL39YyJzawr8GTVaakvcUTCJRAAYdz7W0qfZ2P4= -github.com/google/docsy v0.12.0/go.mod h1:1bioDqA493neyFesaTvQ9reV0V2vYy+xUAnlnz7+miM= -github.com/twbs/bootstrap v5.3.6+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go index 1fd22f1..4ef2180 100644 --- a/pkg/ast/ast.go +++ b/pkg/ast/ast.go @@ -3,3 +3,13 @@ package ast type Stmnt interface{} // TODO: Define AST types. + +type SelectStmt struct{} + +type DeleteStmt struct{} + +type CreateStmt struct{} + +type UpdateStmt struct{} + +type InsertStmt struct{} diff --git a/pkg/parse/parse.go b/pkg/parse/parse.go index 1ee0fa6..b3697a4 100644 --- a/pkg/parse/parse.go +++ b/pkg/parse/parse.go @@ -2,32 +2,67 @@ package parse import ( "io" - "sync" "github.com/pavosql/pavosql/pkg/ast" ) -func Parse(r io.Reader) []ast.Stmnt { - var wg sync.WaitGroup - wg.Add(2) +func Parse(r io.Reader) ([]ast.Stmnt, error) { + var ( + stmt ast.Stmnt + err error + ) - toks := make(chan Token) - go func() { - defer wg.Done() - for tok := range tokenize(r) { - toks <- tok + stmts := []ast.Stmnt{} + toks := readTokens(r) + for tok := range toks { + switch tok.Type { + case Select: + stmt, err = parseSelectStmt(toks) + if err != nil { + return nil, err + } + case Delete: + case Create: + case Update: + case Insert: + default: } - }() + stmts = append(stmts, stmt) + } + + return stmts, nil +} + +func parseSelectStmt(toks <-chan Token) (ast.SelectStmt, error) { + tok := <-toks + + switch tok.Type { + case Asterisk: + case Ident: + } + return ast.SelectStmt{}, nil +} + +func parseDeleteStmt() {} +func parseCreateStmt() {} + +func parseUpdateStmt() {} + +func parseInsertStmt() {} + +func parseFieldSelectList(toks <-chan Token) { + for tok := range toks { + _ = tok + } +} + +func readTokens(r io.Reader) <-chan Token { + toks := make(chan Token) go func() { - defer wg.Done() - for tok := range toks { - _ = tok - // TODO: Implement parsing logic + for _, tok := range tokenize(r) { + toks <- tok } }() - - wg.Wait() - - return nil + return toks } diff --git a/pkg/parse/parse_test.go b/pkg/parse/parse_test.go new file mode 100644 index 0000000..6ec79d4 --- /dev/null +++ b/pkg/parse/parse_test.go @@ -0,0 +1,39 @@ +package parse_test + +import ( + "io" + "testing" + + "github.com/pavosql/pavosql/pkg/ast" + "github.com/pavosql/pavosql/pkg/parse" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string // description of this test case + // Named input parameters for target function. + r io.Reader + want []ast.Stmnt + wantErr bool + }{ + // TODO: Add test cases. + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, gotErr := parse.Parse(tt.r) + if gotErr != nil { + if !tt.wantErr { + t.Errorf("Parse() failed: %v", gotErr) + } + return + } + if tt.wantErr { + t.Fatal("Parse() succeeded unexpectedly") + } + // TODO: update the condition below to compare got with tt.want. + if true { + t.Errorf("Parse() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/parse/tokenize.go b/pkg/parse/tokenize.go index 2ff46ab..3d136df 100644 --- a/pkg/parse/tokenize.go +++ b/pkg/parse/tokenize.go @@ -89,11 +89,12 @@ type Token struct { Line, Column int } -func tokenize(r io.Reader) iter.Seq[Token] { +func tokenize(r io.Reader) iter.Seq2[int, Token] { scan := new(scanner.Scanner) scan.Init(r) - return func(yield func(Token) bool) { + return func(yield func(int, Token) bool) { + var i int for r := scan.Scan(); r != scanner.EOF; r = scan.Scan() { tok := Token{ Val: scan.TokenText(), @@ -124,9 +125,10 @@ func tokenize(r io.Reader) iter.Seq[Token] { tok.Type = LexError } - if !yield(tok) { + if !yield(i, tok) { return } + i++ } } } diff --git a/pkg/parse/tokenize_test.go b/pkg/parse/tokenize_test.go index 56b37f8..803f901 100644 --- a/pkg/parse/tokenize_test.go +++ b/pkg/parse/tokenize_test.go @@ -62,8 +62,7 @@ func Test_tokenize(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { - var i int - for got := range tokenize(strings.NewReader(c.in)) { + for i, got := range tokenize(strings.NewReader(c.in)) { if i >= len(c.want) { t.Fatalf("want %d tokens, got at least %d", len(c.want), i+1) } From 865bf9694c3bb44ec7b9c5feb1f5122dc12fadd1 Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:22:21 +0200 Subject: [PATCH 40/51] feat(node): implement delete function --- internal/tree/node/node.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/tree/node/node.go b/internal/tree/node/node.go index f9334aa..3a158ca 100644 --- a/internal/tree/node/node.go +++ b/internal/tree/node/node.go @@ -160,10 +160,26 @@ func (n *Node) CanSet(k, v []byte) bool { return n.voidSize() >= 6+len(k)+len(v) } -// Returns a copy of n with the given k-v pair set into it. If k already exists its value is -// overwritten otherwise a new cell for k-v is inserted. -func (n *Node) Delete(k []byte) Node { - return Node{} +// Returns a copy of n with the k-v pair at index i deleted. +// +// Delete mereley deletes the reference to the cell and does not free up the cells space. To free up +// the space taken up by unreferenced cells use Vacuum. +func (n *Node) Delete(i uint16) Node { + if !n.indexInBounds(i) { + panic(ErrIndexOutOfBounds) + } + + l := n.N() + + var res Node + copy(res[:], n[:]) + + trailingOffs := n[offPos(i) : offPos(l)+2] + copy(res[offPos(i):], trailingOffs[2:]) + res.setOffset(l, 0) + res.setN(l - 1) + + return res } // Splits n into two separate nodes. From 46af0934d3e8751017613e0ca06860916fc6a61c Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:23:01 +0200 Subject: [PATCH 41/51] feat(node): use common page type --- internal/tree/node/node.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/tree/node/node.go b/internal/tree/node/node.go index 3a158ca..2b52a80 100644 --- a/internal/tree/node/node.go +++ b/internal/tree/node/node.go @@ -57,16 +57,16 @@ The data stored in the cells is formatted as follows: */ type Node [common.PageSize]byte -func New(typ byte) Node { +func New(typ common.PageType) Node { var n Node - n[0] = typ + n[0] = byte(typ) n.setWCursor(common.PageSize) return n } // Returns the type of n. -func (n *Node) Type() byte { - return n[0] +func (n *Node) Type() common.PageType { + return common.PageType(n[0]) } // Returns the number of cells currently stored on n. From 17503135481c9a456b15ec2d5549e4c86f2a809d Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:24:18 +0200 Subject: [PATCH 42/51] feat(node): use common page type --- internal/tree/node/node.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tree/node/node.go b/internal/tree/node/node.go index 2b52a80..88667f0 100644 --- a/internal/tree/node/node.go +++ b/internal/tree/node/node.go @@ -221,7 +221,7 @@ func (n *Node) Split() (left Node, right Node) { // Returns a resorted and reduced copy of n by freeing up space used by unreferenced cells. func (n *Node) Vacuum() Node { var vacuumed Node - vacuumed[0] = n.Type() + vacuumed[0] = byte(n.Type()) vacuumed.setN(n.N()) var wc uint16 = common.PageSize From 6bd8b1194eb62a4caad3b26168cd46f0fdf4d4ba Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:25:07 +0200 Subject: [PATCH 43/51] ci: add testing and building pipeline --- .github/workflows/build.yaml | 0 .github/workflows/test.yaml | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .github/workflows/build.yaml mode change 100644 => 100755 .github/workflows/test.yaml diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml old mode 100644 new mode 100755 diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml old mode 100644 new mode 100755 From 16cc90d98889c9e76a5d155087207e6cdad30ac1 Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:26:14 +0200 Subject: [PATCH 44/51] feat(node): add Pointer method to retrieve page pointer --- internal/tree/node/node.go | 9 +++++++++ 1 file changed, 9 insertions(+) mode change 100644 => 100755 internal/tree/node/node.go diff --git a/internal/tree/node/node.go b/internal/tree/node/node.go old mode 100644 new mode 100755 index 88667f0..e923147 --- a/internal/tree/node/node.go +++ b/internal/tree/node/node.go @@ -99,6 +99,15 @@ func (n *Node) Val(i uint16) []byte { return n[off+4+kLen : off+4+kLen+vLen] } +func (n *Node) Pointer(i uint16) uint64 { + if !n.indexInBounds(i) { + panic(ErrIndexOutOfBounds) + } + off := n.offset(i) + kLen := binary.LittleEndian.Uint16(n[off:]) + return binary.LittleEndian.Uint64(n[off+2+kLen : off+2+kLen+8]) +} + // Binary searches the target key inside n and returns its position and weither it exists. func (n *Node) Search(target []byte) (uint16, bool) { l := n.N() From 983da754892925b7b3230f48a6e055a03c8f57b7 Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:26:44 +0200 Subject: [PATCH 45/51] feat(tree): start implementing Get functionality --- internal/tree/tree.go | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) mode change 100644 => 100755 internal/tree/tree.go diff --git a/internal/tree/tree.go b/internal/tree/tree.go old mode 100644 new mode 100755 index 97cdbbf..1f14aa0 --- a/internal/tree/tree.go +++ b/internal/tree/tree.go @@ -1,7 +1,15 @@ package tree +import ( + "errors" + "fmt" + + "github.com/pavosql/pavosql/internal/common" + "github.com/pavosql/pavosql/internal/tree/node" +) + type pager interface { - ReadPage(off uint64) ([]byte, error) + ReadPage(off uint64) ([common.PageSize]byte, error) Commit() error Rollback() error } @@ -16,7 +24,32 @@ func New() *Tree { } func (t *Tree) Get(k []byte) ([]byte, error) { - return nil, nil + page, err := t.pager.ReadPage(t.root) + if err != nil { + return nil, err + } + cur := node.Node(page) + + for { + i, exists := cur.Search(k) + + switch cur.Type() { + case common.PointerPage: + ptr := cur.Pointer(i) + page, err = t.pager.ReadPage(ptr) + if err != nil { + return nil, fmt.Errorf("tree: failed to read page: %w", err) + } + cur = node.Node(page) + case common.LeafPage: + if !exists { + return nil, errors.New("key does not exists on leaf node") + } + return cur.Val(i), nil + default: + return nil, errors.New("invalid page type") + } + } } func (t *Tree) Set(k []byte, v []byte) error { From 1357ffda202e02c74d1b736ef4f56cb1f2adc021 Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:27:11 +0200 Subject: [PATCH 46/51] feat(common): add page types --- internal/common/common.go | 7 +++++++ 1 file changed, 7 insertions(+) mode change 100644 => 100755 internal/common/common.go diff --git a/internal/common/common.go b/internal/common/common.go old mode 100644 new mode 100755 index 1a1d308..4758715 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -1,3 +1,10 @@ package common const PageSize = 8192 + +type PageType uint8 + +const ( + PointerPage PageType = iota + 1 + LeafPage +) From 70912412fba60d54c76287154b5ba019ab6e06f8 Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:27:40 +0200 Subject: [PATCH 47/51] ci: linter config --- .golangci.toml | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .golangci.toml diff --git a/.golangci.toml b/.golangci.toml old mode 100644 new mode 100755 From 920f88f2235faddc057171ce6ed183acb3bfcb32 Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:28:10 +0200 Subject: [PATCH 48/51] chore: dependencies, license and more --- LICENSE | 0 cmd/pavosql/main.go | 0 docs.go | 0 go.mod | 2 -- go.sum | 4 ---- 5 files changed, 6 deletions(-) mode change 100644 => 100755 LICENSE mode change 100644 => 100755 cmd/pavosql/main.go mode change 100644 => 100755 docs.go mode change 100644 => 100755 go.mod mode change 100644 => 100755 go.sum diff --git a/LICENSE b/LICENSE old mode 100644 new mode 100755 diff --git a/cmd/pavosql/main.go b/cmd/pavosql/main.go old mode 100644 new mode 100755 diff --git a/docs.go b/docs.go old mode 100644 new mode 100755 diff --git a/go.mod b/go.mod old mode 100644 new mode 100755 index 4096e5c..5f29011 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/pavosql/pavosql go 1.24 - -require github.com/google/docsy v0.12.0 // indirect diff --git a/go.sum b/go.sum old mode 100644 new mode 100755 index 3ed3add..e69de29 --- a/go.sum +++ b/go.sum @@ -1,4 +0,0 @@ -github.com/FortAwesome/Font-Awesome v0.0.0-20241216213156-af620534bfc3/go.mod h1:IUgezN/MFpCDIlFezw3L8j83oeiIuYoj28Miwr/KUYo= -github.com/google/docsy v0.12.0 h1:CddZKL39YyJzawr8GTVaakvcUTCJRAAYdz7W0qfZ2P4= -github.com/google/docsy v0.12.0/go.mod h1:1bioDqA493neyFesaTvQ9reV0V2vYy+xUAnlnz7+miM= -github.com/twbs/bootstrap v5.3.6+incompatible/go.mod h1:fZTSrkpSf0/HkL0IIJzvVspTt1r9zuf7XlZau8kpcY0= From 8d73eedfa48590d22a8b7b5610022201ac5f9d7e Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:35:18 +0200 Subject: [PATCH 49/51] chore: go mod tidy --- go.sum | 3 --- 1 file changed, 3 deletions(-) mode change 100755 => 100644 go.sum diff --git a/go.sum b/go.sum old mode 100755 new mode 100644 index 0c5069d..ffae55e --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -<<<<<<< HEAD -======= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -10,4 +8,3 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ->>>>>>> ed1c72c99e4db7262231b070d1ffd313b935cd23 From 795f07f4fe9ae9db6b12c3968fad22f16df62c97 Mon Sep 17 00:00:00 2001 From: gkits Date: Sat, 5 Jul 2025 20:38:12 +0200 Subject: [PATCH 50/51] refactor(tree): merge common package into tree package --- internal/{common => tree}/common.go | 2 +- internal/tree/node.go | 20 +++++++++----------- internal/tree/tree.go | 8 +++----- 3 files changed, 13 insertions(+), 17 deletions(-) rename internal/{common => tree}/common.go (86%) diff --git a/internal/common/common.go b/internal/tree/common.go similarity index 86% rename from internal/common/common.go rename to internal/tree/common.go index 4758715..c49aba6 100755 --- a/internal/common/common.go +++ b/internal/tree/common.go @@ -1,4 +1,4 @@ -package common +package tree const PageSize = 8192 diff --git a/internal/tree/node.go b/internal/tree/node.go index 9d5b9e9..52b83f4 100755 --- a/internal/tree/node.go +++ b/internal/tree/node.go @@ -5,8 +5,6 @@ import ( "encoding/binary" "errors" "iter" - - "github.com/pavosql/pavosql/internal/common" ) const ( @@ -55,18 +53,18 @@ The data stored in the cells is formatted as follows: ------------+--------+--------+--------+------- Size in B | 2 | 2 | KeyLen | ValLen */ -type node [common.PageSize]byte +type node [PageSize]byte -func newNode(typ common.PageType) node { +func newNode(typ PageType) node { var n node n[0] = byte(typ) - n.setWCursor(common.PageSize) + n.setWCursor(PageSize) return n } // Returns the type of n. -func (n *node) Type() common.PageType { - return common.PageType(n[0]) +func (n *node) Type() PageType { + return PageType(n[0]) } // Returns the number of cells currently stored on n. @@ -195,11 +193,11 @@ func (n *node) Delete(i uint16) node { func (n *node) Split() (left node, right node) { var addToRight bool var i uint16 - var wc uint16 = common.PageSize + var wc uint16 = PageSize left, right = newNode(n.Type()), newNode(n.Type()) - thresh := (common.PageSize - wc) / 2 + thresh := (PageSize - wc) / 2 addToNode := func(addTo *node, i uint16, cell []byte, wCursor *uint16) { *wCursor -= uint16(len(cell)) @@ -220,7 +218,7 @@ func (n *node) Split() (left node, right node) { addToNode(&left, i, cell, &wc) i++ - if wc < common.PageSize-thresh { + if wc < PageSize-thresh { addToRight = true } } @@ -233,7 +231,7 @@ func (n *node) Vacuum() node { vacuumed[0] = byte(n.Type()) vacuumed.setN(n.N()) - var wc uint16 = common.PageSize + var wc uint16 = PageSize var i uint16 for k, v := range n.All() { cell := makeCell(k, v) diff --git a/internal/tree/tree.go b/internal/tree/tree.go index 5b3822b..6adc9fd 100755 --- a/internal/tree/tree.go +++ b/internal/tree/tree.go @@ -3,12 +3,10 @@ package tree import ( "errors" "fmt" - - "github.com/pavosql/pavosql/internal/common" ) type pager interface { - ReadPage(off uint64) ([common.PageSize]byte, error) + ReadPage(off uint64) ([PageSize]byte, error) Commit() error Rollback() error } @@ -33,14 +31,14 @@ func (t *Tree) Get(k []byte) ([]byte, error) { i, exists := cur.Search(k) switch cur.Type() { - case common.PointerPage: + case PointerPage: ptr := cur.Pointer(i) page, err = t.pager.ReadPage(ptr) if err != nil { return nil, fmt.Errorf("tree: failed to read page: %w", err) } cur = node(page) - case common.LeafPage: + case LeafPage: if !exists { return nil, errors.New("key does not exists on leaf node") } From f55dcc4ac96cfd8d3c048b7abed404f27ea7ca41 Mon Sep 17 00:00:00 2001 From: gkits Date: Sun, 6 Jul 2025 19:06:27 +0200 Subject: [PATCH 51/51] chore: move ownership of project --- README.md | 6 +++--- cmd/pavosql/cmd/root.go | 4 ++-- cmd/pavosql/main.go | 2 +- go.mod | 2 +- pkg/fmt/fmt_test.go | 2 +- pkg/parse/parse.go | 2 +- pkg/parse/parse_test.go | 4 ++-- 7 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index bf3777f..2c1adfb 100755 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Build](https://github.com/pavosql/pavosql/actions/workflows/build.yaml/badge.svg)](https://github.com/pavosql/pavosql/actions/workflows/build.yaml) -[![Test](https://github.com/pavosql/pavosql/actions/workflows/test.yaml/badge.svg)](https://github.com/pavosql/pavosql/actions/workflows/test.yaml) +[![Build](https://github.com/gkits/pavosql/actions/workflows/build.yaml/badge.svg)](https://github.com/gkits/pavosql/actions/workflows/build.yaml) +[![Test](https://github.com/gkits/pavosql/actions/workflows/test.yaml/badge.svg)](https://github.com/gkits/pavosql/actions/workflows/test.yaml) -PavoSQL is a SQL database written purely in Go. +**This is project is still work in progress and not supposed to be used in any productive setting.** ## Roadmap diff --git a/cmd/pavosql/cmd/root.go b/cmd/pavosql/cmd/root.go index 3a2d0b6..8b45672 100644 --- a/cmd/pavosql/cmd/root.go +++ b/cmd/pavosql/cmd/root.go @@ -3,8 +3,8 @@ package cmd import ( "os" - "github.com/pavosql/pavosql/cmd/pavosql/cmd/serve" - "github.com/pavosql/pavosql/cmd/pavosql/cmd/version" + "github.com/gkits/pavosql/cmd/pavosql/cmd/serve" + "github.com/gkits/pavosql/cmd/pavosql/cmd/version" "github.com/spf13/cobra" ) diff --git a/cmd/pavosql/main.go b/cmd/pavosql/main.go index b78a2d0..204ca64 100755 --- a/cmd/pavosql/main.go +++ b/cmd/pavosql/main.go @@ -23,7 +23,7 @@ SOFTWARE. */ package main -import "github.com/pavosql/pavosql/cmd/pavosql/cmd" +import "github.com/gkits/pavosql/cmd/pavosql/cmd" func main() { cmd.Execute() diff --git a/go.mod b/go.mod index 5f0c379..e765c3c 100755 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/pavosql/pavosql +module github.com/gkits/pavosql go 1.24 diff --git a/pkg/fmt/fmt_test.go b/pkg/fmt/fmt_test.go index 7e91461..832e9ce 100644 --- a/pkg/fmt/fmt_test.go +++ b/pkg/fmt/fmt_test.go @@ -4,7 +4,7 @@ import ( "io" "testing" - "github.com/pavosql/pavosql/pkg/fmt" + "github.com/gkits/pavosql/pkg/fmt" ) func TestFormat(t *testing.T) { diff --git a/pkg/parse/parse.go b/pkg/parse/parse.go index b3697a4..fcbe0af 100644 --- a/pkg/parse/parse.go +++ b/pkg/parse/parse.go @@ -3,7 +3,7 @@ package parse import ( "io" - "github.com/pavosql/pavosql/pkg/ast" + "github.com/gkits/pavosql/pkg/ast" ) func Parse(r io.Reader) ([]ast.Stmnt, error) { diff --git a/pkg/parse/parse_test.go b/pkg/parse/parse_test.go index 6ec79d4..7c6b574 100644 --- a/pkg/parse/parse_test.go +++ b/pkg/parse/parse_test.go @@ -4,8 +4,8 @@ import ( "io" "testing" - "github.com/pavosql/pavosql/pkg/ast" - "github.com/pavosql/pavosql/pkg/parse" + "github.com/gkits/pavosql/pkg/ast" + "github.com/gkits/pavosql/pkg/parse" ) func TestParse(t *testing.T) {
+ + pavosql gopher + +

PavoSQL

+

+

+