feat(m1): backend scaffold - entries CRUD, start/stop, auth, migrations
This commit is contained in:
91
internal/store/db.go
Normal file
91
internal/store/db.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user