From df8c61b7b9f99c22990fdd462fdaa70513459ffb Mon Sep 17 00:00:00 2001 From: edipretoro Date: Tue, 28 Jul 2026 22:26:46 +0200 Subject: [PATCH] Adding the challenge 0204 --- 0204/assert.go | 9 +++ 0204/cell.go | 57 ++++++++++++++++ 0204/cell_test.go | 27 ++++++++ 0204/kv.go | 87 +++++++++++++++++++++++++ 0204/kv_entry.go | 63 ++++++++++++++++++ 0204/kv_test.go | 158 +++++++++++++++++++++++++++++++++++++++++++++ 0204/log.go | 40 ++++++++++++ 0204/os_other.go | 11 ++++ 0204/os_unix.go | 34 ++++++++++ 0204/row.go | 95 +++++++++++++++++++++++++++ 0204/row_test.go | 38 +++++++++++ 0204/table.go | 20 ++++++ 0204/table_test.go | 65 +++++++++++++++++++ 13 files changed, 704 insertions(+) create mode 100644 0204/assert.go create mode 100644 0204/cell.go create mode 100644 0204/cell_test.go create mode 100644 0204/kv.go create mode 100644 0204/kv_entry.go create mode 100644 0204/kv_test.go create mode 100644 0204/log.go create mode 100644 0204/os_other.go create mode 100644 0204/os_unix.go create mode 100644 0204/row.go create mode 100644 0204/row_test.go create mode 100644 0204/table.go create mode 100644 0204/table_test.go diff --git a/0204/assert.go b/0204/assert.go new file mode 100644 index 0000000..3bf285a --- /dev/null +++ b/0204/assert.go @@ -0,0 +1,9 @@ +package db0204 + +func check(cond bool) { + if !cond { + panic("assertion failure") + } +} + +// QzBQWVJJOUhU https://trialofcode.org/ diff --git a/0204/cell.go b/0204/cell.go new file mode 100644 index 0000000..66e6271 --- /dev/null +++ b/0204/cell.go @@ -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/ diff --git a/0204/cell_test.go b/0204/cell_test.go new file mode 100644 index 0000000..856ad8b --- /dev/null +++ b/0204/cell_test.go @@ -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/ diff --git a/0204/kv.go b/0204/kv.go new file mode 100644 index 0000000..9868a82 --- /dev/null +++ b/0204/kv.go @@ -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/ diff --git a/0204/kv_entry.go b/0204/kv_entry.go new file mode 100644 index 0000000..77dad9c --- /dev/null +++ b/0204/kv_entry.go @@ -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/ diff --git a/0204/kv_test.go b/0204/kv_test.go new file mode 100644 index 0000000..89211f4 --- /dev/null +++ b/0204/kv_test.go @@ -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/ diff --git a/0204/log.go b/0204/log.go new file mode 100644 index 0000000..2a1fbfa --- /dev/null +++ b/0204/log.go @@ -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/ diff --git a/0204/os_other.go b/0204/os_other.go new file mode 100644 index 0000000..6967f10 --- /dev/null +++ b/0204/os_other.go @@ -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/ diff --git a/0204/os_unix.go b/0204/os_unix.go new file mode 100644 index 0000000..dec8063 --- /dev/null +++ b/0204/os_unix.go @@ -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/ diff --git a/0204/row.go b/0204/row.go new file mode 100644 index 0000000..be326c1 --- /dev/null +++ b/0204/row.go @@ -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/ diff --git a/0204/row_test.go b/0204/row_test.go new file mode 100644 index 0000000..3b25535 --- /dev/null +++ b/0204/row_test.go @@ -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/ diff --git a/0204/table.go b/0204/table.go new file mode 100644 index 0000000..87aad40 --- /dev/null +++ b/0204/table.go @@ -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/ diff --git a/0204/table_test.go b/0204/table_test.go new file mode 100644 index 0000000..d8dfd23 --- /dev/null +++ b/0204/table_test.go @@ -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/