Repository navigation
ADFA-3305: Migrate the tooltip tables to keyed tables (documentation schema 3.0.0) - #45
davidschachterADFA wants to merge 8 commits into
Conversation
migrate_tooltip_schema.py rewrites a documentation.db's tooltip tables in one transaction, into the shape Code On The Go reads from DocumentationDatabaseVersion 3.0.0 on: - Tooltips: WITHOUT ROWID, keyed on (categoryId, tag), holding the id. The separate UNIQUE (categoryId, tag) index goes. - TooltipTexts: summary and detail by id, AUTOINCREMENT, carrying on from the old Tooltips sequence. - TooltipButtons: WITHOUT ROWID, keyed on (tooltipId, buttonNumberId), its text as ids into TooltipButtonDescriptions (numbered most used first) and TooltipButtonUris. PR #39's TooltipButtonsByTooltip index, which the new key duplicates, is dropped. - DocumentationDatabaseVersion: one row, 3.0.0. Every tooltip and its buttons are read back through the new tables with the app's queries, and both tables compared row for row, before the commit; a difference rolls everything back. It backs up first, VACUUMs after, has --dry-run, does nothing to a converted database, and refuses (exit 1, file untouched) a database not declaring major 2, tables in neither shape, data the new keys cannot hold, and indexes, triggers or views the conversion would drop or break. It runs after every step that writes tooltips, since they all write the old shape, and the release rebuild then packs Content,TooltipTexts: rebuild_database.py cannot pack the WITHOUT ROWID Tooltips. test_migrate_tooltip_schema.py runs under pytest and as a plain script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
Independent Claude review (head 97c0fd9)On the real database the conversion is sound: every tooltip and every button comes back byte-identical and in order. No critical issue; the biggest risk is the rollout plan in the PR body, not the script.
R45-01. The body says to release the app first, then upload the database, and names only dev-assets. CodeOnTheGo's R45-02. Nothing says the 3.0 output must stay off the database ODT's tools edit, and no script converts 3.0 back. On a converted copy, R45-03. With a converted copy set back to version 2.0.0, the script printed "already in the new shape; nothing to do" and exited 0, though the app refuses that file. #29's R45-04. Disabling one guard at a time (40 mutants), 9 passed all 37 tests: Tooltips' exact column check (only TooltipButtons' has a test, so an extra Tooltips column would be dropped silently); all four new-shape conditions (TooltipTexts' shape, the lookup tables, WITHOUT ROWID on each keyed table); and the row count and the two row digests, each caught only in combination. This contradicts the claim that disabling each of 24 guards failed a test named for it. R45-05. R45-06. R45-07. #38's usage line and R45-08. The body says so. #47 adds its test to the pytest path list; this file needs only the standard library, so it is a one-line addition. Checked and sound
🤖 Generated with Claude Code |
The tables already in the 3.0 shape were reported as "nothing to do" with exit 0 whatever the database declared, though the app refuses a file declaring less than 3. populate_db.py's declare_database_version, run after the conversion, leaves exactly that. It now refuses, as every other mismatch here is refused, rather than declaring 3.0.0 itself: only a step run after the conversion can have rewritten the version row, and this step cannot tell what else that step changed. A database in the new shape declaring 3 or later is still left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
The docstring said every tooltip was read back "with the queries the app runs", but read_back looked tooltips up by categoryId (KEY_QUERY) and never ran APP_TOOLTIP_QUERY, the app's lookup by category name. read_back now also runs the app's two queries (QUERY_TOOLTIP and QUERY_TOOLTIP_BUTTONS from TooltipQuery.kt, CodeOnTheGo #2163 at 5cc8d2bc9) for every (category name, tag) the old tables answered, and compares what they find, as sorted lists since two categories may share a name, with what the old tables showed. The lookup by key stays: it reaches every tooltip, including one whose categoryId names no category. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
Disabling one guard at a time, 9 of the review's 40 mutants passed all 37 tests, and a run of the same set here found 4 more. Each now fails a test named for it: - the old shape: Tooltips' exact columns, and neither old table WITHOUT ROWID; - the new shape: TooltipTexts' shape, both lookup tables, WITHOUT ROWID on each keyed table; - TooltipButtonsByTooltip dropped only when it is on TooltipButtons; - the read-back's row count (an orphan TooltipTexts row), its button-row digest (buttons renumbered in the same order), and the per-tooltip lookup by key, which since the previous commit matters only for a tooltip whose categoryId names no category. The Tooltips-row digest is tested directly through read_back: every conversion bug tried that changes a Tooltips row also fails the per-tooltip read-back or the row count. The shared-category test now uses seven categories of one name, so the app's rows come back in an order the read-back has to sort. 49 mutants (the review's set, the KEY_QUERY marker replaced by 6 for the new guards): 48 fail a test. The other is equivalent: without the explicit ROLLBACK, conn.close() discards the open transaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
CLAUDE.md had no 3.0.0 callout, and still pointed tooltip work at the tables without saying which shape. Nothing said the converted file must stay off the master database ODT's tools edit, though all of them read or write the 2.x shape (check-tools' daily regression run selects TooltipButtons.uri and Tooltips.detail; sync_kdoc_json_to_db.py, TooltipManager.py and docdb-studio) and nothing converts back. CLAUDE.md now has a "Schema 3.0.0" callout, the tooltip-work pointer names the 2.x shape, and the scripts entry and the step's docstring say to run it on the copy that ships, never on the master. Both also state that Tooltips.id must be unique and name a TooltipTexts row, taken from that insert: nothing in the schema enforces it, since a UNIQUE (id) index would cost about the 613 pages the conversion saves, and two rows sharing an id share one text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
python-tests.yaml ran pytest only on ProcessKotlinWebsiteJSON, sync_kotlin_stdlib_docs and docdb-studio, so the 56 tests here ran only by hand. They need only the standard library and pytest, both already in that step; collected beside its other suites there is no module-name clash (308 tests), and they pass on Python 3.11, the job's version. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
The docstring's "0.009-0.014 ms instead of 0.011-0.016 ms" implied the tooltip lookup got faster. Re-measured warm on one pinned core (SQLite 3.46, 2,000 keys, the 2026-10-18 database, two runs) it did not: 0.0056 ms against 0.0057 median. The buttons query is the gain: 0.0033 ms instead of 1.36 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
Response to the reviewAll eight findings are addressed (97c0fd9..ce12ffc). On real data the converted database is identical to 97c0fd9's (same content digest of every table, schema and sequence).
🤖 Generated with Claude Code |
After migrate_tooltip_schema.py (#45) Tooltips is a WITHOUT ROWID table, which --pack refuses; the usage example and --pack help named only Content,Tooltips. Found in the #45 review (R45-07). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
|
Merge pair: schema 3.0.0 now also means compressed templates, which #50 adds to this migration; #50 merges with this PR, and #48 (re-minting templates) with both. The app side is appdevforall/CodeOnTheGo#2163 plus appdevforall/CodeOnTheGo#2164. 🤖 Generated with Claude Code |
- CLAUDE.md said the Templates tools "only ever see plain templates, on the master". The minify and the a/ step also run on the shipped copy, before the migration. It now says every Templates tool expects plain templates and so runs before the migration wherever it runs. - hm_pages_to_templates.py (#43) was listed but missing from the ship-time order. CLAUDE.md and the step's docstring now place it after the i/ page steps it lists and before the migration. - The step's docstring and a test comment named populate_db.py's declare_database_version as what rewrites the version after the conversion. No branch has that function (carried over from #45). They now say a later step or a hand edit. - CLAUDE.md said nothing in this repo handles Templates and that the bookshelf plugin populates them. populate_db.py, sync_javadoc_json_to_db.py and load_android_json_db.py write them, verify_android_json_db.py reads them, and the migration compresses them. Only Bookshelf and BookCategories come from the plugin system; appdevforall/bookshelf-plugin is archived (gh repo view). The step's docstring also says which template writers refuse 3.0.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
CLAUDE.md conflicted where both sides add a script to the list of live scripts; both entries are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3
ADFA-3305
Adds
scripts/migrate_tooltip_schema.py, which converts a documentation.db's tooltip tables, in one transaction,to keyed tables (documentation schema 3.0.0). The file shrinks by 5.2 MB and a tooltip's buttons become a key
lookup instead of a scan. The app side is appdevforall/CodeOnTheGo#2163.
Schema 3.0.0 will also mean every template is stored Brotli-compressed (ADFA-3305 decision, one switch for both).
That part comes in a PR stacked on this one, which has to merge with it.
Schema
Tooltips(id, categoryId, tag):WITHOUT ROWID,PRIMARY KEY (categoryId, tag), the key the app looks a tooltipup by. The 613-page
UNIQUE (categoryId, tag)index, a second copy of every tag, goes.TooltipTexts(id INTEGER PRIMARY KEY AUTOINCREMENT, summary, detail): keeps every id and the old sequence.TooltipButtons(tooltipId, buttonNumberId, descriptionId, uriId):WITHOUT ROWID,PRIMARY KEY (tooltipId, buttonNumberId), so a tooltip's buttons are one key range, already in display order.TooltipButtonDescriptions(711 rows, most used first) andTooltipButtonUris(20,453, in text order) hold eachstring once. "View full documentation" was stored 43,703 times. Neither has a
UNIQUEindex, which would costabout as much as the table.
Tooltips.idandTooltipButtons.tooltipIdare declared foreign keys toTooltipTexts(id).DocumentationDatabaseVersionbecomes one row, 3.0.0.The text is not in the
WITHOUT ROWIDtable on purpose. Measured that way the rebuilt file was 1.18 MB larger:such a table is an index B-tree, whose interior pages hold whole rows (340 pages), and its 264 rows over 1,002
bytes spill into mostly empty overflow pages. Its
.brwas 0.14 MB smaller, which is at noise level.Sizes and timings
Base: the 2026-10-18 database after ADFA-6627's first build (#44 at a3c444e), packed as the pipeline packs it.
.brisbrotli -q 11 -w 24.--pack Content,TooltipTextsThe file saving is real. The
.brsaving is at noise level: repacking alone moved it by 0.17 MB.Desktop SQLite 3.46.1, warm, one pinned core, 2,000 keys: a tooltip's buttons took 0.0033 ms instead of 1.36 ms
median (a scan and sort of all 54,799 rows); the tooltip lookup itself is unchanged, 0.0056 ms against 0.0057.
SQLite 3.46.1, 3.44.4, 3.31.1 and 3.22.0 (Android 9's version, desktop build) plan the app's tooltip and button
queries as key searches only.
The step
VACUUMs after, has--dry-run(converts and checks, then rolls back), and does nothing to aconverted database declaring 3 or later.
app's own two queries by (category name, tag). It also compares both tables row for row, and their row counts.
Any difference rolls everything back.
Tooltips.idmust be unique and name aTooltipTextsrow; writers take it from theTooltipTextsinsert. NoUNIQUE (id)index enforces it (it would cost about the 613 pages saved), so the step checks it.than 3, or no version; tooltip tables in neither shape;
data the new keys cannot hold (NULLs, a button with no tooltip, two buttons with one number, duplicate keys or
ids); indexes, triggers or views on these tables. PR ADFA-6597: Index TooltipButtons by (tooltipId, buttonNumberId) #39's
TooltipButtonsByTooltipis dropped instead, since thenew key is the same index.
Pipeline and release order
TooltipManager.py, docdb-studio,sync_kdoc_json_to_db.pyand the one-off scripts all write the old shape), and never on the master database theyedit: check-tools, which
docdb-regression-test.yamlruns daily at 02:00 UTC, selects the old columns, and nothingconverts back.
rebuild_database.py --pack Content,TooltipTexts(ADFA-3305: Convert animated GIFs to VP9 WebM and pack documentation.db #38).--pack Tooltipserrors, because aWITHOUT ROWIDtable has no rowids. That rebuild's comparison passed.compress_docdb.ymlruns daily at 11:00 UTC. It downloadsdocumentation.dbfrom Google Drive (DOCUMENTATION_DB_FILE_ID) and, when its md5 has changed, uploadsdocumentation.db.brto Drive.release.ymlruns daily at 12:00 UTC: it mergesstageintomain, downloadsthat
documentation.db.brintoassets/release/common/database/, and builds the release APK frommain, whichbundles it.
generate_assets.yml, run by hand, bundles the same Drive file into the asset zips. Only local debugbuilds take dev-assets.
a database declaring less than 3, so with a 2.x file it shows no built-in tooltips. The current app reading a 3.0.0
database loses its tooltips too, and from reading its code, a plugin install records the plugin as installed with
no rows. So every daily build has to pair the new reader with a 3.0.0 file, or the current one with 2.x.
on Drive and merge #2163 into
stage; the next 12:00 UTC build takes both. Putting the file there between 11:00and 12:00 UTC needs
compress_docdb.ymlrun by hand. No off-cycle release may run in between.release.yml's schedule until both are in place, then runcompress_docdb.ymlandrelease.ymlby hand.
GOOGLE_DRIVE_FILE_ID, whichbuild-kotlin-docs.yamlcalls "the productiondocumentation.db" and writes back to) is the same Drive file as CodeOnTheGo's
DOCUMENTATION_DB_FILE_ID; both IDsare secrets. If it is, putting the 3.0.0 file there breaks ODT's tools on their next run, so the shipped file would
need a Drive file of its own.
Plugins, in the app PR
A plugin's tooltip text goes into
TooltipTextsfirst, whoseAUTOINCREMENTid keys itsTooltipsrow, so ids arenever reused. Button text is looked up, or added, 500 strings per query. Removal deletes by category, then deletes
button text no button uses. The app refuses a database declaring less than 3.
Conflicts
--operation indexwould add it back to aconverted database. ADFA-6597: Index TooltipButtons by (tooltipId, buttonNumberId) #39's test and buttons query assume the old columns.
python-tests.yamlwith ADFA-3305: Trim the Kotlin API pages' JSON to what layout.pebble reads #47 (both add a test file to the samepytest step). ADFA-3305: Convert animated GIFs to VP9 WebM and pack documentation.db #38 merges cleanly.
--packhelp say--pack Content,Tooltips, which exits 1 on a converted database; after thisstep it is
Content,TooltipTexts.Tests
scripts/test_migrate_tooltip_schema.py: 56 tests, passing under pytest (Python 3.11 and 3.14) and as a plain script;python-tests.yamlnow runs them. 49 mutants: 48 fail a test; the other (no explicit ROLLBACK on an exception) isequivalent, since closing the connection discards the transaction. In the app PR, 18 of 20 mutants are
caught. The 2 that pass: taking the id from the
Tooltipsinsert, which is equivalent there becauselast_insert_rowid()still holds theTooltipTextsid; and dropping the 500-string chunking, which Robolectric'sSQLite does not catch because it binds over 33,000 variables.
Still to check on a device
categories (text, button order, links), and a 2.x debug database refused with the log line and no crash.
and the chunking on a device (checked on a desktop SQLite 3.22.0 only).
🤖 Generated with Claude Code
https://claude.ai/code/session_018CMZQuXT3GT7nEct2w4tg3