Reuse fixed SQL statements for observation pruning

This commit is contained in:
2026-10-05 16:19:31 -04:00
parent 1d9e518feb
commit d8b972cfc3
2 changed files with 30 additions and 5 deletions
+5 -5
View File
@@ -428,11 +428,11 @@ export function pruneHostObservations(): number {
const rows = handle.prepare(`SELECT id FROM ec_host_operations WHERE observation_success=1 AND state='completed'
AND updated_at<? ORDER BY updated_at LIMIT 500`).all(cutoff) as { id: string }[];
if (!rows.length) return 0;
const ids = rows.map(row => row.id);
const placeholders = ids.map(() => '?').join(',');
handle.prepare(`DELETE FROM ec_host_operation_events WHERE operation_id IN (${placeholders})`).run(...ids);
handle.prepare(`DELETE FROM ec_host_operation_payloads WHERE operation_id IN (${placeholders})`).run(...ids);
return handle.prepare(`DELETE FROM ec_host_operations WHERE id IN (${placeholders})`).run(...ids).changes;
// Fixed SQL text prevents each batch size from adding three cached statements.
const ids = JSON.stringify(rows.map(row => row.id));
handle.prepare('DELETE FROM ec_host_operation_events WHERE operation_id IN (SELECT value FROM json_each(?))').run(ids);
handle.prepare('DELETE FROM ec_host_operation_payloads WHERE operation_id IN (SELECT value FROM json_each(?))').run(ids);
return handle.prepare('DELETE FROM ec_host_operations WHERE id IN (SELECT value FROM json_each(?))').run(ids).changes;
})();
}
/** Heartbeats can tell an executing create to stop at its next safe command boundary. */
@@ -141,6 +141,31 @@ describe('routine host observation retention', () => {
expect(api.handle.prepare("SELECT COUNT(*) AS count FROM ec_host_operations WHERE kind='instance.restart'").get()).toEqual({ count: 2000 });
});
it('reuses the same three delete statements across different batch sizes', async () => {
const api = await setup();
const statements = new Map<string, ReturnType<typeof api.handle.prepare>>();
for (const count of [1, 2, 7, 50, 500]) {
api.handle.transaction(() => {
const insert = api.handle.prepare(`INSERT INTO ec_host_operations
(id,host_id,kind,payload,state,created_at,updated_at,observation_success)
VALUES(?,?,'files.stat','{}','completed',?,?,1)`);
for (let i = 0; i < count; i++) insert.run(`read-${count}-${i}`, api.host.id, at(2 * day), at(2 * day));
})();
const prepare = vi.spyOn(api.handle, 'prepare');
expect(api.pruneHostObservations()).toBe(count);
const deletes = prepare.mock.calls.map(([sql]) => sql).filter(sql => sql.startsWith('DELETE FROM ec_host_'));
prepare.mockRestore();
expect(deletes).toHaveLength(3);
for (const sql of deletes) {
const statement = api.handle.prepare(sql);
if (statements.has(sql)) expect(statement).toBe(statements.get(sql));
statements.set(sql, statement);
}
}
expect(statements.size).toBe(3);
expect(api.handle.pragma('foreign_key_check')).toEqual([]);
});
it('backfills legacy observation classification once while preserving actions and negative or invalid results', async () => {
const api = await setup();
const read = api.add('files.manifest');