Compare commits
27
Commits
c95af1ff54
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4cf2921f8 | ||
|
|
9e9e63f4ab | ||
|
|
b45b8dd14c | ||
|
|
c0460dd1e4 | ||
|
|
5eab1adf6a | ||
|
|
715e66ca65 | ||
|
|
2973ccd2b4 | ||
|
|
1d9d74cfde | ||
|
|
f272cbdbbc | ||
|
|
c31a35354c | ||
|
|
86b9835060 | ||
|
|
8a50f55782 | ||
|
|
185ba1878e | ||
|
|
89975de979 | ||
|
|
df8c61b7b9 | ||
|
|
ee49ef185b | ||
|
|
666465bb7d | ||
|
|
19f6f8d323 | ||
|
|
9c71ad1c8f | ||
|
|
4cb1b6d76a | ||
|
|
55e709af17 | ||
|
|
9b32b8b8f2 | ||
|
|
6888f8fd5b | ||
|
|
e3e67ade8b | ||
|
|
0b85e31d94 | ||
|
|
db673f4c98 | ||
|
|
57f2c755ce |
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package db0101
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
kv.mem = map[string][]byte{} // empty
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return nil }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package db0101
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package db0102
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
kv.mem = map[string][]byte{} // empty
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return nil }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package db0102
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], uint32(len(ent.key)))
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.val)))
|
||||||
|
copy(data[8:], ent.key)
|
||||||
|
copy(data[8+len(ent.key):], ent.val)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
key_len := make([]byte, 4)
|
||||||
|
s, err := r.Read(key_len)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s != 4 {
|
||||||
|
return errors.New("invalid size when reading key length")
|
||||||
|
}
|
||||||
|
val_len := make([]byte, 4)
|
||||||
|
s, err = r.Read(val_len)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s != 4 {
|
||||||
|
return errors.New("invalid size when reading val length")
|
||||||
|
}
|
||||||
|
key_data := make([]byte, binary.LittleEndian.Uint32(key_len))
|
||||||
|
s, err = r.Read(key_data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s != int(binary.LittleEndian.Uint32(key_len)) {
|
||||||
|
return errors.New("invalid size when reading key data")
|
||||||
|
}
|
||||||
|
val_data := make([]byte, binary.LittleEndian.Uint32(val_len))
|
||||||
|
s, err = r.Read(val_data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if s != int(binary.LittleEndian.Uint32(val_len)) {
|
||||||
|
return errors.New("invalid size when reading val data")
|
||||||
|
}
|
||||||
|
ent.key = key_data
|
||||||
|
ent.val = val_data
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package db0102
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{2, 0, 0, 0, 3, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
package db0103
|
||||||
|
|
||||||
|
import "bytes"
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
entry := Entry{}
|
||||||
|
eof, err := kv.log.Read(&entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.deleted {
|
||||||
|
delete(kv.mem, string(entry.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(entry.key)] = entry.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
if updated {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: val, deleted: false}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: nil, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package db0103
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], uint32(len(ent.key)))
|
||||||
|
copy(data[9:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[8] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.val)))
|
||||||
|
copy(data[9+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [9]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[0:4]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
deleted := header[8]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package db0103
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package db0103
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
FileName string
|
||||||
|
fp *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Open() (err error) {
|
||||||
|
log.fp, err = os.OpenFile(log.FileName, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Close() error {
|
||||||
|
return log.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Write(ent *Entry) error {
|
||||||
|
_, err := log.fp.Write(ent.Encode())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Read(ent *Entry) (eof bool, err error) {
|
||||||
|
err = ent.Decode(log.fp)
|
||||||
|
if err == io.EOF {
|
||||||
|
return true, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package db0104
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
ent := Entry{}
|
||||||
|
eof, err := kv.log.Read(&ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.deleted {
|
||||||
|
delete(kv.mem, string(ent.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(ent.key)] = ent.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
if updated {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: val}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package db0104
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], uint32(len(ent.key)))
|
||||||
|
copy(data[9:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[8] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.val)))
|
||||||
|
copy(data[9+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [9]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[0:4]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
deleted := header[8]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package db0104
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package db0104
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
FileName string
|
||||||
|
fp *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Open() (err error) {
|
||||||
|
log.fp, err = createFileSync(log.FileName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Close() error {
|
||||||
|
return log.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Write(ent *Entry) error {
|
||||||
|
if _, err := log.fp.Write(ent.Encode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return log.fp.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Read(ent *Entry) (eof bool, err error) {
|
||||||
|
err = ent.Decode(log.fp)
|
||||||
|
if err == io.EOF {
|
||||||
|
return true, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !unix
|
||||||
|
|
||||||
|
package db0104
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
return os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package db0104
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// open or create a file and fsync the directory
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = syncDir(path.Base(file)); err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDir(file string) error {
|
||||||
|
flags := os.O_RDONLY | syscall.O_DIRECTORY
|
||||||
|
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
return syscall.Fsync(dirfd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package db0105
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
ent := Entry{}
|
||||||
|
eof, err := kv.log.Read(&ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.deleted {
|
||||||
|
delete(kv.mem, string(ent.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(ent.key)] = ent.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
if updated {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: val}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package db0105
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
crc32 []byte
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.key)))
|
||||||
|
copy(data[13:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[12] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[8:12], uint32(len(ent.val)))
|
||||||
|
copy(data[13+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
checksum := crc32.ChecksumIEEE(data[4:])
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], checksum)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrBadSum = errors.New("bad checksum")
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [13]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[8:12]))
|
||||||
|
deleted := header[12]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum := crc32.NewIEEE()
|
||||||
|
checksum.Write(header[4:])
|
||||||
|
checksum.Write(data)
|
||||||
|
|
||||||
|
if checksum.Sum32() != binary.LittleEndian.Uint32(header[0:4]) {
|
||||||
|
return ErrBadSum
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package db0105
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVRecovery(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
prepare := func() {
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate truncated log
|
||||||
|
fp, _ := os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ := fp.Stat()
|
||||||
|
fp.Truncate(st.Size() - 1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate bad checksum
|
||||||
|
fp, _ = os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ = fp.Stat()
|
||||||
|
fp.WriteAt([]byte{0}, st.Size()-1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{0xe9, 0xec, 0x4d, 0x9e, 2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{0x4c, 0xd0, 0xfe, 0xe5, 2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package db0105
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
FileName string
|
||||||
|
fp *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Open() (err error) {
|
||||||
|
log.fp, err = createFileSync(log.FileName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Close() error {
|
||||||
|
return log.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Write(ent *Entry) error {
|
||||||
|
if _, err := log.fp.Write(ent.Encode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return log.fp.Sync() // fsync
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Read(ent *Entry) (eof bool, err error) {
|
||||||
|
err = ent.Decode(log.fp)
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF || err == ErrBadSum {
|
||||||
|
return true, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !unix
|
||||||
|
|
||||||
|
package db0105
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
return os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package db0105
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// open or create a file and fsync the directory
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = syncDir(path.Base(file)); err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDir(file string) error {
|
||||||
|
flags := os.O_RDONLY | syscall.O_DIRECTORY
|
||||||
|
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
return syscall.Fsync(dirfd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package db0201
|
||||||
|
|
||||||
|
type CellType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeI64 CellType = 1
|
||||||
|
TypeStr CellType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cell struct {
|
||||||
|
Type CellType
|
||||||
|
I64 int64
|
||||||
|
Str []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Encode(toAppend []byte) []byte
|
||||||
|
|
||||||
|
func (cell *Cell) Decode(data []byte) (rest []byte, err error)
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package db0201
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableCell(t *testing.T) {
|
||||||
|
cell := Cell{Type: TypeI64, I64: -2}
|
||||||
|
data := []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded := Cell{Type: TypeI64}
|
||||||
|
rest, err := decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
|
||||||
|
cell = Cell{Type: TypeStr, Str: []byte("asdf")}
|
||||||
|
data = []byte{4, 0, 0, 0, 'a', 's', 'd', 'f'}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded = Cell{Type: TypeStr}
|
||||||
|
rest, err = decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package db0201
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
ent := Entry{}
|
||||||
|
eof, err := kv.log.Read(&ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.deleted {
|
||||||
|
delete(kv.mem, string(ent.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(ent.key)] = ent.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
if updated {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: val}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package db0201
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.key)))
|
||||||
|
copy(data[4+4+4+1:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[4+4+4] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[8:12], uint32(len(ent.val)))
|
||||||
|
copy(data[4+4+4+1+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], crc32.ChecksumIEEE(data[4:]))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrBadSum = errors.New("bad checksum")
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [4 + 4 + 4 + 1]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[8:12]))
|
||||||
|
deleted := header[4+4+4]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := crc32.NewIEEE()
|
||||||
|
h.Write(header[4:])
|
||||||
|
h.Write(data)
|
||||||
|
if h.Sum32() != binary.LittleEndian.Uint32(header[0:4]) {
|
||||||
|
return ErrBadSum
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package db0201
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVRecovery(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
prepare := func() {
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate truncated log
|
||||||
|
fp, _ := os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ := fp.Stat()
|
||||||
|
fp.Truncate(st.Size() - 1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate bad checksum
|
||||||
|
fp, _ = os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ = fp.Stat()
|
||||||
|
fp.WriteAt([]byte{0}, st.Size()-1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{0xe9, 0xec, 0x4d, 0x9e, 2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{0x4c, 0xd0, 0xfe, 0xe5, 2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package db0201
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
FileName string
|
||||||
|
fp *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Open() (err error) {
|
||||||
|
log.fp, err = createFileSync(log.FileName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Close() error {
|
||||||
|
return log.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Write(ent *Entry) error {
|
||||||
|
if _, err := log.fp.Write(ent.Encode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return log.fp.Sync() // fsync
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Read(ent *Entry) (eof bool, err error) {
|
||||||
|
err = ent.Decode(log.fp)
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF || err == ErrBadSum {
|
||||||
|
return true, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !unix
|
||||||
|
|
||||||
|
package db0201
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
return os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package db0201
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// open or create a file and fsync the directory
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = syncDir(path.Base(file)); err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDir(file string) error {
|
||||||
|
flags := os.O_RDONLY | syscall.O_DIRECTORY
|
||||||
|
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
return syscall.Fsync(dirfd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package db0202
|
||||||
|
|
||||||
|
func check(cond bool) {
|
||||||
|
if !cond {
|
||||||
|
panic("assertion failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package db0202
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CellType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeI64 CellType = 1
|
||||||
|
TypeStr CellType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cell struct {
|
||||||
|
Type CellType
|
||||||
|
I64 int64
|
||||||
|
Str []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Encode(toAppend []byte) []byte {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
return binary.LittleEndian.AppendUint64(toAppend, uint64(cell.I64))
|
||||||
|
case TypeStr:
|
||||||
|
toAppend = binary.LittleEndian.AppendUint32(toAppend, uint32(len(cell.Str)))
|
||||||
|
return append(toAppend, cell.Str...)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Decode(data []byte) (rest []byte, err error) {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
if len(data) < 8 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.I64 = int64(binary.LittleEndian.Uint64(data[0:8]))
|
||||||
|
return data[8:], nil
|
||||||
|
case TypeStr:
|
||||||
|
if len(data) < 4 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
size := int(binary.LittleEndian.Uint32(data[0:4]))
|
||||||
|
if len(data) < 4+size {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.Str = slices.Clone(data[4 : 4+size])
|
||||||
|
return data[4+size:], nil
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package db0202
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableCell(t *testing.T) {
|
||||||
|
cell := Cell{Type: TypeI64, I64: -2}
|
||||||
|
data := []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded := Cell{Type: TypeI64}
|
||||||
|
rest, err := decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
|
||||||
|
cell = Cell{Type: TypeStr, Str: []byte("asdf")}
|
||||||
|
data = []byte{4, 0, 0, 0, 'a', 's', 'd', 'f'}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded = Cell{Type: TypeStr}
|
||||||
|
rest, err = decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+66
@@ -0,0 +1,66 @@
|
|||||||
|
package db0202
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
ent := Entry{}
|
||||||
|
eof, err := kv.log.Read(&ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.deleted {
|
||||||
|
delete(kv.mem, string(ent.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(ent.key)] = ent.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
if updated {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: val}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package db0202
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.key)))
|
||||||
|
copy(data[4+4+4+1:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[4+4+4] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[8:12], uint32(len(ent.val)))
|
||||||
|
copy(data[4+4+4+1+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], crc32.ChecksumIEEE(data[4:]))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrBadSum = errors.New("bad checksum")
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [4 + 4 + 4 + 1]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[8:12]))
|
||||||
|
deleted := header[4+4+4]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := crc32.NewIEEE()
|
||||||
|
h.Write(header[4:])
|
||||||
|
h.Write(data)
|
||||||
|
if h.Sum32() != binary.LittleEndian.Uint32(header[0:4]) {
|
||||||
|
return ErrBadSum
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+126
@@ -0,0 +1,126 @@
|
|||||||
|
package db0202
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVRecovery(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
prepare := func() {
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate truncated log
|
||||||
|
fp, _ := os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ := fp.Stat()
|
||||||
|
fp.Truncate(st.Size() - 1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate bad checksum
|
||||||
|
fp, _ = os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ = fp.Stat()
|
||||||
|
fp.WriteAt([]byte{0}, st.Size()-1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{0xe9, 0xec, 0x4d, 0x9e, 2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{0x4c, 0xd0, 0xfe, 0xe5, 2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package db0202
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
FileName string
|
||||||
|
fp *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Open() (err error) {
|
||||||
|
log.fp, err = createFileSync(log.FileName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Close() error {
|
||||||
|
return log.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Write(ent *Entry) error {
|
||||||
|
if _, err := log.fp.Write(ent.Encode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return log.fp.Sync() // fsync
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Read(ent *Entry) (eof bool, err error) {
|
||||||
|
err = ent.Decode(log.fp)
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF || err == ErrBadSum {
|
||||||
|
return true, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !unix
|
||||||
|
|
||||||
|
package db0202
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
return os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package db0202
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// open or create a file and fsync the directory
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = syncDir(path.Base(file)); err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDir(file string) error {
|
||||||
|
flags := os.O_RDONLY | syscall.O_DIRECTORY
|
||||||
|
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
return syscall.Fsync(dirfd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package db0202
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
Table string
|
||||||
|
Cols []Column
|
||||||
|
PKey []int // indexes of primary key columns
|
||||||
|
}
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
Name string
|
||||||
|
Type CellType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row []Cell
|
||||||
|
|
||||||
|
func (schema *Schema) NewRow() Row {
|
||||||
|
return make(Row, len(schema.Cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) EncodeKey(schema *Schema) (key []byte)
|
||||||
|
|
||||||
|
func (row Row) EncodeVal(schema *Schema) (val []byte)
|
||||||
|
|
||||||
|
func (row Row) DecodeKey(schema *Schema, key []byte) (err error)
|
||||||
|
|
||||||
|
func (row Row) DecodeVal(schema *Schema, val []byte) (err error)
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package db0202
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRowEncode(t *testing.T) {
|
||||||
|
schema := &Schema{
|
||||||
|
Table: "link",
|
||||||
|
Cols: []Column{
|
||||||
|
{Name: "time", Type: TypeI64},
|
||||||
|
{Name: "src", Type: TypeStr},
|
||||||
|
{Name: "dst", Type: TypeStr},
|
||||||
|
},
|
||||||
|
PKey: []int{1, 2}, // (src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := Row{
|
||||||
|
Cell{Type: TypeI64, I64: 123},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
key := []byte{'l', 'i', 'n', 'k', 0, 1, 0, 0, 0, 'a', 1, 0, 0, 0, 'b'}
|
||||||
|
val := []byte{123, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
assert.Equal(t, key, row.EncodeKey(schema))
|
||||||
|
assert.Equal(t, val, row.EncodeVal(schema))
|
||||||
|
|
||||||
|
decoded := schema.NewRow()
|
||||||
|
err := decoded.DecodeKey(schema, key)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = decoded.DecodeVal(schema, val)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, row, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package db0203
|
||||||
|
|
||||||
|
func check(cond bool) {
|
||||||
|
if !cond {
|
||||||
|
panic("assertion failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package db0203
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CellType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeI64 CellType = 1
|
||||||
|
TypeStr CellType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cell struct {
|
||||||
|
Type CellType
|
||||||
|
I64 int64
|
||||||
|
Str []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Encode(toAppend []byte) []byte {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
return binary.LittleEndian.AppendUint64(toAppend, uint64(cell.I64))
|
||||||
|
case TypeStr:
|
||||||
|
toAppend = binary.LittleEndian.AppendUint32(toAppend, uint32(len(cell.Str)))
|
||||||
|
return append(toAppend, cell.Str...)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Decode(data []byte) (rest []byte, err error) {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
if len(data) < 8 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.I64 = int64(binary.LittleEndian.Uint64(data[0:8]))
|
||||||
|
return data[8:], nil
|
||||||
|
case TypeStr:
|
||||||
|
if len(data) < 4 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
size := int(binary.LittleEndian.Uint32(data[0:4]))
|
||||||
|
if len(data) < 4+size {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.Str = slices.Clone(data[4 : 4+size])
|
||||||
|
return data[4+size:], nil
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package db0203
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableCell(t *testing.T) {
|
||||||
|
cell := Cell{Type: TypeI64, I64: -2}
|
||||||
|
data := []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded := Cell{Type: TypeI64}
|
||||||
|
rest, err := decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
|
||||||
|
cell = Cell{Type: TypeStr, Str: []byte("asdf")}
|
||||||
|
data = []byte{4, 0, 0, 0, 'a', 's', 'd', 'f'}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded = Cell{Type: TypeStr}
|
||||||
|
rest, err = decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package db0203
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
ent := Entry{}
|
||||||
|
eof, err := kv.log.Read(&ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.deleted {
|
||||||
|
delete(kv.mem, string(ent.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(ent.key)] = ent.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeUpsert UpdateMode = 0 // insert or update
|
||||||
|
ModeInsert UpdateMode = 1 // insert new
|
||||||
|
ModeUpdate UpdateMode = 2 // update existing
|
||||||
|
)
|
||||||
|
|
||||||
|
func (kv *KV) SetEx(key []byte, val []byte, mode UpdateMode) (updated bool, err error)
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
return kv.SetEx(key, val, ModeUpsert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package db0203
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.key)))
|
||||||
|
copy(data[4+4+4+1:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[4+4+4] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[8:12], uint32(len(ent.val)))
|
||||||
|
copy(data[4+4+4+1+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], crc32.ChecksumIEEE(data[4:]))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrBadSum = errors.New("bad checksum")
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [4 + 4 + 4 + 1]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[8:12]))
|
||||||
|
deleted := header[4+4+4]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := crc32.NewIEEE()
|
||||||
|
h.Write(header[4:])
|
||||||
|
h.Write(data)
|
||||||
|
if h.Sum32() != binary.LittleEndian.Uint32(header[0:4]) {
|
||||||
|
return ErrBadSum
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
package db0203
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVUpdateMode(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeInsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("xx"), ModeInsert)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("yy"), ModeUpdate)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("zz"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k2"), []byte("tt"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVRecovery(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
prepare := func() {
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate truncated log
|
||||||
|
fp, _ := os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ := fp.Stat()
|
||||||
|
fp.Truncate(st.Size() - 1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate bad checksum
|
||||||
|
fp, _ = os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ = fp.Stat()
|
||||||
|
fp.WriteAt([]byte{0}, st.Size()-1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{0xe9, 0xec, 0x4d, 0x9e, 2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{0x4c, 0xd0, 0xfe, 0xe5, 2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package db0203
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
FileName string
|
||||||
|
fp *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Open() (err error) {
|
||||||
|
log.fp, err = createFileSync(log.FileName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Close() error {
|
||||||
|
return log.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Write(ent *Entry) error {
|
||||||
|
if _, err := log.fp.Write(ent.Encode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return log.fp.Sync() // fsync
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Read(ent *Entry) (eof bool, err error) {
|
||||||
|
err = ent.Decode(log.fp)
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF || err == ErrBadSum {
|
||||||
|
return true, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !unix
|
||||||
|
|
||||||
|
package db0203
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
return os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package db0203
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// open or create a file and fsync the directory
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = syncDir(path.Base(file)); err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDir(file string) error {
|
||||||
|
flags := os.O_RDONLY | syscall.O_DIRECTORY
|
||||||
|
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
return syscall.Fsync(dirfd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
package db0203
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
Table string
|
||||||
|
Cols []Column
|
||||||
|
PKey []int // indexes of primary key columns
|
||||||
|
}
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
Name string
|
||||||
|
Type CellType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row []Cell
|
||||||
|
|
||||||
|
func (schema *Schema) NewRow() Row {
|
||||||
|
return make(Row, len(schema.Cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) EncodeKey(schema *Schema) (key []byte) {
|
||||||
|
key = append(key, []byte(schema.Table)...)
|
||||||
|
key = append(key, 0x00)
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
for idx, value := range row {
|
||||||
|
if slices.Contains(schema.PKey, idx) {
|
||||||
|
check(value.Type == schema.Cols[idx].Type)
|
||||||
|
key = row[idx].Encode(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) EncodeVal(schema *Schema) (val []byte) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
for idx, value := range row {
|
||||||
|
if !slices.Contains(schema.PKey, idx) {
|
||||||
|
check(value.Type == schema.Cols[idx].Type)
|
||||||
|
val = row[idx].Encode(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) DecodeKey(schema *Schema, key []byte) (err error) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
|
||||||
|
if len(key) < len(schema.Table)+1 {
|
||||||
|
return errors.New("bad key")
|
||||||
|
}
|
||||||
|
if string(key[:len(schema.Table)+1]) != schema.Table+"\x00" {
|
||||||
|
return errors.New("bad key")
|
||||||
|
}
|
||||||
|
key = key[len(schema.Table)+1:]
|
||||||
|
|
||||||
|
for idx, col := range schema.Cols {
|
||||||
|
if !slices.Contains(schema.PKey, idx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[idx] = Cell{Type: col.Type}
|
||||||
|
if key, err = row[idx].Decode(key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) != 0 {
|
||||||
|
return errors.New("trailing garbage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) DecodeVal(schema *Schema, val []byte) (err error) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
|
||||||
|
for idx, col := range schema.Cols {
|
||||||
|
if slices.Contains(schema.PKey, idx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[idx] = Cell{Type: col.Type}
|
||||||
|
if val, err = row[idx].Decode(val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(val) != 0 {
|
||||||
|
return errors.New("trailing garbage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package db0203
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRowEncode(t *testing.T) {
|
||||||
|
schema := &Schema{
|
||||||
|
Table: "link",
|
||||||
|
Cols: []Column{
|
||||||
|
{Name: "time", Type: TypeI64},
|
||||||
|
{Name: "src", Type: TypeStr},
|
||||||
|
{Name: "dst", Type: TypeStr},
|
||||||
|
},
|
||||||
|
PKey: []int{1, 2}, // (src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := Row{
|
||||||
|
Cell{Type: TypeI64, I64: 123},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
key := []byte{'l', 'i', 'n', 'k', 0, 1, 0, 0, 0, 'a', 1, 0, 0, 0, 'b'}
|
||||||
|
val := []byte{123, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
assert.Equal(t, key, row.EncodeKey(schema))
|
||||||
|
assert.Equal(t, val, row.EncodeVal(schema))
|
||||||
|
|
||||||
|
decoded := schema.NewRow()
|
||||||
|
err := decoded.DecodeKey(schema, key)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = decoded.DecodeVal(schema, val)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, row, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
func check(cond bool) {
|
||||||
|
if !cond {
|
||||||
|
panic("assertion failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CellType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeI64 CellType = 1
|
||||||
|
TypeStr CellType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cell struct {
|
||||||
|
Type CellType
|
||||||
|
I64 int64
|
||||||
|
Str []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Encode(toAppend []byte) []byte {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
return binary.LittleEndian.AppendUint64(toAppend, uint64(cell.I64))
|
||||||
|
case TypeStr:
|
||||||
|
toAppend = binary.LittleEndian.AppendUint32(toAppend, uint32(len(cell.Str)))
|
||||||
|
return append(toAppend, cell.Str...)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Decode(data []byte) (rest []byte, err error) {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
if len(data) < 8 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.I64 = int64(binary.LittleEndian.Uint64(data[0:8]))
|
||||||
|
return data[8:], nil
|
||||||
|
case TypeStr:
|
||||||
|
if len(data) < 4 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
size := int(binary.LittleEndian.Uint32(data[0:4]))
|
||||||
|
if len(data) < 4+size {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.Str = slices.Clone(data[4 : 4+size])
|
||||||
|
return data[4+size:], nil
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableCell(t *testing.T) {
|
||||||
|
cell := Cell{Type: TypeI64, I64: -2}
|
||||||
|
data := []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded := Cell{Type: TypeI64}
|
||||||
|
rest, err := decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
|
||||||
|
cell = Cell{Type: TypeStr, Str: []byte("asdf")}
|
||||||
|
data = []byte{4, 0, 0, 0, 'a', 's', 'd', 'f'}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded = Cell{Type: TypeStr}
|
||||||
|
rest, err = decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
ent := Entry{}
|
||||||
|
eof, err := kv.log.Read(&ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.deleted {
|
||||||
|
delete(kv.mem, string(ent.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(ent.key)] = ent.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeUpsert UpdateMode = 0 // insert or update
|
||||||
|
ModeInsert UpdateMode = 1 // insert new
|
||||||
|
ModeUpdate UpdateMode = 2 // update existing
|
||||||
|
)
|
||||||
|
|
||||||
|
func (kv *KV) SetEx(key []byte, val []byte, mode UpdateMode) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
switch mode {
|
||||||
|
case ModeUpsert:
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
case ModeInsert:
|
||||||
|
updated = !exist
|
||||||
|
case ModeUpdate:
|
||||||
|
updated = exist && !bytes.Equal(prev, val)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
if updated {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: val}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
return kv.SetEx(key, val, ModeUpsert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.key)))
|
||||||
|
copy(data[4+4+4+1:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[4+4+4] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[8:12], uint32(len(ent.val)))
|
||||||
|
copy(data[4+4+4+1+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], crc32.ChecksumIEEE(data[4:]))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrBadSum = errors.New("bad checksum")
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [4 + 4 + 4 + 1]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[8:12]))
|
||||||
|
deleted := header[4+4+4]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := crc32.NewIEEE()
|
||||||
|
h.Write(header[4:])
|
||||||
|
h.Write(data)
|
||||||
|
if h.Sum32() != binary.LittleEndian.Uint32(header[0:4]) {
|
||||||
|
return ErrBadSum
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVUpdateMode(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeInsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("xx"), ModeInsert)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("yy"), ModeUpdate)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("zz"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k2"), []byte("tt"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVRecovery(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
prepare := func() {
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate truncated log
|
||||||
|
fp, _ := os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ := fp.Stat()
|
||||||
|
fp.Truncate(st.Size() - 1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate bad checksum
|
||||||
|
fp, _ = os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ = fp.Stat()
|
||||||
|
fp.WriteAt([]byte{0}, st.Size()-1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{0xe9, 0xec, 0x4d, 0x9e, 2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{0x4c, 0xd0, 0xfe, 0xe5, 2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
FileName string
|
||||||
|
fp *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Open() (err error) {
|
||||||
|
log.fp, err = createFileSync(log.FileName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Close() error {
|
||||||
|
return log.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Write(ent *Entry) error {
|
||||||
|
if _, err := log.fp.Write(ent.Encode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return log.fp.Sync() // fsync
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Read(ent *Entry) (eof bool, err error) {
|
||||||
|
err = ent.Decode(log.fp)
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF || err == ErrBadSum {
|
||||||
|
return true, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !unix
|
||||||
|
|
||||||
|
package db0204
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
return os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// open or create a file and fsync the directory
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = syncDir(path.Base(file)); err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDir(file string) error {
|
||||||
|
flags := os.O_RDONLY | syscall.O_DIRECTORY
|
||||||
|
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
return syscall.Fsync(dirfd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
Table string
|
||||||
|
Cols []Column
|
||||||
|
PKey []int // indexes of primary key columns
|
||||||
|
}
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
Name string
|
||||||
|
Type CellType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row []Cell
|
||||||
|
|
||||||
|
func (schema *Schema) NewRow() Row {
|
||||||
|
return make(Row, len(schema.Cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) EncodeKey(schema *Schema) (key []byte) {
|
||||||
|
key = append(key, []byte(schema.Table)...)
|
||||||
|
key = append(key, 0x00)
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
for idx, value := range row {
|
||||||
|
if slices.Contains(schema.PKey, idx) {
|
||||||
|
check(value.Type == schema.Cols[idx].Type)
|
||||||
|
key = row[idx].Encode(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) EncodeVal(schema *Schema) (val []byte) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
for idx, value := range row {
|
||||||
|
if !slices.Contains(schema.PKey, idx) {
|
||||||
|
check(value.Type == schema.Cols[idx].Type)
|
||||||
|
val = row[idx].Encode(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) DecodeKey(schema *Schema, key []byte) (err error) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
|
||||||
|
if len(key) < len(schema.Table)+1 {
|
||||||
|
return errors.New("bad key")
|
||||||
|
}
|
||||||
|
if string(key[:len(schema.Table)+1]) != schema.Table+"\x00" {
|
||||||
|
return errors.New("bad key")
|
||||||
|
}
|
||||||
|
key = key[len(schema.Table)+1:]
|
||||||
|
|
||||||
|
for idx, col := range schema.Cols {
|
||||||
|
if !slices.Contains(schema.PKey, idx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[idx] = Cell{Type: col.Type}
|
||||||
|
if key, err = row[idx].Decode(key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) != 0 {
|
||||||
|
return errors.New("trailing garbage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) DecodeVal(schema *Schema, val []byte) (err error) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
|
||||||
|
for idx, col := range schema.Cols {
|
||||||
|
if slices.Contains(schema.PKey, idx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[idx] = Cell{Type: col.Type}
|
||||||
|
if val, err = row[idx].Decode(val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(val) != 0 {
|
||||||
|
return errors.New("trailing garbage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRowEncode(t *testing.T) {
|
||||||
|
schema := &Schema{
|
||||||
|
Table: "link",
|
||||||
|
Cols: []Column{
|
||||||
|
{Name: "time", Type: TypeI64},
|
||||||
|
{Name: "src", Type: TypeStr},
|
||||||
|
{Name: "dst", Type: TypeStr},
|
||||||
|
},
|
||||||
|
PKey: []int{1, 2}, // (src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := Row{
|
||||||
|
Cell{Type: TypeI64, I64: 123},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
key := []byte{'l', 'i', 'n', 'k', 0, 1, 0, 0, 0, 'a', 1, 0, 0, 0, 'b'}
|
||||||
|
val := []byte{123, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
assert.Equal(t, key, row.EncodeKey(schema))
|
||||||
|
assert.Equal(t, val, row.EncodeVal(schema))
|
||||||
|
|
||||||
|
decoded := schema.NewRow()
|
||||||
|
err := decoded.DecodeKey(schema, key)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = decoded.DecodeVal(schema, val)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, row, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
KV KV
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Open() error { return db.KV.Open() }
|
||||||
|
func (db *DB) Close() error { return db.KV.Close() }
|
||||||
|
|
||||||
|
func (db *DB) Select(schema *Schema, row Row) (ok bool, err error)
|
||||||
|
|
||||||
|
func (db *DB) Insert(schema *Schema, row Row) (updated bool, err error)
|
||||||
|
|
||||||
|
func (db *DB) Upsert(schema *Schema, row Row) (updated bool, err error)
|
||||||
|
|
||||||
|
func (db *DB) Update(schema *Schema, row Row) (updated bool, err error)
|
||||||
|
|
||||||
|
func (db *DB) Delete(schema *Schema, row Row) (deleted bool, err error)
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package db0204
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableByPKey(t *testing.T) {
|
||||||
|
db := DB{}
|
||||||
|
db.KV.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(db.KV.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(db.KV.log.FileName)
|
||||||
|
err := db.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
schema := &Schema{
|
||||||
|
Table: "link",
|
||||||
|
Cols: []Column{
|
||||||
|
{Name: "time", Type: TypeI64},
|
||||||
|
{Name: "src", Type: TypeStr},
|
||||||
|
{Name: "dst", Type: TypeStr},
|
||||||
|
},
|
||||||
|
PKey: []int{1, 2}, // (src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := Row{
|
||||||
|
Cell{Type: TypeI64, I64: 123},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
ok, err := db.Select(schema, row)
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err := db.Insert(schema, row)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
out := Row{
|
||||||
|
Cell{},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
ok, err = db.Select(schema, out)
|
||||||
|
assert.True(t, ok && err == nil)
|
||||||
|
assert.Equal(t, row, out)
|
||||||
|
|
||||||
|
row[0].I64 = 456
|
||||||
|
updated, err = db.Update(schema, row)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
ok, err = db.Select(schema, out)
|
||||||
|
assert.True(t, ok && err == nil)
|
||||||
|
assert.Equal(t, row, out)
|
||||||
|
|
||||||
|
deleted, err := db.Delete(schema, row)
|
||||||
|
assert.True(t, deleted && err == nil)
|
||||||
|
|
||||||
|
ok, err = db.Select(schema, row)
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
func check(cond bool) {
|
||||||
|
if !cond {
|
||||||
|
panic("assertion failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CellType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeI64 CellType = 1
|
||||||
|
TypeStr CellType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cell struct {
|
||||||
|
Type CellType
|
||||||
|
I64 int64
|
||||||
|
Str []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Encode(toAppend []byte) []byte {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
return binary.LittleEndian.AppendUint64(toAppend, uint64(cell.I64))
|
||||||
|
case TypeStr:
|
||||||
|
toAppend = binary.LittleEndian.AppendUint32(toAppend, uint32(len(cell.Str)))
|
||||||
|
return append(toAppend, cell.Str...)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Decode(data []byte) (rest []byte, err error) {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
if len(data) < 8 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.I64 = int64(binary.LittleEndian.Uint64(data[0:8]))
|
||||||
|
return data[8:], nil
|
||||||
|
case TypeStr:
|
||||||
|
if len(data) < 4 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
size := int(binary.LittleEndian.Uint32(data[0:4]))
|
||||||
|
if len(data) < 4+size {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.Str = slices.Clone(data[4 : 4+size])
|
||||||
|
return data[4+size:], nil
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableCell(t *testing.T) {
|
||||||
|
cell := Cell{Type: TypeI64, I64: -2}
|
||||||
|
data := []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded := Cell{Type: TypeI64}
|
||||||
|
rest, err := decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
|
||||||
|
cell = Cell{Type: TypeStr, Str: []byte("asdf")}
|
||||||
|
data = []byte{4, 0, 0, 0, 'a', 's', 'd', 'f'}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded = Cell{Type: TypeStr}
|
||||||
|
rest, err = decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
ent := Entry{}
|
||||||
|
eof, err := kv.log.Read(&ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.deleted {
|
||||||
|
delete(kv.mem, string(ent.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(ent.key)] = ent.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeUpsert UpdateMode = 0 // insert or update
|
||||||
|
ModeInsert UpdateMode = 1 // insert new
|
||||||
|
ModeUpdate UpdateMode = 2 // update existing
|
||||||
|
)
|
||||||
|
|
||||||
|
func (kv *KV) SetEx(key []byte, val []byte, mode UpdateMode) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
switch mode {
|
||||||
|
case ModeUpsert:
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
case ModeInsert:
|
||||||
|
updated = !exist
|
||||||
|
case ModeUpdate:
|
||||||
|
updated = exist && !bytes.Equal(prev, val)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
if updated {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: val}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
return kv.SetEx(key, val, ModeUpsert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.key)))
|
||||||
|
copy(data[4+4+4+1:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[4+4+4] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[8:12], uint32(len(ent.val)))
|
||||||
|
copy(data[4+4+4+1+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], crc32.ChecksumIEEE(data[4:]))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrBadSum = errors.New("bad checksum")
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [4 + 4 + 4 + 1]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[8:12]))
|
||||||
|
deleted := header[4+4+4]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := crc32.NewIEEE()
|
||||||
|
h.Write(header[4:])
|
||||||
|
h.Write(data)
|
||||||
|
if h.Sum32() != binary.LittleEndian.Uint32(header[0:4]) {
|
||||||
|
return ErrBadSum
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVUpdateMode(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeInsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("xx"), ModeInsert)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("yy"), ModeUpdate)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("zz"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k2"), []byte("tt"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVRecovery(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
prepare := func() {
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate truncated log
|
||||||
|
fp, _ := os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ := fp.Stat()
|
||||||
|
fp.Truncate(st.Size() - 1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate bad checksum
|
||||||
|
fp, _ = os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ = fp.Stat()
|
||||||
|
fp.WriteAt([]byte{0}, st.Size()-1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{0xe9, 0xec, 0x4d, 0x9e, 2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{0x4c, 0xd0, 0xfe, 0xe5, 2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
FileName string
|
||||||
|
fp *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Open() (err error) {
|
||||||
|
log.fp, err = createFileSync(log.FileName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Close() error {
|
||||||
|
return log.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Write(ent *Entry) error {
|
||||||
|
if _, err := log.fp.Write(ent.Encode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return log.fp.Sync() // fsync
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Read(ent *Entry) (eof bool, err error) {
|
||||||
|
err = ent.Decode(log.fp)
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF || err == ErrBadSum {
|
||||||
|
return true, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !unix
|
||||||
|
|
||||||
|
package db0301
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
return os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// open or create a file and fsync the directory
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = syncDir(path.Base(file)); err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDir(file string) error {
|
||||||
|
flags := os.O_RDONLY | syscall.O_DIRECTORY
|
||||||
|
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
return syscall.Fsync(dirfd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
Table string
|
||||||
|
Cols []Column
|
||||||
|
PKey []int // indexes of primary key columns
|
||||||
|
}
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
Name string
|
||||||
|
Type CellType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row []Cell
|
||||||
|
|
||||||
|
func (schema *Schema) NewRow() Row {
|
||||||
|
return make(Row, len(schema.Cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) EncodeKey(schema *Schema) (key []byte) {
|
||||||
|
key = append(key, []byte(schema.Table)...)
|
||||||
|
key = append(key, 0x00)
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
for idx, value := range row {
|
||||||
|
if slices.Contains(schema.PKey, idx) {
|
||||||
|
check(value.Type == schema.Cols[idx].Type)
|
||||||
|
key = row[idx].Encode(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) EncodeVal(schema *Schema) (val []byte) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
for idx, value := range row {
|
||||||
|
if !slices.Contains(schema.PKey, idx) {
|
||||||
|
check(value.Type == schema.Cols[idx].Type)
|
||||||
|
val = row[idx].Encode(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) DecodeKey(schema *Schema, key []byte) (err error) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
|
||||||
|
if len(key) < len(schema.Table)+1 {
|
||||||
|
return errors.New("bad key")
|
||||||
|
}
|
||||||
|
if string(key[:len(schema.Table)+1]) != schema.Table+"\x00" {
|
||||||
|
return errors.New("bad key")
|
||||||
|
}
|
||||||
|
key = key[len(schema.Table)+1:]
|
||||||
|
|
||||||
|
for idx, col := range schema.Cols {
|
||||||
|
if !slices.Contains(schema.PKey, idx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[idx] = Cell{Type: col.Type}
|
||||||
|
if key, err = row[idx].Decode(key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) != 0 {
|
||||||
|
return errors.New("trailing garbage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) DecodeVal(schema *Schema, val []byte) (err error) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
|
||||||
|
for idx, col := range schema.Cols {
|
||||||
|
if slices.Contains(schema.PKey, idx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[idx] = Cell{Type: col.Type}
|
||||||
|
if val, err = row[idx].Decode(val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(val) != 0 {
|
||||||
|
return errors.New("trailing garbage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRowEncode(t *testing.T) {
|
||||||
|
schema := &Schema{
|
||||||
|
Table: "link",
|
||||||
|
Cols: []Column{
|
||||||
|
{Name: "time", Type: TypeI64},
|
||||||
|
{Name: "src", Type: TypeStr},
|
||||||
|
{Name: "dst", Type: TypeStr},
|
||||||
|
},
|
||||||
|
PKey: []int{1, 2}, // (src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := Row{
|
||||||
|
Cell{Type: TypeI64, I64: 123},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
key := []byte{'l', 'i', 'n', 'k', 0, 1, 0, 0, 0, 'a', 1, 0, 0, 0, 'b'}
|
||||||
|
val := []byte{123, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
assert.Equal(t, key, row.EncodeKey(schema))
|
||||||
|
assert.Equal(t, val, row.EncodeVal(schema))
|
||||||
|
|
||||||
|
decoded := schema.NewRow()
|
||||||
|
err := decoded.DecodeKey(schema, key)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = decoded.DecodeVal(schema, val)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, row, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
buf string
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParser(s string) Parser {
|
||||||
|
return Parser{buf: s, pos: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpace(ch byte) bool {
|
||||||
|
switch ch {
|
||||||
|
case '\t', '\n', '\v', '\f', '\r', ' ':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func isAlpha(ch byte) bool {
|
||||||
|
return 'a' <= (ch|32) && (ch|32) <= 'z'
|
||||||
|
}
|
||||||
|
func isDigit(ch byte) bool {
|
||||||
|
return '0' <= ch && ch <= '9'
|
||||||
|
}
|
||||||
|
func isNameStart(ch byte) bool {
|
||||||
|
return isAlpha(ch) || ch == '_'
|
||||||
|
}
|
||||||
|
func isNameContinue(ch byte) bool {
|
||||||
|
return isAlpha(ch) || isDigit(ch) || ch == '_'
|
||||||
|
}
|
||||||
|
func isSeparator(ch byte) bool {
|
||||||
|
return ch < 128 && !isNameContinue(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) skipSpaces() {
|
||||||
|
for p.pos < len(p.buf) && isSpace(p.buf[p.pos]) {
|
||||||
|
p.pos += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) tryKeyword(kw string) bool
|
||||||
|
|
||||||
|
func (p *Parser) tryName() (string, bool)
|
||||||
|
|
||||||
|
func (p *Parser) isEnd() bool {
|
||||||
|
p.skipSpaces()
|
||||||
|
return p.pos >= len(p.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseName(t *testing.T) {
|
||||||
|
p := NewParser(" a b0 _0_ 123 ")
|
||||||
|
name, ok := p.tryName()
|
||||||
|
assert.True(t, ok && name == "a")
|
||||||
|
name, ok = p.tryName()
|
||||||
|
assert.True(t, ok && name == "b0")
|
||||||
|
name, ok = p.tryName()
|
||||||
|
assert.True(t, ok && name == "_0_")
|
||||||
|
_, ok = p.tryName()
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseKeyword(t *testing.T) {
|
||||||
|
p := NewParser(" select HELLO ")
|
||||||
|
assert.False(t, p.tryKeyword("sel"))
|
||||||
|
assert.True(t, p.tryKeyword("SELECT"))
|
||||||
|
assert.True(t, p.tryKeyword("hello") && p.isEnd())
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
KV KV
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Open() error { return db.KV.Open() }
|
||||||
|
func (db *DB) Close() error { return db.KV.Close() }
|
||||||
|
|
||||||
|
func (db *DB) Select(schema *Schema, row Row) (ok bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
val, ok, err := db.KV.Get(key)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return ok, err
|
||||||
|
}
|
||||||
|
if err = row.DecodeVal(schema, val); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Insert(schema *Schema, row Row) (updated bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
val := row.EncodeVal(schema)
|
||||||
|
return db.KV.SetEx(key, val, ModeInsert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Upsert(schema *Schema, row Row) (updated bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
val := row.EncodeVal(schema)
|
||||||
|
return db.KV.SetEx(key, val, ModeUpsert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Update(schema *Schema, row Row) (updated bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
val := row.EncodeVal(schema)
|
||||||
|
return db.KV.SetEx(key, val, ModeUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Delete(schema *Schema, row Row) (deleted bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
return db.KV.Del(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package db0301
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableByPKey(t *testing.T) {
|
||||||
|
db := DB{}
|
||||||
|
db.KV.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(db.KV.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(db.KV.log.FileName)
|
||||||
|
err := db.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
schema := &Schema{
|
||||||
|
Table: "link",
|
||||||
|
Cols: []Column{
|
||||||
|
{Name: "time", Type: TypeI64},
|
||||||
|
{Name: "src", Type: TypeStr},
|
||||||
|
{Name: "dst", Type: TypeStr},
|
||||||
|
},
|
||||||
|
PKey: []int{1, 2}, // (src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := Row{
|
||||||
|
Cell{Type: TypeI64, I64: 123},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
ok, err := db.Select(schema, row)
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err := db.Insert(schema, row)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
out := Row{
|
||||||
|
Cell{},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
ok, err = db.Select(schema, out)
|
||||||
|
assert.True(t, ok && err == nil)
|
||||||
|
assert.Equal(t, row, out)
|
||||||
|
|
||||||
|
row[0].I64 = 456
|
||||||
|
updated, err = db.Update(schema, row)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
ok, err = db.Select(schema, out)
|
||||||
|
assert.True(t, ok && err == nil)
|
||||||
|
assert.Equal(t, row, out)
|
||||||
|
|
||||||
|
deleted, err := db.Delete(schema, row)
|
||||||
|
assert.True(t, deleted && err == nil)
|
||||||
|
|
||||||
|
ok, err = db.Select(schema, row)
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
func check(cond bool) {
|
||||||
|
if !cond {
|
||||||
|
panic("assertion failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CellType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeI64 CellType = 1
|
||||||
|
TypeStr CellType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cell struct {
|
||||||
|
Type CellType
|
||||||
|
I64 int64
|
||||||
|
Str []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Encode(toAppend []byte) []byte {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
return binary.LittleEndian.AppendUint64(toAppend, uint64(cell.I64))
|
||||||
|
case TypeStr:
|
||||||
|
toAppend = binary.LittleEndian.AppendUint32(toAppend, uint32(len(cell.Str)))
|
||||||
|
return append(toAppend, cell.Str...)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Decode(data []byte) (rest []byte, err error) {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
if len(data) < 8 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.I64 = int64(binary.LittleEndian.Uint64(data[0:8]))
|
||||||
|
return data[8:], nil
|
||||||
|
case TypeStr:
|
||||||
|
if len(data) < 4 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
size := int(binary.LittleEndian.Uint32(data[0:4]))
|
||||||
|
if len(data) < 4+size {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.Str = slices.Clone(data[4 : 4+size])
|
||||||
|
return data[4+size:], nil
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableCell(t *testing.T) {
|
||||||
|
cell := Cell{Type: TypeI64, I64: -2}
|
||||||
|
data := []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded := Cell{Type: TypeI64}
|
||||||
|
rest, err := decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
|
||||||
|
cell = Cell{Type: TypeStr, Str: []byte("asdf")}
|
||||||
|
data = []byte{4, 0, 0, 0, 'a', 's', 'd', 'f'}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded = Cell{Type: TypeStr}
|
||||||
|
rest, err = decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
ent := Entry{}
|
||||||
|
eof, err := kv.log.Read(&ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.deleted {
|
||||||
|
delete(kv.mem, string(ent.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(ent.key)] = ent.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeUpsert UpdateMode = 0 // insert or update
|
||||||
|
ModeInsert UpdateMode = 1 // insert new
|
||||||
|
ModeUpdate UpdateMode = 2 // update existing
|
||||||
|
)
|
||||||
|
|
||||||
|
func (kv *KV) SetEx(key []byte, val []byte, mode UpdateMode) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
switch mode {
|
||||||
|
case ModeUpsert:
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
case ModeInsert:
|
||||||
|
updated = !exist
|
||||||
|
case ModeUpdate:
|
||||||
|
updated = exist && !bytes.Equal(prev, val)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
if updated {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: val}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
return kv.SetEx(key, val, ModeUpsert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.key)))
|
||||||
|
copy(data[4+4+4+1:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[4+4+4] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[8:12], uint32(len(ent.val)))
|
||||||
|
copy(data[4+4+4+1+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], crc32.ChecksumIEEE(data[4:]))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrBadSum = errors.New("bad checksum")
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [4 + 4 + 4 + 1]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[8:12]))
|
||||||
|
deleted := header[4+4+4]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := crc32.NewIEEE()
|
||||||
|
h.Write(header[4:])
|
||||||
|
h.Write(data)
|
||||||
|
if h.Sum32() != binary.LittleEndian.Uint32(header[0:4]) {
|
||||||
|
return ErrBadSum
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVUpdateMode(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeInsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("xx"), ModeInsert)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("yy"), ModeUpdate)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("zz"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k2"), []byte("tt"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVRecovery(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
prepare := func() {
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate truncated log
|
||||||
|
fp, _ := os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ := fp.Stat()
|
||||||
|
fp.Truncate(st.Size() - 1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate bad checksum
|
||||||
|
fp, _ = os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ = fp.Stat()
|
||||||
|
fp.WriteAt([]byte{0}, st.Size()-1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{0xe9, 0xec, 0x4d, 0x9e, 2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{0x4c, 0xd0, 0xfe, 0xe5, 2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+40
@@ -0,0 +1,40 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
FileName string
|
||||||
|
fp *os.File
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Open() (err error) {
|
||||||
|
log.fp, err = createFileSync(log.FileName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Close() error {
|
||||||
|
return log.fp.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Write(ent *Entry) error {
|
||||||
|
if _, err := log.fp.Write(ent.Encode()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return log.fp.Sync() // fsync
|
||||||
|
}
|
||||||
|
|
||||||
|
func (log *Log) Read(ent *Entry) (eof bool, err error) {
|
||||||
|
err = ent.Decode(log.fp)
|
||||||
|
if err == io.EOF || err == io.ErrUnexpectedEOF || err == ErrBadSum {
|
||||||
|
return true, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return false, err
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build !unix
|
||||||
|
|
||||||
|
package db0302
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
return os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build unix
|
||||||
|
|
||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// open or create a file and fsync the directory
|
||||||
|
func createFileSync(file string) (*os.File, error) {
|
||||||
|
fp, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = syncDir(path.Base(file)); err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDir(file string) error {
|
||||||
|
flags := os.O_RDONLY | syscall.O_DIRECTORY
|
||||||
|
dirfd, err := syscall.Open(path.Dir(file), flags, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer syscall.Close(dirfd)
|
||||||
|
return syscall.Fsync(dirfd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
Table string
|
||||||
|
Cols []Column
|
||||||
|
PKey []int // indexes of primary key columns
|
||||||
|
}
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
Name string
|
||||||
|
Type CellType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row []Cell
|
||||||
|
|
||||||
|
func (schema *Schema) NewRow() Row {
|
||||||
|
return make(Row, len(schema.Cols))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) EncodeKey(schema *Schema) (key []byte) {
|
||||||
|
key = append(key, []byte(schema.Table)...)
|
||||||
|
key = append(key, 0x00)
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
for idx, value := range row {
|
||||||
|
if slices.Contains(schema.PKey, idx) {
|
||||||
|
check(value.Type == schema.Cols[idx].Type)
|
||||||
|
key = row[idx].Encode(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) EncodeVal(schema *Schema) (val []byte) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
for idx, value := range row {
|
||||||
|
if !slices.Contains(schema.PKey, idx) {
|
||||||
|
check(value.Type == schema.Cols[idx].Type)
|
||||||
|
val = row[idx].Encode(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) DecodeKey(schema *Schema, key []byte) (err error) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
|
||||||
|
if len(key) < len(schema.Table)+1 {
|
||||||
|
return errors.New("bad key")
|
||||||
|
}
|
||||||
|
if string(key[:len(schema.Table)+1]) != schema.Table+"\x00" {
|
||||||
|
return errors.New("bad key")
|
||||||
|
}
|
||||||
|
key = key[len(schema.Table)+1:]
|
||||||
|
|
||||||
|
for idx, col := range schema.Cols {
|
||||||
|
if !slices.Contains(schema.PKey, idx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[idx] = Cell{Type: col.Type}
|
||||||
|
if key, err = row[idx].Decode(key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(key) != 0 {
|
||||||
|
return errors.New("trailing garbage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (row Row) DecodeVal(schema *Schema, val []byte) (err error) {
|
||||||
|
check(len(row) == len(schema.Cols))
|
||||||
|
|
||||||
|
for idx, col := range schema.Cols {
|
||||||
|
if slices.Contains(schema.PKey, idx) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
row[idx] = Cell{Type: col.Type}
|
||||||
|
if val, err = row[idx].Decode(val); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(val) != 0 {
|
||||||
|
return errors.New("trailing garbage")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRowEncode(t *testing.T) {
|
||||||
|
schema := &Schema{
|
||||||
|
Table: "link",
|
||||||
|
Cols: []Column{
|
||||||
|
{Name: "time", Type: TypeI64},
|
||||||
|
{Name: "src", Type: TypeStr},
|
||||||
|
{Name: "dst", Type: TypeStr},
|
||||||
|
},
|
||||||
|
PKey: []int{1, 2}, // (src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := Row{
|
||||||
|
Cell{Type: TypeI64, I64: 123},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
key := []byte{'l', 'i', 'n', 'k', 0, 1, 0, 0, 0, 'a', 1, 0, 0, 0, 'b'}
|
||||||
|
val := []byte{123, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
assert.Equal(t, key, row.EncodeKey(schema))
|
||||||
|
assert.Equal(t, val, row.EncodeVal(schema))
|
||||||
|
|
||||||
|
decoded := schema.NewRow()
|
||||||
|
err := decoded.DecodeKey(schema, key)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = decoded.DecodeVal(schema, val)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, row, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Parser struct {
|
||||||
|
buf string
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewParser(s string) Parser {
|
||||||
|
return Parser{buf: s, pos: 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSpace(ch byte) bool {
|
||||||
|
switch ch {
|
||||||
|
case '\t', '\n', '\v', '\f', '\r', ' ':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
func isAlpha(ch byte) bool {
|
||||||
|
return 'a' <= (ch|32) && (ch|32) <= 'z'
|
||||||
|
}
|
||||||
|
func isDigit(ch byte) bool {
|
||||||
|
return '0' <= ch && ch <= '9'
|
||||||
|
}
|
||||||
|
func isNameStart(ch byte) bool {
|
||||||
|
return isAlpha(ch) || ch == '_'
|
||||||
|
}
|
||||||
|
func isNameContinue(ch byte) bool {
|
||||||
|
return isAlpha(ch) || isDigit(ch) || ch == '_'
|
||||||
|
}
|
||||||
|
func isSeparator(ch byte) bool {
|
||||||
|
return ch < 128 && !isNameContinue(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) skipSpaces() {
|
||||||
|
for p.pos < len(p.buf) && isSpace(p.buf[p.pos]) {
|
||||||
|
p.pos += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) tryKeyword(kw string) bool {
|
||||||
|
p.skipSpaces()
|
||||||
|
if !(p.pos+len(kw) <= len(p.buf) && strings.EqualFold(p.buf[p.pos:p.pos+len(kw)], kw)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if p.pos+len(kw) < len(p.buf) && !isSeparator(p.buf[p.pos+len(kw)]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
p.pos += len(kw)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) tryName() (string, bool) {
|
||||||
|
p.skipSpaces()
|
||||||
|
start, cur := p.pos, p.pos
|
||||||
|
if !(cur < len(p.buf) && isNameStart(p.buf[cur])) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
cur++
|
||||||
|
for cur < len(p.buf) && isNameContinue(p.buf[cur]) {
|
||||||
|
cur++
|
||||||
|
}
|
||||||
|
p.pos = cur
|
||||||
|
return p.buf[start:cur], true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseValue(out *Cell) error {
|
||||||
|
p.skipSpaces()
|
||||||
|
if p.pos >= len(p.buf) {
|
||||||
|
return errors.New("expect value")
|
||||||
|
}
|
||||||
|
ch := p.buf[p.pos]
|
||||||
|
if ch == '"' || ch == '\'' {
|
||||||
|
return p.parseString(out)
|
||||||
|
} else if isDigit(ch) || ch == '-' || ch == '+' {
|
||||||
|
return p.parseInt(out)
|
||||||
|
} else {
|
||||||
|
return errors.New("expect value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Parser) parseString(out *Cell) error
|
||||||
|
|
||||||
|
func (p *Parser) parseInt(out *Cell) (err error)
|
||||||
|
|
||||||
|
func (p *Parser) isEnd() bool {
|
||||||
|
p.skipSpaces()
|
||||||
|
return p.pos >= len(p.buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseName(t *testing.T) {
|
||||||
|
p := NewParser(" a b0 _0_ 123 ")
|
||||||
|
name, ok := p.tryName()
|
||||||
|
assert.True(t, ok && name == "a")
|
||||||
|
name, ok = p.tryName()
|
||||||
|
assert.True(t, ok && name == "b0")
|
||||||
|
name, ok = p.tryName()
|
||||||
|
assert.True(t, ok && name == "_0_")
|
||||||
|
_, ok = p.tryName()
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseKeyword(t *testing.T) {
|
||||||
|
p := NewParser(" select HELLO ")
|
||||||
|
assert.False(t, p.tryKeyword("sel"))
|
||||||
|
assert.True(t, p.tryKeyword("SELECT"))
|
||||||
|
assert.True(t, p.tryKeyword("hello") && p.isEnd())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testParseValue(t *testing.T, s string, ref Cell) {
|
||||||
|
p := NewParser(s)
|
||||||
|
out := Cell{}
|
||||||
|
err := p.parseValue(&out)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.True(t, p.isEnd())
|
||||||
|
assert.Equal(t, ref, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseValue(t *testing.T) {
|
||||||
|
testParseValue(t, " -123 ", Cell{Type: TypeI64, I64: -123})
|
||||||
|
testParseValue(t, ` 'abc\'\"d' `, Cell{Type: TypeStr, Str: []byte("abc'\"d")})
|
||||||
|
testParseValue(t, ` "abc\'\"d" `, Cell{Type: TypeStr, Str: []byte("abc'\"d")})
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
type DB struct {
|
||||||
|
KV KV
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Open() error { return db.KV.Open() }
|
||||||
|
func (db *DB) Close() error { return db.KV.Close() }
|
||||||
|
|
||||||
|
func (db *DB) Select(schema *Schema, row Row) (ok bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
val, ok, err := db.KV.Get(key)
|
||||||
|
if err != nil || !ok {
|
||||||
|
return ok, err
|
||||||
|
}
|
||||||
|
if err = row.DecodeVal(schema, val); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Insert(schema *Schema, row Row) (updated bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
val := row.EncodeVal(schema)
|
||||||
|
return db.KV.SetEx(key, val, ModeInsert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Upsert(schema *Schema, row Row) (updated bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
val := row.EncodeVal(schema)
|
||||||
|
return db.KV.SetEx(key, val, ModeUpsert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Update(schema *Schema, row Row) (updated bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
val := row.EncodeVal(schema)
|
||||||
|
return db.KV.SetEx(key, val, ModeUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DB) Delete(schema *Schema, row Row) (deleted bool, err error) {
|
||||||
|
key := row.EncodeKey(schema)
|
||||||
|
return db.KV.Del(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package db0302
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableByPKey(t *testing.T) {
|
||||||
|
db := DB{}
|
||||||
|
db.KV.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(db.KV.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(db.KV.log.FileName)
|
||||||
|
err := db.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
schema := &Schema{
|
||||||
|
Table: "link",
|
||||||
|
Cols: []Column{
|
||||||
|
{Name: "time", Type: TypeI64},
|
||||||
|
{Name: "src", Type: TypeStr},
|
||||||
|
{Name: "dst", Type: TypeStr},
|
||||||
|
},
|
||||||
|
PKey: []int{1, 2}, // (src, dst)
|
||||||
|
}
|
||||||
|
|
||||||
|
row := Row{
|
||||||
|
Cell{Type: TypeI64, I64: 123},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
ok, err := db.Select(schema, row)
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err := db.Insert(schema, row)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
out := Row{
|
||||||
|
Cell{},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("a")},
|
||||||
|
Cell{Type: TypeStr, Str: []byte("b")},
|
||||||
|
}
|
||||||
|
ok, err = db.Select(schema, out)
|
||||||
|
assert.True(t, ok && err == nil)
|
||||||
|
assert.Equal(t, row, out)
|
||||||
|
|
||||||
|
row[0].I64 = 456
|
||||||
|
updated, err = db.Update(schema, row)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
ok, err = db.Select(schema, out)
|
||||||
|
assert.True(t, ok && err == nil)
|
||||||
|
assert.Equal(t, row, out)
|
||||||
|
|
||||||
|
deleted, err := db.Delete(schema, row)
|
||||||
|
assert.True(t, deleted && err == nil)
|
||||||
|
|
||||||
|
ok, err = db.Select(schema, row)
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package db0303
|
||||||
|
|
||||||
|
func check(cond bool) {
|
||||||
|
if !cond {
|
||||||
|
panic("assertion failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package db0303
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CellType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
TypeI64 CellType = 1
|
||||||
|
TypeStr CellType = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cell struct {
|
||||||
|
Type CellType
|
||||||
|
I64 int64
|
||||||
|
Str []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Encode(toAppend []byte) []byte {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
return binary.LittleEndian.AppendUint64(toAppend, uint64(cell.I64))
|
||||||
|
case TypeStr:
|
||||||
|
toAppend = binary.LittleEndian.AppendUint32(toAppend, uint32(len(cell.Str)))
|
||||||
|
return append(toAppend, cell.Str...)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cell *Cell) Decode(data []byte) (rest []byte, err error) {
|
||||||
|
switch cell.Type {
|
||||||
|
case TypeI64:
|
||||||
|
if len(data) < 8 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.I64 = int64(binary.LittleEndian.Uint64(data[0:8]))
|
||||||
|
return data[8:], nil
|
||||||
|
case TypeStr:
|
||||||
|
if len(data) < 4 {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
size := int(binary.LittleEndian.Uint32(data[0:4]))
|
||||||
|
if len(data) < 4+size {
|
||||||
|
return data, errors.New("expect more data")
|
||||||
|
}
|
||||||
|
cell.Str = slices.Clone(data[4 : 4+size])
|
||||||
|
return data[4+size:], nil
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package db0303
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTableCell(t *testing.T) {
|
||||||
|
cell := Cell{Type: TypeI64, I64: -2}
|
||||||
|
data := []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded := Cell{Type: TypeI64}
|
||||||
|
rest, err := decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
|
||||||
|
cell = Cell{Type: TypeStr, Str: []byte("asdf")}
|
||||||
|
data = []byte{4, 0, 0, 0, 'a', 's', 'd', 'f'}
|
||||||
|
assert.Equal(t, data, cell.Encode(nil))
|
||||||
|
decoded = Cell{Type: TypeStr}
|
||||||
|
rest, err = decoded.Decode(data)
|
||||||
|
assert.True(t, len(rest) == 0 && err == nil)
|
||||||
|
assert.Equal(t, cell, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
package db0303
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KV struct {
|
||||||
|
log Log
|
||||||
|
mem map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Open() error {
|
||||||
|
if err := kv.log.Open(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
kv.mem = map[string][]byte{}
|
||||||
|
for {
|
||||||
|
ent := Entry{}
|
||||||
|
eof, err := kv.log.Read(&ent)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if eof {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if ent.deleted {
|
||||||
|
delete(kv.mem, string(ent.key))
|
||||||
|
} else {
|
||||||
|
kv.mem[string(ent.key)] = ent.val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Close() error { return kv.log.Close() }
|
||||||
|
|
||||||
|
func (kv *KV) Get(key []byte) (val []byte, ok bool, err error) {
|
||||||
|
val, ok = kv.mem[string(key)]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ModeUpsert UpdateMode = 0 // insert or update
|
||||||
|
ModeInsert UpdateMode = 1 // insert new
|
||||||
|
ModeUpdate UpdateMode = 2 // update existing
|
||||||
|
)
|
||||||
|
|
||||||
|
func (kv *KV) SetEx(key []byte, val []byte, mode UpdateMode) (updated bool, err error) {
|
||||||
|
prev, exist := kv.mem[string(key)]
|
||||||
|
switch mode {
|
||||||
|
case ModeUpsert:
|
||||||
|
updated = !exist || !bytes.Equal(prev, val)
|
||||||
|
case ModeInsert:
|
||||||
|
updated = !exist
|
||||||
|
case ModeUpdate:
|
||||||
|
updated = exist && !bytes.Equal(prev, val)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
if updated {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, val: val}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
kv.mem[string(key)] = val
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Set(key []byte, val []byte) (updated bool, err error) {
|
||||||
|
return kv.SetEx(key, val, ModeUpsert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (kv *KV) Del(key []byte) (deleted bool, err error) {
|
||||||
|
_, deleted = kv.mem[string(key)]
|
||||||
|
if deleted {
|
||||||
|
if err = kv.log.Write(&Entry{key: key, deleted: true}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
delete(kv.mem, string(key))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package db0303
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"hash/crc32"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
key []byte
|
||||||
|
val []byte
|
||||||
|
deleted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *Entry) Encode() []byte {
|
||||||
|
data := make([]byte, 4+4+4+1+len(ent.key)+len(ent.val))
|
||||||
|
binary.LittleEndian.PutUint32(data[4:8], uint32(len(ent.key)))
|
||||||
|
copy(data[4+4+4+1:], ent.key)
|
||||||
|
if ent.deleted {
|
||||||
|
data[4+4+4] = 1
|
||||||
|
} else {
|
||||||
|
binary.LittleEndian.PutUint32(data[8:12], uint32(len(ent.val)))
|
||||||
|
copy(data[4+4+4+1+len(ent.key):], ent.val)
|
||||||
|
}
|
||||||
|
binary.LittleEndian.PutUint32(data[0:4], crc32.ChecksumIEEE(data[4:]))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrBadSum = errors.New("bad checksum")
|
||||||
|
|
||||||
|
func (ent *Entry) Decode(r io.Reader) error {
|
||||||
|
var header [4 + 4 + 4 + 1]byte
|
||||||
|
if _, err := io.ReadFull(r, header[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
klen := int(binary.LittleEndian.Uint32(header[4:8]))
|
||||||
|
vlen := int(binary.LittleEndian.Uint32(header[8:12]))
|
||||||
|
deleted := header[4+4+4]
|
||||||
|
|
||||||
|
data := make([]byte, klen+vlen)
|
||||||
|
if _, err := io.ReadFull(r, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := crc32.NewIEEE()
|
||||||
|
h.Write(header[4:])
|
||||||
|
h.Write(data)
|
||||||
|
if h.Sum32() != binary.LittleEndian.Uint32(header[0:4]) {
|
||||||
|
return ErrBadSum
|
||||||
|
}
|
||||||
|
|
||||||
|
ent.key = data[:klen]
|
||||||
|
if deleted != 0 {
|
||||||
|
ent.deleted = true
|
||||||
|
} else {
|
||||||
|
ent.deleted = false
|
||||||
|
ent.val = data[klen:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
+158
@@ -0,0 +1,158 @@
|
|||||||
|
package db0303
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKVBasic(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("xxx"))
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Del([]byte("k1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("xxx"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
// reopen
|
||||||
|
kv.Close()
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
_, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
val, ok, err = kv.Get([]byte("k2"))
|
||||||
|
assert.True(t, string(val) == "v2" && ok && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVUpdateMode(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeUpdate)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("v1"), ModeInsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("xx"), ModeInsert)
|
||||||
|
assert.True(t, !updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("yy"), ModeUpdate)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k1"), []byte("zz"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
|
||||||
|
updated, err = kv.SetEx([]byte("k2"), []byte("tt"), ModeUpsert)
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestKVRecovery(t *testing.T) {
|
||||||
|
kv := KV{}
|
||||||
|
kv.log.FileName = ".test_db"
|
||||||
|
defer os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
prepare := func() {
|
||||||
|
os.Remove(kv.log.FileName)
|
||||||
|
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer kv.Close()
|
||||||
|
|
||||||
|
updated, err := kv.Set([]byte("k1"), []byte("v1"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
updated, err = kv.Set([]byte("k2"), []byte("v2"))
|
||||||
|
assert.True(t, updated && err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate truncated log
|
||||||
|
fp, _ := os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ := fp.Stat()
|
||||||
|
fp.Truncate(st.Size() - 1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err := kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err := kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
|
||||||
|
prepare()
|
||||||
|
// simulate bad checksum
|
||||||
|
fp, _ = os.OpenFile(kv.log.FileName, os.O_RDWR, 0o644)
|
||||||
|
st, _ = fp.Stat()
|
||||||
|
fp.WriteAt([]byte{0}, st.Size()-1)
|
||||||
|
fp.Close()
|
||||||
|
// reopen
|
||||||
|
err = kv.Open()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// test
|
||||||
|
val, ok, err = kv.Get([]byte("k1"))
|
||||||
|
assert.True(t, string(val) == "v1" && ok && err == nil)
|
||||||
|
_, ok, err = kv.Get([]byte("k2")) // bad
|
||||||
|
assert.True(t, !ok && err == nil)
|
||||||
|
kv.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntryEncode(t *testing.T) {
|
||||||
|
ent := Entry{key: []byte("k1"), val: []byte("xxx")}
|
||||||
|
data := []byte{0xe9, 0xec, 0x4d, 0x9e, 2, 0, 0, 0, 3, 0, 0, 0, 0, 'k', '1', 'x', 'x', 'x'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded := Entry{}
|
||||||
|
err := decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
|
||||||
|
ent = Entry{key: []byte("k1"), deleted: true}
|
||||||
|
data = []byte{0x4c, 0xd0, 0xfe, 0xe5, 2, 0, 0, 0, 0, 0, 0, 0, 1, 'k', '1'}
|
||||||
|
|
||||||
|
assert.Equal(t, data, ent.Encode())
|
||||||
|
|
||||||
|
decoded = Entry{}
|
||||||
|
err = decoded.Decode(bytes.NewBuffer(data))
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, ent, decoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// QzBQWVJJOUhU https://trialofcode.org/
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user