feat: sqlite persistence with goose migrations, flashcard schema, zap logger
- Add SQLite as authoritative store alongside Qdrant (best-effort indexing) - Use goose for embedded SQL migrations - Schema matches flashcard format: id(INTEGER PK), note, repos, host, tags, user, context, created - Replace std log with zap structured logger - Split registerTools into registerStoreKnowledge + registerSearchKnowledge - Qdrant index uses SQLite rowid directly as point ID (no SHA256 hash) - Search enriches Qdrant results from SQLite by ID - Lowercase auth header before prefix check - Always handle json.MarshalIndent errors - NixOS: StateDirectory, sqlitePath to /var/lib/st-pierre/st-pierre.sqlite
This commit is contained in:
1 parent
5ac11631d4
commit
39c6185b70
8 files changed
+366
-94
No files matched your search
@@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pressly/goose/v3"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var embedMigrations embed.FS
|
||||
|
||||
type sqliteStore struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func newSqliteStore(path string) (*sqliteStore, error) {
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite: %w", err)
|
||||
}
|
||||
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
goose.SetBaseFS(embedMigrations)
|
||||
|
||||
if err := goose.SetDialect("sqlite3"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("set dialect: %w", err)
|
||||
}
|
||||
|
||||
if err := goose.Up(db, "migrations"); err != nil {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("migrate: %w", err)
|
||||
}
|
||||
|
||||
return &sqliteStore{db: db}, nil
|
||||
}
|
||||
|
||||
func (s *sqliteStore) save(ctx context.Context, note, host, user, context_ string, repos, tags []string) (int64, error) {
|
||||
reposJSON := marshalStringSlice(repos)
|
||||
tagsJSON := marshalStringSlice(tags)
|
||||
|
||||
result, err := s.db.ExecContext(ctx,
|
||||
`INSERT INTO knowledge (note, repos, host, tags, "user", context)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
note, reposJSON, host, tagsJSON, user, context_,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("insert: %w", err)
|
||||
}
|
||||
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("last insert id: %w", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *sqliteStore) getByID(ctx context.Context, id int64) (*entry, error) {
|
||||
row := s.db.QueryRowContext(ctx,
|
||||
`SELECT id, note, repos, host, tags, "user", context, created
|
||||
FROM knowledge WHERE id = ?`, id,
|
||||
)
|
||||
|
||||
var e entry
|
||||
var reposJSON, tagsJSON sql.NullString
|
||||
var host, user, context_ sql.NullString
|
||||
|
||||
err := row.Scan(&e.ID, &e.Note, &reposJSON, &host, &tagsJSON, &user, &context_, &e.Created)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query: %w", err)
|
||||
}
|
||||
|
||||
if reposJSON.Valid {
|
||||
e.Repos = unmarshalStringSlice(reposJSON.String)
|
||||
}
|
||||
if tagsJSON.Valid {
|
||||
e.Tags = unmarshalStringSlice(tagsJSON.String)
|
||||
}
|
||||
if host.Valid {
|
||||
e.Host = host.String
|
||||
}
|
||||
if user.Valid {
|
||||
e.User = user.String
|
||||
}
|
||||
if context_.Valid {
|
||||
e.Context = context_.String
|
||||
}
|
||||
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
func (s *sqliteStore) close() error {
|
||||
return s.db.Close()
|
||||
}
|
||||
|
||||
func marshalStringSlice(ss []string) string {
|
||||
if len(ss) == 0 {
|
||||
return "[]"
|
||||
}
|
||||
b, _ := json.Marshal(ss)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func unmarshalStringSlice(raw string) []string {
|
||||
if raw == "" || raw == "[]" || raw == "null" {
|
||||
return nil
|
||||
}
|
||||
var ss []string
|
||||
if err := json.Unmarshal([]byte(raw), &ss); err != nil {
|
||||
return nil
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
func parseStringSlice(raw any) []string {
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
arr, ok := raw.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(arr))
|
||||
for _, v := range arr {
|
||||
if s, ok := v.(string); ok {
|
||||
result = append(result, s)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func parseOptionalString(raw any) string {
|
||||
if s, ok := raw.(string); ok && strings.TrimSpace(s) != "" {
|
||||
return s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in new issue
Block a user