627 lines
22 KiB
Markdown
627 lines
22 KiB
Markdown
---
|
|
title: "Database Schema & Operations Guide"
|
|
version: 3.8.40
|
|
lastUpdated: 2026-06-28
|
|
---
|
|
|
|
# Database Schema & Operations Guide
|
|
|
|
> **TL;DR**: OmniRoute uses **SQLite with WAL journaling** as its primary store, with **AES-256-GCM** encryption at rest for sensitive fields. This guide covers the schema, migrations, backup/recovery, and operational runbooks.
|
|
|
|
**Sources:**
|
|
|
|
- `src/lib/db/core.ts` — singleton + SCHEMA_SQL (17 base tables)
|
|
- `src/lib/db/migrationRunner.ts` — versioned migrations
|
|
- `src/lib/db/migrations/` — 106 versioned SQL files
|
|
- `src/lib/db/encryption.ts` — encryption helpers
|
|
- `src/lib/db/backup.ts` — backup export/import
|
|
- `src/lib/db/healthCheck.ts` — health diagnostics
|
|
|
|
---
|
|
|
|
## Why SQLite?
|
|
|
|
OmniRoute chose SQLite over PostgreSQL/MySQL for several reasons:
|
|
|
|
| Factor | SQLite | PostgreSQL |
|
|
| --------------- | --------------------------------- | --------------------------------- |
|
|
| **Deployment** | Embedded — no separate server | Requires server setup |
|
|
| **Encryption** | Application-layer (AES-256-GCM) | Built-in TDE |
|
|
| **Performance** | Faster for small/medium workloads | Better for huge concurrent writes |
|
|
| **Concurrency** | WAL mode allows concurrent reads | Full MVCC |
|
|
| **Backup** | Single-file copy | `pg_dump` or filesystem snapshot |
|
|
| **Use case** | Per-user install, embedded | Multi-tenant SaaS |
|
|
|
|
For **single-user, single-instance** deployments (the primary OmniRoute use case), SQLite is simpler and faster.
|
|
|
|
### WAL Journaling
|
|
|
|
`core.ts` opens the database with **WAL (Write-Ahead Logging) mode**:
|
|
|
|
```ts
|
|
// src/lib/db/core.ts
|
|
db.pragma("journal_mode = WAL");
|
|
db.pragma("busy_timeout = 2000");
|
|
db.pragma("synchronous = NORMAL");
|
|
// Settings > System & Storage > Cache Size is applied as KiB.
|
|
db.pragma("cache_size = -16384");
|
|
```
|
|
|
|
WAL allows **concurrent reads** during writes — important for the dashboard, which queries while requests are being recorded.
|
|
|
|
---
|
|
|
|
## Database Location
|
|
|
|
The SQLite file is stored at:
|
|
|
|
| OS | Path |
|
|
| ------- | -------------------------------------------------------- |
|
|
| Linux | `~/.omniroute/storage.sqlite` |
|
|
| macOS | `~/.omniroute/storage.sqlite` |
|
|
| Windows | `%USERPROFILE%\.omniroute\storage.sqlite` |
|
|
| Docker | `/app/data/storage.sqlite` (configurable via `DATA_DIR`) |
|
|
|
|
Companion files:
|
|
|
|
- `storage.sqlite-wal` — write-ahead log
|
|
- `storage.sqlite-shm` — shared memory file
|
|
- `call_logs/` — request payload artifacts (if enabled)
|
|
|
|
**Override the location:**
|
|
|
|
```bash
|
|
DATA_DIR=/custom/path omniroute
|
|
```
|
|
|
|
---
|
|
|
|
## Domain Module Architecture
|
|
|
|
OmniRoute's database has **94 domain modules** in `src/lib/db/`. Each module:
|
|
|
|
- Owns one or more specific tables
|
|
- Exports typed CRUD functions
|
|
- Never touches another module's tables
|
|
- Uses `getDbInstance()` from `core.ts` to access the DB
|
|
|
|
### The 94 DB Modules
|
|
|
|
OmniRoute has **94 module files** in `src/lib/db/`. Below is a sampling of core modules; see the directory listing for the complete list:
|
|
|
|
| Module | Tables | Responsibility |
|
|
| ----------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
|
| `providers.ts` | `provider_connections` | OAuth/API key provider registration and credentials |
|
|
| `models.ts` | `key_value` (model data) | Model definitions, capabilities, pricing |
|
|
| `combos.ts` | `combos` | Combo routing configs and ordering |
|
|
| `apiKeys.ts` | `api_keys` | API key lifecycle, scopes, quota tracking |
|
|
| `settings.ts` | `key_value`, `api_keys`, `combos` | System configuration and shared KV store |
|
|
| `backup.ts` | — | Backup export/import operations |
|
|
| `proxies.ts` | `proxy_registry`, `proxy_assignments`, `provider_connections` | Proxy configs and routing rules |
|
|
| `prompts.ts` | `prompt_templates` | Reusable prompt templates, versioning |
|
|
| `webhooks.ts` | `webhooks` | Event-driven webhook subscriptions and logs |
|
|
| `detailedLogs.ts` | `request_detail_logs` | Per-request audit logging (optional, high volume) |
|
|
| `domainState.ts` | `domain_*` (5 tables) | Domain budgets, circuit breakers, lockouts, fallback chains, cost history |
|
|
| `registeredKeys.ts` | `registered_keys`, `account_key_limits`, `provider_key_limits` | Whitelisted API keys for MCP/A2A |
|
|
| `quotaSnapshots.ts` | `quota_snapshots` | Historical quota usage |
|
|
| `modelComboMappings.ts` | `model_combo_mappings` | Map models to combo defaults |
|
|
| `cliToolState.ts` | `cli_tool_state` | CLI-specific persistent state |
|
|
| `encryption.ts` | — | Helpers for encrypting/decrypting fields |
|
|
| `readCache.ts` | — | In-memory cache for read-heavy ops |
|
|
| `secrets.ts` | `key_value` (encrypted entries) | Encrypted secret storage |
|
|
| `stateReset.ts` | — | Wipe/reset DB state for testing |
|
|
| `contextHandoffs.ts` | `context_handoffs` | Session context for agent handoff |
|
|
| `usage*.ts` | `usage_history`, `call_logs`, `proxy_logs` | Usage tracking |
|
|
| `compression*.ts` | `compression_settings`, `compression_combos` | Compression config |
|
|
|
|
### Module Boundaries
|
|
|
|
A core architectural rule: **modules don't access each other's tables directly**. To work with another module's data, import the function from that module.
|
|
|
|
```ts
|
|
// ❌ WRONG: direct SQL from another module
|
|
db.prepare("SELECT * FROM provider_connections").all();
|
|
|
|
// ✅ RIGHT: use the providers module function
|
|
import { listProviders } from "@/lib/db/providers";
|
|
const providers = await listProviders();
|
|
```
|
|
|
|
This rule is enforced by code review — there's no static check, but violations are flagged.
|
|
|
|
---
|
|
|
|
## Base Schema (17 tables)
|
|
|
|
`core.ts` defines the 17 base tables in `SCHEMA_SQL`. These are created by migration `001_initial_schema.sql` and form the core schema.
|
|
|
|
### Core Tables (created in initial migration)
|
|
|
|
| Table | Purpose | Key columns |
|
|
| -------------------------- | -------------------------------- | ----------------------------------------------------------------------- |
|
|
| `provider_connections` | Provider credentials (encrypted) | `id`, `provider`, `auth_type`, `api_key`, `is_active` |
|
|
| `provider_nodes` | Provider node routing info | `id`, `type`, `name`, `base_url`, `created_at` |
|
|
| `key_value` | General KV store | `namespace`, `key`, `value` |
|
|
| `combos` | Routing combo definitions | `id`, `name`, `data`, `sort_order` |
|
|
| `api_keys` | API keys for the gateway | `id`, `name`, `key`, `machine_id`, `allowed_models` |
|
|
| `db_meta` | Database metadata | `key`, `value` |
|
|
| `usage_history` | Request usage records | `id`, `provider`, `model`, `tokens_input`, `tokens_output`, `timestamp` |
|
|
| `call_logs` | Request payloads & responses | `id`, `timestamp`, `status`, `model`, `provider`, `latency_ms` |
|
|
| `proxy_logs` | Proxy request logs | `id`, `timestamp`, `proxy_type`, `status`, `provider` |
|
|
| `domain_fallback_chains` | Model-to-provider chains | `model`, `chain` |
|
|
| `domain_budgets` | Per-domain spend budgets | `api_key_id`, `daily_limit_usd`, `warning_threshold`, `reset_interval` |
|
|
| `domain_budget_reset_logs` | Budget reset history | `id`, `api_key_id`, `reset_interval`, `previous_spend`, `reset_at` |
|
|
| `domain_cost_history` | Per-domain cost tracking | `id`, `api_key_id`, `cost`, `timestamp` |
|
|
| `domain_lockout_state` | Domain rate-limit state | `identifier`, `attempts`, `locked_until` |
|
|
| `domain_circuit_breakers` | Circuit breaker state per domain | `name`, `state`, `failure_count`, `last_failure_time` |
|
|
| `semantic_cache` | LLM response cache | `id`, `signature`, `model`, `prompt_hash`, `response` |
|
|
| `quota_snapshots` | Historical quota snapshots | `id`, `provider`, `connection_id`, `window_key`, `remaining_percentage` |
|
|
|
|
### Additional Tables (added by later migrations)
|
|
|
|
Subsequent migrations add tables such as:
|
|
|
|
- `cli_tool_state` (migration 011) — CLI tool state
|
|
- `mcp_*` tables — MCP server audit
|
|
- `a2a_*` tables — A2A task state
|
|
- `usage_*` tables — usage tracking
|
|
- `plugin_*` tables — plugin system
|
|
- `skill_executions` — skill execution history
|
|
- `memory_*` tables — memory system
|
|
- `compression_*` tables — compression system
|
|
- `webhook_*` tables — webhook delivery log
|
|
- `acp_*` tables — Agent Client Protocol
|
|
- `oneproxy_*` tables — 1proxy marketplace
|
|
- `proxy_assignments` — proxy scope bindings
|
|
- `detailed_call_artifacts` — call log artifacts metadata
|
|
- `quota_alert_history` — quota alert audit
|
|
- `command_code_auth_sessions` — Command Code OAuth sessions
|
|
|
|
The full list of ~30+ tables is in `src/lib/db/migrations/`.
|
|
|
|
---
|
|
|
|
## Migrations
|
|
|
|
OmniRoute uses **versioned, idempotent migrations** in `src/lib/db/migrations/`. Each migration is a single SQL file named `NNN_description.sql`.
|
|
|
|
### Migration Naming
|
|
|
|
```
|
|
001_initial_schema.sql
|
|
002_mcp_a2a_tables.sql
|
|
003_provider_node_custom_paths.sql
|
|
...
|
|
021_combo_call_log_targets.sql
|
|
```
|
|
|
|
### How Migrations Run
|
|
|
|
At startup, `migrationRunner.ts`:
|
|
|
|
1. Creates `_omniroute_migrations` table if not exists
|
|
2. Queries for already-applied migrations
|
|
3. Applies any new migrations in order, each in a transaction
|
|
4. Records each applied migration with timestamp
|
|
|
|
```ts
|
|
// src/lib/db/migrationRunner.ts (simplified)
|
|
export async function runMigrations(db: SqliteDatabase, migrationsDir: string) {
|
|
const applied = getAppliedMigrations(db);
|
|
const available = readMigrationFiles(migrationsDir);
|
|
|
|
for (const migration of available) {
|
|
if (applied.includes(migration.id)) continue;
|
|
db.transaction(() => {
|
|
db.exec(migration.sql);
|
|
recordAppliedMigration(db, migration.id);
|
|
})();
|
|
}
|
|
}
|
|
```
|
|
|
|
### Idempotency
|
|
|
|
Migrations must be **idempotent** — running them twice should be a no-op:
|
|
|
|
```sql
|
|
-- 004_proxy_registry.sql
|
|
CREATE TABLE IF NOT EXISTS proxy_registry (
|
|
id TEXT PRIMARY KEY,
|
|
host TEXT NOT NULL,
|
|
port INTEGER NOT NULL,
|
|
...
|
|
);
|
|
```
|
|
|
|
Use `IF NOT EXISTS`, `IF EXISTS`, and `OR IGNORE` / `OR REPLACE` clauses liberally.
|
|
|
|
### Adding a New Migration
|
|
|
|
1. **Identify the next number**: `ls src/lib/db/migrations/ | tail -1`
|
|
2. **Create the file**: `NNN_my_change.sql`
|
|
3. **Use safe DDL**: `CREATE TABLE IF NOT EXISTS`, `ALTER TABLE ... ADD COLUMN`
|
|
4. **Backfill data carefully**: use `UPDATE ... WHERE ...` to handle existing rows
|
|
5. **Test on a copy**: never run untested migrations on production
|
|
|
|
Example:
|
|
|
|
```sql
|
|
-- 022_add_combo_priority.sql
|
|
ALTER TABLE combos ADD COLUMN priority INTEGER DEFAULT 100;
|
|
UPDATE combos SET priority = 100 WHERE priority IS NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_combos_priority ON combos(priority);
|
|
```
|
|
|
|
> **Backwards-incompatible changes** (e.g., dropping columns) are tricky. OmniRoute does NOT support downgrade — once a migration is applied, the schema change is permanent. Plan accordingly.
|
|
|
|
---
|
|
|
|
## Encryption at Rest
|
|
|
|
Sensitive fields (API keys, OAuth tokens, connection strings) are encrypted at rest using **AES-256-GCM**.
|
|
|
|
### How It Works
|
|
|
|
```ts
|
|
// src/lib/db/encryption.ts (simplified)
|
|
const key = deriveKeyFromPassphrase(passphrase, salt);
|
|
const iv = randomBytes(12);
|
|
const cipher = createCipheriv("aes-256-gcm", key, iv);
|
|
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
const authTag = cipher.getAuthTag();
|
|
return { encrypted, iv, authTag };
|
|
```
|
|
|
|
### Where It's Used
|
|
|
|
- `provider_connections.api_key` — encrypted at application level
|
|
- `provider_connections.access_token`, `refresh_token`, `id_token` — encrypted at application level
|
|
- `key_value` entries with `namespace = "secrets"` — encrypted at application level
|
|
- `proxy_registry.auth` — encrypted at application level (if present)
|
|
|
|
### Encryption Key
|
|
|
|
The encryption key is derived from a **passphrase** (set via `STORAGE_ENCRYPTION_KEY` env var) and a **salt** (stored in the DB). Both are required to decrypt data.
|
|
|
|
```bash
|
|
# Generate a secure passphrase
|
|
openssl rand -hex 32
|
|
|
|
# Set in .env
|
|
STORAGE_ENCRYPTION_KEY=<your-key>
|
|
```
|
|
|
|
> **Critical**: Losing the encryption key means losing access to all encrypted data. **Back up the key separately from the database**.
|
|
|
|
### What's NOT Encrypted
|
|
|
|
For performance reasons, the following are stored in plaintext:
|
|
|
|
- Provider display names
|
|
- Model definitions (already public)
|
|
- Routing rules
|
|
- Usage records (no PII)
|
|
|
|
---
|
|
|
|
## Encryption Caveats (v3.8.16+)
|
|
|
|
OmniRoute uses **`migrateLegacyEncryptedString()`** to handle two encryption schemes transparently:
|
|
|
|
- **Legacy** (pre-v3.5.0): XOR-based "encryption" (not real crypto)
|
|
- **Current**: AES-256-GCM with proper IV and auth tag
|
|
|
|
The migration helper detects the legacy format and re-encrypts with the new scheme on first read. This means you can upgrade an old database without losing credentials.
|
|
|
|
---
|
|
|
|
## Read Cache
|
|
|
|
For frequently-read data (models, providers, settings), `readCache.ts` provides an **in-memory cache**:
|
|
|
|
```ts
|
|
// Cached at startup, invalidated on write
|
|
const providers = await getCachedProviders(); // Fast, in-memory
|
|
const fresh = await listProviders(); // Slow, hits DB
|
|
```
|
|
|
|
| Cached entity | Cache key | TTL |
|
|
| ---------------------- | -------------- | ----------- |
|
|
| `models` | `models:v1` | Until write |
|
|
| `provider_connections` | `providers:v1` | Until write |
|
|
| `settings` | `settings:v1` | Until write |
|
|
| `combos` | `combos:v1` | Until write |
|
|
|
|
Cache is invalidated on every write to the corresponding table.
|
|
|
|
---
|
|
|
|
## Backup and Recovery
|
|
|
|
### Manual Backup
|
|
|
|
```bash
|
|
# Use the CLI to create a local backup
|
|
omniroute backup create --name pre-migration
|
|
|
|
# Or via the API
|
|
curl -X PUT http://localhost:20128/api/db-backups \
|
|
-H "Authorization: Bearer $MANAGEMENT_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name": "pre-migration"}'
|
|
```
|
|
|
|
The backup file includes:
|
|
|
|
- All DB tables (serialized to JSON)
|
|
- Call log artifacts (base64-encoded, optional)
|
|
- Settings + secrets (encrypted)
|
|
- Plugin configuration
|
|
|
|
### Restore
|
|
|
|
```bash
|
|
# Via CLI
|
|
omniroute restore pre-migration
|
|
|
|
# Via API
|
|
curl -X POST http://localhost:20128/api/db-backups/restore \
|
|
-H "Authorization: Bearer $MANAGEMENT_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name": "pre-migration"}'
|
|
```
|
|
|
|
> **Warning**: Restore overwrites the entire DB. Stop all clients first.
|
|
|
|
### Automated Backups
|
|
|
|
```bash
|
|
# Enable automated daily backups via CLI
|
|
omniroute backup auto enable --cron "0 2 * * *" --retention 7
|
|
```
|
|
|
|
### SQLite Hot Backup
|
|
|
|
For zero-downtime backup of a live DB:
|
|
|
|
```bash
|
|
sqlite3 ~/.omniroute/storage.sqlite ".backup /backups/omniroute-hot.db"
|
|
```
|
|
|
|
This uses SQLite's online backup API — safe to run while OmniRoute is running.
|
|
|
|
---
|
|
|
|
## Performance Tuning
|
|
|
|
### WAL Mode
|
|
|
|
WAL is enabled by default. For high-write workloads, consider:
|
|
|
|
```sql
|
|
PRAGMA wal_autocheckpoint = 1000; -- Checkpoint every 1000 pages
|
|
PRAGMA journal_size_limit = 67108864; -- 64MB WAL cap
|
|
```
|
|
|
|
### Indexes
|
|
|
|
Key indexes for performance (auto-created by migrations):
|
|
|
|
- `idx_models_provider` — model lookups by provider
|
|
- `idx_combo_targets_combo_id` — combo target expansion
|
|
- `idx_usage_history_api_key_timestamp` — usage analytics
|
|
- `idx_quota_snapshots_api_key_window` — quota tracking
|
|
- `idx_call_logs_timestamp` — call log queries
|
|
|
|
To add a new index, create a migration:
|
|
|
|
```sql
|
|
-- 023_add_my_index.sql
|
|
CREATE INDEX IF NOT EXISTS idx_my_table_my_column ON my_table(my_column);
|
|
```
|
|
|
|
### Memory-Mapped I/O
|
|
|
|
For very large databases (>10GB), memory mapping can be adjusted via SQLite pragma:
|
|
|
|
```sql
|
|
-- Set via SQLite pragma (adjust in core.ts or runtime)
|
|
PRAGMA mmap_size = 268435456; -- 256MB
|
|
```
|
|
|
|
### Compaction
|
|
|
|
Long-running OmniRoute instances benefit from occasional `VACUUM`:
|
|
|
|
```bash
|
|
sqlite3 ~/.omniroute/storage.sqlite "VACUUM;"
|
|
```
|
|
|
|
Run monthly during low-traffic windows. (WAL mode reduces the need, but doesn't eliminate it.)
|
|
|
|
---
|
|
|
|
## Health Check
|
|
|
|
`src/lib/db/healthCheck.ts` provides **DB-level health diagnostics**:
|
|
|
|
````bash
|
|
GET /api/db/health
|
|
|
|
Returns:
|
|
|
|
```json
|
|
{
|
|
"status": "healthy",
|
|
"checks": {
|
|
"writable": { "status": "pass" },
|
|
"integrity": { "status": "pass", "result": "ok" },
|
|
"foreign_keys": { "status": "pass", "violations": 0 },
|
|
"orphaned_artifacts": { "status": "warn", "count": 12 },
|
|
"table_sizes": {
|
|
"usage_history": { "rows": 12345, "size_mb": 12.3 },
|
|
"call_logs": { "rows": 567, "size_mb": 2.1 }
|
|
}
|
|
}
|
|
}
|
|
````
|
|
|
|
Run `PRAGMA integrity_check` to detect corruption:
|
|
|
|
```bash
|
|
sqlite3 ~/.omniroute/storage.sqlite "PRAGMA integrity_check;"
|
|
# Should print: ok
|
|
```
|
|
|
|
If it returns anything other than `ok`, **stop using the database immediately** and restore from backup.
|
|
|
|
---
|
|
|
|
## Disaster Recovery
|
|
|
|
### Scenario 1: WAL File Lost
|
|
|
|
The `-wal` file is missing but `-shm` and main DB are intact:
|
|
|
|
```bash
|
|
# Recovers automatically on next open
|
|
omniroute
|
|
```
|
|
|
|
If SQLite can't auto-recover:
|
|
|
|
```bash
|
|
sqlite3 ~/.omniroute/storage.sqlite ".recover" > recovered.sql
|
|
sqlite3 recovered.db < recovered.sql
|
|
mv recovered.db ~/.omniroute/storage.sqlite
|
|
```
|
|
|
|
### Scenario 2: Main DB File Corrupted
|
|
|
|
Restore from backup:
|
|
|
|
```bash
|
|
omniroute sync pull --merge # or: omniroute backup restore <backup-id>
|
|
```
|
|
|
|
### Scenario 3: Encryption Key Lost
|
|
|
|
**No recovery possible** without the key. The encrypted fields are unreadable. Re-add all providers manually with new credentials.
|
|
|
|
> **Mitigation**: Always back up the encryption key separately, ideally in a password manager or KMS.
|
|
|
|
### Scenario 4: Disk Full
|
|
|
|
SQLite will return `SQLITE_FULL` errors. Free disk space, then:
|
|
|
|
```bash
|
|
# Checkpoint WAL to free up space
|
|
sqlite3 ~/.omniroute/storage.sqlite "PRAGMA wal_checkpoint(TRUNCATE);"
|
|
```
|
|
|
|
---
|
|
|
|
## Common Operations
|
|
|
|
### Inspect a Table
|
|
|
|
```bash
|
|
sqlite3 ~/.omniroute/storage.sqlite "SELECT * FROM api_keys LIMIT 5;"
|
|
```
|
|
|
|
### Count Rows in All Tables
|
|
|
|
```bash
|
|
sqlite3 ~/.omniroute/storage.sqlite <<EOF
|
|
SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';
|
|
EOF
|
|
```
|
|
|
|
### Reset (Wipe) All Data
|
|
|
|
```bash
|
|
# Stop OmniRoute first
|
|
omniroute stop
|
|
|
|
# Delete the DB file
|
|
rm ~/.omniroute/storage.sqlite*
|
|
|
|
# Restart (will recreate empty DB)
|
|
omniroute
|
|
```
|
|
|
|
For a **selective** reset (keep providers, wipe usage):
|
|
|
|
```bash
|
|
DELETE FROM usage_history WHERE timestamp < datetime('now', '-30 day');
|
|
DELETE FROM call_logs WHERE timestamp < datetime('now', '-30 day');
|
|
DELETE FROM proxy_logs WHERE timestamp < datetime('now', '-30 day');
|
|
```
|
|
|
|
### Export Single Table
|
|
|
|
```bash
|
|
sqlite3 ~/.omniroute/storage.sqlite <<EOF
|
|
.mode csv
|
|
.output api_keys.csv
|
|
SELECT * FROM api_keys;
|
|
EOF
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### "Database is locked"
|
|
|
|
Another process is holding a write lock. Either:
|
|
|
|
- Wait for the other process to finish (check `lsof | grep storage.sqlite`)
|
|
- Kill the other process
|
|
- If persistent, restart OmniRoute
|
|
|
|
### "Foreign key constraint failed"
|
|
|
|
A domain module is violating referential integrity. Check:
|
|
|
|
- Orphaned rows in dependent tables
|
|
- Cascading deletes that didn't propagate
|
|
- Recent migration that changed a foreign key
|
|
|
|
Run `PRAGMA foreign_key_check;` to find violations.
|
|
|
|
### "Out of memory"
|
|
|
|
SQLite's memory-mapped I/O is exceeding the OS limit. Reduce via SQLite pragma:
|
|
|
|
```sql
|
|
PRAGMA mmap_size = 134217728; -- 128MB instead of 256MB
|
|
```
|
|
|
|
Or disable:
|
|
|
|
```sql
|
|
PRAGMA mmap_size = 0;
|
|
```
|
|
|
|
### "Migration failed mid-way"
|
|
|
|
The migration ran in a transaction, so it should have rolled back. If not:
|
|
|
|
1. **Stop OmniRoute** (prevent further attempts)
|
|
2. **Check the DB state** with `sqlite3`
|
|
3. **Manually fix** the partial migration
|
|
4. **Re-run** OmniRoute (the migration will be retried)
|
|
|
|
To prevent this, always test migrations on a copy first.
|
|
|
|
---
|
|
|
|
## See Also
|
|
|
|
- [USAGE_QUOTA_GUIDE.md](../guides/USAGE_QUOTA_GUIDE.md) — usage tables
|
|
- [MONITORING_GUIDE.md](./MONITORING_GUIDE.md) — health monitoring
|
|
- [RELEASE_CHECKLIST.md](./RELEASE_CHECKLIST.md) — release flow
|
|
- Source: `src/lib/db/` (80+ files, ~25K LOC)
|