diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index 0038ad2..42a5531 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.11.5 + + - Allow profiling queries. + ## 0.11.4 - Add `SqliteConnection.synchronousWrapper` and `SqliteDatabase.singleConnection`. diff --git a/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart index f29520f..d84bdaa 100644 --- a/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart @@ -1,8 +1,12 @@ +import 'dart:developer'; + import 'package:sqlite3/common.dart'; import 'package:sqlite_async/src/common/mutex.dart'; import 'package:sqlite_async/src/sqlite_connection.dart'; +import 'package:sqlite_async/src/sqlite_options.dart'; import 'package:sqlite_async/src/sqlite_queries.dart'; import 'package:sqlite_async/src/update_notification.dart'; +import 'package:sqlite_async/src/utils/profiler.dart'; /// A simple "synchronous" connection which provides the async SqliteConnection /// implementation using a synchronous SQLite connection @@ -14,7 +18,15 @@ class SyncSqliteConnection extends SqliteConnection with SqliteQueries { bool _closed = false; - SyncSqliteConnection(this.db, Mutex m) { + /// Whether queries should be added to the `dart:developer` timeline. + /// + /// This is enabled by default outside of release builds, see + /// [SqliteOptions.profileQueries] for details. + final bool profileQueries; + + SyncSqliteConnection(this.db, Mutex m, {bool? profileQueries}) + : profileQueries = + profileQueries ?? const SqliteOptions().profileQueries { mutex = m.open(); updates = db.updates.map( (event) { @@ -26,15 +38,31 @@ class SyncSqliteConnection extends SqliteConnection with SqliteQueries { @override Future readLock(Future Function(SqliteReadContext tx) callback, {Duration? lockTimeout, String? debugContext}) { - return mutex.lock(() => callback(SyncReadContext(db)), - timeout: lockTimeout); + final task = profileQueries ? TimelineTask() : null; + task?.start('${profilerPrefix}mutex_lock'); + + return mutex.lock( + () { + task?.finish(); + return callback(SyncReadContext(db, parent: task)); + }, + timeout: lockTimeout, + ); } @override Future writeLock(Future Function(SqliteWriteContext tx) callback, {Duration? lockTimeout, String? debugContext}) { - return mutex.lock(() => callback(SyncWriteContext(db)), - timeout: lockTimeout); + final task = profileQueries ? TimelineTask() : null; + task?.start('${profilerPrefix}mutex_lock'); + + return mutex.lock( + () { + task?.finish(); + return callback(SyncWriteContext(db, parent: task)); + }, + timeout: lockTimeout, + ); } @override @@ -53,9 +81,12 @@ class SyncSqliteConnection extends SqliteConnection with SqliteQueries { } class SyncReadContext implements SqliteReadContext { + final TimelineTask? task; + CommonDatabase db; - SyncReadContext(this.db); + SyncReadContext(this.db, {TimelineTask? parent}) + : task = TimelineTask(parent: parent); @override Future computeWithDatabase( @@ -65,13 +96,23 @@ class SyncReadContext implements SqliteReadContext { @override Future get(String sql, [List parameters = const []]) async { - return db.select(sql, parameters).first; + return task.timeSync( + 'get', + () => db.select(sql, parameters).first, + sql: sql, + parameters: parameters, + ); } @override Future getAll(String sql, [List parameters = const []]) async { - return db.select(sql, parameters); + return task.timeSync( + 'getAll', + () => db.select(sql, parameters), + sql: sql, + parameters: parameters, + ); } @override @@ -91,26 +132,32 @@ class SyncReadContext implements SqliteReadContext { } class SyncWriteContext extends SyncReadContext implements SqliteWriteContext { - SyncWriteContext(super.db); + SyncWriteContext(super.db, {super.parent}); @override Future execute(String sql, [List parameters = const []]) async { - return db.select(sql, parameters); + return task.timeSync( + 'execute', + () => db.select(sql, parameters), + sql: sql, + parameters: parameters, + ); } @override Future executeBatch( String sql, List> parameterSets) async { - return computeWithDatabase((db) async { + task.timeSync('executeBatch', () { final statement = db.prepare(sql, checkNoTail: true); try { for (var parameters in parameterSets) { - statement.execute(parameters); + task.timeSync('iteration', () => statement.execute(parameters), + parameters: parameters); } } finally { statement.dispose(); } - }); + }, sql: sql); } } diff --git a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart index 7df4ac8..ae3c376 100644 --- a/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart +++ b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'dart:isolate'; import 'package:sqlite3/sqlite3.dart' as sqlite; @@ -10,6 +11,7 @@ import 'package:sqlite_async/src/native/native_isolate_mutex.dart'; import 'package:sqlite_async/src/sqlite_connection.dart'; import 'package:sqlite_async/src/sqlite_queries.dart'; import 'package:sqlite_async/src/update_notification.dart'; +import 'package:sqlite_async/src/utils/profiler.dart'; import 'package:sqlite_async/src/utils/shared_utils.dart'; import 'upstream_updates.dart'; @@ -33,15 +35,18 @@ class SqliteConnectionImpl final String? debugName; final bool readOnly; - SqliteConnectionImpl( - {required openFactory, - required Mutex mutex, - SerializedPortClient? upstreamPort, - Stream? updates, - this.debugName, - this.readOnly = false, - bool primary = false}) - : _writeMutex = mutex { + final bool profileQueries; + + SqliteConnectionImpl({ + required AbstractDefaultSqliteOpenFactory openFactory, + required Mutex mutex, + SerializedPortClient? upstreamPort, + Stream? updates, + this.debugName, + this.readOnly = false, + bool primary = false, + }) : _writeMutex = mutex, + profileQueries = openFactory.sqliteOptions.profileQueries { isInitialized = _isolateClient.ready; this.upstreamPort = upstreamPort ?? listenForEvents(); // Accept an incoming stream of updates, or expose one if not given. @@ -58,6 +63,11 @@ class SqliteConnectionImpl return _isolateClient.closed; } + _TransactionContext _context() { + return _TransactionContext( + _isolateClient, profileQueries ? TimelineTask() : null); + } + @override Future getAutoCommit() async { if (closed) { @@ -65,7 +75,7 @@ class SqliteConnectionImpl } // We use a _TransactionContext without a lock here. // It is safe to call this in the middle of another transaction. - final ctx = _TransactionContext(_isolateClient); + final ctx = _context(); try { return await ctx.getAutoCommit(); } finally { @@ -120,7 +130,7 @@ class SqliteConnectionImpl // Private lock to synchronize this with other statements on the same connection, // to ensure that transactions aren't interleaved. return _connectionMutex.lock(() async { - final ctx = _TransactionContext(_isolateClient); + final ctx = _context(); try { return await callback(ctx); } finally { @@ -143,7 +153,7 @@ class SqliteConnectionImpl } // DB lock so that only one write happens at a time return await _writeMutex.lock(() async { - final ctx = _TransactionContext(_isolateClient); + final ctx = _context(); try { return await callback(ctx); } finally { @@ -167,7 +177,9 @@ class _TransactionContext implements SqliteWriteContext { bool _closed = false; final int ctxId = _nextCtxId++; - _TransactionContext(this._sendPort); + final TimelineTask? task; + + _TransactionContext(this._sendPort, this.task); @override bool get closed { @@ -187,8 +199,13 @@ class _TransactionContext implements SqliteWriteContext { throw sqlite.SqliteException(0, 'Transaction closed', null, sql); } try { - var future = _sendPort.post( - _SqliteIsolateStatement(ctxId, sql, parameters, readOnly: false)); + var future = _sendPort.post(_SqliteIsolateStatement( + ctxId, + sql, + parameters, + readOnly: false, + timelineTask: task?.pass(), + )); return await future; } on sqlite.SqliteException catch (e) { @@ -314,74 +331,101 @@ Future _sqliteConnectionIsolateInner(_SqliteConnectionParams params, Timer(const Duration(milliseconds: 1), maybeFireUpdates); }); - server.open((data) async { - if (data is _SqliteIsolateClose) { - // This is a transaction close message + ResultSet runStatement(_SqliteIsolateStatement data) { + if (data.sql == 'BEGIN' || data.sql == 'BEGIN IMMEDIATE') { if (txId != null) { - if (!db.autocommit) { - db.execute('ROLLBACK'); - } - txId = null; - txError = null; - throw sqlite.SqliteException( - 0, 'Transaction must be closed within the read or write lock'); + // This will error on db.select } - // We would likely have received updates by this point - fire now. - maybeFireUpdates(); - return null; - } else if (data is _SqliteIsolateStatement) { - if (data.sql == 'BEGIN' || data.sql == 'BEGIN IMMEDIATE') { - if (txId != null) { - // This will error on db.select + txId = data.ctxId; + } else if (txId != null && txId != data.ctxId) { + // Locks should prevent this from happening + throw sqlite.SqliteException( + 0, 'Mixed transactions: $txId and ${data.ctxId}'); + } else if (data.sql == 'ROLLBACK') { + // This is the only valid way to clear an error + txError = null; + txId = null; + } else if (txError != null) { + // Any statement (including COMMIT) after the first error will also error, until the + // transaction is aborted. + throw txError!; + } else if (data.sql == 'COMMIT' || data.sql == 'END TRANSACTION') { + txId = null; + } + try { + final result = db.select(data.sql, mapParameters(data.args)); + return result; + } catch (err) { + if (txId != null) { + if (db.autocommit) { + // Transaction rolled back + txError = sqlite.SqliteException(0, + 'Transaction rolled back by earlier statement: ${err.toString()}'); + } else { + // Recoverable error } - txId = data.ctxId; - } else if (txId != null && txId != data.ctxId) { - // Locks should prevent this from happening - throw sqlite.SqliteException( - 0, 'Mixed transactions: $txId and ${data.ctxId}'); - } else if (data.sql == 'ROLLBACK') { - // This is the only valid way to clear an error - txError = null; - txId = null; - } else if (txError != null) { - // Any statement (including COMMIT) after the first error will also error, until the - // transaction is aborted. - throw txError!; - } else if (data.sql == 'COMMIT' || data.sql == 'END TRANSACTION') { - txId = null; } - try { - final result = db.select(data.sql, mapParameters(data.args)); - return result; - } catch (err) { + rethrow; + } + } + + Future handle(_RemoteIsolateRequest data, TimelineTask? task) async { + switch (data) { + case _SqliteIsolateClose(): + // This is a transaction close message if (txId != null) { - if (db.autocommit) { - // Transaction rolled back - txError = sqlite.SqliteException(0, - 'Transaction rolled back by earlier statement: ${err.toString()}'); - } else { - // Recoverable error + if (!db.autocommit) { + db.execute('ROLLBACK'); } + txId = null; + txError = null; + throw sqlite.SqliteException( + 0, 'Transaction must be closed within the read or write lock'); } - rethrow; - } - } else if (data is _SqliteIsolateClosure) { - try { - return await data.cb(db); - } finally { + // We would likely have received updates by this point - fire now. maybeFireUpdates(); - } - } else if (data is _SqliteIsolateConnectionClose) { - db.dispose(); - return null; - } else { + return null; + case _SqliteIsolateStatement(): + return task.timeSync( + 'execute_remote', + () => runStatement(data), + sql: data.sql, + parameters: data.args, + ); + case _SqliteIsolateClosure(): + try { + return await data.cb(db); + } finally { + maybeFireUpdates(); + } + case _SqliteIsolateConnectionClose(): + db.dispose(); + return null; + } + } + + server.open((data) async { + if (data is! _RemoteIsolateRequest) { throw ArgumentError('Unknown data type $data'); } + + final task = switch (data.timelineTask) { + null => null, + final id => TimelineTask.withTaskId(id), + }; + + return await handle(data, task); }); commandPort.listen((data) async {}); } +sealed class _RemoteIsolateRequest { + final int? timelineTask; + + const _RemoteIsolateRequest({required this.timelineTask}); +} + class _SqliteConnectionParams { final RequestPortServer portServer; final bool readOnly; @@ -398,28 +442,28 @@ class _SqliteConnectionParams { required this.primary}); } -class _SqliteIsolateStatement { +class _SqliteIsolateStatement extends _RemoteIsolateRequest { final int ctxId; final String sql; final List args; final bool readOnly; _SqliteIsolateStatement(this.ctxId, this.sql, this.args, - {this.readOnly = false}); + {this.readOnly = false, super.timelineTask}); } -class _SqliteIsolateClosure { +class _SqliteIsolateClosure extends _RemoteIsolateRequest { final TxCallback cb; - _SqliteIsolateClosure(this.cb); + _SqliteIsolateClosure(this.cb, {super.timelineTask}); } -class _SqliteIsolateClose { +class _SqliteIsolateClose extends _RemoteIsolateRequest { final int ctxId; - const _SqliteIsolateClose(this.ctxId); + const _SqliteIsolateClose(this.ctxId, {super.timelineTask}); } -class _SqliteIsolateConnectionClose { - const _SqliteIsolateConnectionClose(); +class _SqliteIsolateConnectionClose extends _RemoteIsolateRequest { + const _SqliteIsolateConnectionClose({super.timelineTask}); } diff --git a/packages/sqlite_async/lib/src/sqlite_connection.dart b/packages/sqlite_async/lib/src/sqlite_connection.dart index 15f4f6a..bd2292b 100644 --- a/packages/sqlite_async/lib/src/sqlite_connection.dart +++ b/packages/sqlite_async/lib/src/sqlite_connection.dart @@ -93,9 +93,13 @@ abstract class SqliteConnection extends SqliteWriteContext { /// may be easier to wrap a [raw] databases (like unit tests), this method /// may be used as an escape hatch for the asynchronous wrappers provided by /// this package. + /// + /// When [profileQueries] is enabled (it's enabled by default outside of + /// release builds, queries are posted to the `dart:developer` timeline). factory SqliteConnection.synchronousWrapper(CommonDatabase raw, - {Mutex? mutex}) { - return SyncSqliteConnection(raw, mutex ?? Mutex()); + {Mutex? mutex, bool? profileQueries}) { + return SyncSqliteConnection(raw, mutex ?? Mutex(), + profileQueries: profileQueries); } /// Reports table change update notifications diff --git a/packages/sqlite_async/lib/src/sqlite_options.dart b/packages/sqlite_async/lib/src/sqlite_options.dart index 3767213..0489f6b 100644 --- a/packages/sqlite_async/lib/src/sqlite_options.dart +++ b/packages/sqlite_async/lib/src/sqlite_options.dart @@ -30,19 +30,27 @@ class SqliteOptions { /// Set to null or [Duration.zero] to fail immediately when the database is locked. final Duration? lockTimeout; - const SqliteOptions.defaults() - : journalMode = SqliteJournalMode.wal, - journalSizeLimit = 6 * 1024 * 1024, // 1.5x the default checkpoint size - synchronous = SqliteSynchronous.normal, - webSqliteOptions = const WebSqliteOptions.defaults(), - lockTimeout = const Duration(seconds: 30); - - const SqliteOptions( - {this.journalMode = SqliteJournalMode.wal, - this.journalSizeLimit = 6 * 1024 * 1024, - this.synchronous = SqliteSynchronous.normal, - this.webSqliteOptions = const WebSqliteOptions.defaults(), - this.lockTimeout = const Duration(seconds: 30)}); + /// Whether queries should be added to the `dart:developer` timeline. + /// + /// By default, this is enabled if the `dart.vm.product` compile-time variable + /// is not set to `true`. For Flutter apps, this means that [profileQueries] + /// is enabled by default in debug and profile mode. + final bool profileQueries; + + const factory SqliteOptions.defaults() = SqliteOptions; + + const SqliteOptions({ + this.journalMode = SqliteJournalMode.wal, + this.journalSizeLimit = 6 * 1024 * 1024, + this.synchronous = SqliteSynchronous.normal, + this.webSqliteOptions = const WebSqliteOptions.defaults(), + this.lockTimeout = const Duration(seconds: 30), + this.profileQueries = _profileQueriesByDefault, + }); + + // https://api.flutter.dev/flutter/foundation/kReleaseMode-constant.html + static const _profileQueriesByDefault = + !bool.fromEnvironment('dart.vm.product'); } /// SQLite journal mode. Set on the primary connection. diff --git a/packages/sqlite_async/lib/src/utils/profiler.dart b/packages/sqlite_async/lib/src/utils/profiler.dart new file mode 100644 index 0000000..bffbf39 --- /dev/null +++ b/packages/sqlite_async/lib/src/utils/profiler.dart @@ -0,0 +1,64 @@ +import 'dart:developer'; + +extension TimeSync on TimelineTask? { + T timeSync(String name, TimelineSyncFunction function, + {String? sql, List? parameters}) { + final currentTask = this; + if (currentTask == null) { + return function(); + } + + final (resolvedName, args) = + profilerNameAndArgs(name, sql: sql, parameters: parameters); + currentTask.start(resolvedName, arguments: args); + + try { + return function(); + } finally { + currentTask.finish(); + } + } + + Future timeAsync(String name, TimelineSyncFunction> function, + {String? sql, List? parameters}) { + final currentTask = this; + if (currentTask == null) { + return function(); + } + + final (resolvedName, args) = + profilerNameAndArgs(name, sql: sql, parameters: parameters); + currentTask.start(resolvedName, arguments: args); + + return Future.sync(function).whenComplete(() { + currentTask.finish(); + }); + } +} + +(String, Map) profilerNameAndArgs(String name, + {String? sql, List? parameters}) { + // On native platforms, we want static names for tasks because every + // unique key here shows up in a separate line in Perfetto: https://github.com/dart-lang/sdk/issues/56274 + // On the web however, the names are embedded in the timeline slices and + // it's convenient to include the SQL there. + const isWeb = bool.fromEnvironment('dart.library.js_interop'); + var resolvedName = '$profilerPrefix$name'; + if (isWeb && sql != null) { + resolvedName = '$resolvedName $sql'; + } + + return ( + resolvedName, + { + if (sql != null) 'sql': sql, + if (parameters != null) + 'parameters': [ + for (final parameter in parameters) + if (parameter is List) '' else parameter + ], + } + ); +} + +const profilerPrefix = 'sqlite_async:'; diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 04da846..6ba7e6b 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; @@ -6,6 +7,7 @@ import 'package:sqlite3/common.dart'; import 'package:sqlite3_web/sqlite3_web.dart'; import 'package:sqlite3_web/protocol_utils.dart' as proto; import 'package:sqlite_async/sqlite_async.dart'; +import 'package:sqlite_async/src/utils/profiler.dart'; import 'package:sqlite_async/src/utils/shared_utils.dart'; import 'package:sqlite_async/src/web/database/broadcast_updates.dart'; import 'package:sqlite_async/web.dart'; @@ -17,6 +19,7 @@ class WebDatabase implements SqliteDatabase, WebSqliteConnection { final Database _database; final Mutex? _mutex; + final bool profileQueries; /// For persistent databases that aren't backed by a shared worker, we use /// web broadcast channels to forward local update events to other tabs. @@ -25,7 +28,12 @@ class WebDatabase @override bool closed = false; - WebDatabase(this._database, this._mutex, {this.broadcastUpdates}); + WebDatabase( + this._database, + this._mutex, { + required this.profileQueries, + this.broadcastUpdates, + }); @override Future close() async { @@ -175,7 +183,10 @@ class _SharedContext implements SqliteReadContext { final WebDatabase _database; bool _contextClosed = false; - _SharedContext(this._database); + final TimelineTask? _task; + + _SharedContext(this._database) + : _task = _database.profileQueries ? TimelineTask() : null; @override bool get closed => _contextClosed || _database.closed; @@ -196,8 +207,15 @@ class _SharedContext implements SqliteReadContext { @override Future getAll(String sql, [List parameters = const []]) async { - return await wrapSqliteException( - () => _database._database.select(sql, parameters)); + return _task.timeAsync( + 'getAll', + sql: sql, + parameters: parameters, + () async { + return await wrapSqliteException( + () => _database._database.select(sql, parameters)); + }, + ); } @override @@ -221,35 +239,37 @@ class _ExclusiveContext extends _SharedContext implements SqliteWriteContext { _ExclusiveContext(super.database); @override - Future execute(String sql, - [List parameters = const []]) async { - return wrapSqliteException( - () => _database._database.select(sql, parameters)); + Future execute(String sql, [List parameters = const []]) { + return _task.timeAsync('execute', sql: sql, parameters: parameters, () { + return wrapSqliteException( + () => _database._database.select(sql, parameters)); + }); } @override - Future executeBatch( - String sql, List> parameterSets) async { - return wrapSqliteException(() async { - for (final set in parameterSets) { - // use execute instead of select to avoid transferring rows from the - // worker to this context. - await _database._database.execute(sql, set); - } + Future executeBatch(String sql, List> parameterSets) { + return _task.timeAsync('executeBatch', sql: sql, () { + return wrapSqliteException(() async { + for (final set in parameterSets) { + // use execute instead of select to avoid transferring rows from the + // worker to this context. + await _database._database.execute(sql, set); + } + }); }); } } class _ExclusiveTransactionContext extends _ExclusiveContext { SqliteWriteContext baseContext; + _ExclusiveTransactionContext(super.database, this.baseContext); @override bool get closed => baseContext.closed; - @override - Future execute(String sql, - [List parameters = const []]) async { + Future _executeInternal( + String sql, List parameters) async { // Operations inside transactions are executed with custom requests // in order to verify that the connection does not have autocommit enabled. // The worker will check if autocommit = true before executing the SQL. @@ -293,15 +313,22 @@ class _ExclusiveTransactionContext extends _ExclusiveContext { }); } + @override + Future execute(String sql, + [List parameters = const []]) async { + return _task.timeAsync('execute', sql: sql, parameters: parameters, () { + return _executeInternal(sql, parameters); + }); + } + @override Future executeBatch( String sql, List> parameterSets) async { - return await wrapSqliteException(() async { + return _task.timeAsync('executeBatch', sql: sql, () async { for (final set in parameterSets) { await _database._database.customRequest(CustomDatabaseMessage( CustomDatabaseMessageKind.executeBatchInTransaction, sql, set)); } - return; }); } } diff --git a/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart index 5089b95..a724329 100644 --- a/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart +++ b/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart @@ -76,7 +76,8 @@ class DefaultSqliteOpenFactory } return WebDatabase(connection.database, options.mutex ?? mutex, - broadcastUpdates: updates); + broadcastUpdates: updates, + profileQueries: sqliteOptions.profileQueries); } @override diff --git a/packages/sqlite_async/lib/web.dart b/packages/sqlite_async/lib/web.dart index 8051edb..3a65115 100644 --- a/packages/sqlite_async/lib/web.dart +++ b/packages/sqlite_async/lib/web.dart @@ -94,6 +94,7 @@ abstract class WebSqliteConnection implements SqliteConnection { var lock? => Mutex(identifier: lock), null => null, }, + profileQueries: false, ); return database; } diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 8aac3d6..5142162 100644 --- a/packages/sqlite_async/pubspec.yaml +++ b/packages/sqlite_async/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlite_async description: High-performance asynchronous interface for SQLite on Dart and Flutter. -version: 0.11.4 +version: 0.11.5 repository: https://github.com/powersync-ja/sqlite_async.dart environment: sdk: ">=3.5.0 <4.0.0" diff --git a/packages/sqlite_async/test/utils/native_test_utils.dart b/packages/sqlite_async/test/utils/native_test_utils.dart index 945529d..83dea18 100644 --- a/packages/sqlite_async/test/utils/native_test_utils.dart +++ b/packages/sqlite_async/test/utils/native_test_utils.dart @@ -26,6 +26,16 @@ class TestSqliteOpenFactory extends TestDefaultSqliteOpenFactory { sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.linux, () { return DynamicLibrary.open(sqlitePath); }); + + sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.macOS, () { + // Prefer using Homebrew's SQLite which allows loading extensions. + const fromHomebrew = '/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib'; + if (File(fromHomebrew).existsSync()) { + return DynamicLibrary.open(fromHomebrew); + } + + return DynamicLibrary.open('libsqlite3.dylib'); + }); } @override