2023-04-20 18:08:42 +00:00
|
|
|
package store
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"database/sql"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"github.com/gopatchy/metadata"
|
|
|
|
|
// Register sqlite3 db handler.
|
|
|
|
|
_ "github.com/mattn/go-sqlite3"
|
|
|
|
|
)
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
type Store struct {
|
2023-04-20 18:08:42 +00:00
|
|
|
db *sql.DB
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
func NewStore(conn string) (*Store, error) {
|
2023-04-20 18:08:42 +00:00
|
|
|
db, err := sql.Open("sqlite3", conn)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Keep a set of prepared statements with PrepareContext()
|
|
|
|
|
// TODO: Consider tuning per https://phiresky.github.io/blog/2020/sqlite-performance-tuning/
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
return &Store{
|
2023-04-20 18:08:42 +00:00
|
|
|
db: db,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
func (s *Store) Close() {
|
|
|
|
|
s.db.Close()
|
2023-04-20 18:08:42 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
func (s *Store) Write(ctx context.Context, t string, obj any) error {
|
2023-04-20 18:08:42 +00:00
|
|
|
id := metadata.GetMetadata(obj).ID
|
|
|
|
|
|
|
|
|
|
js, err := json.Marshal(obj)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
err = s.exec(ctx, "INSERT INTO `%s` (id, obj) VALUES (?,?) ON CONFLICT(id) DO UPDATE SET obj=?;", t, id, js, js)
|
2023-04-20 18:08:42 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
func (s *Store) Delete(ctx context.Context, t, id string) error {
|
|
|
|
|
err := s.exec(ctx, "DELETE FROM `%s` WHERE id=?", t, id)
|
2023-04-20 18:08:42 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
func (s *Store) Read(ctx context.Context, t, id string, factory func() any) (any, error) {
|
|
|
|
|
rows, err := s.query(ctx, "SELECT obj FROM `%s` WHERE id=?;", t, id)
|
2023-04-20 18:08:42 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
if !rows.Next() {
|
|
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var js []byte
|
|
|
|
|
|
|
|
|
|
err = rows.Scan(&js)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj := factory()
|
|
|
|
|
|
|
|
|
|
err = json.Unmarshal(js, obj)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return obj, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
func (s *Store) List(ctx context.Context, t string, factory func() any) ([]any, error) {
|
|
|
|
|
rows, err := s.query(ctx, "SELECT obj FROM `%s`;", t)
|
2023-04-20 18:08:42 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
|
ret := []any{}
|
|
|
|
|
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var js []byte
|
|
|
|
|
|
|
|
|
|
err = rows.Scan(&js)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
obj := factory()
|
|
|
|
|
|
|
|
|
|
err = json.Unmarshal(js, obj)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ret = append(ret, obj)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ret, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
func (s *Store) exec(ctx context.Context, query, t string, args ...any) error {
|
2023-04-20 18:08:42 +00:00
|
|
|
query = fmt.Sprintf(query, t)
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
_, err := s.db.ExecContext(ctx, query, args...)
|
2023-04-20 18:08:42 +00:00
|
|
|
if err == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
_, err = s.db.ExecContext(ctx, s.tableSQL(t))
|
2023-04-20 18:08:42 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
_, err = s.db.ExecContext(ctx, query, args...)
|
2023-04-20 18:08:42 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
func (s *Store) query(ctx context.Context, query, t string, args ...any) (*sql.Rows, error) {
|
2023-04-20 18:08:42 +00:00
|
|
|
query = fmt.Sprintf(query, t)
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
rows, err := s.db.QueryContext(ctx, query, args...)
|
2023-04-20 18:08:42 +00:00
|
|
|
if err == nil {
|
|
|
|
|
return rows, nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
_, err = s.db.ExecContext(ctx, s.tableSQL(t))
|
2023-04-20 18:08:42 +00:00
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
return s.db.QueryContext(ctx, query, args...)
|
2023-04-20 18:08:42 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-20 18:21:44 +00:00
|
|
|
func (s *Store) tableSQL(t string) string {
|
2023-04-20 18:08:42 +00:00
|
|
|
return fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s` (id TEXT NOT NULL PRIMARY KEY, obj TEXT NOT NULL);", t)
|
|
|
|
|
}
|