SQLite read layer for the canonical pricing database — decision record + Phases 0 & 1 - #61
Open
DiTo97 wants to merge 17 commits into
Open
SQLite read layer for the canonical pricing database — decision record + Phases 0 & 1#61DiTo97 wants to merge 17 commits into
DiTo97 wants to merge 17 commits into
Conversation
Add ADR 0001 recording the decision to keep the git-committed JSON as the canonical, diff-reviewable source of truth while building a derived SQLite artifact (prices.db) in the sync pipeline for fast, indexed, range-request reads from the dashboard and SDKs. Includes a proposed v1 schema and the phased rollout summary. Wire the ADR into the VitePress docs sidebar. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
Relocate ADR 0001 and the decisions index from docs/adr to wiki/decisions, and drop the VitePress sidebar wiring (decisions are not part of the published docs site). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
…(Phase 0) Implements ADR 0001 Phase 0: - Add services/sync/src/tokenpricing_sync/build_db.py: builds a SQLite database from prices.json + history snapshots following the v1 schema (meta, providers, models with indexes, model_sources, price_history, models_fts FTS5 table). VACUUM + ANALYZE at the end. - Wire a `build-db` CLI subcommand into cli.py (mirrors the existing sync/normalize pattern). The main `sync` flow also calls build_db() automatically after writing JSON. - Add database/current/prices.db and *.db-wal/shm to .gitignore so the binary is never committed. - Add tests/test_build_db.py: 11 fixture-based equivalence tests covering model count, field values, FTS search, price_history rows, and PRAGMA user_version. All 17 tests pass; ruff clean. - Update .github/workflows/database-sync.yml to run build-db after the sync step and upload prices.db as a workflow artifact (prices-db). - Update services/sync/README.md and docs/database.md to document the derived prices.db artifact and build-db command. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
DiTo97
marked this pull request as ready for review
June 27, 2026 08:54
There was a problem hiding this comment.
Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.
Once credits are available, reopen this pull request to trigger a review.
…nk check The relative link from docs/database.md into wiki/decisions/ points outside the VitePress site root, which the build's dead-link checker rejected. Use the canonical GitHub URL instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
…ase 1) Introduces an optional sql.js-httpvfs read path for prices.db alongside the existing JSON fetch, keeping the JSON path as a mandatory fallback. Changes: - Install sql.js-httpvfs; copy sqlite.worker.js + sql-wasm.wasm to public/ so Vite serves them at the correct base URL in both dev and the GitHub Pages build (/tokenpricing/). - New src/lib/sqlite-db.ts: opens prices.db (full-file serverMode), checks PRAGMA user_version == 1, queries all models, and maps rows back to the existing RawModelInfo/RawPricingData shape. - src/lib/data.ts: loadPricingData() now tries SQLite first and silently falls back to JSON on ANY error (DB 404, worker failure, schema version mismatch, VITE_SQLITE_ENABLED=false). - Feature flags: VITE_SQLITE_ENABLED=false disables SQLite entirely; VITE_PRICES_DB_URL overrides the DB URL for reviewer testing. - All existing component contracts (ModelRow, ExplorerView, CompareView, HistoryView) are unchanged. Quality gates: typecheck PASSED, lint 0 errors (pre-existing warning in explorer.tsx retained), build PASSED. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
Add a database-release workflow (workflow_dispatch + database-release-* tag trigger) that builds prices.db from the committed canonical JSON and uploads it to a rolling GitHub Release under the fixed tag database-latest, giving a stable download URL. Wire the same publish step into the scheduled database-sync workflow so the release stays fresh on every sync. Record the resolved publish target in ADR 0001 and document the URL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
Gate the commit step to schedule events so a manual workflow_dispatch can build and publish the prices.db release/artifact without pushing a data refresh commit. Scheduled runs remain the canonical data-producing cadence. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
Adds an experimental opt-in SQLite backend to the Python SDK (libraries/python). When TOKENPRICING_USE_SQLITE=1 is set, the SDK downloads prices.db from the rolling 'database-latest' GitHub Release, caches it on disk at ~/.cache/tokenpricing/prices.db with a 6h TTL (mirroring the JSON cache), and answers lookups/searches via indexed SQL and FTS5 instead of parsing the full 2.9 MB JSON. On any failure it silently falls back to the existing HTTP-JSON path; the public API and all return types are unchanged. JSON remains the default backend. Files changed: - src/tokenpricing/sqlite_backend.py (new): download, cache, schema validation (PRAGMA user_version=1), read-only open, row→Pydantic mapping, get_model() (PK lookup), search_models() (WHERE on indexed cols + optional FTS5 via models_fts), get_all_pricing_data(). - src/tokenpricing/pricing.py: _sqlite_enabled() flag check; when ON, runs get_all_pricing_data() via asyncio.to_thread and catches all exceptions to fall back to JSON. - tests/test_sqlite_backend.py (new): 26 tests covering lookup parity, search filter parity, FTS search, user_version validation, missing-FTS validation, broken-DB fallback, and default-off behavior. - README.md: "SQLite backend (experimental, opt-in)" section with env vars, default-off rationale, and speedup notes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
Apply ruff-format to the sync build_db module/tests and add the missing trailing newline to the vendored dashboard SQLite worker, matching the repo-wide pre-commit hooks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
build-db now emits two artifacts from the canonical JSON: - prices.db (~30MB): full v1 schema incl. price_history, for the dashboard - prices-current.db (~1.3MB): same schema minus price_history, for SDKs Both are published to the rolling database-latest release and uploaded as workflow artifacts. The Python SDK now defaults to the slim DB; the dashboard defaults to the full DB (release URL). Docs and ADR 0001 updated to describe the two assets and the interim static-hosting trade-offs (browser CORS/Range on release assets, GitHub Pages as the next step). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
Implements the TypeScript SDK half of Phase 2 of the SQLite migration, mirroring the Python sqlite_backend.py reference implementation. - New `src/sqlite-backend.ts`: downloads and caches `prices-current.db` from the rolling GitHub Release, validates schema version (PRAGMA user_version = 1) and FTS table presence, then converts DB rows to `RawPricingData` — identical in shape to the JSON path output. Activation is opt-in via `TOKENPRICING_USE_SQLITE=1`. Any failure raises `SQLiteBackendError` for transparent JSON fallback. - `src/pricing.ts`: wires in the SQLite path before the HTTP-JSON fetch; catches `SQLiteBackendError` and falls back with a `console.warn`. - `package.json`: adds `better-sqlite3` as optionalDependency and `@types/better-sqlite3` as devDependency. - `tsup.config.ts`: marks `better-sqlite3` as external so it is never bundled into browser-targeted output. - `tests/sqlite-backend.test.ts` + `tests/fixtures/prices-current.db`: 22 tests covering happy-path data mapping, env-var flag parsing, and error paths (404 download, wrong schema version, missing FTS table). - `README.md`: documents the SQLite backend, env-var overrides, and fallback behaviour. All 73 tests pass; typecheck and lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
…ng a binary The SQLite fixture DB is now generated at test time with better-sqlite3 and the committed tests/fixtures/prices-current.db binary is removed — derived SQLite databases are never checked into git, matching the migration's premise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
…s build CI installs with pnpm --frozen-lockfile, so the lockfile must include the new better-sqlite3 / @types/better-sqlite3 entries. Also add pnpm onlyBuiltDependencies for better-sqlite3 so pnpm runs its native build script (prebuild-install) in CI; otherwise the native addon is missing and the SQLite tests cannot load it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
pnpm v11 (used in CI) no longer reads the package.json "pnpm" field and hard-errors on ignored build scripts. Move the build allowance to pnpm-workspace.yaml using the repo's existing allowBuilds convention (already used for esbuild) so better-sqlite3's native install script runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
Resolve ADR 0001's deferred question: JSON stays the canonical, committed source of truth indefinitely; SQLite remains a derived read layer. Record the history-representation problem and options (compact time-series JSON vs out-of-repo vs Pages-served DB vs external store), recommending the infra-free compact-JSON path as the near-term Phase 3 move and deferring Pages/external store as the future 'proper persistence mechanism'. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
…le artifact
- services/sync/src/tokenpricing_sync/history.py: new module; reads all
database/history/prices-*.json snapshots and builds a compact per-model
price time-series {generated_at, models: {id: [{t,in,out,cr,cc}]}}
- services/sync/src/tokenpricing_sync/cli.py: call write_compact_history()
after snapshots are written; add compact_history key to sync() return dict
- services/sync/tests/test_history.py: 12 unit tests covering ordering,
null cache fields, single-point models, and empty-directory edge case
- services/dashboard/src/lib/data.ts: loadHistorySnapshots() now fetches
current/price-history.json in one request; falls back to legacy
multi-snapshot listing on 404 or parse error (loadHistorySnapshotsLegacy)
- database/current/price-history.json: generated from committed history/
snapshots (42 × 3,252 models); 17.5 MB raw / ~430 KB gzip vs 12 × 2.8 MB
(33.6 MB) for the old path — single request replaces up to 12 parallel fetches
- docs/database.md: add price-history.json row to layout table
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d
This was referenced Jul 30, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this PR does
Records the architecture decision and lands Phase 0 + Phase 1 of the migration: keep the git-committed JSON as the canonical, diff-reviewable source of truth, build a derived SQLite
prices.dbin the sync pipeline (never committed), and have the dashboard read from it with the JSON path kept as an airtight fallback.Tracking issue: #60. Decision record:
wiki/decisions/0001-canonical-pricing-database-storage.md.Why
The canonical data is JSON served over the GitHub raw CDN — there is no backend. Every read materializes the entire ~2.9 MB / 3,244-model dataset to answer one question: the dashboard parses it all on mount and filters
O(n)per keystroke; the SDKs parse 2.9 MB on cold start then scan linearly;database/history/is ~115 MB. The cost is payload + parse, not the pricing math. A derived SQLite artifact enables indexed/FTS queries and a compact history time-series without losing the reviewable JSON diff or committing a binary that rewrites every 6 hours.Changes
Decision record (docs)
wiki/decisions/0001-canonical-pricing-database-storage.md— context, three options with trade-offs, the decision, proposed v1 schema, consequences, rollout.wiki/decisions/README.md— decisions index + format conventions.Phase 0 — build & publish
prices.db(non-breaking)services/sync/src/tokenpricing_sync/build_db.py— materializesprices.json+ all history snapshots into SQLite following the v1 schema:meta(PRAGMA user_version = 1),providers,models(indexes on provider/category/model_type),model_sources,price_history, populatedmodels_ftsFTS5. Prices asREAL;VACUUM+ANALYZE.services/sync/.../cli.py— newbuild-dbsubcommand;syncbuilds the DB automatically after writing JSON.services/sync/tests/test_build_db.py— 11 JSON↔DB equivalence tests..gitignore— ignoresprices.db(+ wal/shm); the binary is a published artifact, never committed..github/workflows/database-sync.yml— builds and uploadsprices.dbas theprices-dbworkflow artifact.services/sync/README.md,docs/database.md— document the artifact and command.Phase 1 — dashboard reads SQLite with a guaranteed JSON fallback
services/dashboard/src/lib/sqlite-db.ts— opensprices.dbviasql.js-httpvfs, validatesPRAGMA user_version = 1, runs queries in a worker thread, maps rows back to the existingRawModelInfo/RawPricingDatashapes.services/dashboard/src/lib/data.ts—loadPricingData()tries SQLite first and catches every failure mode (DB 404, worker/WASM error, schema mismatch, flag off) to fall through to the original JSON fetch. No user-visible change on fallback.public/sqlite.worker.js+public/sql-wasm.wasmvendored so Vite serves them at the correct Pages base URL;sql.js-httpvfsadded topackage.json.VITE_SQLITE_ENABLED(default on) andVITE_PRICES_DB_URL(override; defaults toVITE_CANONICAL_DATA_ROOT/current/prices.db).Verification
services/sync: 17 passed, ruff clean.build-dbon live data → 3,244 models / 166 providers / 133,441 history rows /user_version = 1/ FTS working.services/dashboard: typecheck 0 errors, lint clean (1 pre-existing unrelated warning),npm run buildpasses with worker + wasm emitted at the right base..dbtracked in git.Known limitation / follow-up
.dbdownload + in-browser SQL (serverMode: "full"), not HTTP range-request streaming.raw.githubusercontent.comdoes not reliably supportRange(flagged in the decision record). True range streaming can be turned on onceprices.dbis served from GitHub Pages (which supports ranges) with a chunked config — a clean follow-up tracked in Migration: JSON-driven database → derived SQLite read layer for faster UI & SDK queries #60. The in-browser-SQL approach already removes the per-keystrokeO(n)JS scans.Remaining phases (tracked in #60)
Open questions for review
REALvsTEXT-decimal price storage (currentlyREAL).prices.db— workflow artifact (current), GitHub Release, or GitHub Pages (needed to unlock range streaming).🤖 Generated with Claude Code
https://claude.ai/code/session_01Vu4DV9Df3uHTsG87YYVE3d