generated from CubeCraft-Creations/Tracehound
CUB-183: SQLite schema migration + DB init
This commit is contained in:
@@ -1,2 +1,135 @@
|
||||
// Package db provides SQLite database initialization and schema management.
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
//go:embed migrations/001_create_tables.sql
|
||||
var migration001 string
|
||||
|
||||
// DB wraps the sql.DB with connection-level settings.
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
}
|
||||
|
||||
// Open opens the SQLite database at the given path, enables WAL mode,
|
||||
// and runs all migrations if the tables don't exist yet.
|
||||
func Open(path string) (*DB, error) {
|
||||
// Ensure the directory exists
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enable WAL for concurrent read/write performance
|
||||
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Enable foreign keys
|
||||
if _, err := db.Exec("PRAGMA foreign_keys=ON"); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if tables already exist (idempotent migration)
|
||||
var count int
|
||||
if err := db.QueryRow(`
|
||||
SELECT COUNT(*) FROM sqlite_master
|
||||
WHERE type='table' AND name IN ('cameras', 'status_logs', 'recording_events', 'settings')
|
||||
`).Scan(&count); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if count < 4 {
|
||||
log.Printf("Running migrations for %s...", path)
|
||||
if err := migrate(db, migration001); err != nil {
|
||||
db.Close()
|
||||
return nil, err
|
||||
}
|
||||
log.Println("Migrations complete")
|
||||
}
|
||||
|
||||
return &DB{db}, nil
|
||||
}
|
||||
|
||||
// migrate executes a SQL migration string.
|
||||
func migrate(db *sql.DB, sql string) error {
|
||||
// Split on semicolons to handle multiple statements
|
||||
statements := splitSQL(sql)
|
||||
for _, stmt := range statements {
|
||||
stmt = stripWhitespace(stmt)
|
||||
if stmt == "" {
|
||||
continue
|
||||
}
|
||||
if _, err := db.Exec(stmt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// splitSQL splits a SQL string on semicolons, respecting quoted strings.
|
||||
func splitSQL(sql string) []string {
|
||||
var stmts []string
|
||||
var current string
|
||||
inQuote := false
|
||||
quoteChar := rune(0)
|
||||
|
||||
for _, r := range sql {
|
||||
if inQuote {
|
||||
current += string(r)
|
||||
if r == quoteChar {
|
||||
inQuote = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '"', '\'', '`':
|
||||
inQuote = true
|
||||
quoteChar = r
|
||||
current += string(r)
|
||||
case ';':
|
||||
stmts = append(stmts, current)
|
||||
current = ""
|
||||
default:
|
||||
current += string(r)
|
||||
}
|
||||
}
|
||||
if len(current) > 0 {
|
||||
stmts = append(stmts, current)
|
||||
}
|
||||
return stmts
|
||||
}
|
||||
|
||||
// stripWhitespace removes leading/trailing whitespace and normalizes newlines.
|
||||
func stripWhitespace(s string) string {
|
||||
result := ""
|
||||
runningSpace := false
|
||||
for _, r := range s {
|
||||
if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
|
||||
if !runningSpace {
|
||||
result += " "
|
||||
runningSpace = true
|
||||
}
|
||||
} else {
|
||||
result += string(r)
|
||||
runningSpace = false
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user