Skip to content

Commit

Permalink
db: Add Chisel DB v0.1 skeleton
Browse files Browse the repository at this point in the history
This commit adds the skeleton db package that will grow into the full
Chisel DB implementation with forthcoming commits. The package contains
only New(), Save(), and Load() functions for creating, writing, and
reading the database, respectively.

The API exposes underlying jsonwall interfaces for both reading and
writing. An attempt was made to build an abstraction on top of jsonwall,
but it was later abandoned because of Go type system limitations and
difficulty to test.

The schema is set to 0.1 when writing. An error is returned when the
schema isn't 0.1 when reading. The schema and database location in the
output root directory are implementation details that are not exposed by
the API.
  • Loading branch information
woky committed Oct 12, 2023
1 parent 3029b22 commit 7f6ea9c
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 0 deletions.
80 changes: 80 additions & 0 deletions internal/db/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package db

import (
"fmt"
"os"
"path/filepath"

"github.com/canonical/chisel/internal/jsonwall"
"github.com/klauspost/compress/zstd"
)

const schema = "0.1"

// New creates a new Chisel DB writer with the proper schema.
func New() *jsonwall.DBWriter {
options := jsonwall.DBWriterOptions{Schema: schema}
return jsonwall.NewDBWriter(&options)
}

func getDBPath(root string) string {
return filepath.Join(root, ".chisel.db")
}

// Save uses the provided writer dbw to write the Chisel DB into the standard
// path under the provided root directory.
func Save(dbw *jsonwall.DBWriter, root string) (err error) {
dbPath := getDBPath(root)
defer func() {
if err != nil {
err = fmt.Errorf("cannot save state to %q: %w", dbPath, err)
}
}()
f, err := os.OpenFile(dbPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return
}
defer f.Close()
// chmod the existing file
if err = f.Chmod(0644); err != nil {
return
}
zw, err := zstd.NewWriter(f)
if err != nil {
return
}
if _, err = dbw.WriteTo(zw); err != nil {
return
}
return zw.Close()
}

// Load reads a Chisel DB from the standard path under the provided root
// directory. If the Chisel DB doesn't exist, the returned error satisfies
// errors.Is(err, fs.ErrNotExist))
func Load(root string) (db *jsonwall.DB, err error) {
dbPath := getDBPath(root)
defer func() {
if err != nil {
err = fmt.Errorf("cannot load state from %q: %w", dbPath, err)
}
}()
f, err := os.Open(dbPath)
if err != nil {
return
}
defer f.Close()
zr, err := zstd.NewReader(f)
if err != nil {
return
}
defer zr.Close()
db, err = jsonwall.ReadDB(zr)
if err != nil {
return nil, err
}
if s := db.Schema(); s != schema {
return nil, fmt.Errorf("invalid schema %#v", s)
}
return
}
74 changes: 74 additions & 0 deletions internal/db/db_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package db_test

import (
"sort"

"github.com/canonical/chisel/internal/db"
. "gopkg.in/check.v1"
)

type testEntry struct {
S string `json:"s,omitempty"`
I int64 `json:"i,omitempty"`
L []string `json:"l,omitempty"`
M map[string]bool `json:"m,omitempty"`
}

var saveLoadTestCase = []testEntry{
{"", 0, nil, nil},
{"hello", -1, nil, nil},
{"", 0, nil, nil},
{"", 100, []string{"a", "b"}, nil},
{"", 0, nil, map[string]bool{"a": true, "b": false}},
{"abc", 123, []string{"foo", "bar"}, nil},
}

func (s *S) TestSaveLoadRoundTrip(c *C) {
// To compare expected and obtained entries we first wrap the original
// entries in wrappers with increasing K. When we read the wrappers back
// they may be in different order because jsonwall sorts them serialized
// as JSON. So we sort them by K to compare them in the original order.

type wrapper struct {
// test values
testEntry
// sort key for comparison
K int `json:"key"`
}

// wrap the entries with increasing K
expected := make([]wrapper, len(saveLoadTestCase))
for i, entry := range saveLoadTestCase {
expected[i] = wrapper{entry, i}
}

workDir := c.MkDir()
dbw := db.New()
for _, entry := range expected {
err := dbw.Add(entry)
c.Assert(err, IsNil)
}
err := db.Save(dbw, workDir)
c.Assert(err, IsNil)

dbr, err := db.Load(workDir)
c.Assert(err, IsNil)
c.Assert(dbr.Schema(), Equals, db.Schema)

iter, err := dbr.Iterate(nil)
c.Assert(err, IsNil)

obtained := make([]wrapper, 0, len(expected))
for iter.Next() {
var wrapped wrapper
err := iter.Get(&wrapped)
c.Assert(err, IsNil)
obtained = append(obtained, wrapped)
}

// sort the entries by K to get the original order
sort.Slice(obtained, func(i, j int) bool {
return obtained[i].K < obtained[j].K
})
c.Assert(obtained, DeepEquals, expected)
}
3 changes: 3 additions & 0 deletions internal/db/export_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package db

var Schema = schema
15 changes: 15 additions & 0 deletions internal/db/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package db_test

import (
"testing"

. "gopkg.in/check.v1"
)

func Test(t *testing.T) {
TestingT(t)
}

type S struct{}

var _ = Suite(&S{})

0 comments on commit 7f6ea9c

Please sign in to comment.