Skip to content

Commit f6642f9

Browse files
committed
Support raw tables
1 parent 0b4e94c commit f6642f9

File tree

5 files changed

+126
-9
lines changed

5 files changed

+126
-9
lines changed

.changeset/bright-snakes-clean.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@powersync/common': minor
3+
'@powersync/node': minor
4+
'@powersync/web': minor
5+
'@powersync/react-native': minor
6+
---
7+
8+
Add experimental support for raw tables, giving you full control over the table structure to sync into.
9+
While PowerSync manages tables as JSON views by default, raw tables have to be created by the application
10+
developer. Also, the upsert and delete statements for raw tables needs to be specified in the app schema:
11+
12+
```JavaScript
13+
const customSchema = new Schema({});
14+
customSchema.withRawTables({
15+
lists: {
16+
put: {
17+
sql: 'INSERT OR REPLACE INTO lists (id, name) VALUES (?, ?)',
18+
// put statements can use `Id` and extracted columns to bind parameters.
19+
params: ['Id', { Column: 'name' }]
20+
},
21+
delete: {
22+
sql: 'DELETE FROM lists WHERE id = ?',
23+
// delete statements can only use the id (but a CTE querying existing rows by id could
24+
// be used as a workaround).
25+
params: ['Id']
26+
}
27+
}
28+
});
29+
30+
const powersync = // open powersync database;
31+
await powersync.execute('CREATE TABLE lists (id TEXT NOT NULL PRIMARY KEY, name TEXT);');
32+
33+
// Ready to sync into your custom table at this point
34+
```
35+
36+
The main benefit of raw tables is better query performance (since SQLite doesn't have to
37+
extract rows from JSON) and more control (allowing the use of e.g. column and table constraints).

packages/common/rollup.config.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export default (commandLineArgs) => {
3636
ReadableStream: ['web-streams-polyfill/ponyfill', 'ReadableStream'],
3737
// Used by can-ndjson-stream
3838
TextDecoder: ['text-encoding', 'TextDecoder']
39-
}),
40-
terser({ sourceMap })
39+
})
40+
//terser({ sourceMap })
4141
],
4242
// This makes life easier
4343
external: [

packages/node/tests/sync.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
OplogEntryJSON,
88
PowerSyncConnectionOptions,
99
ProgressWithOperations,
10+
Schema,
1011
SyncClientImplementation,
1112
SyncStreamConnectionMethod
1213
} from '@powersync/common';
@@ -638,6 +639,85 @@ function defineSyncTests(impl: SyncClientImplementation) {
638639
expect(another.currentStatus.statusForPriority(0).hasSynced).toBeTruthy();
639640
await another.waitForFirstSync({ priority: 0 });
640641
});
642+
643+
if (impl == SyncClientImplementation.RUST) {
644+
mockSyncServiceTest('raw tables', async ({ syncService }) => {
645+
const customSchema = new Schema({});
646+
customSchema.withRawTables({
647+
lists: {
648+
put: {
649+
sql: 'INSERT OR REPLACE INTO lists (id, name) VALUES (?, ?)',
650+
params: ['Id', { Column: 'name' }]
651+
},
652+
delete: {
653+
sql: 'DELETE FROM lists WHERE id = ?',
654+
params: ['Id']
655+
}
656+
}
657+
});
658+
659+
const powersync = await syncService.createDatabase({ schema: customSchema });
660+
await powersync.execute('CREATE TABLE lists (id TEXT NOT NULL PRIMARY KEY, name TEXT);');
661+
662+
const query = powersync.watchWithAsyncGenerator('SELECT * FROM lists')[Symbol.asyncIterator]();
663+
expect((await query.next()).value.rows._array).toStrictEqual([]);
664+
665+
powersync.connect(new TestConnector(), options);
666+
await vi.waitFor(() => expect(syncService.connectedListeners).toHaveLength(1));
667+
668+
syncService.pushLine({
669+
checkpoint: {
670+
last_op_id: '1',
671+
buckets: [bucket('a', 1)]
672+
}
673+
});
674+
syncService.pushLine({
675+
data: {
676+
bucket: 'a',
677+
data: [
678+
{
679+
checksum: 0,
680+
op_id: '1',
681+
op: 'PUT',
682+
object_id: 'my_list',
683+
object_type: 'lists',
684+
data: '{"name": "custom list"}'
685+
}
686+
]
687+
}
688+
});
689+
syncService.pushLine({ checkpoint_complete: { last_op_id: '1' } });
690+
await powersync.waitForFirstSync();
691+
692+
expect((await query.next()).value.rows._array).toStrictEqual([{ id: 'my_list', name: 'custom list' }]);
693+
694+
syncService.pushLine({
695+
checkpoint: {
696+
last_op_id: '2',
697+
buckets: [bucket('a', 2)]
698+
}
699+
});
700+
await vi.waitFor(() => powersync.currentStatus.dataFlowStatus.downloading == true);
701+
syncService.pushLine({
702+
data: {
703+
bucket: 'a',
704+
data: [
705+
{
706+
checksum: 0,
707+
op_id: '2',
708+
op: 'REMOVE',
709+
object_id: 'my_list',
710+
object_type: 'lists'
711+
}
712+
]
713+
}
714+
});
715+
syncService.pushLine({ checkpoint_complete: { last_op_id: '2' } });
716+
await vi.waitFor(() => powersync.currentStatus.dataFlowStatus.downloading == false);
717+
718+
expect((await query.next()).value.rows._array).toStrictEqual([]);
719+
});
720+
}
641721
}
642722

643723
function bucket(name: string, count: number, options: { priority: number } = { priority: 3 }): BucketChecksum {

packages/node/tests/utils.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
column,
1010
NodePowerSyncDatabaseOptions,
1111
PowerSyncBackendConnector,
12-
PowerSyncConnectionOptions,
1312
PowerSyncCredentials,
1413
PowerSyncDatabase,
1514
Schema,
@@ -56,12 +55,12 @@ async function createDatabase(
5655
options: Partial<NodePowerSyncDatabaseOptions> = {}
5756
): Promise<PowerSyncDatabase> {
5857
const database = new PowerSyncDatabase({
59-
...options,
6058
schema: AppSchema,
6159
database: {
6260
dbFilename: 'test.db',
6361
dbLocation: tmpdir
64-
}
62+
},
63+
...options
6564
});
6665
await database.init();
6766
return database;
@@ -128,8 +127,9 @@ export const mockSyncServiceTest = tempDirectoryTest.extend<{
128127
}
129128
};
130129

131-
const newConnection = async () => {
130+
const newConnection = async (options?: Partial<NodePowerSyncDatabaseOptions>) => {
132131
const db = await createDatabase(tmpdir, {
132+
...options,
133133
remoteOptions: {
134134
fetchImplementation: inMemoryFetch
135135
}
@@ -156,7 +156,7 @@ export const mockSyncServiceTest = tempDirectoryTest.extend<{
156156
export interface MockSyncService {
157157
pushLine: (line: StreamingSyncLine) => void;
158158
connectedListeners: any[];
159-
createDatabase: () => Promise<PowerSyncDatabase>;
159+
createDatabase: (options?: Partial<NodePowerSyncDatabaseOptions>) => Promise<PowerSyncDatabase>;
160160
}
161161

162162
export class TestConnector implements PowerSyncBackendConnector {

packages/web/src/worker/sync/SharedSyncImplementation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ export class SharedSyncImplementation
239239
*/
240240
async connect(options?: PowerSyncConnectionOptions) {
241241
this.lastConnectOptions = options;
242-
return this.connectionManager.connect(CONNECTOR_PLACEHOLDER, options);
242+
return this.connectionManager.connect(CONNECTOR_PLACEHOLDER, options ?? {});
243243
}
244244

245245
async disconnect() {
@@ -318,7 +318,7 @@ export class SharedSyncImplementation
318318
this.dbAdapter = null;
319319

320320
if (shouldReconnect) {
321-
await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions);
321+
await this.connectionManager.connect(CONNECTOR_PLACEHOLDER, this.lastConnectOptions ?? {});
322322
}
323323
}
324324

0 commit comments

Comments
 (0)