-
Notifications
You must be signed in to change notification settings - Fork 7
Schema
Complete reference for Memory Journal's SQLite database schema - the persistent storage layer for project context.
Schema Design: Optimized for three critical AI context operations: fast entry retrieval, relationship graph traversal, and full-text search.
Memory Journal uses SQLite with:
- WAL (Write-Ahead Logging) mode
- FTS5 full-text search
- Foreign key constraints
- Automatic triggers
- Comprehensive indexes
Main table storing journal entries.
CREATE TABLE memory_journal (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entry_type TEXT NOT NULL,
content TEXT NOT NULL,
timestamp TEXT DEFAULT CURRENT_TIMESTAMP,
is_personal INTEGER DEFAULT 1,
significance_type TEXT,
auto_context TEXT,
deleted_at TEXT,
-- GitHub Projects (v1.2.0)
project_number INTEGER,
project_item_id INTEGER,
github_project_url TEXT,
-- GitHub Issues & PRs (v2.0.0)
issue_number INTEGER,
issue_url TEXT,
pr_number INTEGER,
pr_url TEXT,
pr_status TEXT,
-- Team Collaboration (v2.0.0)
share_with_team INTEGER DEFAULT 0,
-- GitHub Actions (v2.1.0)
workflow_run_id INTEGER,
workflow_name TEXT,
workflow_status TEXT
);Columns:
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
INTEGER | No | Primary key (auto-increment) |
entry_type |
TEXT | No | Type of entry (personal_reflection, technical_achievement, etc.) |
content |
TEXT | No | Entry content (max 50KB) |
timestamp |
TEXT | No | ISO 8601 timestamp (auto-set) |
is_personal |
INTEGER | No | 1 = personal, 0 = project (default: 1) |
significance_type |
TEXT | Yes | Significance marker (technical_breakthrough, milestone, etc.) |
auto_context |
TEXT | Yes | JSON string of Git/GitHub context |
deleted_at |
TEXT | Yes | Soft delete timestamp (NULL = not deleted) |
project_number |
INTEGER | Yes | GitHub Project number (v1.2.0) |
project_item_id |
INTEGER | Yes | GitHub Project item ID (v1.2.0) |
github_project_url |
TEXT | Yes | Full URL to GitHub Project (v1.2.0) |
issue_number |
INTEGER | Yes | GitHub Issue number (v2.0.0) |
issue_url |
TEXT | Yes | Full URL to GitHub Issue (v2.0.0) |
pr_number |
INTEGER | Yes | Pull Request number (v2.0.0) |
pr_url |
TEXT | Yes | Full URL to Pull Request (v2.0.0) |
pr_status |
TEXT | Yes | PR status: draft/open/merged/closed (v2.0.0) |
share_with_team |
INTEGER | No | 1 = shared with team, 0 = not shared (v2.0.0) |
workflow_run_id |
INTEGER | Yes | GitHub Actions workflow run ID (v2.1.0) |
workflow_name |
TEXT | Yes | Workflow name for quick reference (v2.1.0) |
workflow_status |
TEXT | Yes | Workflow status: queued/in_progress/completed (v2.1.0) |
Indexes:
CREATE INDEX idx_memory_journal_timestamp ON memory_journal(timestamp);
CREATE INDEX idx_memory_journal_type ON memory_journal(entry_type);
CREATE INDEX idx_memory_journal_personal ON memory_journal(is_personal);
CREATE INDEX idx_memory_journal_deleted ON memory_journal(deleted_at);
-- GitHub Projects indexes (v1.2.0)
CREATE INDEX idx_memory_journal_project_number ON memory_journal(project_number);
CREATE INDEX idx_memory_journal_project_item_id ON memory_journal(project_item_id);
-- GitHub Issues & PRs indexes (v2.0.0)
CREATE INDEX idx_memory_journal_issue_number ON memory_journal(issue_number);
CREATE INDEX idx_memory_journal_pr_number ON memory_journal(pr_number);
-- GitHub Actions index (v2.1.0)
CREATE INDEX idx_memory_journal_workflow_run_id ON memory_journal(workflow_run_id);Entry Types:
personal_reflectionproject_decisiontechnical_achievementbug_fixfeature_implementationcode_reviewmeeting_noteslearningresearchplanningretrospectivestandupother
Significance Types:
milestonebreakthroughtechnical_breakthroughdecisionlesson_learnedblocker_resolvedrelease
Tag definitions with usage tracking.
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
usage_count INTEGER DEFAULT 0
);Columns:
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
INTEGER | No | Primary key (auto-increment) |
name |
TEXT | No | Tag name (unique, max 100 chars) |
usage_count |
INTEGER | No | Number of entries using this tag |
Indexes:
CREATE INDEX idx_tags_name ON tags(name);Tag Management:
- Auto-created on first use (
INSERT OR IGNORE) -
usage_countupdated automatically - Lowercase recommended (not enforced)
Junction table linking entries to tags (many-to-many).
CREATE TABLE entry_tags (
entry_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (entry_id, tag_id),
FOREIGN KEY (entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);Columns:
| Column | Type | Nullable | Description |
|---|---|---|---|
entry_id |
INTEGER | No | Reference to memory_journal.id |
tag_id |
INTEGER | No | Reference to tags.id |
Composite Primary Key: (entry_id, tag_id)
Indexes:
CREATE INDEX idx_entry_tags_entry ON entry_tags(entry_id);
CREATE INDEX idx_entry_tags_tag ON entry_tags(tag_id);Cascading Deletes:
- Deleting an entry removes all its tag associations
- Deleting a tag removes all entry associations
Typed relationships between entries.
CREATE TABLE relationships (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_entry_id INTEGER NOT NULL,
to_entry_id INTEGER NOT NULL,
relationship_type TEXT NOT NULL,
description TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (from_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE,
FOREIGN KEY (to_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE
);Columns:
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
INTEGER | No | Primary key (auto-increment) |
from_entry_id |
INTEGER | No | Source entry ID |
to_entry_id |
INTEGER | No | Target entry ID |
relationship_type |
TEXT | No | Type of relationship |
description |
TEXT | Yes | Optional description |
created_at |
TEXT | No | ISO 8601 timestamp (auto-set) |
Indexes:
CREATE INDEX idx_relationships_from ON relationships(from_entry_id);
CREATE INDEX idx_relationships_to ON relationships(to_entry_id);Relationship Types:
-
references- General connection -
implements- Implementation -
clarifies- Explanation -
evolves_from- Iteration -
response_to- Reply -
blocked_by- Blocked by (causal) -
resolved- Resolved (causal) -
caused- Caused (causal)
Cascading Deletes:
- Deleting an entry removes all its relationships (both directions)
Vector embeddings for semantic search (optional).
CREATE TABLE embeddings (
entry_id INTEGER PRIMARY KEY,
embedding BLOB NOT NULL,
model_name TEXT NOT NULL,
FOREIGN KEY (entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE
);Columns:
| Column | Type | Nullable | Description |
|---|---|---|---|
entry_id |
INTEGER | No | Primary key, reference to memory_journal.id |
embedding |
BLOB | No | Binary vector data (384 dimensions) |
model_name |
TEXT | No | Model name (e.g., 'all-MiniLM-L6-v2') |
Storage:
- Each embedding: ~1.5KB (384 floats × 4 bytes)
- 1000 entries: ~1.5MB
- 10000 entries: ~15MB
Cascading Deletes:
- Deleting an entry removes its embedding
FTS5 virtual table for full-text search.
CREATE VIRTUAL TABLE memory_journal_fts USING fts5(
content,
entry_type,
content='memory_journal',
content_rowid='id',
tokenize='porter unicode61'
);Configuration:
-
content='memory_journal'- External content table -
content_rowid='id'- Links to memory_journal.id -
tokenize='porter unicode61'- Porter stemming + Unicode support
Features:
- Automatic tokenization
- Stop word filtering
- Porter stemming (e.g., "optimize" matches "optimization")
- Rank-ordered results
- BM25 scoring
Keep FTS5 index in sync with main table.
Insert trigger:
CREATE TRIGGER memory_journal_ai AFTER INSERT ON memory_journal BEGIN
INSERT INTO memory_journal_fts(rowid, content, entry_type)
VALUES (new.id, new.content, new.entry_type);
END;Update trigger:
CREATE TRIGGER memory_journal_au AFTER UPDATE ON memory_journal BEGIN
UPDATE memory_journal_fts
SET content = new.content, entry_type = new.entry_type
WHERE rowid = new.id;
END;Delete trigger:
CREATE TRIGGER memory_journal_ad AFTER DELETE ON memory_journal BEGIN
DELETE FROM memory_journal_fts WHERE rowid = old.id;
END;Note: Soft deletes don't trigger the delete trigger (deleted_at is an UPDATE).
Added soft delete and relationships.
function runMigrations(db: Database): void {
// Add deleted_at column if missing
const columns = db.exec("PRAGMA table_info(memory_journal)");
const columnNames = columns[0]?.values.map((row) => row[1]) ?? [];
if (!columnNames.includes("deleted_at")) {
console.error("Running migration: Adding deleted_at column");
db.run("ALTER TABLE memory_journal ADD COLUMN deleted_at TEXT");
}
// Create relationships table if missing
const tables = db.exec(
"SELECT name FROM sqlite_master WHERE type='table' AND name='relationships'",
);
if (tables.length === 0 || tables[0].values.length === 0) {
console.error("Running migration: Creating relationships table");
db.run(`
CREATE TABLE relationships (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_entry_id INTEGER NOT NULL,
to_entry_id INTEGER NOT NULL,
relationship_type TEXT NOT NULL,
description TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (from_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE,
FOREIGN KEY (to_entry_id) REFERENCES memory_journal(id) ON DELETE CASCADE
)
`);
db.run(
"CREATE INDEX idx_relationships_from ON relationships(from_entry_id)",
);
db.run("CREATE INDEX idx_relationships_to ON relationships(to_entry_id)");
}
}Migration Strategy:
- Check if columns/tables exist
- Add if missing
- Non-destructive (data preserved)
- Idempotent (safe to run multiple times)
- Content: 500 bytes (average)
- Metadata: 200 bytes
- Tags: 50 bytes (2-3 tags)
- Embedding: 1500 bytes (if enabled)
- Total: ~2250 bytes per entry
| Entries | With Embeddings | Without Embeddings |
|---|---|---|
| 100 | 220 KB | 75 KB |
| 1,000 | 2.2 MB | 750 KB |
| 10,000 | 22 MB | 7.5 MB |
| 100,000 | 220 MB | 75 MB |
Plus:
- FTS5 index: ~50% of content size
- Tags table: negligible
- Relationships: ~100 bytes each
SELECT
e.*,
GROUP_CONCAT(t.name, ', ') as tags
FROM memory_journal e
LEFT JOIN entry_tags et ON e.id = et.entry_id
LEFT JOIN tags t ON et.tag_id = t.id
WHERE e.id = ?
AND e.deleted_at IS NULL
GROUP BY e.id;SELECT
e.id,
e.content,
e.entry_type,
e.timestamp,
snippet(memory_journal_fts, 0, '**', '**', '...', 64) as snippet
FROM memory_journal_fts fts
JOIN memory_journal e ON fts.rowid = e.id
WHERE fts MATCH ?
AND e.deleted_at IS NULL
ORDER BY rank
LIMIT ?;WITH RECURSIVE related_entries(entry_id, depth) AS (
VALUES(?, 0)
UNION
SELECT DISTINCT
CASE
WHEN r.from_entry_id = re.entry_id THEN r.to_entry_id
ELSE r.from_entry_id
END,
re.depth + 1
FROM relationships r
JOIN related_entries re ON
r.from_entry_id = re.entry_id OR r.to_entry_id = re.entry_id
WHERE re.depth < ?
)
SELECT DISTINCT e.*, re.depth
FROM related_entries re
JOIN memory_journal e ON e.id = re.entry_id
WHERE e.deleted_at IS NULL
ORDER BY re.depth, e.timestamp DESC;Reclaim space after deletes:
VACUUM;When to run:
- After many permanent deletes
- Database size is much larger than data
- Typically monthly or quarterly
Update query planner statistics:
ANALYZE;When to run:
- After bulk inserts
- After schema changes
- Typically monthly
Note: v1.1.3 removed automatic ANALYZE from startup (too expensive).
Verify database integrity:
PRAGMA integrity_check;Check if indexes are being used:
EXPLAIN QUERY PLAN
SELECT * FROM memory_journal WHERE timestamp > '2025-10-01';-- SQLite backup API
VACUUM INTO '/path/to/backup.db';
-- Or simple copy (while WAL mode handles consistency)
cp memory_journal.db backup.db# Stop server
# Replace database file
cp backup.db memory_journal.db
# Restart serverPlanned schema changes:
- Attachments table - File attachments
- Versions table - Entry version history
- Metadata table - Schema version tracking
- Collections table - User-defined collections
- Migrations preserve existing data
- Old clients continue to work (graceful degradation)
- New columns have defaults
Next: Check Architecture or Performance.