authorgravatar for otavio@ossystems.com.brOtavio Salvador <otavio@ossystems.com.br> 2026-10-13 17:19:00-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-13 13:19:00-07:00
logfb8d0fc19c27ff860bdefb7dcfe69eae546f837c
tree52071739bdffa5c93d52550caeb0edacba6f403c
parentc616e16c8fd1da1953fd5d0039fffa9e9b146560
signature Signed by PGP key B5690EEEBB952194

fix(server): collect SQLite planner statistics when the database opens (#18471)


2 files changed, 34 insertions(+), 0 deletions(-)

apps/server/src/persistence/Sqlite.test.ts+30
...@@ -84,6 +84,36 @@ it.effect("shrinks the WAL file back to the size limit after a large write", ()...@@ -84,6 +84,36 @@ it.effect("shrinks the WAL file back to the size limit after a large write", ()
84 );84 );
85});85});
8686
87it.effect("collects planner statistics for an existing database on open", () => {
88 const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-sqlite-stats-"));
89 const dbPath = NodePath.join(tempDir, "state.sqlite");
90 const persistence = () =>
91 SqlitePersistence.layerFromPath(dbPath).pipe(Layer.provide(NodeServices.layer));
92
93 return Effect.gen(function* () {
94 yield* Effect.gen(function* () {
95 const sql = yield* SqlClient.SqlClient;
96 yield* sql`CREATE TABLE stats_probe(id INTEGER PRIMARY KEY, thread_id TEXT NOT NULL)`;
97 yield* sql`CREATE INDEX stats_probe_thread_idx ON stats_probe(thread_id)`;
98 yield* sql`
99 WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 1000)
100 INSERT INTO stats_probe(thread_id) SELECT 'thread-' || (i % 10) FROM n
101 `;
102 }).pipe(Effect.provide(persistence()));
103
104 // A database written before statistics existed gets them on the next open.
105 yield* Effect.gen(function* () {
106 const sql = yield* SqlClient.SqlClient;
107 const rows = yield* sql<{ readonly stat: string }>`
108 SELECT stat FROM sqlite_stat1 WHERE idx = 'stats_probe_thread_idx'
109 `;
110 assert.deepEqual([...rows], [{ stat: "1000 100" }]);
111 }).pipe(Effect.provide(persistence()));
112 }).pipe(
113 Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),
114 );
115});
116
87it.effect("applies busy_timeout in the shared persistence setup", () =>117it.effect("applies busy_timeout in the shared persistence setup", () =>
88 Effect.gen(function* () {118 Effect.gen(function* () {
89 const sql = yield* SqlClient.SqlClient;119 const sql = yield* SqlClient.SqlClient;
apps/server/src/persistence/Sqlite.ts+4
...@@ -23,6 +23,10 @@ const layerSetup = Layer.effectDiscard(...@@ -23,6 +23,10 @@ const layerSetup = Layer.effectDiscard(
23 // largest size until the last connection closes.23 // largest size until the last connection closes.
24 yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`);24 yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`);
25 yield* runMigrations();25 yield* runMigrations();
26 // Without sqlite_stat1 the planner guesses at index selectivity. 0x10012
27 // analyzes tables whose indexes lack stats or whose row count changed 10x,
28 // under a temporary analysis_limit. Otherwise it only checks table sizes.
29 yield* sql`PRAGMA optimize = 0x10012;`;
26 }),30 }),
27);31);
2832