Skip to content

Commit 200d766

Browse files
committed
Initial package.
0 parents  commit 200d766

23 files changed

+1827
-0
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# https://dart.dev/guides/libraries/private-files
2+
# Created by `dart pub`
3+
.dart_tool/
4+
5+
# Avoid committing pubspec.lock for library packages; see
6+
# https://dart.dev/guides/libraries/private-files#pubspeclock.
7+
pubspec.lock
8+
9+
.idea

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 0.1.0
2+
3+
- Initial version.

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# sqlite_async
2+
3+
High-performance asynchronous interface for SQLite on Dart & Flutter.
4+
5+
[SQLite](https://www.sqlite.org/) is small, fast, has a lot of built-in functionality, and works
6+
great as an in-app database. However, SQLite is designed for many different use cases, and requires
7+
some configuration for optimal performance as an in-app database.
8+
9+
The [sqlite3](https://pub.dev/packages/sqlite3) Dart bindings are great for direct synchronous access
10+
to a SQLite database, but leaves the configuration the developer.
11+
12+
This library wraps the bindings and configures the database with a good set of defaults, with
13+
all database calls being asynchronous to avoid blocking the UI, while still providing direct SQL
14+
query access.
15+
16+
## Features
17+
18+
* Fast.
19+
* Direct SQL query access.
20+
* Uses a connection pool to allow concurrent queries.

analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include: package:lints/recommended.yaml

lib/sqlite_async.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// Support for doing something awesome.
2+
///
3+
/// More dartdocs go here.
4+
library sqlite_dart;
5+
6+
export 'src/sqlite_connection.dart';
7+
export 'src/update_notification.dart';
8+
export 'src/sqlite_database.dart';
9+
export 'src/sqlite_options.dart';
10+
export 'src/sqlite_open_factory.dart';

lib/src/connection_pool.dart

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import 'dart:async';
2+
3+
import 'sqlite_connection.dart';
4+
import 'sqlite_connection_factory.dart';
5+
import 'sqlite_connection_impl.dart';
6+
import 'sqlite_queries.dart';
7+
import 'update_notification.dart';
8+
9+
/// A connection pool with a single write connection and multiple read connections.
10+
class SqliteConnectionPool with SqliteQueries implements SqliteConnection {
11+
SqliteConnection? _writeConnection;
12+
13+
final List<SqliteConnectionImpl> _readConnections = [];
14+
15+
final SqliteConnectionFactory _factory;
16+
17+
@override
18+
final Stream<UpdateNotification>? updates;
19+
20+
final int maxReaders;
21+
22+
final String? debugName;
23+
24+
/// Open a new connection pool.
25+
///
26+
/// The provided factory is used to open connections on demand. Connections
27+
/// are only opened when requested for the first time.
28+
///
29+
/// [maxReaders] specifies the maximum number of read connections.
30+
/// A maximum of one write connection will be opened.
31+
///
32+
/// Read connections are opened in read-only mode, and will reject any statements
33+
/// that modify the database.
34+
SqliteConnectionPool(this._factory,
35+
{this.updates,
36+
this.maxReaders = 5,
37+
SqliteConnection? writeConnection,
38+
this.debugName})
39+
: _writeConnection = writeConnection;
40+
41+
@override
42+
Future<T> readLock<T>(Future<T> Function(SqliteReadContext tx) callback,
43+
{Duration? lockTimeout}) async {
44+
await _expandPool();
45+
46+
bool haveLock = false;
47+
var completer = Completer<T>();
48+
49+
var futures = _readConnections.map((connection) async {
50+
try {
51+
return await connection.readLock((ctx) async {
52+
if (haveLock) {
53+
// Already have a different lock - release this one.
54+
return false;
55+
}
56+
haveLock = true;
57+
58+
var future = callback(ctx);
59+
completer.complete(future);
60+
61+
// We have to wait for the future to complete before we can release the
62+
// lock.
63+
try {
64+
await future;
65+
} catch (_) {
66+
// Ignore
67+
}
68+
69+
return true;
70+
}, lockTimeout: lockTimeout);
71+
} on TimeoutException {
72+
return false;
73+
}
74+
});
75+
76+
final stream = Stream<bool>.fromFutures(futures);
77+
var gotAny = await stream.any((element) => element);
78+
79+
if (!gotAny) {
80+
// All TimeoutExceptions
81+
throw TimeoutException('Failed to get a read connection', lockTimeout);
82+
}
83+
84+
try {
85+
return await completer.future;
86+
} catch (e) {
87+
// throw e;
88+
rethrow;
89+
}
90+
}
91+
92+
@override
93+
Future<T> writeLock<T>(Future<T> Function(SqliteWriteContext tx) callback,
94+
{Duration? lockTimeout}) {
95+
_writeConnection ??= _factory.openConnection(
96+
debugName: debugName != null ? '$debugName-writer' : null);
97+
return _writeConnection!.writeLock(callback, lockTimeout: lockTimeout);
98+
}
99+
100+
Future<void> _expandPool() async {
101+
if (_readConnections.length >= maxReaders) {
102+
return;
103+
}
104+
bool hasCapacity = _readConnections.any((connection) => !connection.locked);
105+
if (!hasCapacity) {
106+
var name = debugName == null
107+
? null
108+
: '$debugName-${_readConnections.length + 1}';
109+
var connection = _factory.openConnection(
110+
updates: updates,
111+
debugName: name,
112+
readOnly: true) as SqliteConnectionImpl;
113+
_readConnections.add(connection);
114+
115+
// Edge case:
116+
// If we don't await here, there is a chance that a different connection
117+
// is used for the transaction, and that it finishes and deletes the database
118+
// while this one is still opening. This is specifically triggered in tests.
119+
// To avoid that, we wait for the connection to be ready.
120+
await connection.ready;
121+
}
122+
}
123+
}

lib/src/database_utils.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import 'dart:async';
2+
3+
import 'package:sqlite3/sqlite3.dart' as sqlite;
4+
5+
import 'sqlite_connection.dart';
6+
7+
Future<T> internalReadTransaction<T>(SqliteReadContext ctx,
8+
Future<T> Function(SqliteReadContext tx) callback) async {
9+
try {
10+
await ctx.getAll('BEGIN');
11+
final result = await callback(ctx);
12+
await ctx.getAll('END TRANSACTION');
13+
return result;
14+
} catch (e) {
15+
try {
16+
await ctx.getAll('ROLLBACK');
17+
} catch (e) {
18+
// In rare cases, a ROLLBACK may fail.
19+
// Safe to ignore.
20+
}
21+
rethrow;
22+
}
23+
}
24+
25+
Future<T> internalWriteTransaction<T>(SqliteWriteContext ctx,
26+
Future<T> Function(SqliteWriteContext tx) callback) async {
27+
try {
28+
await ctx.execute('BEGIN IMMEDIATE');
29+
final result = await callback(ctx);
30+
await ctx.execute('COMMIT');
31+
return result;
32+
} catch (e) {
33+
try {
34+
await ctx.execute('ROLLBACK');
35+
} catch (e) {
36+
// In rare cases, a ROLLBACK may fail.
37+
// Safe to ignore.
38+
}
39+
rethrow;
40+
}
41+
}
42+
43+
Future<T> asyncDirectTransaction<T>(sqlite.Database db,
44+
FutureOr<T> Function(sqlite.Database db) callback) async {
45+
for (var i = 50; i >= 0; i--) {
46+
try {
47+
db.execute('BEGIN IMMEDIATE');
48+
late T result;
49+
try {
50+
result = await callback(db);
51+
db.execute('COMMIT');
52+
} catch (e) {
53+
try {
54+
db.execute('ROLLBACK');
55+
} catch (e2) {
56+
// Safe to ignore
57+
}
58+
rethrow;
59+
}
60+
61+
return result;
62+
} catch (e) {
63+
if (e is sqlite.SqliteException) {
64+
if (e.resultCode == 5 && i != 0) {
65+
// SQLITE_BUSY
66+
await Future.delayed(const Duration(milliseconds: 50));
67+
continue;
68+
}
69+
}
70+
rethrow;
71+
}
72+
}
73+
throw AssertionError('Should not reach this');
74+
}
75+
76+
/// Given a SELECT query, return the tables that the query depends on.
77+
Future<Set<String>> getSourceTables(SqliteReadContext ctx, String sql) async {
78+
final rows = await ctx.getAll('EXPLAIN QUERY PLAN $sql');
79+
Set<String> tables = {};
80+
final re = RegExp(r'^(SCAN|SEARCH)( TABLE)? (.+?)( USING .+)?$');
81+
for (var row in rows) {
82+
final detail = row['detail'];
83+
final match = re.firstMatch(detail);
84+
if (match != null) {
85+
tables.add(match.group(3)!);
86+
}
87+
}
88+
return tables;
89+
}

lib/src/isolate_completer.dart

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import 'dart:async';
2+
import 'dart:isolate';
3+
4+
class IsolateResult<T> {
5+
final ReceivePort receivePort = ReceivePort();
6+
late Future<T> future;
7+
8+
IsolateResult() {
9+
final sendResult = receivePort.first;
10+
sendResult.whenComplete(() {
11+
receivePort.close();
12+
});
13+
14+
future = sendResult.then((response) {
15+
if (response is PortResult) {
16+
return response.value as T;
17+
} else if (response == abortedResponse) {
18+
throw const IsolateTerminatedError();
19+
} else {
20+
throw AssertionError('Invalid response: $response');
21+
}
22+
});
23+
}
24+
25+
PortCompleter<T> get completer {
26+
return PortCompleter(receivePort.sendPort);
27+
}
28+
29+
close() {
30+
receivePort.close();
31+
}
32+
}
33+
34+
const abortedResponse = 'aborted';
35+
36+
class PortCompleter<T> {
37+
final SendPort sendPort;
38+
39+
PortCompleter(this.sendPort);
40+
41+
void complete([FutureOr<T>? value]) {
42+
sendPort.send(PortResult.success(value));
43+
}
44+
45+
void completeError(Object error, [StackTrace? stackTrace]) {
46+
sendPort.send(PortResult.error(error, stackTrace));
47+
}
48+
49+
addExitHandler() {
50+
Isolate.current.addOnExitListener(sendPort, response: abortedResponse);
51+
}
52+
53+
Future<void> handle(FutureOr<T> Function() callback,
54+
{bool ignoreStackTrace = false}) async {
55+
addExitHandler();
56+
try {
57+
final result = await callback();
58+
complete(result);
59+
} catch (error, stacktrace) {
60+
if (ignoreStackTrace) {
61+
completeError(error);
62+
} else {
63+
completeError(error, stacktrace);
64+
}
65+
}
66+
}
67+
}
68+
69+
class PortResult<T> {
70+
final bool success;
71+
final T? _result;
72+
final Object? _error;
73+
final StackTrace? stackTrace;
74+
75+
const PortResult.success(T result)
76+
: success = true,
77+
_error = null,
78+
stackTrace = null,
79+
_result = result;
80+
const PortResult.error(Object error, [this.stackTrace])
81+
: success = false,
82+
_result = null,
83+
_error = error;
84+
85+
T get value {
86+
if (success) {
87+
return _result as T;
88+
} else {
89+
if (_error != null && stackTrace != null) {
90+
Error.throwWithStackTrace(_error!, stackTrace!);
91+
} else {
92+
throw _error!;
93+
}
94+
}
95+
}
96+
97+
T get result {
98+
assert(success);
99+
return _result as T;
100+
}
101+
102+
Object get error {
103+
assert(!success);
104+
return _error!;
105+
}
106+
}
107+
108+
class IsolateTerminatedError implements Error {
109+
const IsolateTerminatedError();
110+
111+
@override
112+
StackTrace? get stackTrace {
113+
return null;
114+
}
115+
}

lib/src/log.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import 'package:logging/logging.dart';
2+
3+
final log = Logger('SQLite');

0 commit comments

Comments
 (0)