Feat/mysql support#2949
Conversation
Greptile SummaryThis PR adds MySQL/MariaDB as an alternative database backend alongside SQLite, including a full async
Confidence Score: 2/5Not safe to merge — six route handlers will silently return empty objects instead of real data when the MySQL backend is active, and the import endpoint will discard all submitted data without error. The async/await gaps in DataRoutes.ts affect the majority of read endpoints and the entire import flow in MySQL mode. Every one of these handlers completes without error (no exception is thrown), so the breakage is invisible in logs — callers just receive empty responses. The SQLite-specific import hardcoded in the by-file handler adds a hard crash path on top of that. src/services/worker/http/routes/DataRoutes.ts needs the most attention — six handlers need to be converted to async and all MySQL store calls need await. src/services/mysql/migrations/runner.ts should be deleted. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[HTTP Request] --> B{getDatabaseType}
B -->|sqlite| C[ClaudeMemDatabase bun:sqlite sync]
B -->|mysql| D[MySQLDatabase mysql2 async]
C --> E[SQLite SessionStore sync methods]
D --> F[MySQL SessionStore async methods]
E --> G[Route Handlers no await needed]
F --> H{Route Handler async flag?}
H -->|async: getObservations getSummaries getStats getProjects| I[await store method correct response]
H -->|sync: getObservationById import getSessionById| J[missing await Promise serializes to empty object]
G --> K[PaginationHelper resolveResults handles sync and async]
F --> K
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[HTTP Request] --> B{getDatabaseType}
B -->|sqlite| C[ClaudeMemDatabase bun:sqlite sync]
B -->|mysql| D[MySQLDatabase mysql2 async]
C --> E[SQLite SessionStore sync methods]
D --> F[MySQL SessionStore async methods]
E --> G[Route Handlers no await needed]
F --> H{Route Handler async flag?}
H -->|async: getObservations getSummaries getStats getProjects| I[await store method correct response]
H -->|sync: getObservationById import getSessionById| J[missing await Promise serializes to empty object]
G --> K[PaginationHelper resolveResults handles sync and async]
F --> K
|
| @@ -96,7 +107,8 @@ export class PaginationHelper { | |||
| query += ' ORDER BY o.created_at_epoch DESC LIMIT ? OFFSET ?'; | |||
| params.push(limit + 1, offset); | |||
|
|
|||
| const results = db.prepare(query).all(...params) as Observation[]; | |||
| const rawResults = db.prepare(query).all(...params) as Observation[] | Promise<Observation[]>; | |||
| const results = await resolveResults(rawResults); | |||
There was a problem hiding this comment.
merged_into_project column absent from MySQL schema
The getObservations and getSummaries queries both reference o.merged_into_project / ss.merged_into_project in the SELECT list and, when project is supplied, in the WHERE clause. No MySQL migration adds this column to observations or session_summaries. Every call to the /api/observations and /api/summaries endpoints in MySQL mode will fail immediately with Error: Unknown column 'o.merged_into_project' in 'field list'. The column reference was pre-existing for SQLite (where the column does exist), but this PR routes MySQL through the same PaginationHelper and so now exposes the gap.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/worker/PaginationHelper.ts
Line: 65-111
Comment:
**`merged_into_project` column absent from MySQL schema**
The `getObservations` and `getSummaries` queries both reference `o.merged_into_project` / `ss.merged_into_project` in the SELECT list and, when `project` is supplied, in the WHERE clause. No MySQL migration adds this column to `observations` or `session_summaries`. Every call to the `/api/observations` and `/api/summaries` endpoints in MySQL mode will fail immediately with `Error: Unknown column 'o.merged_into_project' in 'field list'`. The column reference was pre-existing for SQLite (where the column does exist), but this PR routes MySQL through the same `PaginationHelper` and so now exposes the gap.
How can I resolve this? If you propose a fix, please make it concise.- Add DatabaseFactory for SQLite/MySQL backend selection - Implement complete MySQL store layer (SessionStore, Observations, Auth, etc.) - Add MySQL config via CLAUDE_MEM_DB_TYPE and CLAUDE_MEM_MYSQL_* env vars - Maintain full backward compatibility with SQLite (default) - All TypeScript compilation errors resolved (0 errors) Based on official v13.4.0
- Add src/services/db/index.ts for backend selection (env/settings.json) - Update DatabaseManager.ts to support both SQLite and MySQL - Update DataRoutes.ts handleGetStats to use async queries for MySQL - Update paths.ts getDatabaseType/getMySQLConfig to read settings.json - Increase worker bundle size limit to 2950KB (SettingsDefaultsManager import) - Build artifacts updated Verified: Worker running with MySQL backend (16083 observations, 753 sessions)
- Add CLAUDE_MEM_WORKER_MODE config (client/server) to SettingsDefaultsManager - Client mode: only writes data, does not process queue or start generator - Server mode: processes queue and starts generator (default for single worker) - SessionManager.getMessageIterator: skip queue processing in client mode - SessionRoutes.ensureGeneratorRunning: skip generator start in client mode - Prevents concurrent processing conflicts when multiple workers share MySQL - Build artifacts updated (worker-service.cjs, etc.) Verified: Worker running with MySQL backend + WORKER_MODE=server
- 添加 stmtAll/stmtGet 辅助函数处理 sync/async 分发 - ChromaSync 构造函数接受 isMySQL 参数 - ensureBackfilled/backfillAllProjects 支持 MySQL 模式 - 修复 db.close() 改为 await db.close() - 移除硬编码 SessionStore 直接调用
- 添加 resolveResults() 辅助函数处理 sync/async 分发 - getObservations/getSummaries/getPrompts 改为 async 方法 - DataRoutes 调用方添加 await - 修复 MySQL 模式下 .all() 返回 Promise 导致的 .slice() 报错
- Add visible date picker inputs (start/end date) - Add quick preset buttons: 3天, 7天, 30天, 全部 - Default to last 3 days on initial load - Add CSS styles for time-range-quick-select - Fix getProjectCatalog() async calls in ViewerRoutes and DataRoutes - Rebuild worker-service.cjs and viewer bundle
- Fix P0: runMigrations now checks if schema_versions table exists before querying - Fix Migration 8: use loop instead of multi-statement exec() for ALTER TABLE - Fix AuthStore: use bcrypt for password hashing (with SHA-256 fallback) - Fix SQL injection: validate index names before using in DROP INDEX - Add IF NOT EXISTS to user_prompts and pending_messages CREATE TABLE
- Guard sessionSearch.close() with typeof check (MySQLSessionSearch has no close method) - Remove redundant db.close() call (sessionStore.close() already ends the shared pool) - Prevents TypeError on MySQL shutdown
Replace 32-bit djb2 hash with SHA-256 (256-bit hash space). With 16,000+ observations, the old 32-bit hash had non-trivial collision probability that could silently drop observations.
- Migration 004 now records version 4 in schema_versions to prevent re-run - Mark migrations/runner.ts as deprecated (unused, active system is migrations.ts) - Resolves Greptile P1 review comments
PaginationHelper queries reference merged_into_project but MySQL schema was missing this column. Add migration to create columns and indexes on observations and session_summaries tables.
9313e63 to
5e369c4
Compare
# Conflicts: # .claude-plugin/plugin.json # .codex-plugin/plugin.json # plugin/package.json # plugin/scripts/mcp-server.cjs # plugin/ui/viewer-bundle.js
|
Closing as outdated. This large MySQL backend targets an older storage architecture and would now require a fresh design against the current SQLite/Postgres storage boundary. |
Summary
Add MySQL/MariaDB as an alternative database backend alongside SQLite.
Changes
Backend
CLAUDE_MEM_DB_TYPEsettingresolveResults()handles sync (SQLite) and async (MySQL).all()callsawaitfor asyncgetProjectCatalog()Frontend
Security & Migration Fixes
runMigrations()now checks ifschema_versionstable exists before queryingexec()for ALTER TABLEsessionSearch.close()methodConfiguration
{ "CLAUDE_MEM_DB_TYPE": "mysql", "CLAUDE_MEM_MYSQL_HOST": "localhost", "CLAUDE_MEM_MYSQL_PORT": "3306", "CLAUDE_MEM_MYSQL_USER": "claude_mem", "CLAUDE_MEM_MYSQL_PASSWORD": "***", "CLAUDE_MEM_MYSQL_DATABASE": "claude_mem" }Default is
sqlite— fully backward compatible.Testing
Stats
56 files, +11,670 / -1,011 lines