Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions packages/powersync/example/batch_writes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:powersync/powersync.dart';
late PowerSyncDatabase db;

const schema = Schema([
Table.localOnly('data', [Column.text('contents')])
Table.localOnly('data', [Column.text('contents')]),
]);

final parameterSets = List.generate(1000, (i) => [uuid.v4(), 'Row $i']);
Expand Down Expand Up @@ -37,7 +37,9 @@ Future<void> batchWrites() async {
// This avoids the overhead of asynchronously waiting for each call to complete,
// and also only parses the SQL statement once.
await db.executeBatch(
'INSERT INTO data(id, contents) VALUES(?, ?)', parameterSets);
'INSERT INTO data(id, contents) VALUES(?, ?)',
parameterSets,
);
}

Future<void> inIsolateWrites() async {
Expand Down Expand Up @@ -66,7 +68,7 @@ Future<void> main() async {
singleWrites,
transactionalWrites,
batchWrites,
inIsolateWrites
inIsolateWrites,
]) {
await db.execute('DELETE FROM data WHERE 1');
var watch = Stopwatch()..start();
Expand Down
7 changes: 4 additions & 3 deletions packages/powersync/example/getting_started.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:powersync/powersync.dart';

const schema = Schema([
Table('customers', [Column.text('name'), Column.text('email')])
Table('customers', [Column.text('name'), Column.text('email')]),
]);

late PowerSyncDatabase db;
Expand Down Expand Up @@ -38,8 +38,9 @@ Future<void> openDatabase() async {

// Run local statements.
await db.execute(
'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)',
['Fred', 'fred@example.org']);
'INSERT INTO customers(id, name, email) VALUES(uuid(), ?, ?)',
['Fred', 'fred@example.org'],
);

// Connect to backend
db.connect(connector: BackendConnector(db));
Expand Down
49 changes: 31 additions & 18 deletions packages/powersync/example/watching_changes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import 'package:powersync/powersync.dart';
late PowerSyncDatabase db;

const schema = Schema([
Table.localOnly('data', [Column.text('contents')])
Table.localOnly('data', [Column.text('contents')]),
]);

final parameterSets = List.generate(1000, (i) => [uuid.v4(), 'Row $i']);
Expand All @@ -20,30 +20,43 @@ Future<void> main() async {

// Watch a single query.
// The query is executed every time one of its source tables are changed.
var subscription1 =
db.watch('SELECT count() AS count FROM data').listen((results) {
print('Results: $results');
}, onError: (Object e) {
print('Query failed: $e');
});
var subscription1 = db
.watch('SELECT count() AS count FROM data')
.listen(
(results) {
print('Results: $results');
},
onError: (Object e) {
print('Query failed: $e');
},
);

// Watch for changes to one or more tables.
// For this form, the tables to watch must be manually specified.
// Use asyncMap here to avoid the event being triggered while previous queries
// are still running.
var subscription2 = db.onChange(['data']).asyncMap((update) async {
var count = await db.get('SELECT count() AS count FROM data');
var length =
await db.get('SELECT sum(length(contents)) AS length FROM data');
print(
'Results after change to ${update.tables}: ${count['count']} entries, ${length['length']} characters');
}).listen((_) {}, onError: (Object e) {
print('Query failed: $e');
});
var subscription2 = db
.onChange(['data'])
.asyncMap((update) async {
var count = await db.get('SELECT count() AS count FROM data');
var length = await db.get(
'SELECT sum(length(contents)) AS length FROM data',
);
print(
'Results after change to ${update.tables}: ${count['count']} entries, ${length['length']} characters',
);
})
.listen(
(_) {},
onError: (Object e) {
print('Query failed: $e');
},
);

for (var i = 0; i < 10; i++) {
await db.execute(
'INSERT INTO data(id, contents) VALUES(uuid(), ?)', ['Row $i']);
await db.execute('INSERT INTO data(id, contents) VALUES(uuid(), ?)', [
'Row $i',
]);
await Future<void>.delayed(Duration(milliseconds: 500));
}

Expand Down
67 changes: 41 additions & 26 deletions packages/powersync/hook/build.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,38 @@ void main(List<String> args) async {
coreExtension = await _reuseOrDownloadCoreExtension(input);
}

output.assets.code.add(CodeAsset(
package: 'powersync',
name: 'src/open_factory/native/sqlite3_powersync_init.dart',
linkMode: DynamicLoadingBundled(),
file: coreExtension.uri,
));
output.assets.code.add(
CodeAsset(
package: 'powersync',
name: 'src/open_factory/native/sqlite3_powersync_init.dart',
linkMode: DynamicLoadingBundled(),
file: coreExtension.uri,
),
);
});
}

Future<File> _reuseOrDownloadCoreExtension(BuildInput input) async {
final sourceFileName = _fileNameForBuild(input.config.code);
final digest = assetNameToSha256Hash[sourceFileName]!;

final targetUri = input.outputDirectoryShared
.resolve('download-${digest.substring(0, 8)}/');
final targetUri = input.outputDirectoryShared.resolve(
'download-${digest.substring(0, 8)}/',
);
final targetDirectory = Directory(targetUri.toFilePath());
if (!targetDirectory.existsSync()) {
targetDirectory.createSync();
}
final file = File(targetUri
.resolve(input.config.code.targetOS.libraryFileName(
'powersync_core',
DynamicLoadingBundled(),
))
.toFilePath());
final file = File(
targetUri
.resolve(
input.config.code.targetOS.libraryFileName(
'powersync_core',
DynamicLoadingBundled(),
),
)
.toFilePath(),
);

if (file.existsSync()) {
// Hook is re-run with an existing cache. Does the file match the digest we
Expand All @@ -78,11 +85,13 @@ Future<File> _reuseOrDownloadCoreExtension(BuildInput input) async {
}

Future<Uint8List> _fetchCoreExtension(String fileName, String hash) async {
final client = IOClient(HttpClient()
// From Dart 3.11, proxy-related environment variables are passed to
// hooks. We respect them to ensure we can download these binaries in
// environments where that's required.
..findProxy = HttpClient.findProxyFromEnvironment);
final client = IOClient(
HttpClient()
// From Dart 3.11, proxy-related environment variables are passed to
// hooks. We respect them to ensure we can download these binaries in
// environments where that's required.
..findProxy = HttpClient.findProxyFromEnvironment,
);
final uri = Uri.https(
'github.com',
'powersync-ja/powersync-sqlite-core/releases/download/$releaseVersion/$fileName',
Expand All @@ -92,13 +101,15 @@ Future<Uint8List> _fetchCoreExtension(String fileName, String hash) async {
final response = await client.get(uri);
if (response.statusCode != 200) {
throw Exception(
'Could not download $uri, got ${response.statusCode}: ${response.body}');
'Could not download $uri, got ${response.statusCode}: ${response.body}',
);
}

final digest = sha256.convert(response.bodyBytes);
if (digest.toString() != hash) {
throw Exception(
'Unexpected digest for $uri, expected $hash got $digest.');
'Unexpected digest for $uri, expected $hash got $digest.',
);
}

return response.bodyBytes;
Expand Down Expand Up @@ -153,7 +164,10 @@ String _fileNameForBuild(CodeConfig config) {
return 'libpowersync_${architectureName()}.ios-sim.dylib';
case OS.linux:
final archName = architectureName(
supportArmv7: true, supportX86: true, supportRiscv: true);
supportArmv7: true,
supportX86: true,
supportRiscv: true,
);
return 'libpowersync_$archName.linux.so';
case OS.macOS:
return 'libpowersync_${architectureName()}.macos.dylib';
Expand All @@ -169,7 +183,10 @@ String _fileNameForBuild(CodeConfig config) {
}

Future<File> _useLocalCoreExtension(
String root, BuildInput input, BuildOutputBuilder output) async {
String root,
BuildInput input,
BuildOutputBuilder output,
) async {
final config = input.config.code;
if (config.targetOS != OS.current ||
config.targetArchitecture != Architecture.current) {
Expand All @@ -193,9 +210,7 @@ Future<File> _useLocalCoreExtension(
final rootUri = Uri.directory(root);
final library = rootUri.resolve('target/debug/$outputName');
final depsFile = File.fromUri(
library.resolve(
Platform.isWindows ? 'powersync.d' : 'libpowersync.d',
),
library.resolve(Platform.isWindows ? 'powersync.d' : 'libpowersync.d'),
);

// Parse generated depfile to re-run this hook when a Rust source has changed.
Expand Down
34 changes: 18 additions & 16 deletions packages/powersync/lib/src/attachments/attachment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,22 +177,24 @@ extension type AttachmentsQueueTable._(Table _) implements Table {
List<Index> indexes = const [],
String? viewName,
}) {
return AttachmentsQueueTable._(Table.localOnly(
attachmentsQueueTableName,
[
const Column.text('filename'),
const Column.text('local_uri'),
const Column.integer('timestamp'),
const Column.integer('size'),
const Column.text('media_type'),
const Column.integer('state'),
const Column.integer('has_synced'),
const Column.text('meta_data'),
...additionalColumns,
],
viewName: viewName,
indexes: indexes,
));
return AttachmentsQueueTable._(
Table.localOnly(
attachmentsQueueTableName,
[
const Column.text('filename'),
const Column.text('local_uri'),
const Column.integer('timestamp'),
const Column.integer('size'),
const Column.text('media_type'),
const Column.integer('state'),
const Column.integer('has_synced'),
const Column.text('meta_data'),
...additionalColumns,
],
viewName: viewName,
indexes: indexes,
),
);
}

static const defaultTableName = 'attachments_queue';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ final class WatchedAttachmentItem {
this.filename,
this.metaData,
}) : assert(
fileExtension != null || filename != null,
'Either fileExtension or filename must be provided.',
);
fileExtension != null || filename != null,
'Either fileExtension or filename must be provided.',
);
}

/// Class used to implement the attachment queue.
Expand All @@ -78,21 +78,21 @@ base class AttachmentQueue {
final AttachmentService _attachmentsService;
final SyncingService _syncingService;

AttachmentQueue._(
{required PowerSyncDatabase db,
required Stream<List<WatchedAttachmentItem>> Function() watchAttachments,
required LocalStorage localStorage,
required bool downloadAttachments,
required Logger logger,
required AttachmentService attachmentsService,
required SyncingService syncingService})
: _db = db,
_watchAttachments = watchAttachments,
_localStorage = localStorage,
_downloadAttachments = downloadAttachments,
_logger = logger,
_attachmentsService = attachmentsService,
_syncingService = syncingService;
AttachmentQueue._({
required PowerSyncDatabase db,
required Stream<List<WatchedAttachmentItem>> Function() watchAttachments,
required LocalStorage localStorage,
required bool downloadAttachments,
required Logger logger,
required AttachmentService attachmentsService,
required SyncingService syncingService,
}) : _db = db,
_watchAttachments = watchAttachments,
_localStorage = localStorage,
_downloadAttachments = downloadAttachments,
_logger = logger,
_attachmentsService = attachmentsService,
_syncingService = syncingService;

/// Creates a new attachment queue.
///
Expand Down Expand Up @@ -172,8 +172,9 @@ base class AttachmentQueue {
// Listen for connectivity changes and watched attachments
await _syncingService.startSync();

_watchedAttachmentsSubscription =
_watchAttachments().listen((items) async {
_watchedAttachmentsSubscription = _watchAttachments().listen((
items,
) async {
await _processWatchedAttachments(items);
});

Expand Down Expand Up @@ -249,15 +250,17 @@ base class AttachmentQueue {
final List<Attachment> attachmentUpdates = [];

for (final item in items) {
final existingQueueItem =
currentAttachments.where((a) => a.id == item.id).firstOrNull;
final existingQueueItem = currentAttachments
.where((a) => a.id == item.id)
.firstOrNull;

if (existingQueueItem == null) {
if (!_downloadAttachments) continue;

// This item should be added to the queue.
// This item is assumed to be coming from an upstream sync.
final String filename = item.filename ??
final String filename =
item.filename ??
await resolveNewAttachmentFilename(item.id, item.fileExtension);

attachmentUpdates.add(
Expand Down Expand Up @@ -334,8 +337,10 @@ base class AttachmentQueue {
String? metaData,
String? id,
required Future<void> Function(
SqliteWriteContext context, Attachment attachment)
updateHook,
SqliteWriteContext context,
Attachment attachment,
)
updateHook,
}) async {
final resolvedId = id ?? await generateAttachmentId();

Expand Down Expand Up @@ -372,8 +377,10 @@ base class AttachmentQueue {
Future<Attachment> deleteFile({
required String attachmentId,
required Future<void> Function(
SqliteWriteContext context, Attachment attachment)
updateHook,
SqliteWriteContext context,
Attachment attachment,
)
updateHook,
}) async {
return await _attachmentsService.withContext((attachmentContext) async {
final attachment = await attachmentContext.getAttachment(attachmentId);
Expand Down
Loading