commit 9d4e1346d9a91161e0f52694436b2efecf121f9c
parent 8d77f50a3abb094631ec23915a164e9cc37147bb
Author: lash <dev@holbrook.no>
Date: Sun, 1 Sep 2024 20:09:53 +0100
Move postgres to separate package
Diffstat:
M | db/db.go | | | 32 | ++++++++++++++++++++++---------- |
M | db/db_test.go | | | 301 | ------------------------------------------------------------------------------- |
M | db/fs.go | | | 7 | ++++--- |
M | db/gdbm.go | | | 7 | ++++--- |
M | db/mem.go | | | 8 | +++++--- |
D | db/pg.go | | | 142 | ------------------------------------------------------------------------------- |
D | db/pg_test.go | | | 65 | ----------------------------------------------------------------- |
A | db/postgres/pg.go | | | 144 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | db/postgres/pg_test.go | | | 67 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | db/testutil.go | | | 308 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
10 files changed, 554 insertions(+), 527 deletions(-)
diff --git a/db/db.go b/db/db.go
@@ -83,8 +83,8 @@ func ToDbKey(typ uint8, b []byte, l *lang.Language) []byte {
return append(k, b...)
}
-// baseDb is a base class for all Db implementations.
-type baseDb struct {
+// BaseDb is a base class for all Db implementations.
+type BaseDb struct {
pfx uint8
sid []byte
lock uint8
@@ -92,31 +92,37 @@ type baseDb struct {
seal bool
}
+func NewBaseDb() *BaseDb {
+ db := &BaseDb{}
+ db.defaultLock()
+ return db
+}
+
// ensures default locking of read-only entries
-func(db *baseDb) defaultLock() {
+func(db *BaseDb) defaultLock() {
db.lock |= safeLock
}
-func(db *baseDb) Safe() bool {
+func(db *BaseDb) Safe() bool {
return db.lock & safeLock == safeLock
}
// SetPrefix implements the Db interface.
-func(db *baseDb) SetPrefix(pfx uint8) {
+func(db *BaseDb) SetPrefix(pfx uint8) {
db.pfx = pfx
}
// SetLanguage implements the Db interface.
-func(db *baseDb) SetLanguage(ln *lang.Language) {
+func(db *BaseDb) SetLanguage(ln *lang.Language) {
db.lang = ln
}
// SetSession implements the Db interface.
-func(db *baseDb) SetSession(sessionId string) {
+func(db *BaseDb) SetSession(sessionId string) {
db.sid = append([]byte(sessionId), 0x2E)
}
// SetLock implements the Db interface.
-func(db *baseDb) SetLock(pfx uint8, lock bool) error {
+func(db *BaseDb) SetLock(pfx uint8, lock bool) error {
if db.seal {
return errors.New("SetLock on sealed db")
}
@@ -133,14 +139,20 @@ func(db *baseDb) SetLock(pfx uint8, lock bool) error {
return nil
}
-func(db *baseDb) checkPut() bool {
+func(db *BaseDb) checkPut() bool {
return db.pfx & db.lock == 0
}
+// CheckPut returns true if the current selected data type can be written to.
+func(db *BaseDb) CheckPut() bool {
+ return db.checkPut()
+}
+
+
// ToKey creates a DbKey within the current session context.
//
// TODO: hard to read, clean up
-func(db *baseDb) ToKey(ctx context.Context, key []byte) (lookupKey, error) {
+func(db *BaseDb) ToKey(ctx context.Context, key []byte) (lookupKey, error) {
var ln *lang.Language
var lk lookupKey
var b []byte
diff --git a/db/db_test.go b/db/db_test.go
@@ -1,302 +1 @@
package db
-
-import (
- "bytes"
- "context"
- "encoding/hex"
- "errors"
- "fmt"
- "path"
- "testing"
-
- "git.defalsify.org/vise.git/lang"
-)
-
-
-type testCase struct {
- typ uint8
- s string
- k []byte
- v []byte
- x []byte
- l *lang.Language
- t string
-}
-
-type testVector struct {
- c map[string]*testCase
- v []string
- i int
- s string
-}
-
-type testFunc func() testVector
-
-var (
- tests = []testFunc{
- generateSessionTestVectors,
- generateMultiSessionTestVectors,
- generateLanguageTestVectors,
- generateMultiLanguageTestVectors,
- generateSessionLanguageTestVectors,
- }
- dataTypeDebug = map[uint8]string{
- DATATYPE_BIN: "bytecode",
- DATATYPE_TEMPLATE: "template",
- DATATYPE_MENU: "menu",
- DATATYPE_STATICLOAD: "staticload",
- DATATYPE_STATE: "state",
- DATATYPE_USERDATA: "udata",
- }
-)
-
-func(tc *testCase) Key() []byte {
- return tc.k
-}
-
-func(tc *testCase) Val() []byte {
- return tc.v
-}
-
-func(tc *testCase) Typ() uint8 {
- return tc.typ
-}
-
-func(tc *testCase) Session() string {
- return tc.s
-}
-
-func(tc *testCase) Lang() string {
- if tc.l == nil {
- return ""
- }
- return tc.l.Code
-}
-
-func(tc *testCase) Expect() []byte {
- return tc.x
-}
-
-func(tc *testCase) Label() string {
- return tc.t
-}
-
-func(tv *testVector) add(typ uint8, k string, v string, session string, expect string, language string) {
- var b []byte
- var x []byte
- var err error
- var ln *lang.Language
-
- if typ == DATATYPE_BIN {
- b, err = hex.DecodeString(v)
- if err != nil {
- panic(err)
- }
- x, err = hex.DecodeString(expect)
- if err != nil {
- panic(err)
- }
- } else {
- b = []byte(v)
- x = []byte(expect)
- }
-
- if language != "" {
- lo, err := lang.LanguageFromCode(language)
- if err != nil {
- panic(err)
- }
- ln = &lo
- }
- s := dataTypeDebug[typ]
- s = path.Join(s, session)
- s = path.Join(s, k)
- if ln != nil {
- s = path.Join(s, language)
- }
- o := &testCase {
- typ: typ,
- k: []byte(k),
- v: b,
- s: session,
- x: x,
- l: ln,
- t: s,
- }
- tv.c[s] = o
- i := len(tv.v)
- tv.v = append(tv.v, s)
- logg.Tracef("add testcase", "i", i, "s", s, "k", o.k)
-}
-
-func(tv *testVector) next() (int, *testCase) {
- i := tv.i
- if i == len(tv.v) {
- return -1, nil
- }
- tv.i++
- return i, tv.c[tv.v[i]]
-}
-
-func(tv *testVector) rewind() {
- tv.i = 0
-}
-
-func(tv *testVector) put(ctx context.Context, db Db) error {
- var i int
- var tc *testCase
- defer tv.rewind()
-
- for true {
- i, tc = tv.next()
- if i == -1 {
- break
- }
- logg.TraceCtxf(ctx, "running put for test", "vector", tv.label(), "case", tc.Label())
- db.SetPrefix(tc.Typ())
- db.SetSession(tc.Session())
- db.SetLock(tc.Typ(), false)
- db.SetLanguage(nil)
- if tc.Lang() != "" {
- ln, err := lang.LanguageFromCode(tc.Lang())
- if err != nil {
- return err
- }
- db.SetLanguage(&ln)
- }
- err := db.Put(ctx, tc.Key(), tc.Val())
- if err != nil {
- return err
- }
- db.SetLock(tc.Typ(), true)
- }
- return nil
-}
-
-func(tv *testVector) label() string {
- return tv.s
-}
-
-func generateSessionTestVectors() testVector {
- tv := testVector{c: make(map[string]*testCase), s: "session"}
- tv.add(DATATYPE_BIN, "foo", "deadbeef", "", "beeffeed", "")
- tv.add(DATATYPE_BIN, "foo", "beeffeed", "inky", "beeffeed", "")
- tv.add(DATATYPE_TEMPLATE, "foo", "tinkywinky", "", "dipsy", "")
- tv.add(DATATYPE_TEMPLATE, "foo", "dipsy", "pinky", "dipsy", "")
- tv.add(DATATYPE_MENU, "foo", "lala", "", "pu", "")
- tv.add(DATATYPE_MENU, "foo", "pu", "blinky", "pu", "")
- tv.add(DATATYPE_STATICLOAD, "foo", "bar", "", "baz", "")
- tv.add(DATATYPE_STATICLOAD, "foo", "baz", "clyde", "baz", "")
- tv.add(DATATYPE_STATE, "foo", "xyzzy", "", "xyzzy", "")
- tv.add(DATATYPE_STATE, "foo", "plugh", "sue", "plugh", "")
- tv.add(DATATYPE_USERDATA, "foo", "itchy", "", "itchy", "")
- tv.add(DATATYPE_USERDATA, "foo", "scratchy", "poochie", "scratchy", "")
- return tv
-}
-
-func generateLanguageTestVectors() testVector {
- tv := testVector{c: make(map[string]*testCase), s: "language"}
- tv.add(DATATYPE_BIN, "foo", "deadbeef", "", "beeffeed", "")
- tv.add(DATATYPE_BIN, "foo", "beeffeed", "", "beeffeed", "nor")
- tv.add(DATATYPE_TEMPLATE, "foo", "tinkywinky", "", "tinkywinky", "")
- tv.add(DATATYPE_TEMPLATE, "foo", "dipsy", "", "dipsy", "nor")
- tv.add(DATATYPE_MENU, "foo", "lala", "", "lala", "")
- tv.add(DATATYPE_MENU, "foo", "pu", "", "pu", "nor")
- tv.add(DATATYPE_STATICLOAD, "foo", "bar", "", "bar", "")
- tv.add(DATATYPE_STATICLOAD, "foo", "baz", "", "baz", "nor")
- tv.add(DATATYPE_STATE, "foo", "xyzzy", "", "plugh", "")
- tv.add(DATATYPE_STATE, "foo", "plugh", "", "plugh", "nor")
- tv.add(DATATYPE_USERDATA, "foo", "itchy", "", "scratchy", "")
- tv.add(DATATYPE_USERDATA, "foo", "scratchy", "", "scratchy", "nor")
- return tv
-}
-
-func generateMultiLanguageTestVectors() testVector {
- tv := testVector{c: make(map[string]*testCase), s: "multilanguage"}
- tv.add(DATATYPE_TEMPLATE, "foo", "tinkywinky", "", "pu", "")
- tv.add(DATATYPE_TEMPLATE, "foo", "dipsy", "", "dipsy", "nor")
- tv.add(DATATYPE_TEMPLATE, "foo", "lala", "", "lala", "swa")
- tv.add(DATATYPE_TEMPLATE, "foo", "pu", "", "pu", "")
- return tv
-}
-
-func generateSessionLanguageTestVectors() testVector {
- tv := testVector{c: make(map[string]*testCase), s: "sessionlanguage"}
- tv.add(DATATYPE_TEMPLATE, "foo", "tinkywinky", "", "pu", "")
- tv.add(DATATYPE_TEMPLATE, "foo", "dipsy", "", "lala", "nor")
- tv.add(DATATYPE_TEMPLATE, "foo", "lala", "bar", "lala", "nor")
- tv.add(DATATYPE_TEMPLATE, "foo", "pu", "bar", "pu", "")
- tv.add(DATATYPE_STATE, "foo", "inky", "", "pinky", "")
- tv.add(DATATYPE_STATE, "foo", "pinky", "", "pinky", "nor")
- tv.add(DATATYPE_STATE, "foo", "blinky", "bar", "clyde", "nor")
- tv.add(DATATYPE_STATE, "foo", "clyde", "bar", "clyde", "")
- tv.add(DATATYPE_BIN, "foo", "deadbeef", "", "feebdaed", "")
- tv.add(DATATYPE_BIN, "foo", "beeffeed", "", "feebdaed", "nor")
- tv.add(DATATYPE_BIN, "foo", "deeffeeb", "baz", "feebdaed", "nor")
- tv.add(DATATYPE_BIN, "foo", "feebdaed", "baz", "feebdaed", "")
- return tv
-}
-
-func generateMultiSessionTestVectors() testVector {
- tv := testVector{c: make(map[string]*testCase), s: "multisession"}
- tv.add(DATATYPE_TEMPLATE, "foo", "red", "", "blue", "")
- tv.add(DATATYPE_TEMPLATE, "foo", "green", "bar", "blue", "")
- tv.add(DATATYPE_TEMPLATE, "foo", "blue", "baz", "blue", "")
- tv.add(DATATYPE_STATE, "foo", "inky", "", "inky", "")
- tv.add(DATATYPE_STATE, "foo", "pinky", "clyde", "pinky", "")
- tv.add(DATATYPE_STATE, "foo", "blinky", "sue", "blinky", "")
- tv.add(DATATYPE_BIN, "foo", "deadbeef", "", "feebdeef", "")
- tv.add(DATATYPE_BIN, "foo", "feedbeef", "bar", "feebdeef", "")
- tv.add(DATATYPE_BIN, "foo", "feebdeef", "baz", "feebdeef", "")
- return tv
-}
-
-func runTest(t *testing.T, ctx context.Context, db Db, vs testVector) error {
- err := vs.put(ctx, db)
- if err != nil {
- return err
- }
- for true {
- i, tc := vs.next()
- if i == -1 {
- break
- }
- s := fmt.Sprintf("Test%s[%d]%s", vs.label(), i, tc.Label())
- r := t.Run(s, func(t *testing.T) {
- db.SetPrefix(tc.Typ())
- db.SetSession(tc.Session())
- if tc.Lang() != "" {
- ln, err := lang.LanguageFromCode(tc.Lang())
- if err != nil {
- t.Fatal(err)
- }
- db.SetLanguage(&ln)
- } else {
- db.SetLanguage(nil)
- }
- db.SetSession(tc.Session())
- v, err := db.Get(ctx, tc.Key())
- if err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(tc.Expect(), v) {
- t.Fatalf("expected %s, got %s", tc.Expect(), v)
- }
- })
- if !r {
- return errors.New("subtest fail")
- }
- }
- return nil
-
-}
-func runTests(t *testing.T, ctx context.Context, db Db) error {
- for _, fn := range tests {
- err := runTest(t, ctx, db, fn())
- if err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/db/fs.go b/db/fs.go
@@ -17,14 +17,15 @@ type fsLookupKey struct {
// pure filesystem backend implementation if the Db interface.
type fsDb struct {
- baseDb
+ *BaseDb
dir string
}
// NewFsDb creates a filesystem backed Db implementation.
func NewFsDb() *fsDb {
- db := &fsDb{}
- db.baseDb.defaultLock()
+ db := &fsDb{
+ BaseDb: NewBaseDb(),
+ }
return db
}
diff --git a/db/gdbm.go b/db/gdbm.go
@@ -10,15 +10,16 @@ import (
// gdbmDb is a gdbm backend implementation of the Db interface.
type gdbmDb struct {
- baseDb
+ *BaseDb
conn *gdbm.Database
prefix uint8
}
// Creates a new gdbm backed Db implementation.
func NewGdbmDb() *gdbmDb {
- db := &gdbmDb{}
- db.baseDb.defaultLock()
+ db := &gdbmDb{
+ BaseDb: NewBaseDb(),
+ }
return db
}
diff --git a/db/mem.go b/db/mem.go
@@ -14,14 +14,16 @@ type memLookupKey struct {
// memDb is a memory backend implementation of the Db interface.
type memDb struct {
- baseDb
+ *BaseDb
store map[string][]byte
}
// NewmemDb returns an in-process volatile Db implementation.
func NewMemDb() *memDb {
- db := &memDb{}
- db.baseDb.defaultLock()
+ db := &memDb{
+ BaseDb: NewBaseDb(),
+ }
+ db.BaseDb.defaultLock()
return db
}
diff --git a/db/pg.go b/db/pg.go
@@ -1,142 +0,0 @@
-package db
-
-import (
- "context"
- "errors"
- "fmt"
-
- "github.com/jackc/pgx/v5/pgxpool"
-)
-
-// pgDb is a Postgres backend implementation of the Db interface.
-type pgDb struct {
- baseDb
- conn *pgxpool.Pool
- schema string
- prefix uint8
-}
-
-// NewpgDb creates a new Postgres backed Db implementation.
-func NewPgDb() *pgDb {
- db := &pgDb{
- schema: "public",
- }
- db.baseDb.defaultLock()
- return db
-}
-
-// WithSchema sets the Postgres schema to use for the storage table.
-func(pdb *pgDb) WithSchema(schema string) *pgDb {
- pdb.schema = schema
- return pdb
-}
-
-// Connect implements Db.
-func(pdb *pgDb) Connect(ctx context.Context, connStr string) error {
- if pdb.conn != nil {
- panic("already connected")
- }
- var err error
- conn, err := pgxpool.New(ctx, connStr)
- if err != nil {
- return err
- }
- pdb.conn = conn
- return pdb.prepare(ctx)
-}
-
-// Put implements Db.
-func(pdb *pgDb) Put(ctx context.Context, key []byte, val []byte) error {
- if !pdb.checkPut() {
- return errors.New("unsafe put and safety set")
- }
- k, err := pdb.ToKey(ctx, key)
- if err != nil {
- return err
- }
- tx, err := pdb.conn.Begin(ctx)
- if err != nil {
- return err
- }
- query := fmt.Sprintf("INSERT INTO %s.kv_vise (key, value) VALUES ($1, $2) ON CONFLICT(key) DO UPDATE SET value = $2;", pdb.schema)
- _, err = tx.Exec(ctx, query, k, val)
- if err != nil {
- tx.Rollback(ctx)
- return err
- }
- tx.Commit(ctx)
- return nil
-}
-
-// Get implements Db.
-func(pdb *pgDb) Get(ctx context.Context, key []byte) ([]byte, error) {
- lk, err := pdb.ToKey(ctx, key)
- if err != nil {
- return nil, err
- }
- if lk.Translation != nil {
- tx, err := pdb.conn.Begin(ctx)
- if err != nil {
- return nil, err
- }
- query := fmt.Sprintf("SELECT value FROM %s.kv_vise WHERE key = $1", pdb.schema)
- rs, err := tx.Query(ctx, query, lk.Translation)
- if err != nil {
- return nil, err
- }
- defer rs.Close()
- if rs.Next() {
- r := rs.RawValues()
- return r[0], nil
- }
- }
-
- tx, err := pdb.conn.Begin(ctx)
- if err != nil {
- return nil, err
- }
- query := fmt.Sprintf("SELECT value FROM %s.kv_vise WHERE key = $1", pdb.schema)
- rs, err := tx.Query(ctx, query, lk.Translation)
- if err != nil {
- return nil, err
- }
- defer rs.Close()
- if !rs.Next() {
- return nil, NewErrNotFound(key)
- }
- r := rs.RawValues()
- return r[0], nil
-}
-
-// Close implements Db.
-func(pdb *pgDb) Close() error {
- pdb.Close()
- return nil
-}
-
-// set up table
-func(pdb *pgDb) prepare(ctx context.Context) error {
- tx, err := pdb.conn.Begin(ctx)
- if err != nil {
- tx.Rollback(ctx)
- return err
- }
- query := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.kv_vise (
- id SERIAL NOT NULL,
- key BYTEA NOT NULL UNIQUE,
- value BYTEA NOT NULL
- );
-`, pdb.schema)
- _, err = tx.Exec(ctx, query)
- if err != nil {
- tx.Rollback(ctx)
- return err
- }
-
- err = tx.Commit(ctx)
- if err != nil {
- tx.Rollback(ctx)
- return err
- }
- return nil
-}
diff --git a/db/pg_test.go b/db/pg_test.go
@@ -1,65 +0,0 @@
-package db
-
-import (
- "bytes"
- "context"
- "testing"
-)
-
-func TestCasesPg(t *testing.T) {
- ctx := context.Background()
-
- db := NewPgDb().WithSchema("vvise")
- t.Skip("need postgresql mock")
-
- err := db.Connect(ctx, "postgres://vise:esiv@localhost:5432/visedb")
- if err != nil {
- t.Fatal(err)
- }
-
- err = runTests(t, ctx, db)
- if err != nil {
- t.Fatal(err)
- }
-}
-
-func TestPutGetPg(t *testing.T) {
- var dbi Db
- ses := "xyzzy"
- db := NewPgDb().WithSchema("vvise")
- db.SetPrefix(DATATYPE_USERDATA)
- db.SetSession(ses)
- ctx := context.Background()
-
- dbi = db
- _ = dbi
-
- t.Skip("need postgresql mock")
- err := db.Connect(ctx, "postgres://vise:esiv@localhost:5432/visedb")
- if err != nil {
- t.Fatal(err)
- }
- err = db.Put(ctx, []byte("foo"), []byte("bar"))
- if err != nil {
- t.Fatal(err)
- }
- b, err := db.Get(ctx, []byte("foo"))
- if err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(b, []byte("bar")) {
- t.Fatalf("expected 'bar', got %x", b)
- }
- err = db.Put(ctx, []byte("foo"), []byte("plugh"))
- if err != nil {
- t.Fatal(err)
- }
- b, err = db.Get(ctx, []byte("foo"))
- if err != nil {
- t.Fatal(err)
- }
- if !bytes.Equal(b, []byte("plugh")) {
- t.Fatalf("expected 'plugh', got %x", b)
- }
-
-}
diff --git a/db/postgres/pg.go b/db/postgres/pg.go
@@ -0,0 +1,144 @@
+package db
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/jackc/pgx/v5/pgxpool"
+
+ "git.defalsify.org/vise.git/db"
+)
+
+// pgDb is a Postgres backend implementation of the Db interface.
+type pgDb struct {
+ *db.BaseDb
+ conn *pgxpool.Pool
+ schema string
+ prefix uint8
+}
+
+// NewpgDb creates a new Postgres backed Db implementation.
+func NewPgDb() *pgDb {
+ db := &pgDb{
+ BaseDb: db.NewBaseDb(),
+ schema: "public",
+ }
+ return db
+}
+
+// WithSchema sets the Postgres schema to use for the storage table.
+func(pdb *pgDb) WithSchema(schema string) *pgDb {
+ pdb.schema = schema
+ return pdb
+}
+
+// Connect implements Db.
+func(pdb *pgDb) Connect(ctx context.Context, connStr string) error {
+ if pdb.conn != nil {
+ panic("already connected")
+ }
+ var err error
+ conn, err := pgxpool.New(ctx, connStr)
+ if err != nil {
+ return err
+ }
+ pdb.conn = conn
+ return pdb.prepare(ctx)
+}
+
+// Put implements Db.
+func(pdb *pgDb) Put(ctx context.Context, key []byte, val []byte) error {
+ if !pdb.CheckPut() {
+ return errors.New("unsafe put and safety set")
+ }
+ k, err := pdb.ToKey(ctx, key)
+ if err != nil {
+ return err
+ }
+ tx, err := pdb.conn.Begin(ctx)
+ if err != nil {
+ return err
+ }
+ query := fmt.Sprintf("INSERT INTO %s.kv_vise (key, value) VALUES ($1, $2) ON CONFLICT(key) DO UPDATE SET value = $2;", pdb.schema)
+ _, err = tx.Exec(ctx, query, k, val)
+ if err != nil {
+ tx.Rollback(ctx)
+ return err
+ }
+ tx.Commit(ctx)
+ return nil
+}
+
+// Get implements Db.
+func(pdb *pgDb) Get(ctx context.Context, key []byte) ([]byte, error) {
+ lk, err := pdb.ToKey(ctx, key)
+ if err != nil {
+ return nil, err
+ }
+ if lk.Translation != nil {
+ tx, err := pdb.conn.Begin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ query := fmt.Sprintf("SELECT value FROM %s.kv_vise WHERE key = $1", pdb.schema)
+ rs, err := tx.Query(ctx, query, lk.Translation)
+ if err != nil {
+ return nil, err
+ }
+ defer rs.Close()
+ if rs.Next() {
+ r := rs.RawValues()
+ return r[0], nil
+ }
+ }
+
+ tx, err := pdb.conn.Begin(ctx)
+ if err != nil {
+ return nil, err
+ }
+ query := fmt.Sprintf("SELECT value FROM %s.kv_vise WHERE key = $1", pdb.schema)
+ rs, err := tx.Query(ctx, query, lk.Translation)
+ if err != nil {
+ return nil, err
+ }
+ defer rs.Close()
+ if !rs.Next() {
+ return nil, db.NewErrNotFound(key)
+ }
+ r := rs.RawValues()
+ return r[0], nil
+}
+
+// Close implements Db.
+func(pdb *pgDb) Close() error {
+ pdb.Close()
+ return nil
+}
+
+// set up table
+func(pdb *pgDb) prepare(ctx context.Context) error {
+ tx, err := pdb.conn.Begin(ctx)
+ if err != nil {
+ tx.Rollback(ctx)
+ return err
+ }
+ query := fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s.kv_vise (
+ id SERIAL NOT NULL,
+ key BYTEA NOT NULL UNIQUE,
+ value BYTEA NOT NULL
+ );
+`, pdb.schema)
+ _, err = tx.Exec(ctx, query)
+ if err != nil {
+ tx.Rollback(ctx)
+ return err
+ }
+
+ err = tx.Commit(ctx)
+ if err != nil {
+ tx.Rollback(ctx)
+ return err
+ }
+ return nil
+}
diff --git a/db/postgres/pg_test.go b/db/postgres/pg_test.go
@@ -0,0 +1,67 @@
+package db
+
+import (
+ "bytes"
+ "context"
+ "testing"
+
+ "git.defalsify.org/vise.git/db"
+)
+
+func TestCasesPg(t *testing.T) {
+ ctx := context.Background()
+
+ store := NewPgDb().WithSchema("vvise")
+ t.Skip("need postgresql mock")
+
+ err := store.Connect(ctx, "postgres://vise:esiv@localhost:5432/visedb")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ err = db.RunTests(t, ctx, store)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPutGetPg(t *testing.T) {
+ var dbi db.Db
+ ses := "xyzzy"
+ store := NewPgDb().WithSchema("vvise")
+ store.SetPrefix(db.DATATYPE_USERDATA)
+ store.SetSession(ses)
+ ctx := context.Background()
+
+ dbi = store
+ _ = dbi
+
+ t.Skip("need postgresql mock")
+ err := store.Connect(ctx, "postgres://vise:esiv@localhost:5432/visedb")
+ if err != nil {
+ t.Fatal(err)
+ }
+ err = store.Put(ctx, []byte("foo"), []byte("bar"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ b, err := store.Get(ctx, []byte("foo"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(b, []byte("bar")) {
+ t.Fatalf("expected 'bar', got %x", b)
+ }
+ err = store.Put(ctx, []byte("foo"), []byte("plugh"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ b, err = store.Get(ctx, []byte("foo"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(b, []byte("plugh")) {
+ t.Fatalf("expected 'plugh', got %x", b)
+ }
+
+}
diff --git a/db/testutil.go b/db/testutil.go
@@ -0,0 +1,308 @@
+package db
+
+import (
+ "bytes"
+ "context"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "path"
+ "testing"
+
+ "git.defalsify.org/vise.git/lang"
+)
+
+
+type testCase struct {
+ typ uint8
+ s string
+ k []byte
+ v []byte
+ x []byte
+ l *lang.Language
+ t string
+}
+
+type testVector struct {
+ c map[string]*testCase
+ v []string
+ i int
+ s string
+}
+
+type testFunc func() testVector
+
+var (
+ tests = []testFunc{
+ generateSessionTestVectors,
+ generateMultiSessionTestVectors,
+ generateLanguageTestVectors,
+ generateMultiLanguageTestVectors,
+ generateSessionLanguageTestVectors,
+ }
+ dataTypeDebug = map[uint8]string{
+ DATATYPE_BIN: "bytecode",
+ DATATYPE_TEMPLATE: "template",
+ DATATYPE_MENU: "menu",
+ DATATYPE_STATICLOAD: "staticload",
+ DATATYPE_STATE: "state",
+ DATATYPE_USERDATA: "udata",
+ }
+)
+
+func(tc *testCase) Key() []byte {
+ return tc.k
+}
+
+func(tc *testCase) Val() []byte {
+ return tc.v
+}
+
+func(tc *testCase) Typ() uint8 {
+ return tc.typ
+}
+
+func(tc *testCase) Session() string {
+ return tc.s
+}
+
+func(tc *testCase) Lang() string {
+ if tc.l == nil {
+ return ""
+ }
+ return tc.l.Code
+}
+
+func(tc *testCase) Expect() []byte {
+ return tc.x
+}
+
+func(tc *testCase) Label() string {
+ return tc.t
+}
+
+func(tv *testVector) add(typ uint8, k string, v string, session string, expect string, language string) {
+ var b []byte
+ var x []byte
+ var err error
+ var ln *lang.Language
+
+ if typ == DATATYPE_BIN {
+ b, err = hex.DecodeString(v)
+ if err != nil {
+ panic(err)
+ }
+ x, err = hex.DecodeString(expect)
+ if err != nil {
+ panic(err)
+ }
+ } else {
+ b = []byte(v)
+ x = []byte(expect)
+ }
+
+ if language != "" {
+ lo, err := lang.LanguageFromCode(language)
+ if err != nil {
+ panic(err)
+ }
+ ln = &lo
+ }
+ s := dataTypeDebug[typ]
+ s = path.Join(s, session)
+ s = path.Join(s, k)
+ if ln != nil {
+ s = path.Join(s, language)
+ }
+ o := &testCase {
+ typ: typ,
+ k: []byte(k),
+ v: b,
+ s: session,
+ x: x,
+ l: ln,
+ t: s,
+ }
+ tv.c[s] = o
+ i := len(tv.v)
+ tv.v = append(tv.v, s)
+ logg.Tracef("add testcase", "i", i, "s", s, "k", o.k)
+}
+
+func(tv *testVector) next() (int, *testCase) {
+ i := tv.i
+ if i == len(tv.v) {
+ return -1, nil
+ }
+ tv.i++
+ return i, tv.c[tv.v[i]]
+}
+
+func(tv *testVector) rewind() {
+ tv.i = 0
+}
+
+func(tv *testVector) put(ctx context.Context, db Db) error {
+ var i int
+ var tc *testCase
+ defer tv.rewind()
+
+ for true {
+ i, tc = tv.next()
+ if i == -1 {
+ break
+ }
+ logg.TraceCtxf(ctx, "running put for test", "vector", tv.label(), "case", tc.Label())
+ db.SetPrefix(tc.Typ())
+ db.SetSession(tc.Session())
+ db.SetLock(tc.Typ(), false)
+ db.SetLanguage(nil)
+ if tc.Lang() != "" {
+ ln, err := lang.LanguageFromCode(tc.Lang())
+ if err != nil {
+ return err
+ }
+ db.SetLanguage(&ln)
+ }
+ err := db.Put(ctx, tc.Key(), tc.Val())
+ if err != nil {
+ return err
+ }
+ db.SetLock(tc.Typ(), true)
+ }
+ return nil
+}
+
+func(tv *testVector) label() string {
+ return tv.s
+}
+
+func generateSessionTestVectors() testVector {
+ tv := testVector{c: make(map[string]*testCase), s: "session"}
+ tv.add(DATATYPE_BIN, "foo", "deadbeef", "", "beeffeed", "")
+ tv.add(DATATYPE_BIN, "foo", "beeffeed", "inky", "beeffeed", "")
+ tv.add(DATATYPE_TEMPLATE, "foo", "tinkywinky", "", "dipsy", "")
+ tv.add(DATATYPE_TEMPLATE, "foo", "dipsy", "pinky", "dipsy", "")
+ tv.add(DATATYPE_MENU, "foo", "lala", "", "pu", "")
+ tv.add(DATATYPE_MENU, "foo", "pu", "blinky", "pu", "")
+ tv.add(DATATYPE_STATICLOAD, "foo", "bar", "", "baz", "")
+ tv.add(DATATYPE_STATICLOAD, "foo", "baz", "clyde", "baz", "")
+ tv.add(DATATYPE_STATE, "foo", "xyzzy", "", "xyzzy", "")
+ tv.add(DATATYPE_STATE, "foo", "plugh", "sue", "plugh", "")
+ tv.add(DATATYPE_USERDATA, "foo", "itchy", "", "itchy", "")
+ tv.add(DATATYPE_USERDATA, "foo", "scratchy", "poochie", "scratchy", "")
+ return tv
+}
+
+func generateLanguageTestVectors() testVector {
+ tv := testVector{c: make(map[string]*testCase), s: "language"}
+ tv.add(DATATYPE_BIN, "foo", "deadbeef", "", "beeffeed", "")
+ tv.add(DATATYPE_BIN, "foo", "beeffeed", "", "beeffeed", "nor")
+ tv.add(DATATYPE_TEMPLATE, "foo", "tinkywinky", "", "tinkywinky", "")
+ tv.add(DATATYPE_TEMPLATE, "foo", "dipsy", "", "dipsy", "nor")
+ tv.add(DATATYPE_MENU, "foo", "lala", "", "lala", "")
+ tv.add(DATATYPE_MENU, "foo", "pu", "", "pu", "nor")
+ tv.add(DATATYPE_STATICLOAD, "foo", "bar", "", "bar", "")
+ tv.add(DATATYPE_STATICLOAD, "foo", "baz", "", "baz", "nor")
+ tv.add(DATATYPE_STATE, "foo", "xyzzy", "", "plugh", "")
+ tv.add(DATATYPE_STATE, "foo", "plugh", "", "plugh", "nor")
+ tv.add(DATATYPE_USERDATA, "foo", "itchy", "", "scratchy", "")
+ tv.add(DATATYPE_USERDATA, "foo", "scratchy", "", "scratchy", "nor")
+ return tv
+}
+
+func generateMultiLanguageTestVectors() testVector {
+ tv := testVector{c: make(map[string]*testCase), s: "multilanguage"}
+ tv.add(DATATYPE_TEMPLATE, "foo", "tinkywinky", "", "pu", "")
+ tv.add(DATATYPE_TEMPLATE, "foo", "dipsy", "", "dipsy", "nor")
+ tv.add(DATATYPE_TEMPLATE, "foo", "lala", "", "lala", "swa")
+ tv.add(DATATYPE_TEMPLATE, "foo", "pu", "", "pu", "")
+ return tv
+}
+
+func generateSessionLanguageTestVectors() testVector {
+ tv := testVector{c: make(map[string]*testCase), s: "sessionlanguage"}
+ tv.add(DATATYPE_TEMPLATE, "foo", "tinkywinky", "", "pu", "")
+ tv.add(DATATYPE_TEMPLATE, "foo", "dipsy", "", "lala", "nor")
+ tv.add(DATATYPE_TEMPLATE, "foo", "lala", "bar", "lala", "nor")
+ tv.add(DATATYPE_TEMPLATE, "foo", "pu", "bar", "pu", "")
+ tv.add(DATATYPE_STATE, "foo", "inky", "", "pinky", "")
+ tv.add(DATATYPE_STATE, "foo", "pinky", "", "pinky", "nor")
+ tv.add(DATATYPE_STATE, "foo", "blinky", "bar", "clyde", "nor")
+ tv.add(DATATYPE_STATE, "foo", "clyde", "bar", "clyde", "")
+ tv.add(DATATYPE_BIN, "foo", "deadbeef", "", "feebdaed", "")
+ tv.add(DATATYPE_BIN, "foo", "beeffeed", "", "feebdaed", "nor")
+ tv.add(DATATYPE_BIN, "foo", "deeffeeb", "baz", "feebdaed", "nor")
+ tv.add(DATATYPE_BIN, "foo", "feebdaed", "baz", "feebdaed", "")
+ return tv
+}
+
+func generateMultiSessionTestVectors() testVector {
+ tv := testVector{c: make(map[string]*testCase), s: "multisession"}
+ tv.add(DATATYPE_TEMPLATE, "foo", "red", "", "blue", "")
+ tv.add(DATATYPE_TEMPLATE, "foo", "green", "bar", "blue", "")
+ tv.add(DATATYPE_TEMPLATE, "foo", "blue", "baz", "blue", "")
+ tv.add(DATATYPE_STATE, "foo", "inky", "", "inky", "")
+ tv.add(DATATYPE_STATE, "foo", "pinky", "clyde", "pinky", "")
+ tv.add(DATATYPE_STATE, "foo", "blinky", "sue", "blinky", "")
+ tv.add(DATATYPE_BIN, "foo", "deadbeef", "", "feebdeef", "")
+ tv.add(DATATYPE_BIN, "foo", "feedbeef", "bar", "feebdeef", "")
+ tv.add(DATATYPE_BIN, "foo", "feebdeef", "baz", "feebdeef", "")
+ return tv
+}
+
+func runTest(t *testing.T, ctx context.Context, db Db, vs testVector) error {
+ err := vs.put(ctx, db)
+ if err != nil {
+ return err
+ }
+ for true {
+ i, tc := vs.next()
+ if i == -1 {
+ break
+ }
+ s := fmt.Sprintf("Test%s[%d]%s", vs.label(), i, tc.Label())
+ r := t.Run(s, func(t *testing.T) {
+ db.SetPrefix(tc.Typ())
+ db.SetSession(tc.Session())
+ if tc.Lang() != "" {
+ ln, err := lang.LanguageFromCode(tc.Lang())
+ if err != nil {
+ t.Fatal(err)
+ }
+ db.SetLanguage(&ln)
+ } else {
+ db.SetLanguage(nil)
+ }
+ db.SetSession(tc.Session())
+ v, err := db.Get(ctx, tc.Key())
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(tc.Expect(), v) {
+ t.Fatalf("expected %s, got %s", tc.Expect(), v)
+ }
+ })
+ if !r {
+ return errors.New("subtest fail")
+ }
+ }
+ return nil
+
+}
+
+
+func runTests(t *testing.T, ctx context.Context, db Db) error {
+ for _, fn := range tests {
+ err := runTest(t, ctx, db, fn())
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func RunTests(t *testing.T, ctx context.Context, db Db) error {
+ return runTests(t, ctx, db)
+}