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