Files
OwnCord/docs/architecture/data-model.md
T
Claude 5f1d6fc287 refactor(server): remove the store abstraction layer (D3)
Deletes Server/store (SQLiteStore, MemStore, the composed Store
interface) and collapses to a single sqlc-backed db package, executing
the prior audit's P4 "single data layer" direction (finding #6).

SQLiteStore was a pure pass-through to *db.DB, so consumers now depend
on narrow interfaces that *db.DB satisfies directly:

  - service.Store   (service/datastore.go, renamed from store/store.go)
  - ws.EventStore   (ws/eventstore.go)
  - plugin.PluginStore (plugin/pluginstore.go)

The event- and plugin-KV methods that lived in the store's SQLite
implementation move into the db package (db/event_queries.go,
db/plugin_queries.go), keeping their raw-SQL form.

Tests: the MemStore-based unit tests now run against a real in-memory
SQLite db opened per-test with migrations applied, via package-local
seed helpers. Fault-injection tests embed a real *db.DB and override the
single method under test, preserving error-path coverage. Full server
suite and sqlc-verify are green.

Docs: audit finding #6 and A-2026-07-06 marked resolved; decisions D3
updated; architecture server.md / data-model.md diagrams and prose
updated to the api -> service -> db layering.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 16:33:58 +00:00

129 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Data Model
**Verified against:** commit `ddc49f0`, 2026-07-19
The canonical schema is the ordered migration set `Server/migrations/001015`
(embedded via `go:embed`, applied by the custom runner in `Server/db/migrate.go`,
tracked in the `schema_versions` table). SQLite is the only supported engine —
`Server/main.go` rejects any other `database.type` at startup.
## D5 — Entity-relationship overview
All 23 application tables, grouped by domain. Junction/leaf detail columns are
elided; the goal is the relationship graph, not full DDL (see `docs/schema.md`
for DDL — note it is currently 6 migrations behind, see
[audit-2026-07-19.md §2](../audit-2026-07-19.md)).
```mermaid
erDiagram
%% ── Identity & access ──
roles ||--o{ users : "role_id"
users ||--o{ sessions : "user_id"
roles ||--o{ channel_overrides : "role_id"
channels ||--o{ channel_overrides : "channel_id"
users ||--o{ user_blocks : "blocker_id / blocked_id"
users ||--o{ invites : "created_by / redeemed_by"
%% ── Messaging ──
channels ||--o{ messages : "channel_id"
users ||--o{ messages : "user_id"
messages ||--o{ messages : "reply_to"
messages ||--o{ attachments : "message_id"
messages ||--o{ reactions : "message_id"
users ||--o{ reactions : "user_id"
users ||--o{ read_states : "user_id"
channels ||--o{ read_states : "channel_id"
%% ── Direct messages (channels with type='dm') ──
channels ||--o{ dm_participants : "channel_id"
users ||--o{ dm_participants : "user_id"
channels ||--o{ dm_open_state : "channel_id"
users ||--o{ dm_open_state : "user_id"
%% ── Voice ──
users ||--o| voice_states : "user_id (PK)"
channels ||--o{ voice_states : "channel_id"
%% ── Plugins ──
plugins ||--o{ plugin_kv : "plugin_id"
%% ── Standalone (no FK edges) ──
events
settings
audit_log
login_attempts
rate_lockouts
emoji
sounds
users {
int id PK
int role_id FK
string username
string password_hash
bool banned
}
roles {
int id PK
string name
int permissions "31-bit bitfield"
int position
}
channels {
int id PK
string name
string type "text | voice | dm (trigger-enforced)"
}
messages {
int id PK
int channel_id FK
int user_id FK
int reply_to FK
string content
}
attachments {
string id PK "UUID"
int message_id FK
int uploader_id "added by 010"
}
events {
int seq PK "AUTOINCREMENT, hub seq seeded from MAX(seq)"
string type
string payload
}
```
### Domain notes
| Domain | Tables | Notes |
|--------|--------|-------|
| Identity & access | `roles`, `users`, `sessions`, `channel_overrides`, `user_blocks`, `invites`, `login_attempts`, `rate_lockouts` | Sessions store only SHA-256 token hashes. Permissions are a bitfield on `roles.permissions`; channel overrides use Discord semantics `(role &^ deny) \| allow`. `rate_lockouts` (011) persists rate-limiter lockouts across restarts. |
| Messaging | `channels`, `messages`, `attachments`, `reactions`, `read_states`, `emoji` | `channels.type` is constrained to `text \| voice \| announcement \| dm` by INSERT/UPDATE triggers (migration 013, extended by 016 to allow `announcement`). Announcement channels read like text but require `MANAGE_MESSAGES` to post. `attachments.uploader_id` (010) backs upload-ownership checks. |
| Direct messages | `dm_participants`, `dm_open_state` | DMs are `channels` rows with `type='dm'`; these tables track membership and per-user open/closed UI state (009). |
| Voice | `voice_states` | One row per user (`user_id` is the PK) — a user occupies at most one voice channel. |
| Real-time replay | `events` | Cold tier of the 3-tier reconnect replay ([websocket.md](websocket.md)); written by the async `EventPersister`, pruned by retention. Hub seq counter is seeded from `MAX(events.seq)` at startup so seqs stay monotonic across restarts. |
| Plugins | `plugins`, `plugin_kv` | 015. `plugin_kv` is per-plugin namespaced KV via composite PK `(plugin_id, key)`. |
| Ops | `settings`, `audit_log`, `sounds` | `settings` is a generic KV read by admin and the WS hub (via `db.GetSetting`). Migration 003 rebuilds `audit_log` through a transient `audit_log_v6` rename — only `audit_log` exists at runtime. `sounds` is **dead schema** — the soundboard feature was removed but the table remains. |
### How the schema is accessed
The `Server/db` package is the single data layer. Its methods live in
`*_queries.go` and mostly delegate to the sqlc-generated `Server/db/dbgen` code
(D2), so sqlc is the type-checked query layer rather than dead generated code; a
documented remainder of raw queries stays hand-written where sqlc can't express
them (variable-length `IN` lists, FTS, multi-statement transactions,
PRAGMA/VACUUM) — see [plans/sqlc-adoption.md](../plans/sqlc-adoption.md).
Consumers depend on narrow interfaces that `*db.DB` satisfies rather than on the
concrete type: `service.Store` (the service layer), `ws.EventStore` (cold-tier
replay), and `plugin.PluginStore` (plugin registry). D3 removed the former
`store` package (a pass-through `SQLiteStore` plus a `MemStore` test double);
tests now run against a real in-memory SQLite `db`. The residual style issue is
that many `Server/api` handlers and the `Server/admin` package still take a raw
`*db.DB` directly instead of going through the service layer — the remaining
consolidation work (audit A-2026-07-06).
**Source of truth:** `Server/migrations/*.sql` (schema), `Server/db/migrate.go`
(runner), `Server/db/*_queries.go` (live queries), `Server/service/datastore.go`
(service interface), `sqlc.yaml` + `Server/db/dbgen/` (generated query layer).