Skip to content

Commit af5da32

Browse files
simolus3whygee-dev
authored andcommitted
Raw tables (powersync-ja#654)
1 parent 8f0e8df commit af5da32

File tree

11 files changed

+370
-27
lines changed

11 files changed

+370
-27
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/src/client/AbstractPowerSyncDatabase.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { CrudTransaction } from './sync/bucket/CrudTransaction.js';
2727
import {
2828
DEFAULT_CRUD_UPLOAD_THROTTLE_MS,
2929
DEFAULT_RETRY_DELAY_MS,
30+
InternalConnectionOptions,
3031
StreamingSyncImplementation,
3132
StreamingSyncImplementationListener,
3233
type AdditionalConnectionOptions,
@@ -462,7 +463,10 @@ export abstract class AbstractPowerSyncDatabase extends BaseObserver<PowerSyncDB
462463
* Connects to stream of events from the PowerSync instance.
463464
*/
464465
async connect(connector: PowerSyncBackendConnector, options?: PowerSyncConnectionOptions) {
465-
return this.connectionManager.connect(connector, options);
466+
const resolvedOptions: InternalConnectionOptions = options ?? {};
467+
resolvedOptions.serializedSchema = this.schema.toJSON();
468+
469+
return this.connectionManager.connect(connector, resolvedOptions);
466470
}
467471

468472
/**

packages/common/src/client/ConnectionManager.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ILogger } from 'js-logger';
22
import { BaseListener, BaseObserver } from '../utils/BaseObserver.js';
33
import { PowerSyncBackendConnector } from './connection/PowerSyncBackendConnector.js';
44
import {
5-
PowerSyncConnectionOptions,
5+
InternalConnectionOptions,
66
StreamingSyncImplementation
77
} from './sync/stream/AbstractStreamingSyncImplementation.js';
88

@@ -24,14 +24,14 @@ export interface ConnectionManagerSyncImplementationResult {
2424
export interface ConnectionManagerOptions {
2525
createSyncImplementation(
2626
connector: PowerSyncBackendConnector,
27-
options: PowerSyncConnectionOptions
27+
options: InternalConnectionOptions
2828
): Promise<ConnectionManagerSyncImplementationResult>;
2929
logger: ILogger;
3030
}
3131

3232
type StoredConnectionOptions = {
3333
connector: PowerSyncBackendConnector;
34-
options: PowerSyncConnectionOptions;
34+
options: InternalConnectionOptions;
3535
};
3636

3737
/**
@@ -95,7 +95,7 @@ export class ConnectionManager extends BaseObserver<ConnectionManagerListener> {
9595
await this.syncDisposer?.();
9696
}
9797

98-
async connect(connector: PowerSyncBackendConnector, options?: PowerSyncConnectionOptions) {
98+
async connect(connector: PowerSyncBackendConnector, options: InternalConnectionOptions) {
9999
// Keep track if there were pending operations before this call
100100
const hadPendingOptions = !!this.pendingConnectionOptions;
101101

@@ -140,7 +140,7 @@ export class ConnectionManager extends BaseObserver<ConnectionManagerListener> {
140140
}
141141

142142
protected async connectInternal() {
143-
let appliedOptions: PowerSyncConnectionOptions | null = null;
143+
let appliedOptions: InternalConnectionOptions | null = null;
144144

145145
// This method ensures a disconnect before any connection attempt
146146
await this.disconnectInternal();

packages/common/src/client/sync/stream/AbstractStreamingSyncImplementation.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ export interface StreamingSyncImplementationListener extends BaseListener {
123123
* Configurable options to be used when connecting to the PowerSync
124124
* backend instance.
125125
*/
126-
export interface PowerSyncConnectionOptions extends BaseConnectionOptions, AdditionalConnectionOptions {}
126+
export type PowerSyncConnectionOptions = Omit<InternalConnectionOptions, 'serializedSchema'>;
127+
128+
export interface InternalConnectionOptions extends BaseConnectionOptions, AdditionalConnectionOptions {}
127129

128130
/** @internal */
129131
export interface BaseConnectionOptions {
@@ -152,6 +154,11 @@ export interface BaseConnectionOptions {
152154
* These parameters are passed to the sync rules, and will be available under the`user_parameters` object.
153155
*/
154156
params?: Record<string, StreamingSyncRequestParameterType>;
157+
158+
/**
159+
* The serialized schema - mainly used to forward information about raw tables to the sync client.
160+
*/
161+
serializedSchema?: any;
155162
}
156163

157164
/** @internal */
@@ -176,7 +183,7 @@ export interface StreamingSyncImplementation extends BaseObserver<StreamingSyncI
176183
/**
177184
* Connects to the sync service
178185
*/
179-
connect(options?: PowerSyncConnectionOptions): Promise<void>;
186+
connect(options?: InternalConnectionOptions): Promise<void>;
180187
/**
181188
* Disconnects from the sync services.
182189
* @throws if not connected or if abort is not controlled internally
@@ -208,7 +215,8 @@ export const DEFAULT_STREAM_CONNECTION_OPTIONS: RequiredPowerSyncConnectionOptio
208215
connectionMethod: SyncStreamConnectionMethod.WEB_SOCKET,
209216
clientImplementation: DEFAULT_SYNC_CLIENT_IMPLEMENTATION,
210217
fetchStrategy: FetchStrategy.Buffered,
211-
params: {}
218+
params: {},
219+
serializedSchema: undefined
212220
};
213221

214222
// The priority we assume when we receive checkpoint lines where no priority is set.
@@ -1019,12 +1027,12 @@ The next upload iteration will be delayed.`);
10191027
}
10201028

10211029
try {
1022-
await control(
1023-
PowerSyncControlCommand.START,
1024-
JSON.stringify({
1025-
parameters: resolvedOptions.params
1026-
})
1027-
);
1030+
const options: any = { parameters: resolvedOptions.params };
1031+
if (resolvedOptions.serializedSchema) {
1032+
options.schema = resolvedOptions.serializedSchema;
1033+
}
1034+
1035+
await control(PowerSyncControlCommand.START, JSON.stringify(options));
10281036

10291037
this.notifyCompletedUploads = () => {
10301038
controlInvocations?.enqueueData({ command: PowerSyncControlCommand.NOTIFY_CRUD_UPLOAD_COMPLETED });
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* A pending variant of a {@link RawTable} that doesn't have a name (because it would be inferred when creating the
3+
* schema).
4+
*/
5+
export type RawTableType = {
6+
/**
7+
* The statement to run when PowerSync detects that a row needs to be inserted or updated.
8+
*/
9+
put: PendingStatement;
10+
/**
11+
* The statement to run when PowerSync detects that a row needs to be deleted.
12+
*/
13+
delete: PendingStatement;
14+
};
15+
16+
/**
17+
* A parameter to use as part of {@link PendingStatement}.
18+
*
19+
* For delete statements, only the `"Id"` value is supported - the sync client will replace it with the id of the row to
20+
* be synced.
21+
*
22+
* For insert and replace operations, the values of columns in the table are available as parameters through
23+
* `{Column: 'name'}`.
24+
*/
25+
export type PendingStatementParameter = 'Id' | { Column: string };
26+
27+
/**
28+
* A statement that the PowerSync client should use to insert or delete data into a table managed by the user.
29+
*/
30+
export type PendingStatement = {
31+
sql: string;
32+
params: PendingStatementParameter[];
33+
};
34+
35+
/**
36+
* Instructs PowerSync to sync data into a "raw" table.
37+
*
38+
* Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
39+
* using client-side table and column constraints.
40+
*
41+
* Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
42+
*
43+
* @experimental Please note that this feature is experimental at the moment, and not covered by PowerSync semver or
44+
* stability guarantees.
45+
*/
46+
export class RawTable implements RawTableType {
47+
/**
48+
* The name of the table.
49+
*
50+
* This does not have to match the actual table name in the schema - {@link put} and {@link delete} are free to use
51+
* another table. Instead, this name is used by the sync client to recognize that operations on this table (as it
52+
* appears in the source / backend database) are to be handled specially.
53+
*/
54+
name: string;
55+
put: PendingStatement;
56+
delete: PendingStatement;
57+
58+
constructor(name: string, type: RawTableType) {
59+
this.name = name;
60+
this.put = type.put;
61+
this.delete = type.delete;
62+
}
63+
}

packages/common/src/db/schema/Schema.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { RawTable, RawTableType } from './RawTable.js';
12
import { RowType, Table } from './Table.js';
23

34
type SchemaType = Record<string, Table<any>>;
@@ -16,6 +17,7 @@ export class Schema<S extends SchemaType = SchemaType> {
1617
readonly types: SchemaTableType<S>;
1718
readonly props: S;
1819
readonly tables: Table[];
20+
readonly rawTables: RawTable[];
1921

2022
constructor(tables: Table[] | S) {
2123
if (Array.isArray(tables)) {
@@ -36,6 +38,24 @@ export class Schema<S extends SchemaType = SchemaType> {
3638
this.props = tables as S;
3739
this.tables = this.convertToClassicTables(this.props);
3840
}
41+
42+
this.rawTables = [];
43+
}
44+
45+
/**
46+
* Adds raw tables to this schema. Raw tables are identified by their name, but entirely managed by the application
47+
* developer instead of automatically by PowerSync.
48+
* Since raw tables are not backed by JSON, running complex queries on them may be more efficient. Further, they allow
49+
* using client-side table and column constraints.
50+
* Note that raw tables are only supported when using the new `SyncClientImplementation.rust` sync client.
51+
*
52+
* @param tables An object of (table name, raw table definition) entries.
53+
* @experimental Note that the raw tables API is still experimental and may change in the future.
54+
*/
55+
withRawTables(tables: Record<string, RawTableType>) {
56+
for (const [name, rawTableDefinition] of Object.entries(tables)) {
57+
this.rawTables.push(new RawTable(name, rawTableDefinition));
58+
}
3959
}
4060

4161
validate() {
@@ -47,7 +67,8 @@ export class Schema<S extends SchemaType = SchemaType> {
4767
toJSON() {
4868
return {
4969
// This is required because "name" field is not present in TableV2
50-
tables: this.tables.map((t) => t.toJSON())
70+
tables: this.tables.map((t) => t.toJSON()),
71+
raw_tables: this.rawTables
5172
};
5273
}
5374

packages/common/tests/db/schema/Schema.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ describe('Schema', () => {
8080
content: column.text
8181
})
8282
});
83+
schema.withRawTables({
84+
lists: {
85+
put: {
86+
sql: 'SELECT 1',
87+
params: [{ Column: 'foo' }]
88+
},
89+
delete: {
90+
sql: 'SELECT 2',
91+
params: ['Id']
92+
}
93+
}
94+
});
8395

8496
const json = schema.toJSON();
8597

@@ -115,6 +127,19 @@ describe('Schema', () => {
115127
],
116128
indexes: []
117129
}
130+
],
131+
raw_tables: [
132+
{
133+
name: 'lists',
134+
delete: {
135+
sql: 'SELECT 2',
136+
params: ['Id']
137+
},
138+
put: {
139+
sql: 'SELECT 1',
140+
params: [{ Column: 'foo' }]
141+
}
142+
}
118143
]
119144
});
120145
});

0 commit comments

Comments
 (0)