From d8b972cfc305e14ae6fad6e040348dd6d21e6f62 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Mon, 5 Oct 2026 16:19:31 -0400 Subject: [PATCH] Reuse fixed SQL statements for observation pruning --- src/lib/server/hosts/index.ts | 10 ++++---- tests/unit/host-observation-retention.test.ts | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/lib/server/hosts/index.ts b/src/lib/server/hosts/index.ts index b9d939f..5857f75 100644 --- a/src/lib/server/hosts/index.ts +++ b/src/lib/server/hosts/index.ts @@ -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 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. */ diff --git a/tests/unit/host-observation-retention.test.ts b/tests/unit/host-observation-retention.test.ts index b715e16..a93a131 100644 --- a/tests/unit/host-observation-retention.test.ts +++ b/tests/unit/host-observation-retention.test.ts @@ -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>(); + 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');