Database schema
Lexicon stores everything in a single SQLite file, lexicon.db, initialized and upgraded automatically at startup. This page documents the schema as defined by the migrations in DatabaseManager.cpp.
Overview
Seven domain tables plus one bookkeeping table:
| Table | Purpose |
|---|---|
map | Top-level buckets for terms |
term | The concepts themselves: title, disambiguation, status, understanding, pinned, Markdown content |
alias | Alternate names per term |
tag | Classification labels per term |
flag | Workflow markers per term |
link | Typed, directed relationships between terms |
log | Operation log (create/update/delete/read events) |
db_version | Applied migration versions |
Relationships at a glance:
map 1 ──── n term 1 ──── n alias
│ ├───── n tag
│ └───── n flag
│
└── n link n ── (term → term, typed, directed)
Migration system
On startup, DatabaseManager::initialize():
- Opens (or creates)
lexicon.dband executesPRAGMA foreign_keys = ON. - Ensures the bookkeeping table exists:
CREATE TABLE IF NOT EXISTS db_version (version INTEGER PRIMARY KEY). - Walks an ordered list of migrations (currently versions 1–8) and applies every version greater than the current one, each inside a transaction. The version row is written in the same transaction, so a failed migration rolls back completely.
| Version | What it introduced |
|---|---|
| 1 | Core tables: map, term, alias, tag, flag + unique and lookup indexes |
| 2 | term.status |
| 3 | term.understanding |
| 4 | term.pinned |
| 5 | log table |
| 6 | term.content (Markdown source) |
| 7 | link table + indexes |
| 8 | link.position (ordering) |
Table reference
map
CREATE TABLE map (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX map_name_unique ON map(name);
term
CREATE TABLE term (
id INTEGER PRIMARY KEY AUTOINCREMENT,
map_id INTEGER NOT NULL,
title TEXT NOT NULL,
disambiguation TEXT,
status INTEGER NOT NULL DEFAULT 0, -- TermStatus
understanding INTEGER NOT NULL DEFAULT 0, -- UnderstandingLevel
pinned INTEGER NOT NULL DEFAULT 0,
content TEXT, -- Markdown source
FOREIGN KEY(map_id) REFERENCES map(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX term_unique
ON term(map_id, title, COALESCE(disambiguation, ''));
CREATE INDEX idx_term_map_id ON term(map_id);
CREATE INDEX idx_term_title ON term(title);
The unique index is why the same title can exist twice in one map only when the disambiguations differ.
alias, tag, flag
Three structurally identical child tables (the value column is alias in alias, name in tag and flag):
CREATE TABLE alias (
id INTEGER PRIMARY KEY AUTOINCREMENT,
term_id INTEGER NOT NULL,
alias TEXT NOT NULL,
UNIQUE(term_id, alias),
FOREIGN KEY(term_id) REFERENCES term(id) ON DELETE CASCADE
);
CREATE INDEX idx_alias_term_id ON alias(term_id);
CREATE INDEX idx_alias_alias ON alias(alias);
The per-term UNIQUE constraint deduplicates values; updates are implemented as replace-all per term (replaceStringValues()).
link
CREATE TABLE link (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_term_id INTEGER NOT NULL,
to_term_id INTEGER NOT NULL,
link_type INTEGER NOT NULL DEFAULT 0, -- LinkType
position INTEGER NOT NULL DEFAULT 0, -- ordering within a term
FOREIGN KEY(from_term_id) REFERENCES term(id) ON DELETE CASCADE,
FOREIGN KEY(to_term_id) REFERENCES term(id) ON DELETE CASCADE
);
CREATE INDEX idx_link_from_term_id ON link(from_term_id);
CREATE INDEX idx_link_to_term_id ON link(to_term_id);
Links are directed: querying by from_term_id yields a term's outgoing links, querying by to_term_id yields its backlinks. link_type stores the LinkType enum value (see Architecture).
log
CREATE TABLE log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT NOT NULL,
record_id INTEGER NOT NULL,
log_type INTEGER NOT NULL, -- 1=created, 2=updated, 3=deleted, 4=read
happened_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
Integrity rules
- Foreign keys are enforced —
PRAGMA foreign_keys = ONruns on every connection. - Cascade deletes — deleting a map deletes its terms; deleting a term deletes its aliases, tags, flags, and any links in either direction.
- Uniqueness — map names are globally unique;
(map_id, title, disambiguation)is unique per term; alias/tag/flag values are unique per term. - Enum stability —
status,understanding, andlink_typestore raw enum integers; enum values must only ever be appended, never renumbered.
Adding a migration
To evolve the schema, follow the pattern in DatabaseManager::initialize():
- Append a new
{version, statements}entry to themigrationslist with the next sequential version number. Never edit an already-shipped migration — existing databases have already recorded it as applied. - Use idempotent DDL where possible (
CREATE TABLE IF NOT EXISTS,CREATE INDEX IF NOT EXISTS) andALTER TABLE ... ADD COLUMNfor extensions. - Update the record structs and queries in
DatabaseManagerto read/write the new columns. - Test by launching with an old
lexicon.dbcopy and with no database at all — both paths must succeed.
Inspecting your data Since it's plain SQLite, you can explore your lexicon with any tool: sqlite3 lexicon.db ".schema", DB Browser for SQLite, or DataGrip. Close Lexicon first to avoid lock conflicts.