Skip to content

Commit 37f67a0

Browse files
authored
Merge pull request #300 from powersync-ja/raw-tables
Raw tables
2 parents df5665a + ffd5bf2 commit 37f67a0

13 files changed

+353
-54
lines changed

packages/powersync_core/lib/src/database/native/native_powersync_database.dart

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:convert';
23
import 'dart:isolate';
34
import 'package:meta/meta.dart';
45

@@ -83,8 +84,12 @@ class PowerSyncDatabaseImpl
8384
DefaultSqliteOpenFactory factory =
8485
// ignore: deprecated_member_use_from_same_package
8586
PowerSyncOpenFactory(path: path, sqliteSetup: sqliteSetup);
86-
return PowerSyncDatabaseImpl.withFactory(factory,
87-
schema: schema, maxReaders: maxReaders, logger: logger);
87+
return PowerSyncDatabaseImpl.withFactory(
88+
factory,
89+
schema: schema,
90+
maxReaders: maxReaders,
91+
logger: logger,
92+
);
8893
}
8994

9095
/// Open a [PowerSyncDatabase] with a [PowerSyncOpenFactory].
@@ -96,22 +101,29 @@ class PowerSyncDatabaseImpl
96101
///
97102
/// [logger] defaults to [autoLogger], which logs to the console in debug builds.
98103
factory PowerSyncDatabaseImpl.withFactory(
99-
DefaultSqliteOpenFactory openFactory,
100-
{required Schema schema,
101-
int maxReaders = SqliteDatabase.defaultMaxReaders,
102-
Logger? logger}) {
104+
DefaultSqliteOpenFactory openFactory, {
105+
required Schema schema,
106+
int maxReaders = SqliteDatabase.defaultMaxReaders,
107+
Logger? logger,
108+
}) {
103109
final db = SqliteDatabase.withFactory(openFactory, maxReaders: maxReaders);
104110
return PowerSyncDatabaseImpl.withDatabase(
105-
schema: schema, database: db, logger: logger);
111+
schema: schema,
112+
database: db,
113+
logger: logger,
114+
);
106115
}
107116

108117
/// Open a PowerSyncDatabase on an existing [SqliteDatabase].
109118
///
110119
/// Migrations are run on the database when this constructor is called.
111120
///
112121
/// [logger] defaults to [autoLogger], which logs to the console in debug builds.s
113-
PowerSyncDatabaseImpl.withDatabase(
114-
{required this.schema, required this.database, Logger? logger}) {
122+
PowerSyncDatabaseImpl.withDatabase({
123+
required this.schema,
124+
required this.database,
125+
Logger? logger,
126+
}) {
115127
this.logger = logger ?? autoLogger;
116128
isInitialized = baseInit();
117129
}
@@ -247,6 +259,7 @@ class PowerSyncDatabaseImpl
247259
options,
248260
crudMutex.shared,
249261
syncMutex.shared,
262+
jsonEncode(schema),
250263
),
251264
debugName: 'Sync ${database.openFactory.path}',
252265
onError: receiveUnhandledErrors.sendPort,
@@ -290,13 +303,15 @@ class _PowerSyncDatabaseIsolateArgs {
290303
final ResolvedSyncOptions options;
291304
final SerializedMutex crudMutex;
292305
final SerializedMutex syncMutex;
306+
final String schemaJson;
293307

294308
_PowerSyncDatabaseIsolateArgs(
295309
this.sPort,
296310
this.dbRef,
297311
this.options,
298312
this.crudMutex,
299313
this.syncMutex,
314+
this.schemaJson,
300315
);
301316
}
302317

@@ -392,6 +407,7 @@ Future<void> _syncIsolate(_PowerSyncDatabaseIsolateArgs args) async {
392407
final storage = BucketStorage(connection);
393408
final sync = StreamingSyncImplementation(
394409
adapter: storage,
410+
schemaJson: args.schemaJson,
395411
connector: InternalConnector(
396412
getCredentialsCached: getCredentialsCached,
397413
prefetchCredentials: prefetchCredentials,

packages/powersync_core/lib/src/database/powersync_database.dart

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,21 @@ abstract class PowerSyncDatabase
3232
/// A maximum of [maxReaders] concurrent read transactions are allowed.
3333
///
3434
/// [logger] defaults to [autoLogger], which logs to the console in debug builds.
35-
factory PowerSyncDatabase(
36-
{required Schema schema,
37-
required String path,
38-
Logger? logger,
39-
@Deprecated("Use [PowerSyncDatabase.withFactory] instead.")
40-
// ignore: deprecated_member_use_from_same_package
41-
SqliteConnectionSetup? sqliteSetup}) {
35+
factory PowerSyncDatabase({
36+
required Schema schema,
37+
required String path,
38+
Logger? logger,
39+
@Deprecated("Use [PowerSyncDatabase.withFactory] instead.")
40+
// ignore: deprecated_member_use_from_same_package
41+
SqliteConnectionSetup? sqliteSetup,
42+
}) {
4243
return PowerSyncDatabaseImpl(
43-
schema: schema,
44-
path: path,
45-
logger: logger,
46-
// ignore: deprecated_member_use_from_same_package
47-
sqliteSetup: sqliteSetup);
44+
schema: schema,
45+
path: path,
46+
logger: logger,
47+
// ignore: deprecated_member_use_from_same_package
48+
sqliteSetup: sqliteSetup,
49+
);
4850
}
4951

5052
/// Open a [PowerSyncDatabase] with a [PowerSyncOpenFactory].
@@ -55,12 +57,18 @@ abstract class PowerSyncDatabase
5557
/// Subclass [PowerSyncOpenFactory] to add custom logic to this process.
5658
///
5759
/// [logger] defaults to [autoLogger], which logs to the console in debug builds.
58-
factory PowerSyncDatabase.withFactory(DefaultSqliteOpenFactory openFactory,
59-
{required Schema schema,
60-
int maxReaders = SqliteDatabase.defaultMaxReaders,
61-
Logger? logger}) {
62-
return PowerSyncDatabaseImpl.withFactory(openFactory,
63-
schema: schema, maxReaders: maxReaders, logger: logger);
60+
factory PowerSyncDatabase.withFactory(
61+
DefaultSqliteOpenFactory openFactory, {
62+
required Schema schema,
63+
int maxReaders = SqliteDatabase.defaultMaxReaders,
64+
Logger? logger,
65+
}) {
66+
return PowerSyncDatabaseImpl.withFactory(
67+
openFactory,
68+
schema: schema,
69+
maxReaders: maxReaders,
70+
logger: logger,
71+
);
6472
}
6573

6674
/// Open a PowerSyncDatabase on an existing [SqliteDatabase].

packages/powersync_core/lib/src/database/powersync_database_impl_stub.dart

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@ class PowerSyncDatabaseImpl
8282
/// Migrations are run on the database when this constructor is called.
8383
///
8484
/// [logger] defaults to [autoLogger], which logs to the console in debug builds.s
85-
factory PowerSyncDatabaseImpl.withDatabase(
86-
{required Schema schema,
87-
required SqliteDatabase database,
88-
Logger? logger}) {
85+
factory PowerSyncDatabaseImpl.withDatabase({
86+
required Schema schema,
87+
required SqliteDatabase database,
88+
Logger? logger,
89+
}) {
8990
throw UnimplementedError();
9091
}
9192

packages/powersync_core/lib/src/database/powersync_db_mixin.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,13 @@ mixin PowerSyncDatabaseMixin implements SqliteConnection {
297297
params: params,
298298
);
299299

300+
if (schema.rawTables.isNotEmpty &&
301+
resolvedOptions.source.syncImplementation !=
302+
SyncClientImplementation.rust) {
303+
throw UnsupportedError(
304+
'Raw tables are only supported by the Rust client.');
305+
}
306+
300307
// ignore: deprecated_member_use_from_same_package
301308
clientParams = params;
302309
var thisConnectAborter = AbortController();

packages/powersync_core/lib/src/database/web/web_powersync_database.dart

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:async';
2+
import 'dart:convert';
23
import 'package:meta/meta.dart';
34
import 'package:http/browser_client.dart';
45
import 'package:logging/logging.dart';
@@ -75,8 +76,12 @@ class PowerSyncDatabaseImpl
7576
SqliteConnectionSetup? sqliteSetup}) {
7677
// ignore: deprecated_member_use_from_same_package
7778
DefaultSqliteOpenFactory factory = PowerSyncOpenFactory(path: path);
78-
return PowerSyncDatabaseImpl.withFactory(factory,
79-
maxReaders: maxReaders, logger: logger, schema: schema);
79+
return PowerSyncDatabaseImpl.withFactory(
80+
factory,
81+
maxReaders: maxReaders,
82+
logger: logger,
83+
schema: schema,
84+
);
8085
}
8186

8287
/// Open a [PowerSyncDatabase] with a [PowerSyncOpenFactory].
@@ -94,16 +99,22 @@ class PowerSyncDatabaseImpl
9499
Logger? logger}) {
95100
final db = SqliteDatabase.withFactory(openFactory, maxReaders: 1);
96101
return PowerSyncDatabaseImpl.withDatabase(
97-
schema: schema, logger: logger, database: db);
102+
schema: schema,
103+
logger: logger,
104+
database: db,
105+
);
98106
}
99107

100108
/// Open a PowerSyncDatabase on an existing [SqliteDatabase].
101109
///
102110
/// Migrations are run on the database when this constructor is called.
103111
///
104112
/// [logger] defaults to [autoLogger], which logs to the console in debug builds.
105-
PowerSyncDatabaseImpl.withDatabase(
106-
{required this.schema, required this.database, Logger? logger}) {
113+
PowerSyncDatabaseImpl.withDatabase({
114+
required this.schema,
115+
required this.database,
116+
Logger? logger,
117+
}) {
107118
if (logger != null) {
108119
this.logger = logger;
109120
} else {
@@ -141,6 +152,7 @@ class PowerSyncDatabaseImpl
141152

142153
sync = StreamingSyncImplementation(
143154
adapter: storage,
155+
schemaJson: jsonEncode(schema),
144156
connector: InternalConnector.wrap(connector, this),
145157
crudUpdateTriggerStream: crudStream,
146158
options: options,

packages/powersync_core/lib/src/schema.dart

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,23 @@ import 'schema_logic.dart';
77
/// No migrations are required on the client.
88
class Schema {
99
/// List of tables in the schema.
10+
///
11+
/// When opening a PowerSync database, these tables will be created and
12+
/// migrated automatically.
1013
final List<Table> tables;
1114

12-
const Schema(this.tables);
15+
/// A list of [RawTable]s in addition to PowerSync-managed [tables].
16+
///
17+
/// Raw tables give users full control over the SQLite tables, but that
18+
/// includes the responsibility to create those tables and to write migrations
19+
/// for them.
20+
///
21+
/// For more information on raw tables, see [RawTable] and [the documentation](https://docs.powersync.com/usage/use-case-examples/raw-tables).
22+
final List<RawTable> rawTables;
23+
24+
const Schema(this.tables, {this.rawTables = const []});
1325

14-
Map<String, dynamic> toJson() => {'tables': tables};
26+
Map<String, dynamic> toJson() => {'raw_tables': rawTables, 'tables': tables};
1527

1628
void validate() {
1729
Set<String> tableNames = {};
@@ -315,6 +327,120 @@ class Column {
315327
Map<String, dynamic> toJson() => {'name': name, 'type': type.sqlite};
316328
}
317329

330+
/// A raw table, defined by the user instead of being managed by PowerSync.
331+
///
332+
/// Any ordinary SQLite table can be defined as a raw table, which enables:
333+
///
334+
/// - More performant queries, since data is stored in typed rows instead of the
335+
/// schemaless JSON view PowerSync uses by default.
336+
/// - More control over the table, since custom column constraints can be used
337+
/// in its definition.
338+
///
339+
/// PowerSync doesn't know anything about the internal structure of raw tables -
340+
/// instead, it relies on user-defined [put] and [delete] statements to sync
341+
/// data into them.
342+
///
343+
/// When using raw tables, you are responsible for creating and migrating them
344+
/// when they've changed. Further, triggers are necessary to collect local
345+
/// writes to those tables. For more information, see
346+
/// [the documentation](https://docs.powersync.com/usage/use-case-examples/raw-tables).
347+
///
348+
/// Note that raw tables are only supported by the Rust sync client, which needs
349+
/// to be enabled when connecting with raw tables.
350+
final class RawTable {
351+
/// The name of the table as used by the sync service.
352+
///
353+
/// This doesn't necessarily have to match the name of the SQLite table that
354+
/// [put] and [delete] write to. Instead, it's used by the sync client to
355+
/// identify which statements to use when it encounters sync operations for
356+
/// this table.
357+
final String name;
358+
359+
/// A statement responsible for inserting or updating a row in this raw table
360+
/// based on data from the sync service.
361+
///
362+
/// See [PendingStatement] for details.
363+
final PendingStatement put;
364+
365+
/// A statement responsible for deleting a row based on its PowerSync id.
366+
///
367+
/// See [PendingStatement] for details. Note that [PendingStatementValue]s
368+
/// used here must all be [PendingStatementValue.id].
369+
final PendingStatement delete;
370+
371+
const RawTable({
372+
required this.name,
373+
required this.put,
374+
required this.delete,
375+
});
376+
377+
Map<String, dynamic> toJson() => {
378+
'name': name,
379+
'put': put,
380+
'delete': delete,
381+
};
382+
}
383+
384+
/// An SQL statement to be run by the sync client against raw tables.
385+
///
386+
/// Since raw tables are managed by the user, PowerSync can't know how to apply
387+
/// serverside changes to them. These statements bridge raw tables and PowerSync
388+
/// by providing upserts and delete statements.
389+
///
390+
/// For more information, see [the documentation](https://docs.powersync.com/usage/use-case-examples/raw-tables)
391+
final class PendingStatement {
392+
/// The SQL statement to run to upsert or delete data from a raw table.
393+
final String sql;
394+
395+
/// A list of value identifiers for parameters in [sql].
396+
///
397+
/// Put statements can use both [PendingStatementValue.id] and
398+
/// [PendingStatementValue.column], whereas delete statements can only use
399+
/// [PendingStatementValue.id].
400+
final List<PendingStatementValue> params;
401+
402+
PendingStatement({required this.sql, required this.params});
403+
404+
Map<String, dynamic> toJson() => {
405+
'sql': sql,
406+
'params': params,
407+
};
408+
}
409+
410+
/// A description of a value that will be resolved in the sync client when
411+
/// running a [PendingStatement] for a [RawTable].
412+
sealed class PendingStatementValue {
413+
/// A value that is bound to the textual id used in the PowerSync protocol.
414+
factory PendingStatementValue.id() = _PendingStmtValueId;
415+
416+
/// A value that is bound to the value of a column in a replace (`PUT`)
417+
/// operation of the PowerSync protocol.
418+
factory PendingStatementValue.column(String column) = _PendingStmtValueColumn;
419+
420+
dynamic toJson();
421+
}
422+
423+
class _PendingStmtValueColumn implements PendingStatementValue {
424+
final String column;
425+
const _PendingStmtValueColumn(this.column);
426+
427+
@override
428+
dynamic toJson() {
429+
return {
430+
'Column': column,
431+
};
432+
}
433+
}
434+
435+
class _PendingStmtValueId implements PendingStatementValue {
436+
const _PendingStmtValueId();
437+
438+
@override
439+
dynamic toJson() {
440+
return 'Id';
441+
}
442+
}
443+
318444
/// Type of column.
319445
enum ColumnType {
320446
/// TEXT column.

0 commit comments

Comments
 (0)