Reference for documentation.db, the SQLite database backing all in-app help: Tier 1/2 tooltips, plus the Tier 3 web content they link to, served by WebServer. Read this before touching anything under localWebServer/, idetooltips/, or plugin-manager/.../documentation/, or before writing/editing SQL against this database.
This is a read-only, prebuilt database — CoGo never creates or migrates its schema at runtime (see ADR 0001, exception 1). The schema is owned by the separate OfflineDocumentationTools project (the docdb-studio tool); never change it from this repo.
- Installed path:
context.getDatabasePath("documentation.db")(Environment.DOC_DBincommon/.../utils/Environment.java), i.e. the app's privatedatabases/dir. - Bundled as an asset and extracted on install/update by
BundledAssetsInstaller/SplitAssetsInstaller. - Debug override: if
/sdcard/Download/documentation.dbexists and is newer than the installed copy,WebServerandToolTipManagerswap to it at request time (timestamp-compared per request, not just at startup) — a fast way to test a new database on-device without reinstalling.WebServer's debug logging and experiment flags are also file-flag-gated under/sdcard/Download/(CodeOnTheGo.webserver.debug,CodeOnTheGo.exp,CodeOnTheGo.webserver.cs0). - Don't trust a local copy's on-disk schema or row content as ground truth without checking freshness first. Any manually downloaded or debug-override copy is independent of git history — a stale one can have a different schema (e.g. missing
UNIQUE(path)ortemplateId) or be missing rows that already exist in the current, maintained database. A stale copy caused a real near-miss in ADFA-5088: a SQL script validated against it would have silently overwritten curated production tooltip content for several tags. Diff or re-download before authoring SQL against a local copy's state, not just before shipping it.
A star schema: a large fact table at the center, small dimension tables around it. There are two fact tables — Content (Tier 3 web content) and Tooltips (Tier 1/2 tooltips) — because they serve different lookup patterns.
CREATE TABLE Content (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT NOT NULL,
languageID INTEGER NOT NULL,
content BLOB NOT NULL,
contentTypeID INTEGER NOT NULL,
templateId INTEGER,
FOREIGN KEY (languageID) REFERENCES Languages(id),
FOREIGN KEY (contentTypeID) REFERENCES ContentTypes(id),
UNIQUE(path)
);One row per file the web server can serve (HTML, CSS, JS, image, video, PDF, ...) — 30,000+ rows. Key points:
pathis the lookup key (indexed via theUNIQUEconstraint) and is whatWebServermatches the HTTP request path against. Paths carry a short source prefix to avoid collisions between doc sets, e.g.k/index.html(Kotlin) vsj/index.html(Java).contentis compressed — Brotli for text-like formats, format-specific compression otherwise (images/video/fonts).ContentTypes.compressionsays which. Content over 1 MB is split across multiple rows: the first row's path is the base path, continuation rows arepath-1,path-2, ... (languageId = 1), reassembled byWebServerbefore returning.templateId:0(or unset) meanscontentis legacy HTML with presentation baked in (the pre-CMS Release 0/1 format). A positive value meanscontentis JSON facts only, rendered through the matching row inTemplates(a Pebble template) — the ongoing move to a proper CMS that de-duplicates presentation across near-identical pages (e.g.sin/cosdocs).- The
UNIQUE(path)constraint rejects any duplicatepath, regardless oflanguageID— a second language for an existing path isn't supported yet (onlyEN-uscurrently exists). Getting there needs an upstream schema change to composite uniqueness on(path, languageID)(see Known rough edges below).
Dimensions: Languages(id, value) (4-letter codes, e.g. EN-us); ContentTypes(id, value, compression) (MIME type + compression scheme, ~30 rows).
CREATE TABLE Tooltips (
id INTEGER PRIMARY KEY AUTOINCREMENT,
categoryId INTEGER NOT NULL,
tag TEXT NOT NULL,
summary TEXT NOT NULL,
detail TEXT NOT NULL,
UNIQUE(categoryId, tag),
FOREIGN KEY(categoryId) REFERENCES TooltipCategories(id)
);summaryis Tier 1 (the initial popup),detailis Tier 2 (after "See more"); both may contain HTML.- Looked up by
(categoryId, tag), which the IDE stamps on the UI widget that owns the tooltip. TheUNIQUEconstraint gives this lookup an index, so it's fast. TooltipCategories(id, category)is a tiny dimension table (four categories at the time of writing).TooltipButtons(tooltipId, buttonNumberId, description, uri)holds the Tier 3 links shown at the bottom of a Tier 2 tooltip —tooltipId->Tooltips.id,buttonNumberId->TooltipButtonNumbers.id.urishould resolve to aContent.path(after stripping?query/#fragment).TooltipButtonNumbers(id)exists only to pin a fixed, manually-assigned display order when a tooltip has multiple Tier 3 links. (Flagged in the source design doc as something worth redoing without a whole extra table.)
Templates(id, name, content)— Pebble template source, keyed by id (and bynamefor well-known templates likebookshelf). Referenced byContent.templateId.Bookshelf(contentID, bookCategoryID, title, description)/BookCategories(id, category, description)— the Dynamic Bookshelf: one row per "book" (PDF or similar), linked to its Tier 3 page viacontentID->Content.id. Two DB triggers keepBookshelfin sync when a PDF row is inserted/deleted fromContent;title/descriptiondon't come from those triggers and must be set by hand. Non-PDF books need a separate ingestion path (plugin-provided, e.g. viaPluginDocumentationManager).LastChange(documentationSet, changeTime, who)— audit trail for edits made throughdocdb-studio; not shown to end users.DatabaseVersionResolverreads thedocumentationSet = 'wholedb'row to report the DB's build/edit stamp in debug logging, falling back to the most recent row of any set if'wholedb'is missing.- Misc
ide_tooltip_tableandPUCCtables are historical/example artifacts — not part of the live lookup paths above.
All three sites below open the file with SQLiteDatabase.openDatabase(..., OPEN_READONLY) — no writes, ever, from this app (see ADR 0001 for why raw SQLite is justified here instead of Room).
-
app/.../localWebServer/WebServer.kt— serves Tier 3. On eachGET, runs:SELECT C.content, CT.value, CT.compression, C.templateId FROM Content C, ContentTypes CT WHERE C.contentTypeID = CT.id AND C.path = ?
then reassembles chunked blobs, decompresses Brotli when the client can't accept it (or when a Pebble template needs a string to render), and instantiates the template if
templateId > 0. Also serves a Dynamic Bookshelf JSON payload (joiningContent/Bookshelf/BookCategories, rendered through thebookshelftemplate) and debug-only HTML dumps at/pr/db(LastChange, last 20 rows) and/pr/pr(recent projects, from a different database). -
idetooltips/.../ToolTipManager.kt— serves Tier 1/2. Looks upTooltipsjoined toTooltipCategoriesby(category, tag), thenTooltipButtonsfor the Tier 3 links shown at the bottom. -
plugin-manager/.../documentation/PluginDocumentationManager.kt(withTier3AssetWalker.kt, and theDocumentationExtensioncontract inplugin-api) — lets plugins contribute their own help content into the same lookup paths.
Schema changes and data edits happen outside this repo, in OfflineDocumentationTools/docdb-studio (a Flet GUI over this same documentation.db). Conventions enforced there that matter if you're reasoning about data correctness here:
- The schema is locked —
docdb-studio's ownAGENTS.mdsays never change it. If a new column/table is genuinely needed, it's a cross-repo change coordinated with that project, not something to route around in CoGo. - Tooltip uniqueness is
(categoryId, tag);TooltipButtons.urivalues are validated there againstContent.path(post?query/#fragmentstripping) before being allowed into the database. - Every edit made through the tool updates
LastChangefor the affected documentation set, which is howDatabaseVersionResolver's debug logging can say what build of the docs is loaded.
Some tickets (e.g. ADFA-5088) ship a one-off .sql script under docs/docdb/ for a docdb-studio maintainer to run against the real database, rather than editing it directly through the tool. Gotchas found writing those scripts:
- Keep each
.systemline simple. The sqlite3 CLI's.systemdot-command can hit a content-dependent shell-parsing failure when a line chains multiple operators (;,&&,||, parentheses) — it reproduces for some input strings and not others, so it won't necessarily show up in a quick test. Stick to one plaincommand | pipe > fileper.systemline. .bail onis required forBEGIN/COMMITto actually mean atomic. Without it, a mid-script SQL error prints to stderr but the script keeps going — including reaching the finalCOMMIT, which then persists whatever succeeded before the error (verified empirically, not just documented behavior)..bailalso can't see.systemshell failures directly, so a failed or empty Brotli payload (which leaves its target file missing or zero-length) needs its own check: insert itsREADFILE()into a throwawayCREATE TEMP TABLEguarded byNOT NULL CHECK (length(content) > 0)immediately before the realContentinsert, turning that failure into a real SQL error.bailwill catch. Seedocs/docdb/ADFA-5088-preference-tooltips.sqlfor the working pattern.- Don't write Brotli payloads to bare
/tmp/*.brfilenames. A fixed, guessable name directly under world-writable/tmplets another local user pre-plant a symlink or race the write/read pair between the.system echo | brotliwrite and theREADFILE()read (CWE-377). Create an owner-only working directory instead —rm -rfit, thenmkdir -m 700it (the mode is set atomically at creation, with no window where it's briefly world-accessible) — write every payload under that directory, and remove it again beforeCOMMIT. See the same script for the working pattern.
- A Tier 3 link that points off-device is a bug in the content, not the code — the web server and webview will happily follow it. If you see one while working in this area, it's a data problem to report upstream, not a
WebServerbug to fix here. Content'sUNIQUE(path)constraint (rather thanUNIQUE(path, languageID)) means a second language for an existing path can't currently be added without a schema change upstream — multi-language content isn't fully wired yet even though theLanguagesdimension anticipates it.TooltipButtonNumbersis a whole table whose only job is pinning a manual sort order; a lighter-weight mechanism (e.g. an ordering column directly onTooltipButtons) would remove a table.