From a4ced70e157efda2656226a8a53d0d655b4ffe58 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Mon, 3 Aug 2026 13:34:56 +0000 Subject: [PATCH] fix(openontology): migrate in one transaction, and stop timing out on slow disks CI failed on an unrelated PR when "seeds a package and hydrates it back identically" passed vitest's default 5s timeout. The assertions were fine; the suite is I/O bound and the runner was slow. Two changes. migrate() ran every DDL statement through its own client.execute(), so migration 1's ~30 statements each became a separate durable commit and opening a store paid ~30 fsyncs. Batch each migration into one write transaction instead: locally a fresh migration drops from ~8.2ms to ~5.0ms, and the gap widens as fsync gets more expensive. It also closes a real hole -- a crash part-way could previously leave the schema half-applied while schema_migrations recorded the migration as done, because the statements and the bookkeeping insert were not atomic. Then give the package a 30s testTimeout. These suites drive a real file-backed SQLite database, so their wall time is set by the host filesystem, not by our code. The 5s default is tuned for CPU-bound unit tests and leaves no headroom on a contended runner. Co-Authored-By: Claude Opus 5 (1M context) --- packages/openontology/src/libsql.ts | 22 +++++++++++++++------- packages/openontology/vitest.config.ts | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 packages/openontology/vitest.config.ts diff --git a/packages/openontology/src/libsql.ts b/packages/openontology/src/libsql.ts index 0ef4dc1..a1eab05 100644 --- a/packages/openontology/src/libsql.ts +++ b/packages/openontology/src/libsql.ts @@ -225,13 +225,21 @@ export async function migrate(client: Client): Promise { let count = 0; for (const migration of MIGRATIONS) { if (have.has(migration.version)) continue; - for (const statement of migration.statements) { - await client.execute(statement); - } - await client.execute({ - sql: "INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)", - args: [migration.version, migration.name, new Date().toISOString()] - }); + // One transaction per migration, rather than one implicit transaction per + // statement. Migration 1 alone is ~30 DDL statements, so opening a store + // used to cost ~30 durable commits; on a filesystem with slow fsync that + // dominated the open. It is also safer: a crash part-way can no longer + // leave the schema half-applied while schema_migrations claims it is done. + await client.batch( + [ + ...migration.statements, + { + sql: "INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)", + args: [migration.version, migration.name, new Date().toISOString()] as InArgs + } + ], + "write" + ); count += 1; } return count; diff --git a/packages/openontology/vitest.config.ts b/packages/openontology/vitest.config.ts new file mode 100644 index 0000000..c366517 --- /dev/null +++ b/packages/openontology/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + // These suites drive a real file-backed SQLite database through + // @libsql/client, so their wall time is set by the host filesystem's + // durability cost, not by our code. Locally the whole libsql suite runs in + // ~250ms; on a contended CI runner a single seed-and-reopen case has been + // seen to pass 5s, which is vitest's default and is tuned for CPU-bound + // unit tests. Give the I/O-bound cases enough headroom that a slow disk + // reports as slow rather than as a spurious failure. + testTimeout: 30_000, + hookTimeout: 30_000 + } +});