Files
OwnCord/Server/db/db.go
T
jevb 36640e3051 feat: implement Phase 4 real-time chat (WebSocket hub + message REST)
- db: channel_queries (ListChannels, GetChannel, CRUD, permissions),
  message_queries (CreateMessage, GetMessage, GetMessages paginated,
  EditMessage, DeleteMessage soft, AddReaction, RemoveReaction,
  GetReactions, SearchMessages FTS5, UpdateReadState)
- db: fix in-memory DB isolation — SetMaxOpenConns(1) for :memory: path
- ws/hub: replace stub with full Hub (register/unregister, broadcast to
  channel/all, send to user, thread-safe, buffered broadcast channel)
- ws/client: Client with send channel, NewTestClient helpers for tests
- ws/handlers: dispatch chat_send/edit/delete, reaction_add/remove,
  typing_start, presence_update — all with rate limiting and permission checks
- ws/messages: JSON builder helpers for all server→client message types
- ws/serve: ServeWS HTTP handler, WS auth handshake (10s timeout),
  ready payload, writePump/readPump goroutines, graceful disconnect
- api: channel_handler — GET /channels, GET /channels/{id}/messages,
  GET /search; fixed double-mount of /api/v1 route group
- api/router: mount channel routes, start hub, register /api/v1/ws

Test coverage: api 77.6%, auth 90.9%, db 84.2%, ws 26.7% (serve.go
requires live WS connection; hub/handlers/messages fully covered)
2026-03-14 21:17:09 +01:00

123 lines
3.4 KiB
Go

// Package db provides database access for the OwnCord server.
// It uses modernc.org/sqlite — a pure-Go SQLite driver requiring no CGO.
package db
import (
"database/sql"
"fmt"
"io/fs"
"sort"
"strings"
"github.com/owncord/server/migrations"
_ "modernc.org/sqlite" // register the sqlite3 driver
)
// DB wraps *sql.DB and exposes the subset of methods needed by the server.
type DB struct {
sqlDB *sql.DB
}
// Open opens (or creates) a SQLite database at path, enables WAL mode and
// foreign key enforcement, and returns a ready-to-use DB.
func Open(path string) (*DB, error) {
sqlDB, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("opening sqlite db: %w", err)
}
// Verify the connection is actually usable.
if err := sqlDB.Ping(); err != nil {
sqlDB.Close()
return nil, fmt.Errorf("pinging sqlite db: %w", err)
}
// In-memory databases are per-connection in SQLite; pin to one connection
// so all callers share the same in-memory state.
if path == ":memory:" {
sqlDB.SetMaxOpenConns(1)
}
// Enable WAL mode for better concurrent read performance.
if _, err := sqlDB.Exec("PRAGMA journal_mode=WAL;"); err != nil {
sqlDB.Close()
return nil, fmt.Errorf("enabling WAL mode: %w", err)
}
// Enforce foreign key constraints.
if _, err := sqlDB.Exec("PRAGMA foreign_keys=ON;"); err != nil {
sqlDB.Close()
return nil, fmt.Errorf("enabling foreign keys: %w", err)
}
return &DB{sqlDB: sqlDB}, nil
}
// Migrate runs all SQL migration files from the embedded migrations FS in
// lexicographic order. It is idempotent — SQL uses IF NOT EXISTS / INSERT OR
// IGNORE, so re-running is safe.
func Migrate(database *DB) error {
return MigrateFS(database, migrations.FS)
}
// MigrateFS runs all *.sql files from the given FS in sorted order.
// This is exposed for testing with custom FS implementations.
func MigrateFS(database *DB, fsys fs.FS) error {
entries, err := fs.ReadDir(fsys, ".")
if err != nil {
return fmt.Errorf("reading migrations dir: %w", err)
}
// Sort files to ensure deterministic order.
sort.Slice(entries, func(i, j int) bool {
return entries[i].Name() < entries[j].Name()
})
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
continue
}
raw, readErr := fs.ReadFile(fsys, entry.Name())
if readErr != nil {
return fmt.Errorf("reading migration %s: %w", entry.Name(), readErr)
}
if _, execErr := database.sqlDB.Exec(string(raw)); execErr != nil {
return fmt.Errorf("executing migration %s: %w", entry.Name(), execErr)
}
}
return nil
}
// Close releases the underlying database connection.
func (d *DB) Close() error {
return d.sqlDB.Close()
}
// QueryRow executes a query that returns at most one row.
func (d *DB) QueryRow(query string, args ...interface{}) *sql.Row {
return d.sqlDB.QueryRow(query, args...)
}
// Exec executes a query that doesn't return rows.
func (d *DB) Exec(query string, args ...interface{}) (sql.Result, error) {
return d.sqlDB.Exec(query, args...)
}
// Query executes a query that returns multiple rows.
func (d *DB) Query(query string, args ...interface{}) (*sql.Rows, error) {
return d.sqlDB.Query(query, args...)
}
// Begin starts a database transaction.
func (d *DB) Begin() (*sql.Tx, error) {
return d.sqlDB.Begin()
}
// SQLDb returns the underlying *sql.DB for cases requiring direct access.
func (d *DB) SQLDb() *sql.DB {
return d.sqlDB
}