package database import ( "context" "database/sql" "fmt" "os" "path/filepath" "time" _ "modernc.org/sqlite" ) func Open(path string) (*sql.DB, error) { directory := filepath.Dir(path) if directory != "." { if err := os.MkdirAll(directory, 0o750); err != nil { return nil, fmt.Errorf("create database directory: %w", err) } } db, err := sql.Open("sqlite", path+"?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)&_pragma=busy_timeout(5000)") if err != nil { return nil, err } db.SetMaxOpenConns(1) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { _ = db.Close() return nil, err } if err := migrate(ctx, db); err != nil { _ = db.Close() return nil, err } return db, nil } func migrate(ctx context.Context, db *sql.DB) error { const schema = ` CREATE TABLE IF NOT EXISTS users ( id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE COLLATE NOCASE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS refresh_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, token_hash TEXT NOT NULL UNIQUE, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL, revoked_at TEXT ); CREATE INDEX IF NOT EXISTS refresh_tokens_user_idx ON refresh_tokens(user_id); CREATE TABLE IF NOT EXISTS records ( user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, entity TEXT NOT NULL, record_id TEXT NOT NULL, payload TEXT, updated_at TEXT NOT NULL, deleted INTEGER NOT NULL DEFAULT 0, version INTEGER NOT NULL, PRIMARY KEY (user_id, entity, record_id) ); CREATE TABLE IF NOT EXISTS changes ( sequence INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, entity TEXT NOT NULL, record_id TEXT NOT NULL, payload TEXT, updated_at TEXT NOT NULL, deleted INTEGER NOT NULL, version INTEGER NOT NULL ); CREATE INDEX IF NOT EXISTS changes_user_sequence_idx ON changes(user_id, sequence); ` _, err := db.ExecContext(ctx, schema) return err }