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 9, 2023
1 parent 6556dec commit 2f63b05
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 0 deletions.
68 changes: 68 additions & 0 deletions internal/db/db.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 dbPath(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) error {
f, err := os.OpenFile(dbPath(root), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
return err
}
defer f.Close()
// chmod the existing file
if err := f.Chmod(0644); err != nil {
return err
}
zw, err := zstd.NewWriter(f)
if err != nil {
return err
}
if _, err := dbw.WriteTo(zw); err != nil {
return err
}
zw.Close()
return nil
}

// 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
// os.IsNotExist(err).
func Load(root string) (*jsonwall.DB, error) {
f, err := os.Open(dbPath(root))
if err != nil {
return nil, err
}
defer f.Close()
zr, err := zstd.NewReader(f)
if err != nil {
return nil, err
}
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 db, nil
}
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 2f63b05

Please sign in to comment.