go-io/store/store.go
Virgil 9fb978dc75
Some checks failed
CI / test (push) Failing after 2s
CI / auto-fix (push) Failing after 0s
CI / auto-merge (push) Failing after 0s
refactor(ax): make docs and helpers example-driven
2026-03-30 20:47:41 +00:00

163 lines
4.4 KiB
Go

package store
import (
"database/sql"
"text/template"
core "dappco.re/go/core"
_ "modernc.org/sqlite"
)
// ErrNotFound is returned when a key does not exist in the store.
var ErrNotFound = core.E("store.ErrNotFound", "key not found", nil)
// Store is a group-namespaced key-value store backed by SQLite.
type Store struct {
database *sql.DB
}
// Use New to open a SQLite-backed key-value store.
// Use ":memory:" for tests.
//
// Example usage:
//
// kvStore, _ := store.New(":memory:")
// _ = kvStore.Set("app", "theme", "midnight")
func New(dbPath string) (*Store, error) {
database, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, core.E("store.New", "open db", err)
}
if _, err := database.Exec("PRAGMA journal_mode=WAL"); err != nil {
database.Close()
return nil, core.E("store.New", "WAL mode", err)
}
if _, err := database.Exec(`CREATE TABLE IF NOT EXISTS kv (
grp TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (grp, key)
)`); err != nil {
database.Close()
return nil, core.E("store.New", "create schema", err)
}
return &Store{database: database}, nil
}
// Example: _ = kvStore.Close()
func (s *Store) Close() error {
return s.database.Close()
}
// Example: theme, _ := kvStore.Get("app", "theme")
func (s *Store) Get(group, key string) (string, error) {
var value string
err := s.database.QueryRow("SELECT value FROM kv WHERE grp = ? AND key = ?", group, key).Scan(&value)
if err == sql.ErrNoRows {
return "", core.E("store.Get", core.Concat("not found: ", group, "/", key), ErrNotFound)
}
if err != nil {
return "", core.E("store.Get", "query", err)
}
return value, nil
}
// Example: _ = kvStore.Set("app", "theme", "midnight")
func (s *Store) Set(group, key, value string) error {
_, err := s.database.Exec(
`INSERT INTO kv (grp, key, value) VALUES (?, ?, ?)
ON CONFLICT(grp, key) DO UPDATE SET value = excluded.value`,
group, key, value,
)
if err != nil {
return core.E("store.Set", "exec", err)
}
return nil
}
// Example: _ = kvStore.Delete("app", "theme")
func (s *Store) Delete(group, key string) error {
_, err := s.database.Exec("DELETE FROM kv WHERE grp = ? AND key = ?", group, key)
if err != nil {
return core.E("store.Delete", "exec", err)
}
return nil
}
// Example: count, _ := kvStore.Count("app")
func (s *Store) Count(group string) (int, error) {
var count int
err := s.database.QueryRow("SELECT COUNT(*) FROM kv WHERE grp = ?", group).Scan(&count)
if err != nil {
return 0, core.E("store.Count", "query", err)
}
return count, nil
}
// Example: _ = kvStore.DeleteGroup("app")
func (s *Store) DeleteGroup(group string) error {
_, err := s.database.Exec("DELETE FROM kv WHERE grp = ?", group)
if err != nil {
return core.E("store.DeleteGroup", "exec", err)
}
return nil
}
// Example: values, _ := kvStore.GetAll("app")
func (s *Store) GetAll(group string) (map[string]string, error) {
rows, err := s.database.Query("SELECT key, value FROM kv WHERE grp = ?", group)
if err != nil {
return nil, core.E("store.GetAll", "query", err)
}
defer rows.Close()
result := make(map[string]string)
for rows.Next() {
var key, value string
if err := rows.Scan(&key, &value); err != nil {
return nil, core.E("store.GetAll", "scan", err)
}
result[key] = value
}
if err := rows.Err(); err != nil {
return nil, core.E("store.GetAll", "rows", err)
}
return result, nil
}
// Render loads all key-value pairs from a group and renders a Go template.
//
// Example usage:
//
// kvStore, _ := store.New(":memory:")
// _ = kvStore.Set("user", "name", "alice")
// out, _ := kvStore.Render("hello {{ .name }}", "user")
func (s *Store) Render(templateText, group string) (string, error) {
rows, err := s.database.Query("SELECT key, value FROM kv WHERE grp = ?", group)
if err != nil {
return "", core.E("store.Render", "query", err)
}
defer rows.Close()
templateValues := make(map[string]string)
for rows.Next() {
var key, value string
if err := rows.Scan(&key, &value); err != nil {
return "", core.E("store.Render", "scan", err)
}
templateValues[key] = value
}
if err := rows.Err(); err != nil {
return "", core.E("store.Render", "rows", err)
}
tmpl, err := template.New("render").Parse(templateText)
if err != nil {
return "", core.E("store.Render", "parse template", err)
}
builder := core.NewBuilder()
if err := tmpl.Execute(builder, templateValues); err != nil {
return "", core.E("store.Render", "execute template", err)
}
return builder.String(), nil
}