Skip to content

Commit 6606888

Browse files
committed
Add SQL operation snapshot testing to Db tests
Introduces a test console to capture and snapshot all SQL queries executed during DbWorker tests. This enables easier review and regression testing of SQL operations by matching them against stored snapshots.
1 parent e501089 commit 6606888

2 files changed

Lines changed: 269 additions & 44 deletions

File tree

packages/common/test/Evolu/Db.test.ts

Lines changed: 96 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { describe, expect, test } from "vitest";
22
import { CallbackId } from "../../src/CallbackRegistry.js";
3-
import { createConsole } from "../../src/Console.js";
43
import { defaultConfig } from "../../src/Evolu/Config.js";
54
import {
65
createDbWorkerForPlatform,
@@ -13,7 +12,9 @@ import { wait } from "../../src/Promise.js";
1312
import { getOrThrow } from "../../src/Result.js";
1413
import { createSqlite, Sqlite } from "../../src/Sqlite.js";
1514
import {
15+
createTestConsole,
1616
createTestWebSocket,
17+
TestConsole,
1718
testCreateId,
1819
testCreateSqliteDriver,
1920
testNanoIdLib,
@@ -26,50 +27,15 @@ import {
2627
} from "../_deps.js";
2728
import { getDbSnapshot } from "./_utils.js";
2829

29-
const createDbWorkerWithDeps = async (): Promise<{
30-
readonly worker: DbWorker;
31-
readonly sqlite: Sqlite;
32-
readonly transports: ReadonlyArray<TestWebSocket>;
33-
}> => {
34-
const sqliteDriver = await testCreateSqliteDriver(testSimpleName);
35-
const sqliteResult = await createSqlite({
36-
createSqliteDriver: () => Promise.resolve(sqliteDriver),
37-
})(testSimpleName);
38-
const sqlite = getOrThrow(sqliteResult);
39-
40-
// Track all created WebSocket transports
41-
const transports: Array<TestWebSocket> = [];
42-
43-
const deps: DbWorkerPlatformDeps = {
44-
console: createConsole(),
45-
createSqliteDriver: () => Promise.resolve(sqliteDriver),
46-
createWebSocket: (url, options) => {
47-
const testWebSocket = createTestWebSocket(url, options);
48-
transports.push(testWebSocket);
49-
return testWebSocket;
50-
},
51-
nanoIdLib: testNanoIdLib,
52-
random: testRandom,
53-
randomBytes: testRandomBytes,
54-
time: testTime,
55-
};
56-
57-
const worker = createDbWorkerForPlatform(deps);
58-
59-
return {
60-
worker,
61-
sqlite,
62-
transports,
63-
};
64-
};
65-
6630
const createInitializedDbWorker = async (): Promise<{
6731
readonly worker: DbWorker;
6832
readonly sqlite: Sqlite;
6933
readonly transports: ReadonlyArray<TestWebSocket>;
7034
readonly workerOutput: Array<unknown>;
35+
readonly testConsole: ReturnType<typeof createTestConsole>;
7136
}> => {
72-
const { worker, sqlite, transports } = await createDbWorkerWithDeps();
37+
const { worker, sqlite, transports, testConsole } =
38+
await createDbWorkerWithDeps();
7339

7440
// Track worker output messages
7541
const workerOutput: Array<unknown> = [];
@@ -110,14 +76,90 @@ const createInitializedDbWorker = async (): Promise<{
11076
sqlite,
11177
transports,
11278
workerOutput,
79+
testConsole,
80+
};
81+
};
82+
83+
const createDbWorkerWithDeps = async (): Promise<{
84+
readonly worker: DbWorker;
85+
readonly sqlite: Sqlite;
86+
readonly transports: ReadonlyArray<TestWebSocket>;
87+
readonly testConsole: ReturnType<typeof createTestConsole>;
88+
}> => {
89+
const sqliteDriver = await testCreateSqliteDriver(testSimpleName);
90+
const testConsole = createTestConsole();
91+
const sqliteResult = await createSqlite({
92+
createSqliteDriver: () => Promise.resolve(sqliteDriver),
93+
console: testConsole,
94+
})(testSimpleName);
95+
const sqlite = getOrThrow(sqliteResult);
96+
97+
// Track all created WebSocket transports
98+
const transports: Array<TestWebSocket> = [];
99+
100+
const deps: DbWorkerPlatformDeps = {
101+
console: testConsole,
102+
createSqliteDriver: () => Promise.resolve(sqliteDriver),
103+
createWebSocket: (url, options) => {
104+
const testWebSocket = createTestWebSocket(url, options);
105+
transports.push(testWebSocket);
106+
return testWebSocket;
107+
},
108+
nanoIdLib: testNanoIdLib,
109+
random: testRandom,
110+
randomBytes: testRandomBytes,
111+
time: testTime,
112+
};
113+
114+
const worker = createDbWorkerForPlatform(deps);
115+
116+
return {
117+
worker,
118+
sqlite,
119+
transports,
120+
testConsole,
113121
};
114122
};
115123

116124
const appOwner = createAppOwner(testOwnerSecret);
117125
const tabId = testCreateId();
118126

127+
const checkSqlOperations = (testConsole: TestConsole): void => {
128+
const logs = testConsole.getLogsSnapshot();
129+
130+
// Only capture SQL strings from query logs: deps.console?.log("[sql]", { query });
131+
const sqlStrings = logs
132+
.filter(
133+
(log) =>
134+
Array.isArray(log) &&
135+
log[0] === "[sql]" &&
136+
log[1] &&
137+
typeof log[1] === "object" &&
138+
"query" in log[1],
139+
)
140+
.map((log) => {
141+
const query = log[1] as { query: { sql: string } };
142+
return normalizeSql(query.query.sql);
143+
});
144+
145+
// Snapshot the normalized SQL strings for easy review
146+
expect(sqlStrings).toMatchSnapshot();
147+
};
148+
149+
const normalizeSql = (sql: string): string => {
150+
// Remove extra whitespace and normalize to single line
151+
const normalized = sql.replace(/\s+/g, " ").trim();
152+
153+
// Truncate if too long, with ellipsis
154+
if (normalized.length > 80) {
155+
return normalized.substring(0, 77) + "...";
156+
}
157+
158+
return normalized;
159+
};
160+
119161
test("initializes DbWorker with external AppOwner", async () => {
120-
const { transports, sqlite } = await createInitializedDbWorker();
162+
const { transports, sqlite, testConsole } = await createInitializedDbWorker();
121163

122164
// Should show empty database with Evolu system tables created
123165
expect(getDbSnapshot({ sqlite })).toMatchInlineSnapshot(`
@@ -257,10 +299,13 @@ test("initializes DbWorker with external AppOwner", async () => {
257299

258300
// Check that we have no WebSocket messages yet (no sync)
259301
expect(transports[0]?.sentMessages ?? []).toEqual([]);
302+
303+
// Check SQL operations
304+
checkSqlOperations(testConsole);
260305
});
261306

262307
test("local mutations", async () => {
263-
const { worker, sqlite, transports, workerOutput } =
308+
const { worker, sqlite, transports, workerOutput, testConsole } =
264309
await createInitializedDbWorker();
265310

266311
const recordId = testCreateId();
@@ -407,7 +452,7 @@ test("local mutations", async () => {
407452
subscribedQueries: [subscribedQuery],
408453
});
409454

410-
// _localTable should be emptu
455+
// _localTable should be empty
411456
expect(getDbSnapshot({ sqlite }).tables).toMatchInlineSnapshot(`
412457
[
413458
{
@@ -495,10 +540,12 @@ test("local mutations", async () => {
495540

496541
// No WebSocket messages (local mutations don't sync)
497542
expect(transports[0]?.sentMessages ?? []).toEqual([]);
543+
544+
checkSqlOperations(testConsole);
498545
});
499546

500547
test("sync mutations", async () => {
501-
const { worker, sqlite, transports, workerOutput } =
548+
const { worker, sqlite, transports, workerOutput, testConsole } =
502549
await createInitializedDbWorker();
503550

504551
const recordId = testCreateId();
@@ -942,11 +989,14 @@ test("sync mutations", async () => {
942989

943990
// WebSocket was not opened.
944991
expect(transports[0]?.sentMessages ?? []).toEqual([]);
992+
993+
checkSqlOperations(testConsole);
945994
});
946995

947996
describe("WebSocket", () => {
948997
test("sends messages when socket is opened", async () => {
949-
const { worker, transports } = await createInitializedDbWorker();
998+
const { worker, transports, testConsole } =
999+
await createInitializedDbWorker();
9501000

9511001
const recordId = testCreateId();
9521002

@@ -981,6 +1031,8 @@ describe("WebSocket", () => {
9811031
]
9821032
`,
9831033
);
1034+
1035+
checkSqlOperations(testConsole);
9841036
});
9851037

9861038
// TODO: test on message (a received message)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`WebSocket > sends messages when socket is opened 2`] = `
4+
[
5+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
6+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
7+
"create table evolu_version ( "protocolVersion" integer not null ) strict;",
8+
"insert into evolu_version ("protocolVersion") values (?);",
9+
"create table evolu_config ( "clock" text not null, "appOwnerId" text not null...",
10+
"insert into evolu_config ( "clock", "appOwnerId", "appOwnerEncryptionKey", "a...",
11+
"create table evolu_history ( "ownerId" blob not null, "table" text not null, ...",
12+
"create index evolu_history_ownerId_timestamp on evolu_history ( "ownerId", "t...",
13+
"create unique index evolu_history_ownerId_table_id_column_timestampDesc on ev...",
14+
"create table "testTable" ( "id" text primary key, "name" blob, "createdAt" bl...",
15+
"create table "_localTable" ( "id" text primary key, "value" blob, "createdAt"...",
16+
"create table if not exists evolu_timestamp ( "ownerId" blob not null, "t" blo...",
17+
"create index if not exists evolu_timestamp_index on evolu_timestamp ( "ownerI...",
18+
"with lastTimestamp as ( select "timestamp" from evolu_history where "ownerId"...",
19+
"select min(t) as minT, max(t) as maxT from evolu_timestamp where ownerId = ?;",
20+
"insert into evolu_timestamp (ownerId, l, t, h1, h2, c) values (?, 1, ?, ?, ?,...",
21+
"with p(l, t, h1, h2) as ( select ( select max(l) + 1 from evolu_timestamp whe...",
22+
"insert into evolu_history ("ownerId", "table", "id", "column", "value", "time...",
23+
"update evolu_config set "clock" = ?;",
24+
"with ml(ml) as ( select max(l) from evolu_timestamp where ownerId = ? ), sc(l...",
25+
"with fi(b, cl, ic, pt, mt, nt, nc) as ( select 0, ( select max(l) from evolu_...",
26+
]
27+
`;
28+
29+
exports[`initializes DbWorker with external AppOwner 2`] = `
30+
[
31+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
32+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
33+
"create table evolu_version ( "protocolVersion" integer not null ) strict;",
34+
"insert into evolu_version ("protocolVersion") values (?);",
35+
"create table evolu_config ( "clock" text not null, "appOwnerId" text not null...",
36+
"insert into evolu_config ( "clock", "appOwnerId", "appOwnerEncryptionKey", "a...",
37+
"create table evolu_history ( "ownerId" blob not null, "table" text not null, ...",
38+
"create index evolu_history_ownerId_timestamp on evolu_history ( "ownerId", "t...",
39+
"create unique index evolu_history_ownerId_table_id_column_timestampDesc on ev...",
40+
"create table "testTable" ( "id" text primary key, "name" blob, "createdAt" bl...",
41+
"create table "_localTable" ( "id" text primary key, "value" blob, "createdAt"...",
42+
"create table if not exists evolu_timestamp ( "ownerId" blob not null, "t" blo...",
43+
"create index if not exists evolu_timestamp_index on evolu_timestamp ( "ownerI...",
44+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
45+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
46+
"select * from "evolu_version";",
47+
"select * from "evolu_config";",
48+
"select * from "evolu_history";",
49+
"select * from "testTable";",
50+
"select * from "_localTable";",
51+
"select * from "evolu_timestamp";",
52+
]
53+
`;
54+
55+
exports[`local mutations 8`] = `
56+
[
57+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
58+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
59+
"create table evolu_version ( "protocolVersion" integer not null ) strict;",
60+
"insert into evolu_version ("protocolVersion") values (?);",
61+
"create table evolu_config ( "clock" text not null, "appOwnerId" text not null...",
62+
"insert into evolu_config ( "clock", "appOwnerId", "appOwnerEncryptionKey", "a...",
63+
"create table evolu_history ( "ownerId" blob not null, "table" text not null, ...",
64+
"create index evolu_history_ownerId_timestamp on evolu_history ( "ownerId", "t...",
65+
"create unique index evolu_history_ownerId_table_id_column_timestampDesc on ev...",
66+
"create table "testTable" ( "id" text primary key, "name" blob, "createdAt" bl...",
67+
"create table "_localTable" ( "id" text primary key, "value" blob, "createdAt"...",
68+
"create table if not exists evolu_timestamp ( "ownerId" blob not null, "t" blo...",
69+
"create index if not exists evolu_timestamp_index on evolu_timestamp ( "ownerI...",
70+
"insert into "_localTable" ("id", "value", createdAt, updatedAt) values (?, ?,...",
71+
"select * from "_localTable" where "isDeleted" is null",
72+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
73+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
74+
"select * from "evolu_version";",
75+
"select * from "evolu_config";",
76+
"select * from "evolu_history";",
77+
"select * from "testTable";",
78+
"select * from "_localTable";",
79+
"select * from "evolu_timestamp";",
80+
"select * from "_localTable" where "isDeleted" is null",
81+
"delete from "_localTable" where id = ?;",
82+
"select * from "_localTable" where "isDeleted" is null",
83+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
84+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
85+
"select * from "evolu_version";",
86+
"select * from "evolu_config";",
87+
"select * from "evolu_history";",
88+
"select * from "testTable";",
89+
"select * from "_localTable";",
90+
"select * from "evolu_timestamp";",
91+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
92+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
93+
"drop table "evolu_version";",
94+
"drop table "evolu_config";",
95+
"drop table "evolu_history";",
96+
"drop table "testTable";",
97+
"drop table "_localTable";",
98+
"drop table "evolu_timestamp";",
99+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
100+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
101+
]
102+
`;
103+
104+
exports[`sync mutations 9`] = `
105+
[
106+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
107+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
108+
"create table evolu_version ( "protocolVersion" integer not null ) strict;",
109+
"insert into evolu_version ("protocolVersion") values (?);",
110+
"create table evolu_config ( "clock" text not null, "appOwnerId" text not null...",
111+
"insert into evolu_config ( "clock", "appOwnerId", "appOwnerEncryptionKey", "a...",
112+
"create table evolu_history ( "ownerId" blob not null, "table" text not null, ...",
113+
"create index evolu_history_ownerId_timestamp on evolu_history ( "ownerId", "t...",
114+
"create unique index evolu_history_ownerId_table_id_column_timestampDesc on ev...",
115+
"create table "testTable" ( "id" text primary key, "name" blob, "createdAt" bl...",
116+
"create table "_localTable" ( "id" text primary key, "value" blob, "createdAt"...",
117+
"create table if not exists evolu_timestamp ( "ownerId" blob not null, "t" blo...",
118+
"create index if not exists evolu_timestamp_index on evolu_timestamp ( "ownerI...",
119+
"with lastTimestamp as ( select "timestamp" from evolu_history where "ownerId"...",
120+
"with lastTimestamp as ( select "timestamp" from evolu_history where "ownerId"...",
121+
"select min(t) as minT, max(t) as maxT from evolu_timestamp where ownerId = ?;",
122+
"insert into evolu_timestamp (ownerId, t, l) values (?, ?, ?) on conflict do n...",
123+
"with c0(b, cl, pt, nt, h1, h2, c) as ( select 0, ( select max(l) from evolu_t...",
124+
"insert into evolu_history ("ownerId", "table", "id", "column", "value", "time...",
125+
"insert into evolu_history ("ownerId", "table", "id", "column", "value", "time...",
126+
"update evolu_config set "clock" = ?;",
127+
"select * from "testTable" where "isDeleted" is null",
128+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
129+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
130+
"select * from "evolu_version";",
131+
"select * from "evolu_config";",
132+
"select * from "evolu_history";",
133+
"select * from "testTable";",
134+
"select * from "_localTable";",
135+
"select * from "evolu_timestamp";",
136+
"with lastTimestamp as ( select "timestamp" from evolu_history where "ownerId"...",
137+
"insert into evolu_timestamp (ownerId, l, t, h1, h2, c) values (?, 1, ?, ?, ?,...",
138+
"insert into evolu_history ("ownerId", "table", "id", "column", "value", "time...",
139+
"update evolu_config set "clock" = ?;",
140+
"select * from "testTable" where "isDeleted" is null",
141+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
142+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
143+
"select * from "evolu_version";",
144+
"select * from "evolu_config";",
145+
"select * from "evolu_history";",
146+
"select * from "testTable";",
147+
"select * from "_localTable";",
148+
"select * from "evolu_timestamp";",
149+
"with lastTimestamp as ( select "timestamp" from evolu_history where "ownerId"...",
150+
"insert into evolu_timestamp (ownerId, l, t, h1, h2, c) values (?, 1, ?, ?, ?,...",
151+
"insert into evolu_history ("ownerId", "table", "id", "column", "value", "time...",
152+
"update evolu_config set "clock" = ?;",
153+
"select * from "testTable" where "isDeleted" is null",
154+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
155+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
156+
"select * from "evolu_version";",
157+
"select * from "evolu_config";",
158+
"select * from "evolu_history";",
159+
"select * from "testTable";",
160+
"select * from "_localTable";",
161+
"select * from "evolu_timestamp";",
162+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
163+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
164+
"drop table "evolu_version";",
165+
"drop table "evolu_config";",
166+
"drop table "evolu_history";",
167+
"drop table "testTable";",
168+
"drop table "_localTable";",
169+
"drop table "evolu_timestamp";",
170+
"select sqlite_master.name as tableName, table_info.name as columnName from sq...",
171+
"select name, sql from sqlite_master where type = 'index' and name not like 's...",
172+
]
173+
`;

0 commit comments

Comments
 (0)