-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add database backend for data storage
Signed-off-by: Knut Ahlers <knut@ahlers.me>
- Loading branch information
Showing
12 changed files
with
459 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
package database | ||
|
||
import ( | ||
"bytes" | ||
"database/sql" | ||
"embed" | ||
"encoding/json" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/jmoiron/sqlx" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
type ( | ||
connector struct { | ||
db *sqlx.DB | ||
} | ||
) | ||
|
||
var ( | ||
// ErrCoreMetaNotFound is the error thrown when reading a non-existent | ||
// core_meta key | ||
ErrCoreMetaNotFound = errors.New("core meta entry not found") | ||
|
||
//go:embed schema/** | ||
schema embed.FS | ||
|
||
migrationFilename = regexp.MustCompile(`^([0-9]+)\.sql$`) | ||
) | ||
|
||
// New creates a new Connector with the given driver and database | ||
func New(driverName, dataSourceName string) (Connector, error) { | ||
db, err := sqlx.Connect(driverName, dataSourceName) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "connecting database") | ||
} | ||
|
||
conn := &connector{db: db} | ||
return conn, errors.Wrap(conn.applyCoreSchema(), "applying core schema") | ||
} | ||
|
||
func (c connector) Close() error { | ||
return errors.Wrap(c.db.Close(), "closing database") | ||
} | ||
|
||
func (c connector) DB() *sqlx.DB { | ||
return c.db | ||
} | ||
|
||
// ReadCoreMeta reads an entry of the core_meta table specified by | ||
// the given `key` and unmarshals it into the `value`. The value must | ||
// be a valid variable to `json.NewDecoder(...).Decode(value)` | ||
// (pointer to struct, string, int, ...). In case the key does not | ||
// exist a check to 'errors.Is(err, sql.ErrNoRows)' will succeed | ||
func (c connector) ReadCoreMeta(key string, value any) error { | ||
var data struct{ Key, Value string } | ||
data.Key = key | ||
|
||
if err := c.db.Get(&data, "SELECT * FROM core_meta WHERE key = $1", data.Key); err != nil { | ||
if errors.Is(err, sql.ErrNoRows) { | ||
return ErrCoreMetaNotFound | ||
} | ||
return errors.Wrap(err, "querying core meta table") | ||
} | ||
|
||
if data.Value == "" { | ||
return errors.New("empty value returned") | ||
} | ||
|
||
if err := json.NewDecoder(strings.NewReader(data.Value)).Decode(value); err != nil { | ||
return errors.Wrap(err, "JSON decoding value") | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// StoreCoreMeta stores an entry to the core_meta table soecified by | ||
// the given `key`. The value given must be a valid variable to | ||
// `json.NewEncoder(...).Encode(value)`. | ||
func (c connector) StoreCoreMeta(key string, value any) error { | ||
buf := new(bytes.Buffer) | ||
if err := json.NewEncoder(buf).Encode(value); err != nil { | ||
return errors.Wrap(err, "JSON encoding value") | ||
} | ||
|
||
_, err := c.db.NamedExec( | ||
"INSERT INTO core_meta (key, value) VALUES (:key, :value) ON CONFLICT DO UPDATE SET value=excluded.value;", | ||
map[string]any{ | ||
"key": key, | ||
"value": buf.String(), | ||
}, | ||
) | ||
|
||
return errors.Wrap(err, "upserting core meta value") | ||
} | ||
|
||
func (c connector) applyCoreSchema() error { | ||
coreSQL, err := schema.ReadFile("schema/core.sql") | ||
if err != nil { | ||
return errors.Wrap(err, "reading core.sql content") | ||
} | ||
|
||
_, err = c.db.Exec(string(coreSQL)) | ||
return errors.Wrap(err, "applying core schema") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package database | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
func TestNewConnector(t *testing.T) { | ||
dbc, err := New("sqlite", ":memory:") | ||
if err != nil { | ||
t.Fatalf("creating database connector: %s", err) | ||
} | ||
defer dbc.Close() | ||
|
||
row := dbc.DB().QueryRow("SELECT count(1) AS tables FROM sqlite_master WHERE type='table' AND name='core_meta';") | ||
|
||
var count int | ||
if err = row.Scan(&count); err != nil { | ||
t.Fatalf("reading table count result") | ||
} | ||
|
||
if count != 1 { | ||
t.Errorf("expected to find one result, got %d in count of core_meta table", count) | ||
} | ||
} | ||
|
||
func TestCoreMetaRoundtrip(t *testing.T) { | ||
dbc, err := New("sqlite", ":memory:") | ||
if err != nil { | ||
t.Fatalf("creating database connector: %s", err) | ||
} | ||
defer dbc.Close() | ||
|
||
var ( | ||
arbitrary struct{ A string } | ||
testKey = "arbitrary" | ||
) | ||
|
||
if err = dbc.ReadCoreMeta(testKey, &arbitrary); !errors.Is(err, ErrCoreMetaNotFound) { | ||
t.Error("expected core_meta not to contain key after init") | ||
} | ||
|
||
checkWriteRead := func(testString string) { | ||
arbitrary.A = testString | ||
if err = dbc.StoreCoreMeta(testKey, arbitrary); err != nil { | ||
t.Errorf("storing core_meta: %s", err) | ||
} | ||
|
||
arbitrary.A = "" // Clear to test unmarshal | ||
if err = dbc.ReadCoreMeta(testKey, &arbitrary); err != nil { | ||
t.Errorf("reading core_meta: %s", err) | ||
} | ||
|
||
if arbitrary.A != testString { | ||
t.Errorf("expected meta entry to have %q, got %q", testString, arbitrary.A) | ||
} | ||
} | ||
|
||
checkWriteRead("just a string") // Turn one: Init from not existing | ||
checkWriteRead("another random string") // Turn two: Overwrite | ||
} |
Oops, something went wrong.