feat(m1): backend scaffold - entries CRUD, start/stop, auth, migrations

This commit is contained in:
2026-04-30 16:35:06 +02:00
parent 4905c6f570
commit 3aa068efd2
19 changed files with 1483 additions and 0 deletions

91
internal/store/db.go Normal file
View File

@@ -0,0 +1,91 @@
package store
import (
"context"
"database/sql"
_ "embed"
"fmt"
"strings"
_ "modernc.org/sqlite"
)
//go:embed 001_initial.sql
var schema string
// Open opens (or creates) the SQLite database at path and runs migrations.
func Open(path string) (*sql.DB, error) {
dsn := fmt.Sprintf(
"file:%s?_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)&_pragma=foreign_keys(ON)&_pragma=synchronous(NORMAL)",
path,
)
db, err := sql.Open("sqlite", dsn)
if err != nil {
return nil, fmt.Errorf("open sqlite: %w", err)
}
db.SetMaxOpenConns(1) // SQLite WAL: single writer
if err := migrate(db); err != nil {
db.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
return db, nil
}
// migrate runs embedded SQL migrations. Simple single-file approach: we track
// a user_version pragma and apply the schema once if version == 0.
func migrate(db *sql.DB) error {
var version int
if err := db.QueryRow("PRAGMA user_version").Scan(&version); err != nil {
return err
}
if version >= 1 {
return nil
}
ctx := context.Background()
// Strip migration comments and split into individual statements
stmts := splitStatements(schema)
for _, stmt := range stmts {
stmt = strings.TrimSpace(stmt)
if stmt == "" {
continue
}
if _, err := db.ExecContext(ctx, stmt); err != nil {
return fmt.Errorf("exec %q: %w", stmt[:min(len(stmt), 60)], err)
}
}
// PRAGMA user_version cannot be set inside a regular transaction in all SQLite versions;
// execute it as a standalone statement.
if _, err := db.ExecContext(ctx, "PRAGMA user_version = 1"); err != nil {
return err
}
return nil
}
func splitStatements(sql string) []string {
// Only process statements up to the "-- +migrate Down" marker.
var lines []string
for _, line := range strings.Split(sql, "\n") {
trimmed := strings.TrimSpace(line)
if trimmed == "-- +migrate Down" {
break
}
if strings.HasPrefix(trimmed, "-- +migrate") {
continue
}
lines = append(lines, line)
}
joined := strings.Join(lines, "\n")
// Split on semicolons
parts := strings.Split(joined, ";")
return parts
}
func min(a, b int) int {
if a < b {
return a
}
return b
}