commit
d10e3dcd4a
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2022 John Olheiser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,104 @@
|
|||
package sqkv
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const DefaultBucket = "SQKV"
|
||||
|
||||
var (
|
||||
ErrBucketNameEmpty = errors.New("bucket name cannot be empty")
|
||||
ErrBucketDoesNotExist = errors.New("bucket does not exist")
|
||||
ErrBucketExists = errors.New("bucket already exists")
|
||||
ErrKeyDoesNotExist = errors.New("key does not exist in bucket")
|
||||
)
|
||||
|
||||
// Bucket is a KV bucket
|
||||
type Bucket struct {
|
||||
db *sql.DB
|
||||
table string
|
||||
}
|
||||
|
||||
// Bucket gets a bucket by name, or returns nil if it doesn't exist
|
||||
func (d *DB) Bucket(name string) (*Bucket, error) {
|
||||
name = escapeIdent(name)
|
||||
if !d.bucketExists(name) {
|
||||
return nil, ErrBucketDoesNotExist
|
||||
}
|
||||
return &Bucket{
|
||||
db: d.db,
|
||||
table: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateBucket creates a new bucket and returns it. Returns an error if the bucket already exists or name is empty.
|
||||
func (d *DB) CreateBucket(name string) (*Bucket, error) {
|
||||
name = escapeIdent(name)
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return nil, ErrBucketNameEmpty
|
||||
}
|
||||
if ok := d.bucketExists(name); ok {
|
||||
return nil, ErrBucketExists
|
||||
}
|
||||
if _, err := d.db.Exec(fmt.Sprintf(`CREATE TABLE %s (
|
||||
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
KEY TEXT UNIQUE,
|
||||
VALUE TEXT
|
||||
)`, name)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Bucket{
|
||||
db: d.db,
|
||||
table: name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateBucketIfNotExist creates a new bucket (if it does not exist) and returns it. Returns an error if name is empty.
|
||||
func (d *DB) CreateBucketIfNotExist(name string) (*Bucket, error) {
|
||||
name = escapeIdent(name)
|
||||
b, err := d.CreateBucket(name)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrBucketExists) {
|
||||
return d.Bucket(name)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (d *DB) bucketExists(name string) bool {
|
||||
res, err := d.db.Query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?;", name)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer res.Close()
|
||||
return res.Next()
|
||||
}
|
||||
|
||||
// Get returns a value from the bucket
|
||||
func (b *Bucket) Get(key string) (string, error) {
|
||||
row := b.db.QueryRow(fmt.Sprintf("SELECT value FROM %s WHERE key = ?", b.table), key)
|
||||
var value string
|
||||
if err := row.Scan(&value); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", ErrKeyDoesNotExist
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
return value, row.Err()
|
||||
}
|
||||
|
||||
// Put sets a value in the bucket
|
||||
func (b *Bucket) Put(key, value string) error {
|
||||
_, err := b.db.Exec(fmt.Sprintf("INSERT INTO %s ( key, value ) VALUES( ?, ? ) ON CONFLICT(key) DO UPDATE SET value=excluded.value", b.table), key, value)
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete removes a value from the bucket
|
||||
func (b *Bucket) Delete(key string) error {
|
||||
_, err := b.db.Exec(fmt.Sprintf("DELETE FROM %s WHERE key = ?", b.table), key)
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
module go.jolheiser.com/sqkv
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/matryer/is v1.4.0
|
||||
modernc.org/sqlite v1.19.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
golang.org/x/mod v0.3.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
lukechampine.com/uint128 v1.2.0 // indirect
|
||||
modernc.org/cc/v3 v3.40.0 // indirect
|
||||
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||
modernc.org/libc v1.21.4 // indirect
|
||||
modernc.org/mathutil v1.5.0 // indirect
|
||||
modernc.org/memory v1.4.0 // indirect
|
||||
modernc.org/opt v0.1.3 // indirect
|
||||
modernc.org/strutil v1.1.3 // indirect
|
||||
modernc.org/token v1.0.1 // indirect
|
||||
)
|
|
@ -0,0 +1,64 @@
|
|||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs=
|
||||
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
|
||||
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
|
||||
modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
|
||||
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
|
||||
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
|
||||
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
|
||||
modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/libc v1.21.4 h1:CzTlumWeIbPV5/HVIMzYHNPCRP8uiU/CWiN2gtd/Qu8=
|
||||
modernc.org/libc v1.21.4/go.mod h1:przBsL5RDOZajTVslkugzLBj1evTue36jEomFQOoYuI=
|
||||
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
|
||||
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
|
||||
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
|
||||
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
|
||||
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
|
||||
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
|
||||
modernc.org/sqlite v1.19.4 h1:nlPIDqumn6/mSvs7T5C8MNYEuN73sISzPdKtMdURpUI=
|
||||
modernc.org/sqlite v1.19.4/go.mod h1:x/yZNb3h5+I3zGQSlwIv4REL5eJhiRkUH5MReogAeIc=
|
||||
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
|
||||
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
|
||||
modernc.org/tcl v1.15.0 h1:oY+JeD11qVVSgVvodMJsu7Edf8tr5E/7tuhF5cNYz34=
|
||||
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
|
||||
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
|
|
@ -0,0 +1,62 @@
|
|||
package sqkv
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// DB is an SQKV database
|
||||
type DB struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
// Open opens (or creates) a new SQKV database
|
||||
func Open(path string) (*DB, error) {
|
||||
dsn := fmt.Sprintf("file:%s?cache=shared&mode=rwc", path)
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqkv := &DB{
|
||||
db: db,
|
||||
}
|
||||
_, err = sqkv.CreateBucketIfNotExist(DefaultBucket)
|
||||
return sqkv, err
|
||||
}
|
||||
|
||||
// Get returns a value from the default bucket
|
||||
func (d *DB) Get(key string) (string, error) {
|
||||
b, err := d.Bucket(DefaultBucket)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return b.Get(key)
|
||||
}
|
||||
|
||||
// Put sets a value in the default bucket
|
||||
func (d *DB) Put(key, value string) error {
|
||||
b, err := d.Bucket(DefaultBucket)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return b.Put(key, value)
|
||||
}
|
||||
|
||||
// Delete removes a value from the default bucket
|
||||
func (d *DB) Delete(key string) error {
|
||||
b, err := d.Bucket(DefaultBucket)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return b.Delete(key)
|
||||
}
|
||||
|
||||
var escapeIdentRe = regexp.MustCompile(`[^A-Za-z]`)
|
||||
|
||||
func escapeIdent(in string) string {
|
||||
return escapeIdentRe.ReplaceAllString(in, "_")
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package sqkv
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/matryer/is"
|
||||
)
|
||||
|
||||
func TestDefault(t *testing.T) {
|
||||
assert := is.New(t)
|
||||
|
||||
tmp := t.TempDir()
|
||||
dbPath := filepath.Join(tmp, "sqkv.db")
|
||||
|
||||
db, err := Open(dbPath)
|
||||
assert.NoErr(err) // Should create database
|
||||
|
||||
err = db.Put("foo", "bar")
|
||||
assert.NoErr(err) // Should put kv
|
||||
|
||||
val, err := db.Get("foo")
|
||||
assert.NoErr(err) // Should get kv
|
||||
assert.Equal(val, "bar") // key(foo) == value(bar)
|
||||
|
||||
err = db.Delete("foo")
|
||||
assert.NoErr(err) // Should delete kv
|
||||
|
||||
_, err = db.Get("foo")
|
||||
assert.True(errors.Is(err, ErrKeyDoesNotExist))
|
||||
}
|
||||
|
||||
func TestBucket(t *testing.T) {
|
||||
assert := is.New(t)
|
||||
|
||||
tmp := t.TempDir()
|
||||
dbPath := filepath.Join(tmp, "sqkv.db")
|
||||
|
||||
db, err := Open(dbPath)
|
||||
assert.NoErr(err) // Should create database
|
||||
|
||||
_, err = db.Bucket("honk")
|
||||
assert.True(errors.Is(err, ErrBucketDoesNotExist)) // Bucket does not exist; should error
|
||||
|
||||
_, err = db.CreateBucket("honk")
|
||||
assert.NoErr(err) // Should create bucket
|
||||
|
||||
_, err = db.Bucket("honk")
|
||||
assert.NoErr(err) // Bucket should exist
|
||||
|
||||
b, err := db.CreateBucketIfNotExist("honk")
|
||||
assert.NoErr(err) // Bucket should exist and be returned
|
||||
|
||||
err = b.Put("foo", "bar")
|
||||
assert.NoErr(err) // Should put kv
|
||||
|
||||
val, err := b.Get("foo")
|
||||
assert.NoErr(err) // Should get kv
|
||||
assert.Equal(val, "bar") // key(foo) == value(bar)
|
||||
|
||||
err = b.Delete("foo")
|
||||
assert.NoErr(err) // Should delete kv
|
||||
|
||||
_, err = b.Get("foo")
|
||||
assert.True(errors.Is(err, ErrKeyDoesNotExist))
|
||||
}
|
Loading…
Reference in New Issue