Skip to content

Commit 288d69b

Browse files
committed
Added switch to online tests.
1 parent 2bb6bce commit 288d69b

File tree

1 file changed

+174
-0
lines changed

1 file changed

+174
-0
lines changed

packages/web/tests/offline.test.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { AbstractPowerSyncDatabase, column, Column, ColumnType, Schema, Table } from '@powersync/common';
2+
import { PowerSyncDatabase } from '@powersync/web';
3+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
4+
5+
const assetId = '2290de4f-0488-4e50-abed-f8e8eb1d0b42';
6+
const userId = '3390de4f-0488-4e50-abed-f8e8eb1d0b42';
7+
const customerId = '4490de4f-0488-4e50-abed-f8e8eb1d0b42';
8+
9+
const assetsDef = {
10+
name: 'assets',
11+
columns: {
12+
created_at: column.text,
13+
make: column.text,
14+
model: column.text,
15+
serial_number: column.text,
16+
quantity: column.integer,
17+
user_id: column.text,
18+
customer_id: column.text,
19+
description: column.text
20+
},
21+
options: { indexes: { makemodel: ['make, model'] } }
22+
};
23+
24+
const customersDef = {
25+
name: 'customers',
26+
columns: {
27+
name: column.text,
28+
email: column.text
29+
},
30+
options: {}
31+
};
32+
33+
function makeSchema(synced: boolean) {
34+
const syncedName = (table: string): string => {
35+
if (synced) {
36+
return table;
37+
} else {
38+
return `inactive_synced_${table}`;
39+
}
40+
};
41+
42+
const localName = (table: string): string => {
43+
if (synced) {
44+
return `inactive_local_${table}`;
45+
} else {
46+
return table;
47+
}
48+
};
49+
return new Schema({
50+
assets: new Table(assetsDef.columns, { ...assetsDef.options, viewName: syncedName(assetsDef.name) }),
51+
local_assets: new Table(assetsDef.columns, {
52+
...assetsDef.options,
53+
localOnly: true,
54+
viewName: localName(assetsDef.name)
55+
}),
56+
customers: new Table(customersDef.columns, { ...customersDef.options, viewName: syncedName(customersDef.name) }),
57+
local_customers: new Table(customersDef.columns, {
58+
...customersDef.options,
59+
localOnly: true,
60+
viewName: localName(customersDef.name)
61+
})
62+
});
63+
}
64+
65+
describe('Schema Tests', () => {
66+
let db: AbstractPowerSyncDatabase;
67+
68+
beforeEach(async () => {
69+
db = new PowerSyncDatabase({
70+
/**
71+
* Deleting the IndexDB seems to freeze the test.
72+
* Use a new DB for each run to keep CRUD counters
73+
* consistent
74+
*/
75+
database: { dbFilename: 'test.db' },
76+
schema: makeSchema(false),
77+
flags: {
78+
enableMultiTabs: false
79+
}
80+
});
81+
});
82+
83+
afterEach(async () => {
84+
await db.disconnectAndClear();
85+
await db.close();
86+
});
87+
88+
it('Switch from offline-only to online schema', async () => {
89+
await db.execute('INSERT INTO customers(id, name, email) VALUES(?, ?, ?)', [
90+
customerId,
91+
'test customer',
92+
93+
]);
94+
95+
await db.execute('INSERT INTO assets(id, description, customer_id) VALUES(?, ?, ?)', [assetId, 'test', customerId]);
96+
await db.execute('UPDATE assets SET description = description || ?', ['.']);
97+
98+
expect(await db.getAll('SELECT data FROM ps_crud ORDER BY id')).toEqual([]);
99+
100+
// Now switch to the "online" schema
101+
await db.updateSchema(makeSchema(true));
102+
103+
// Note that updateSchema cannot be called inside a transaction, and there
104+
// is a possibility of crash between updating the schema, and when the data
105+
// has been moved. It may be best to attempt the data move on every application
106+
// start where the online schema is used, if there is any local_ data still present.
107+
108+
await db.writeTransaction(async (tx) => {
109+
// Copy local data to the "online" views.
110+
// This records each operation to the crud queue.
111+
await tx.execute('INSERT INTO customers SELECT * FROM inactive_local_customers');
112+
await tx.execute(
113+
'INSERT INTO assets(id, description, customer_id, user_id) SELECT id, description, customer_id, ? FROM inactive_local_assets',
114+
[userId]
115+
);
116+
117+
// Delete the "offline-only" data.
118+
await tx.execute('DELETE FROM inactive_local_customers');
119+
await tx.execute('DELETE FROM inactive_local_assets');
120+
});
121+
122+
const crud = (await db.getAll<{ data: string }>('SELECT data FROM ps_crud ORDER BY id')).map((d) =>
123+
JSON.parse(d.data)
124+
);
125+
126+
expect(crud).toEqual([
127+
{
128+
op: 'PUT',
129+
type: 'customers',
130+
id: customerId,
131+
data: { email: '[email protected]', name: 'test customer' }
132+
},
133+
{
134+
op: 'PUT',
135+
type: 'assets',
136+
id: assetId,
137+
data: {
138+
user_id: userId,
139+
customer_id: customerId,
140+
description: 'test.'
141+
}
142+
}
143+
]);
144+
});
145+
146+
// Indicates that we don't need to refresh the refresh the schema explicitly
147+
it('Correct source table after switching schema', async () => {
148+
const customerWatchTables = await getSourceTables(db, 'SELECT * FROM customers');
149+
expect(customerWatchTables.includes('ps_data_local__local_customers')).toBeTruthy();
150+
151+
await db.updateSchema(makeSchema(true));
152+
153+
const onlineCustomerWatchTables = await getSourceTables(db, 'SELECT * FROM customers');
154+
expect(onlineCustomerWatchTables.includes('ps_data__customers')).toBeTruthy();
155+
});
156+
});
157+
158+
export async function getSourceTables(db: AbstractPowerSyncDatabase, sql: string, parameters: Array<any> = []) {
159+
const rows = await db.getAll<{ opcode: string; p3: number; p2: string }>(`EXPLAIN ${sql}`, parameters);
160+
const rootpages: number[] = [];
161+
162+
for (const row of rows) {
163+
if (row.opcode === 'OpenRead' && row.p3 === 0 && typeof row.p2 === 'number') {
164+
rootpages.push(row.p2);
165+
}
166+
}
167+
168+
const tableRows = await db.getAll<{ tbl_name: string }>(
169+
`SELECT tbl_name FROM sqlite_master WHERE rootpage IN (SELECT json_each.value FROM json_each(?))`,
170+
[JSON.stringify(rootpages)]
171+
);
172+
173+
return tableRows.map((row: { tbl_name: string }) => row.tbl_name);
174+
}

0 commit comments

Comments
 (0)