Skip to content

Commit

Permalink
Major performance refactor with a few breaking changes
Browse files Browse the repository at this point in the history
- Change how tables and primary keys are fetched to minimize reads
- Allow for more efficient bulk writing using Sqlite batches
- Rework abstractions to expose the Sqlite batch API
- Rename classes to better reflect their goals
- Correctly identify and forbid semicolon separated statements
  • Loading branch information
cachapa authored Mar 3, 2024
1 parent 358fd65 commit 9d65277
Show file tree
Hide file tree
Showing 7 changed files with 506 additions and 385 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 3.0.0

Major performance refactor with a few breaking changes

- Change how tables and primary keys are fetched to minimize reads
- Allow for more efficient bulk writing using Sqlite batches
- Rework abstractions to expose the Sqlite batch API
- Rename classes to better reflect their goals
- Correctly identify and forbid semicolon separated statements

## 2.1.7

- Add support for inserts from select queries
Expand Down
59 changes: 42 additions & 17 deletions lib/sqlite_crdt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
import 'package:sql_crdt/sql_crdt.dart';
import 'package:sqlite_crdt/src/sqlite_api.dart';

import 'src/batch_executor.dart';
import 'src/is_web_locator.dart';

export 'package:sqflite_common/sqlite_api.dart';
Expand All @@ -17,7 +18,7 @@ export 'package:sql_crdt/sql_crdt.dart';
class SqliteCrdt extends SqlCrdt {
final Database _db;

SqliteCrdt._(this._db) : super(SqliteApi(_db));
SqliteCrdt._(this._db) : super(ExecutorApi(_db));

/// Open or create a SQLite container as a SqlCrdt instance.
///
Expand All @@ -27,8 +28,8 @@ class SqliteCrdt extends SqlCrdt {
String path, {
bool singleInstance = true,
int? version,
FutureOr<void> Function(BaseCrdt crdt, int version)? onCreate,
FutureOr<void> Function(BaseCrdt crdt, int from, int to)? onUpgrade,
FutureOr<void> Function(Database db, int version)? onCreate,
FutureOr<void> Function(Database db, int from, int to)? onUpgrade,
}) =>
_open(path, false, singleInstance, version, onCreate, onUpgrade);

Expand All @@ -37,20 +38,31 @@ class SqliteCrdt extends SqlCrdt {
static Future<SqliteCrdt> openInMemory({
bool singleInstance = false,
int? version,
FutureOr<void> Function(BaseCrdt crdt, int version)? onCreate,
FutureOr<void> Function(BaseCrdt crdt, int from, int to)? onUpgrade,
FutureOr<void> Function(CrdtTableExecutor db, int version)? onCreate,
FutureOr<void> Function(CrdtTableExecutor db, int from, int to)? onUpgrade,
}) =>
_open(null, true, singleInstance, version, onCreate, onUpgrade);

Future<void> close() => _db.close();
_open(
null,
true,
singleInstance,
version,
onCreate == null
? null
: (db, version) =>
onCreate(CrdtTableExecutor(ExecutorApi(db)), version),
onUpgrade == null
? null
: (db, from, to) =>
onUpgrade(CrdtTableExecutor(ExecutorApi(db)), from, to),
);

static Future<SqliteCrdt> _open(
String? path,
bool inMemory,
bool singleInstance,
int? version,
FutureOr<void> Function(BaseCrdt crdt, int version)? onCreate,
FutureOr<void> Function(BaseCrdt crdt, int from, int to)? onUpgrade,
FutureOr<void> Function(Database db, int version)? onCreate,
FutureOr<void> Function(Database db, int from, int to)? onUpgrade,
) async {
if (sqliteCrdtIsWeb && !inMemory && path!.contains('/')) {
path = path.substring(path.lastIndexOf('/') + 1);
Expand All @@ -68,18 +80,31 @@ class SqliteCrdt extends SqlCrdt {
options: SqfliteOpenDatabaseOptions(
singleInstance: singleInstance,
version: version,
onCreate: onCreate == null
? null
: (db, version) => onCreate.call(BaseCrdt(SqliteApi(db)), version),
onUpgrade: onUpgrade == null
? null
: (db, from, to) =>
onUpgrade.call(BaseCrdt(SqliteApi(db)), from, to),
onCreate: onCreate,
onUpgrade: onUpgrade,
),
);

final crdt = SqliteCrdt._(db);
await crdt.init();
return crdt;
}

Future<void> close() => _db.close();

@override
Future<Iterable<String>> getTables() async => (await _db.rawQuery('''
SELECT name FROM sqlite_schema
WHERE type ='table' AND name NOT LIKE 'sqlite_%'
''')).map((e) => e['name'] as String);

@override
Future<Iterable<String>> getTableKeys(String table) async =>
(await _db.rawQuery('''
SELECT name FROM pragma_table_info(?1)
WHERE pk > 0
''', [table])).map((e) => e['name'] as String);

BatchExecutor batch() =>
BatchExecutor(_db.batch(), canonicalTime.increment());
}
17 changes: 17 additions & 0 deletions lib/src/batch_executor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'package:sqflite_common/sqlite_api.dart';
import 'package:sql_crdt/sql_crdt.dart';
import 'package:sqlite_crdt/src/sqlite_api.dart';

/// Wrapper around Sqlite batches that automatically applies CRDT metadata to
/// inserted records.
///
/// Note that timestamps are fixed at the moment of instantiation, so creating
/// long-lived batches is discouraged.
class BatchExecutor extends CrdtExecutor {
final Batch _batch;

BatchExecutor(this._batch, Hlc hlc) : super(BatchApi(_batch), hlc);

/// Commit this batch atomically. See Sqlite documentation for details.
Future<List<Object?>> commit() => _batch.commit();
}
44 changes: 26 additions & 18 deletions lib/src/sqlite_api.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
import 'dart:async';

import 'package:sqflite_common/sqlite_api.dart';
import 'package:sql_crdt/sql_crdt.dart';

class SqliteApi extends DatabaseApi {
class ExecutorApi extends DatabaseApi {
final DatabaseExecutor _db;

SqliteApi(this._db);

@override
Future<Iterable<String>> getTables() async => (await _db.rawQuery('''
SELECT name FROM sqlite_schema
WHERE type ='table' AND name NOT LIKE 'sqlite_%'
''')).map((e) => e['name'] as String);

@override
Future<Iterable<String>> getPrimaryKeys(String table) async =>
(await _db.rawQuery('''
SELECT name FROM pragma_table_info(?1)
WHERE pk > 0
''', [table])).map((e) => e['name'] as String);
ExecutorApi(this._db);

@override
Future<void> execute(String sql, [List<Object?>? args]) =>
Expand All @@ -28,9 +17,28 @@ class SqliteApi extends DatabaseApi {
_db.rawQuery(sql, args);

@override
Future<void> transaction(
Future<void> Function(DatabaseApi txn) action) async {
Future<void> transaction(Future<void> Function(ReadWriteApi api) actions) {
assert(_db is Database, 'Cannot start a transaction within a transaction');
return (_db as Database).transaction((t) => action(SqliteApi(t)));
return (_db as Database).transaction((t) => actions(ExecutorApi(t)));
}

@override
Future<void> executeBatch(
FutureOr<void> Function(WriteApi api) actions) async {
final batch = _db.batch();
actions(BatchApi(batch));
await batch.commit();
}
}

/// Simplified wrapper intended for Sqlite batches.
class BatchApi extends WriteApi {
final Batch _batch;

BatchApi(this._batch);

void query(String sql, [List<Object?>? args]) => _batch.rawQuery(sql, args);

@override
void execute(String sql, [List<Object?>? args]) => _batch.execute(sql, args);
}
4 changes: 2 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: sqlite_crdt
description: Dart implementation of Conflict-free Replicated Data Types (CRDTs) using Sqlite
version: 2.1.7
version: 3.0.0
homepage: https://github.com/cachapa/sqlite_crdt
repository: https://github.com/cachapa/sqlite_crdt
issue_tracker: https://github.com/cachapa/sqlite_crdt/issues
Expand All @@ -12,7 +12,7 @@ dependencies:
sqflite_common: ^2.5.3
sqflite_common_ffi: ^2.3.2+1
sqflite_common_ffi_web: ^0.4.2+3
sql_crdt: ^2.1.7
sql_crdt: ^3.0.0
# path: ../sql_crdt

dev_dependencies:
Expand Down
Loading

0 comments on commit 9d65277

Please sign in to comment.