Adding the challenge 0304
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package db0304
|
||||
|
||||
func check(cond bool) {
|
||||
if !cond {
|
||||
panic("assertion failure")
|
||||
}
|
||||
}
|
||||
|
||||
// QzBQWVJJOUhU https://trialofcode.org/
|
||||
@@ -0,0 +1,57 @@
|
||||
package db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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,273 @@
|
||||
package db0304
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Parser struct {
|
||||
buf string
|
||||
pos int
|
||||
}
|
||||
|
||||
func NewParser(s string) Parser {
|
||||
return Parser{buf: s, pos: 0}
|
||||
}
|
||||
|
||||
type StmtSelect struct {
|
||||
table string
|
||||
cols []string
|
||||
keys []NamedCell
|
||||
}
|
||||
|
||||
type NamedCell struct {
|
||||
column string
|
||||
value Cell
|
||||
}
|
||||
|
||||
type StmtCreatTable struct {
|
||||
table string
|
||||
cols []Column
|
||||
pkey []string
|
||||
}
|
||||
|
||||
type StmtInsert struct {
|
||||
table string
|
||||
value []Cell
|
||||
}
|
||||
|
||||
type StmtUpdate struct {
|
||||
table string
|
||||
keys []NamedCell
|
||||
value []NamedCell
|
||||
}
|
||||
|
||||
type StmtDelete struct {
|
||||
table string
|
||||
keys []NamedCell
|
||||
}
|
||||
|
||||
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(kws ...string) bool {
|
||||
save := p.pos
|
||||
for _, kw := range kws {
|
||||
p.skipSpaces()
|
||||
if !(p.pos+len(kw) <= len(p.buf) && strings.EqualFold(p.buf[p.pos:p.pos+len(kw)], kw)) {
|
||||
p.pos = save
|
||||
return false
|
||||
}
|
||||
if p.pos+len(kw) < len(p.buf) && !isSeparator(p.buf[p.pos+len(kw)]) {
|
||||
p.pos = save
|
||||
return false
|
||||
}
|
||||
p.pos += len(kw)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Parser) tryPunctuation(tok string) bool {
|
||||
p.skipSpaces()
|
||||
if !(p.pos+len(tok) <= len(p.buf) && p.buf[p.pos:p.pos+len(tok)] == tok) {
|
||||
return false
|
||||
}
|
||||
p.pos += len(tok)
|
||||
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 {
|
||||
quote := p.buf[p.pos]
|
||||
cur := p.pos + 1
|
||||
for cur < len(p.buf) {
|
||||
ch := p.buf[cur]
|
||||
if ch == '\\' {
|
||||
cur++
|
||||
if cur < len(p.buf) && (p.buf[cur] == '"' || p.buf[cur] == '\'') {
|
||||
out.Str = append(out.Str, p.buf[cur])
|
||||
cur++
|
||||
} else {
|
||||
return errors.New("bad escape")
|
||||
}
|
||||
} else if ch == quote {
|
||||
out.Type = TypeStr
|
||||
p.pos = cur + 1
|
||||
return nil
|
||||
} else {
|
||||
out.Str = append(out.Str, p.buf[cur])
|
||||
cur++
|
||||
}
|
||||
}
|
||||
return errors.New("string is not terminated")
|
||||
}
|
||||
|
||||
func (p *Parser) parseInt(out *Cell) (err error) {
|
||||
start, cur := p.pos, p.pos
|
||||
if p.buf[cur] == '-' || p.buf[cur] == '+' {
|
||||
cur++
|
||||
}
|
||||
for cur < len(p.buf) && isDigit(p.buf[cur]) {
|
||||
cur++
|
||||
}
|
||||
|
||||
if out.I64, err = strconv.ParseInt(p.buf[start:cur], 10, 64); err != nil {
|
||||
return err
|
||||
}
|
||||
out.Type = TypeI64
|
||||
p.pos = cur
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseEqual(out *NamedCell) error {
|
||||
var ok bool
|
||||
out.column, ok = p.tryName()
|
||||
if !ok {
|
||||
return errors.New("expect column")
|
||||
}
|
||||
if !p.tryPunctuation("=") {
|
||||
return errors.New("expect =")
|
||||
}
|
||||
return p.parseValue(&out.value)
|
||||
}
|
||||
|
||||
func (p *Parser) parseSelect(out *StmtSelect) error {
|
||||
for !p.tryKeyword("FROM") {
|
||||
if len(out.cols) > 0 && !p.tryPunctuation(",") {
|
||||
return errors.New("expect comma")
|
||||
}
|
||||
if name, ok := p.tryName(); ok {
|
||||
out.cols = append(out.cols, name)
|
||||
} else {
|
||||
return errors.New("expect column")
|
||||
}
|
||||
}
|
||||
if len(out.cols) == 0 {
|
||||
return errors.New("expect column list")
|
||||
}
|
||||
var ok bool
|
||||
if out.table, ok = p.tryName(); !ok {
|
||||
return errors.New("expect table name")
|
||||
}
|
||||
return p.parseWhere(&out.keys)
|
||||
}
|
||||
|
||||
func (p *Parser) parseWhere(out *[]NamedCell) error {
|
||||
if !p.tryKeyword("WHERE") {
|
||||
return errors.New("expect keyword")
|
||||
}
|
||||
for !p.tryPunctuation(";") {
|
||||
expr := NamedCell{}
|
||||
if len(*out) > 0 && !p.tryKeyword("AND") {
|
||||
return errors.New("expect AND")
|
||||
}
|
||||
if err := p.parseEqual(&expr); err != nil {
|
||||
return err
|
||||
}
|
||||
*out = append(*out, expr)
|
||||
}
|
||||
if len(*out) == 0 {
|
||||
return errors.New("expect where clause")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) parseCreateTable(out *StmtCreatTable) error
|
||||
|
||||
func (p *Parser) parseInsert(out *StmtInsert) error
|
||||
|
||||
func (p *Parser) parseUpdate(out *StmtUpdate) error
|
||||
|
||||
func (p *Parser) parseDelete(out *StmtDelete) error
|
||||
|
||||
func (p *Parser) parseStmt() (out interface{}, err error) {
|
||||
if p.tryKeyword("SELECT") {
|
||||
stmt := &StmtSelect{}
|
||||
err = p.parseSelect(stmt)
|
||||
out = stmt
|
||||
} else if p.tryKeyword("CREATE", "TABLE") {
|
||||
stmt := &StmtCreatTable{}
|
||||
err = p.parseCreateTable(stmt)
|
||||
out = stmt
|
||||
} else if p.tryKeyword("INSERT", "INTO") {
|
||||
stmt := &StmtInsert{}
|
||||
err = p.parseInsert(stmt)
|
||||
out = stmt
|
||||
} else if p.tryKeyword("UPDATE") {
|
||||
stmt := &StmtUpdate{}
|
||||
err = p.parseUpdate(stmt)
|
||||
out = stmt
|
||||
} else if p.tryKeyword("DELETE", "FROM") {
|
||||
stmt := &StmtDelete{}
|
||||
err = p.parseDelete(stmt)
|
||||
out = stmt
|
||||
} else {
|
||||
err = errors.New("unknown statement")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *Parser) isEnd() bool {
|
||||
p.skipSpaces()
|
||||
return p.pos >= len(p.buf)
|
||||
}
|
||||
|
||||
// QzBQWVJJOUhU https://trialofcode.org/
|
||||
@@ -0,0 +1,110 @@
|
||||
package db0304
|
||||
|
||||
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())
|
||||
|
||||
p = NewParser(" select HELLO ")
|
||||
assert.False(t, p.tryKeyword("select", "hi"))
|
||||
assert.True(t, p.tryKeyword("select", "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")})
|
||||
}
|
||||
|
||||
func testParseStmt(t *testing.T, s string, ref interface{}) {
|
||||
p := NewParser(s)
|
||||
out, err := p.parseStmt()
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, p.isEnd())
|
||||
assert.Equal(t, ref, out)
|
||||
}
|
||||
|
||||
func TestParseStmt(t *testing.T) {
|
||||
var stmt interface{}
|
||||
s := "select a from t where c=1;"
|
||||
stmt = &StmtSelect{
|
||||
table: "t",
|
||||
cols: []string{"a"},
|
||||
keys: []NamedCell{{column: "c", value: Cell{Type: TypeI64, I64: 1}}},
|
||||
}
|
||||
testParseStmt(t, s, stmt)
|
||||
|
||||
s = "select a,b_02 from T where c=1 and d='e';"
|
||||
stmt = &StmtSelect{
|
||||
table: "T",
|
||||
cols: []string{"a", "b_02"},
|
||||
keys: []NamedCell{
|
||||
{column: "c", value: Cell{Type: TypeI64, I64: 1}},
|
||||
{column: "d", value: Cell{Type: TypeStr, Str: []byte("e")}},
|
||||
},
|
||||
}
|
||||
testParseStmt(t, s, stmt)
|
||||
|
||||
s = "select a, b_02 from T where c = 1 and d = 'e' ; "
|
||||
testParseStmt(t, s, stmt)
|
||||
|
||||
s = "create table t (a string, b int64, primary key (b));"
|
||||
stmt = &StmtCreatTable{
|
||||
table: "t",
|
||||
cols: []Column{{"a", TypeStr}, {"b", TypeI64}},
|
||||
pkey: []string{"b"},
|
||||
}
|
||||
testParseStmt(t, s, stmt)
|
||||
|
||||
s = "insert into t values (1, 'hi');"
|
||||
stmt = &StmtInsert{
|
||||
table: "t",
|
||||
value: []Cell{{Type: TypeI64, I64: 1}, {Type: TypeStr, Str: []byte("hi")}},
|
||||
}
|
||||
testParseStmt(t, s, stmt)
|
||||
|
||||
s = "update t set a = 1, b = 2 where c = 3 and d = 4;"
|
||||
stmt = &StmtUpdate{
|
||||
table: "t",
|
||||
value: []NamedCell{{"a", Cell{Type: TypeI64, I64: 1}}, {"b", Cell{Type: TypeI64, I64: 2}}},
|
||||
keys: []NamedCell{{"c", Cell{Type: TypeI64, I64: 3}}, {"d", Cell{Type: TypeI64, I64: 4}}},
|
||||
}
|
||||
testParseStmt(t, s, stmt)
|
||||
|
||||
s = "delete from t where c = 3 and d = 4;"
|
||||
stmt = &StmtDelete{
|
||||
table: "t",
|
||||
keys: []NamedCell{{"c", Cell{Type: TypeI64, I64: 3}}, {"d", Cell{Type: TypeI64, I64: 4}}},
|
||||
}
|
||||
testParseStmt(t, s, stmt)
|
||||
}
|
||||
|
||||
// QzBQWVJJOUhU https://trialofcode.org/
|
||||
@@ -0,0 +1,45 @@
|
||||
package db0304
|
||||
|
||||
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 db0304
|
||||
|
||||
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/
|
||||
Reference in New Issue
Block a user