Skip to content

Commit 439f95a

Browse files
committed
fix(sqlite): use upsert instead of replace to prevent cascade deletion of child records
1 parent d14534b commit 439f95a

2 files changed

Lines changed: 98 additions & 2 deletions

File tree

src/infra/db/sqlite/sqlite-store.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export class SqliteStore implements ExtendedStore {
6060
// ========== 数据库初始化 ==========
6161

6262
private initialize(): void {
63+
this.db.pragma('foreign_keys = ON');
6364
this.createTables();
6465
this.createIndexes();
6566
}
@@ -365,10 +366,16 @@ export class SqliteStore implements ExtendedStore {
365366

366367
async saveSnapshot(agentId: string, snapshot: Snapshot): Promise<void> {
367368
const stmt = this.db.prepare(`
368-
INSERT OR REPLACE INTO snapshots (
369+
INSERT INTO snapshots (
369370
agent_id, snapshot_id, messages, last_sfp_index,
370371
last_bookmark, created_at, metadata
371372
) VALUES (?, ?, ?, ?, ?, ?, ?)
373+
ON CONFLICT(agent_id, snapshot_id) DO UPDATE SET
374+
messages = excluded.messages,
375+
last_sfp_index = excluded.last_sfp_index,
376+
last_bookmark = excluded.last_bookmark,
377+
created_at = excluded.created_at,
378+
metadata = excluded.metadata
372379
`);
373380

374381
stmt.run(
@@ -441,11 +448,21 @@ export class SqliteStore implements ExtendedStore {
441448

442449
async saveInfo(agentId: string, info: AgentInfo): Promise<void> {
443450
const stmt = this.db.prepare(`
444-
INSERT OR REPLACE INTO agents (
451+
INSERT INTO agents (
445452
agent_id, template_id, created_at, config_version,
446453
lineage, message_count, last_sfp_index, last_bookmark,
447454
breakpoint, metadata
448455
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
456+
ON CONFLICT(agent_id) DO UPDATE SET
457+
template_id = excluded.template_id,
458+
created_at = excluded.created_at,
459+
config_version = excluded.config_version,
460+
lineage = excluded.lineage,
461+
message_count = excluded.message_count,
462+
last_sfp_index = excluded.last_sfp_index,
463+
last_bookmark = excluded.last_bookmark,
464+
breakpoint = excluded.breakpoint,
465+
metadata = excluded.metadata
449466
`);
450467

451468
stmt.run(

tests/unit/infra/db/sqlite-store.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,85 @@ runner.test('aggregateStats - 统计准确性', async () => {
478478
expect.toEqual(stats.toolCallsByName!['fs_read'], 1);
479479
});
480480

481+
// ========== 5.1.6b 回归测试 - saveInfo 不应级联删除子表数据 (Issue #49) ==========
482+
483+
runner.test('saveInfo - 更新不应删除 messages 和 tool_calls (Issue #49)', async () => {
484+
const agentId = 'agt-issue49';
485+
486+
// 1. 创建 Agent
487+
await store.saveInfo(agentId, {
488+
agentId,
489+
templateId: 'test-template',
490+
createdAt: new Date().toISOString(),
491+
configVersion: 'v2.7.0',
492+
lineage: [],
493+
messageCount: 0,
494+
lastSfpIndex: -1,
495+
metadata: {}
496+
});
497+
498+
// 2. 保存 messages
499+
await store.saveMessages(agentId, [
500+
{ role: 'user', content: [{ type: 'text', text: 'Hello' }] },
501+
{ role: 'assistant', content: [{ type: 'text', text: 'Hi!' }] }
502+
]);
503+
504+
// 3. 保存 tool_calls
505+
await store.saveToolCallRecords(agentId, [
506+
{
507+
id: 'call_issue49',
508+
name: 'fs_read',
509+
input: { path: '/test.txt' },
510+
state: 'COMPLETED' as any,
511+
approval: { required: false },
512+
result: { content: 'ok' },
513+
isError: false,
514+
createdAt: Date.now(),
515+
updatedAt: Date.now(),
516+
auditTrail: []
517+
}
518+
]);
519+
520+
// 4. 保存 snapshot
521+
await store.saveSnapshot(agentId, {
522+
id: 'snap:issue49',
523+
messages: [{ role: 'user', content: [{ type: 'text', text: 'snap' }] }],
524+
lastSfpIndex: 0,
525+
lastBookmark: { seq: 1, timestamp: Date.now() },
526+
createdAt: new Date().toISOString()
527+
});
528+
529+
// 5. 再次调用 saveInfo 更新 agent 元数据(这是触发 bug 的操作)
530+
await store.saveInfo(agentId, {
531+
agentId,
532+
templateId: 'test-template',
533+
createdAt: new Date().toISOString(),
534+
configVersion: 'v2.7.1', // 版本更新
535+
lineage: [],
536+
messageCount: 2,
537+
lastSfpIndex: 0,
538+
metadata: { updated: true }
539+
});
540+
541+
// 6. 验证子表数据未被删除
542+
const messages = await store.loadMessages(agentId);
543+
expect.toHaveLength(messages, 2);
544+
expect.toEqual((messages[0].content[0] as any).text, 'Hello');
545+
546+
const toolCalls = await store.loadToolCallRecords(agentId);
547+
expect.toHaveLength(toolCalls, 1);
548+
expect.toEqual(toolCalls[0].id, 'call_issue49');
549+
550+
const snapshots = await store.listSnapshots(agentId);
551+
expect.toHaveLength(snapshots, 1);
552+
expect.toEqual(snapshots[0].id, 'snap:issue49');
553+
554+
// 7. 验证 agent info 确实已更新
555+
const info = await store.loadInfo(agentId);
556+
expect.toEqual(info!.configVersion, 'v2.7.1');
557+
expect.toDeepEqual(info!.metadata, { updated: true });
558+
});
559+
481560
// ========== 5.1.7 测试事务一致性 ==========
482561

483562
runner.test('saveMessages - 事务回滚测试', async () => {

0 commit comments

Comments
 (0)