initial commit

Signed-off-by: jolheiser <john.olheiser@gmail.com>
main
jolheiser 2022-11-14 23:29:40 -06:00
commit d10e3dcd4a
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
7 changed files with 351 additions and 0 deletions

19
LICENSE 100644
View File

@ -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.

7
README.md 100644
View File

@ -0,0 +1,7 @@
# SQKV
SQLite3 K/V database
## License
[MIT](LICENSE)

104
bucket.go 100644
View File

@ -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
}

28
go.mod 100644
View File

@ -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
)

64
go.sum 100644
View File

@ -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=

62
sqkv.go 100644
View File

@ -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, "_")
}

67
sqkv_test.go 100644
View File

@ -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))
}