diff --git a/CHANGELOG.md b/CHANGELOG.md index 3611d5c01a..a542738b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Enable JS SDK native integration by default ([#2688](https://github.com/getsentry/sentry-dart/pull/2688)) - Remove `enableTracing` ([#2695](https://github.com/getsentry/sentry-dart/pull/2695)) - Remove `options.autoAppStart` and `setAppStartEnd` ([#2680](https://github.com/getsentry/sentry-dart/pull/2680)) +- Bump Drift min version to `2.24.0` and use `QueryInterceptor` instead of `QueryExecutor` ([#2679](https://github.com/getsentry/sentry-dart/pull/2679)) - Add hint for transactions ([#2675](https://github.com/getsentry/sentry-dart/pull/2675)) - `BeforeSendTransactionCallback` now has a `Hint` parameter - Remove `dart:html` usage in favour of `package:web` ([#2710](https://github.com/getsentry/sentry-dart/pull/2710)) diff --git a/dart/lib/sentry.dart b/dart/lib/sentry.dart index f9ce19842b..ee9a93f568 100644 --- a/dart/lib/sentry.dart +++ b/dart/lib/sentry.dart @@ -51,7 +51,7 @@ export 'src/utils/http_header_utils.dart'; // ignore: invalid_export_of_internal_element export 'src/sentry_trace_origins.dart'; // ignore: invalid_export_of_internal_element -export 'src/sentry_span_operations.dart'; +export 'src/constants.dart'; // ignore: invalid_export_of_internal_element export 'src/utils.dart'; // spotlight debugging diff --git a/dart/lib/src/constants.dart b/dart/lib/src/constants.dart new file mode 100644 index 0000000000..caa7641db0 --- /dev/null +++ b/dart/lib/src/constants.dart @@ -0,0 +1,30 @@ +import 'package:meta/meta.dart'; + +@internal +class SentrySpanOperations { + static const String uiLoad = 'ui.load'; + static const String uiTimeToInitialDisplay = 'ui.load.initial_display'; + static const String uiTimeToFullDisplay = 'ui.load.full_display'; + static const String dbSqlQuery = 'db.sql.query'; + static const String dbSqlTransaction = 'db.sql.transaction'; + static const String dbSqlBatch = 'db.sql.batch'; + static const String dbOpen = 'db.open'; + static const String dbClose = 'db.close'; +} + +@internal +class SentrySpanData { + static const String dbSystemKey = 'db.system'; + static const String dbNameKey = 'db.name'; + + static const String dbSystemSqlite = 'db.sqlite'; +} + +@internal +class SentrySpanDescriptions { + static const String dbTransaction = 'Transaction'; + static String dbBatch({required List statements}) => + 'Batch $statements'; + static String dbOpen({required String dbName}) => 'Open database $dbName'; + static String dbClose({required String dbName}) => 'Close database $dbName'; +} diff --git a/dart/lib/src/sentry_span_operations.dart b/dart/lib/src/sentry_span_operations.dart deleted file mode 100644 index 27b1d22496..0000000000 --- a/dart/lib/src/sentry_span_operations.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:meta/meta.dart'; - -@internal -class SentrySpanOperations { - static const String uiLoad = 'ui.load'; - static const String uiTimeToInitialDisplay = 'ui.load.initial_display'; - static const String uiTimeToFullDisplay = 'ui.load.full_display'; -} diff --git a/dart/lib/src/sentry_trace_origins.dart b/dart/lib/src/sentry_trace_origins.dart index 4377fa2c03..23359bf9f2 100644 --- a/dart/lib/src/sentry_trace_origins.dart +++ b/dart/lib/src/sentry_trace_origins.dart @@ -24,9 +24,7 @@ class SentryTraceOrigins { static const autoDbHiveBoxBase = 'auto.db.hive.box_base'; static const autoDbHiveLazyBox = 'auto.db.hive.lazy_box'; static const autoDbHiveBoxCollection = 'auto.db.hive.box_collection'; - static const autoDbDriftQueryExecutor = 'auto.db.drift.query.executor'; - static const autoDbDriftTransactionExecutor = - 'auto.db.drift.transaction.executor'; + static const autoDbDriftQueryInterceptor = 'auto.db.drift.query.interceptor'; static const autoUiTimeToDisplay = 'auto.ui.time_to_display'; static const manualUiTimeToDisplay = 'manual.ui.time_to_display'; } diff --git a/drift/analysis_options.yaml b/drift/analysis_options.yaml index c5f7c0d066..7119dc352d 100644 --- a/drift/analysis_options.yaml +++ b/drift/analysis_options.yaml @@ -24,7 +24,6 @@ analyzer: linter: rules: - prefer_final_locals - - public_member_api_docs - prefer_single_quotes - prefer_relative_imports - unnecessary_brace_in_string_interps diff --git a/drift/example/database.g.dart b/drift/example/database.g.dart index 1f9d234456..3f01770182 100644 --- a/drift/example/database.g.dart +++ b/drift/example/database.g.dart @@ -160,6 +160,15 @@ class TodoItem extends DataClass implements Insertable { content: content ?? this.content, category: category.present ? category.value : this.category, ); + TodoItem copyWithCompanion(TodoItemsCompanion data) { + return TodoItem( + id: data.id.present ? data.id.value : this.id, + title: data.title.present ? data.title.value : this.title, + content: data.content.present ? data.content.value : this.content, + category: data.category.present ? data.category.value : this.category, + ); + } + @override String toString() { return (StringBuffer('TodoItem(') @@ -260,6 +269,7 @@ class TodoItemsCompanion extends UpdateCompanion { abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); + $AppDatabaseManager get managers => $AppDatabaseManager(this); late final $TodoItemsTable todoItems = $TodoItemsTable(this); @override Iterable> get allTables => @@ -267,3 +277,155 @@ abstract class _$AppDatabase extends GeneratedDatabase { @override List get allSchemaEntities => [todoItems]; } + +typedef $$TodoItemsTableCreateCompanionBuilder = TodoItemsCompanion Function({ + Value id, + required String title, + required String content, + Value category, +}); +typedef $$TodoItemsTableUpdateCompanionBuilder = TodoItemsCompanion Function({ + Value id, + Value title, + Value content, + Value category, +}); + +class $$TodoItemsTableFilterComposer + extends Composer<_$AppDatabase, $TodoItemsTable> { + $$TodoItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get title => $composableBuilder( + column: $table.title, builder: (column) => ColumnFilters(column)); + + ColumnFilters get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnFilters(column)); + + ColumnFilters get category => $composableBuilder( + column: $table.category, builder: (column) => ColumnFilters(column)); +} + +class $$TodoItemsTableOrderingComposer + extends Composer<_$AppDatabase, $TodoItemsTable> { + $$TodoItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get title => $composableBuilder( + column: $table.title, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get category => $composableBuilder( + column: $table.category, builder: (column) => ColumnOrderings(column)); +} + +class $$TodoItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $TodoItemsTable> { + $$TodoItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get title => + $composableBuilder(column: $table.title, builder: (column) => column); + + GeneratedColumn get content => + $composableBuilder(column: $table.content, builder: (column) => column); + + GeneratedColumn get category => + $composableBuilder(column: $table.category, builder: (column) => column); +} + +class $$TodoItemsTableTableManager extends RootTableManager< + _$AppDatabase, + $TodoItemsTable, + TodoItem, + $$TodoItemsTableFilterComposer, + $$TodoItemsTableOrderingComposer, + $$TodoItemsTableAnnotationComposer, + $$TodoItemsTableCreateCompanionBuilder, + $$TodoItemsTableUpdateCompanionBuilder, + (TodoItem, BaseReferences<_$AppDatabase, $TodoItemsTable, TodoItem>), + TodoItem, + PrefetchHooks Function()> { + $$TodoItemsTableTableManager(_$AppDatabase db, $TodoItemsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$TodoItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$TodoItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$TodoItemsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value title = const Value.absent(), + Value content = const Value.absent(), + Value category = const Value.absent(), + }) => + TodoItemsCompanion( + id: id, + title: title, + content: content, + category: category, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + required String title, + required String content, + Value category = const Value.absent(), + }) => + TodoItemsCompanion.insert( + id: id, + title: title, + content: content, + category: category, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$TodoItemsTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $TodoItemsTable, + TodoItem, + $$TodoItemsTableFilterComposer, + $$TodoItemsTableOrderingComposer, + $$TodoItemsTableAnnotationComposer, + $$TodoItemsTableCreateCompanionBuilder, + $$TodoItemsTableUpdateCompanionBuilder, + (TodoItem, BaseReferences<_$AppDatabase, $TodoItemsTable, TodoItem>), + TodoItem, + PrefetchHooks Function()>; + +class $AppDatabaseManager { + final _$AppDatabase _db; + $AppDatabaseManager(this._db); + $$TodoItemsTableTableManager get todoItems => + $$TodoItemsTableTableManager(_db, _db.todoItems); +} diff --git a/drift/example/example.dart b/drift/example/example.dart index bfe8b6d8e6..02a4b09998 100644 --- a/drift/example/example.dart +++ b/drift/example/example.dart @@ -22,18 +22,45 @@ Future main() async { Future runApp() async { final tr = Sentry.startTransaction('drift', 'op', bindToScope: true); - final executor = SentryQueryExecutor( - () => NativeDatabase.memory(), - databaseName: 'your_db_name', + final executor = NativeDatabase.memory().interceptWith( + SentryQueryInterceptor(databaseName: 'your_db_name'), ); + final db = AppDatabase(executor); - await db.into(db.todoItems).insert( + await db.transaction(() async { + await db.into(db.todoItems).insert( + TodoItemsCompanion.insert( + title: 'This is a test thing', + content: 'test', + ), + ); + + await db.transaction(() async { + await db.into(db.todoItems).insert( + TodoItemsCompanion.insert( + title: 'This is a test thing in the tx', + content: 'test', + ), + ); + }); + + await db.batch((batch) { + batch.insertAll(db.todoItems, [ + TodoItemsCompanion.insert(title: 'First entry', content: 'My content'), TodoItemsCompanion.insert( - title: 'This is a test thing', - content: 'test', + title: 'Another entry', + content: 'More content', ), - ); + ]); + }); + }); + + await db.batch((batch) async { + batch.insertAll(db.todoItems, [ + TodoItemsCompanion.insert(title: 'This is a test', content: 'test'), + ]); + }); final items = await db.select(db.todoItems).get(); print(items); diff --git a/drift/lib/sentry_drift.dart b/drift/lib/sentry_drift.dart index d2df194d04..8f160e44d3 100644 --- a/drift/lib/sentry_drift.dart +++ b/drift/lib/sentry_drift.dart @@ -1,3 +1,3 @@ library; -export 'src/sentry_query_executor.dart'; +export 'src/sentry_query_interceptor.dart'; diff --git a/drift/lib/src/constants.dart b/drift/lib/src/constants.dart new file mode 100644 index 0000000000..c096dbc0af --- /dev/null +++ b/drift/lib/src/constants.dart @@ -0,0 +1,2 @@ +const String integrationName = 'sentryDriftTracing'; +const String loggerName = 'sentry_drift'; diff --git a/drift/lib/src/sentry_query_executor.dart b/drift/lib/src/sentry_query_executor.dart deleted file mode 100644 index 95784670c8..0000000000 --- a/drift/lib/src/sentry_query_executor.dart +++ /dev/null @@ -1,195 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart'; -import 'package:meta/meta.dart'; -import 'package:sentry/sentry.dart'; - -import 'sentry_span_helper.dart'; -import 'sentry_transaction_executor.dart'; -import 'version.dart'; - -/// Signature of a function that opens a database connection when instructed to. -typedef DatabaseOpener = FutureOr Function(); - -/// The Sentry Query Executor. -/// -/// If the constructor parameter queryExecutor is null, [LazyDatabase] will be -/// used as a default. -@experimental -class SentryQueryExecutor extends QueryExecutor { - Hub _hub; - - final _spanHelper = SentrySpanHelper( - // ignore: invalid_use_of_internal_member - SentryTraceOrigins.autoDbDriftQueryExecutor, - ); - - final QueryExecutor _executor; - - final String _dbName; - - @internal - // ignore: public_member_api_docs - static const dbNameKey = 'db.name'; - - @internal - // ignore: public_member_api_docs - static const dbOp = 'db'; - - @internal - // ignore: public_member_api_docs - static const dbSystemKey = 'db.system'; - - @internal - // ignore: public_member_api_docs - static const dbSystem = 'sqlite'; - - bool _isOpen = false; - - /// Declares a [SentryQueryExecutor] that will run [opener] when the database is - /// first requested to be opened. You must specify the same [dialect] as the - /// underlying database has - SentryQueryExecutor( - DatabaseOpener opener, { - @internal Hub? hub, - @internal QueryExecutor? queryExecutor, - required String databaseName, - }) : _hub = hub ?? HubAdapter(), - _dbName = databaseName, - _executor = queryExecutor ?? LazyDatabase(opener) { - // ignore: invalid_use_of_internal_member - final options = _hub.options; - options.sdk.addIntegration('SentryDriftTracing'); - options.sdk.addPackage(packageName, sdkVersion); - _spanHelper.setHub(_hub); - } - - /// @nodoc - @internal - void setHub(Hub hub) { - _hub = hub; - _spanHelper.setHub(hub); - } - - @override - TransactionExecutor beginTransaction() { - final transactionExecutor = _executor.beginTransaction(); - final sentryTransactionExecutor = SentryTransactionExecutor( - transactionExecutor, - _hub, - dbName: _dbName, - ); - sentryTransactionExecutor.beginTransaction(); - return sentryTransactionExecutor; - } - - @override - Future runBatched(BatchedStatements statements) { - return _spanHelper.asyncWrapInSpan( - statements.toString(), - () async { - return await _executor.runBatched(statements); - }, - dbName: _dbName, - ); - } - - @override - Future ensureOpen(QueryExecutorUser user) { - if (_isOpen) { - return Future.value(true); - } - return _spanHelper.asyncWrapInSpan( - 'Open DB: $_dbName', - () async { - final res = await _executor.ensureOpen(user); - _isOpen = true; - return res; - }, - dbName: _dbName, - ); - } - - @override - Future runCustom(String statement, [List? args]) { - return _spanHelper.asyncWrapInSpan( - statement, - () async { - return await _executor.runCustom(statement, args); - }, - dbName: _dbName, - ); - } - - @override - Future runDelete(String statement, List args) { - return _spanHelper.asyncWrapInSpan( - statement, - () async { - return await _executor.runDelete(statement, args); - }, - dbName: _dbName, - ); - } - - @override - Future runInsert(String statement, List args) { - return _spanHelper.asyncWrapInSpan( - statement, - () async { - return await _executor.runInsert(statement, args); - }, - dbName: _dbName, - ); - } - - @override - Future>> runSelect( - String statement, - List args, - ) { - return _spanHelper.asyncWrapInSpan( - statement, - () async { - return await _executor.runSelect(statement, args); - }, - dbName: _dbName, - ); - } - - @override - Future runUpdate(String statement, List args) { - return _spanHelper.asyncWrapInSpan( - statement, - () async { - return await _executor.runUpdate(statement, args); - }, - dbName: _dbName, - ); - } - - @override - // ignore: override_on_non_overriding_member, public_member_api_docs - QueryExecutor beginExclusive() { - final dynamic uncheckedExecutor = _executor; - try { - return uncheckedExecutor.beginExclusive() as QueryExecutor; - } on NoSuchMethodError catch (_) { - throw Exception('This method is not supported in Drift versions <2.19.0'); - } - } - - @override - Future close() { - return _spanHelper.asyncWrapInSpan( - 'Close DB: $_dbName', - () async { - return await _executor.close(); - }, - dbName: _dbName, - ); - } - - @override - SqlDialect get dialect => _executor.dialect; -} diff --git a/drift/lib/src/sentry_query_interceptor.dart b/drift/lib/src/sentry_query_interceptor.dart new file mode 100644 index 0000000000..89bcabdf65 --- /dev/null +++ b/drift/lib/src/sentry_query_interceptor.dart @@ -0,0 +1,171 @@ +// ignore_for_file: invalid_use_of_internal_member + +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:meta/meta.dart'; +import 'package:sentry/sentry.dart'; + +import 'constants.dart' as drift_constants; +import 'sentry_span_helper.dart'; +import 'version.dart'; + +/// A Sentry query interceptor that wraps database operations in performance monitoring spans. +/// +/// This interceptor tracks all database operations executed through a Drift database connection, +/// including transactions, batches, and individual CRUD operations. Each operation is captured +/// as a Sentry span with relevant context. +class SentryQueryInterceptor extends QueryInterceptor { + final String _dbName; + late final SentrySpanHelper _spanHelper; + bool _isDbOpen = false; + + @visibleForTesting + SentrySpanHelper get spanHelper => _spanHelper; + + SentryQueryInterceptor({required String databaseName, @internal Hub? hub}) + : _dbName = databaseName { + hub = hub ?? HubAdapter(); + _spanHelper = SentrySpanHelper( + SentryTraceOrigins.autoDbDriftQueryInterceptor, + hub: hub, + ); + final options = hub.options; + options.sdk.addIntegration(drift_constants.integrationName); + options.sdk.addPackage(packageName, sdkVersion); + } + + /// Wraps database operations in Sentry spans. + /// + /// This handles most CRUD operations but excludes transaction lifecycle methods + /// (begin/commit/rollback), which require maintaining an ongoing transaction span + /// across multiple operations. Those are handled separately via [SentrySpanHelper]. + Future _instrumentOperation( + String description, + FutureOr Function() execute, { + String? operation, + }) async => + _spanHelper.asyncWrapInSpan( + description, + () async => execute(), + dbName: _dbName, + operation: operation, + ); + + @override + Future ensureOpen(QueryExecutor executor, QueryExecutorUser user) { + if (_isDbOpen) { + return super.ensureOpen(executor, user); + } + return _instrumentOperation( + SentrySpanDescriptions.dbOpen(dbName: _dbName), + () async { + final result = await super.ensureOpen(executor, user); + _isDbOpen = true; + return result; + }, + operation: SentrySpanOperations.dbOpen, + ); + } + + @override + TransactionExecutor beginTransaction(QueryExecutor parent) { + return _spanHelper.beginTransaction( + () => super.beginTransaction(parent), + dbName: _dbName, + ); + } + + @override + Future close(QueryExecutor inner) { + return _instrumentOperation( + SentrySpanDescriptions.dbClose(dbName: _dbName), + () => super.close(inner), + operation: SentrySpanOperations.dbClose, + ); + } + + @override + Future runBatched( + QueryExecutor executor, + BatchedStatements statements, + ) { + final description = + SentrySpanDescriptions.dbBatch(statements: statements.statements); + return _instrumentOperation( + description, + () => super.runBatched(executor, statements), + operation: SentrySpanOperations.dbSqlBatch, + ); + } + + @override + Future commitTransaction(TransactionExecutor inner) { + return _spanHelper.finishTransaction(() => super.commitTransaction(inner)); + } + + @override + Future rollbackTransaction(TransactionExecutor inner) { + return _spanHelper.abortTransaction(() => super.rollbackTransaction(inner)); + } + + @override + Future runInsert( + QueryExecutor executor, + String statement, + List args, + ) { + return _instrumentOperation( + statement, + () => executor.runInsert(statement, args), + ); + } + + @override + Future runUpdate( + QueryExecutor executor, + String statement, + List args, + ) { + return _instrumentOperation( + statement, + () => executor.runUpdate(statement, args), + ); + } + + @override + Future runDelete( + QueryExecutor executor, + String statement, + List args, + ) { + return _instrumentOperation( + statement, + () => executor.runDelete(statement, args), + ); + } + + @override + Future runCustom( + QueryExecutor executor, + String statement, + List args, + ) { + return _instrumentOperation( + statement, + () => executor.runCustom(statement, args), + ); + } + + @override + Future>> runSelect( + QueryExecutor executor, + String statement, + List args, + ) { + return _instrumentOperation( + statement, + () => executor.runSelect(statement, args), + ); + } +} diff --git a/drift/lib/src/sentry_span_helper.dart b/drift/lib/src/sentry_span_helper.dart index 762bd24d08..f9a04e0ed7 100644 --- a/drift/lib/src/sentry_span_helper.dart +++ b/drift/lib/src/sentry_span_helper.dart @@ -1,155 +1,172 @@ -import 'package:meta/meta.dart'; +// ignore_for_file: invalid_use_of_internal_member + +import 'dart:collection'; +import 'package:meta/meta.dart'; import 'package:sentry/sentry.dart'; -import 'sentry_query_executor.dart'; +import 'constants.dart'; -/// @nodoc @internal class SentrySpanHelper { - /// @nodoc - Hub _hub = HubAdapter(); - - /// @nodoc + final Hub _hub; final String _origin; - /// @nodoc - SentrySpanHelper(this._origin); + /// Represents a stack of Drift transaction spans. + /// These are used to allow nested spans if the user nests Drift transactions. + /// If the transaction stack is empty, the spans are attached to the + /// active span in the Hub's scope. + final ListQueue _transactionStack = ListQueue(); - /// @nodoc - void setHub(Hub hub) { - _hub = hub; - } + @visibleForTesting + ListQueue get transactionStack => _transactionStack; + + SentrySpanHelper(this._origin, {Hub? hub}) : _hub = hub ?? HubAdapter(); - /// @nodoc - @internal Future asyncWrapInSpan( String description, Future Function() execute, { String? dbName, - bool useTransactionSpan = false, + String? operation, }) async { - ISentrySpan? currentSpan = _hub.getSpan(); - if (useTransactionSpan) { - currentSpan = transactionSpan; + final parentSpan = _transactionStack.lastOrNull ?? _hub.getSpan(); + if (parentSpan == null) { + _hub.options.logger( + SentryLevel.warning, + 'Active Sentry transaction does not exist, could not start span for the Drift operation: $description', + logger: loggerName, + ); + return execute(); } - final span = currentSpan?.startChild( - SentryQueryExecutor.dbOp, + + final span = parentSpan.startChild( + operation ?? SentrySpanOperations.dbSqlQuery, description: description, ); - // ignore: invalid_use_of_internal_member - span?.origin = _origin; + span.origin = _origin; - span?.setData( - SentryQueryExecutor.dbSystemKey, - SentryQueryExecutor.dbSystem, + span.setData( + SentrySpanData.dbSystemKey, + SentrySpanData.dbSystemSqlite, ); if (dbName != null) { - span?.setData(SentryQueryExecutor.dbNameKey, dbName); + span.setData(SentrySpanData.dbNameKey, dbName); } try { final result = await execute(); - span?.status = SpanStatus.ok(); + span.status = SpanStatus.ok(); return result; } catch (exception) { - span?.throwable = exception; - span?.status = SpanStatus.internalError(); + span.throwable = exception; + span.status = SpanStatus.internalError(); rethrow; } finally { - await span?.finish(); + await span.finish(); } } - /// This span is used for the database transaction. - @internal - ISentrySpan? transactionSpan; - - /// @nodoc - @internal T beginTransaction( - String description, T Function() execute, { String? dbName, }) { - final currentSpan = _hub.getSpan(); - final span = currentSpan?.startChild( - SentryQueryExecutor.dbOp, - description: description, + final parentSpan = _transactionStack.lastOrNull ?? _hub.getSpan(); + if (parentSpan == null) { + _hub.options.logger( + SentryLevel.warning, + 'Active Sentry transaction does not exist, could not start span for Drift operation: Begin Transaction', + logger: loggerName, + ); + return execute(); + } + + final newParent = parentSpan.startChild( + SentrySpanOperations.dbSqlTransaction, + description: SentrySpanDescriptions.dbTransaction, ); - // ignore: invalid_use_of_internal_member - span?.origin = _origin; + newParent.origin = _origin; - span?.setData( - SentryQueryExecutor.dbSystemKey, - SentryQueryExecutor.dbSystem, + newParent.setData( + SentrySpanData.dbSystemKey, + SentrySpanData.dbSystemSqlite, ); if (dbName != null) { - span?.setData(SentryQueryExecutor.dbNameKey, dbName); + newParent.setData(SentrySpanData.dbNameKey, dbName); } try { final result = execute(); - span?.status = SpanStatus.unknown(); + newParent.status = SpanStatus.unknown(); + + // Only add to the stack if no error occurred + _transactionStack.add(newParent); return result; } catch (exception) { - span?.throwable = exception; - span?.status = SpanStatus.internalError(); + newParent.throwable = exception; + newParent.status = SpanStatus.internalError(); rethrow; - } finally { - transactionSpan = span; } } - /// @nodoc - @internal - Future finishTransaction( - Future Function() execute, { - String? dbName, - }) async { + Future finishTransaction(Future Function() execute) async { + final parentSpan = _transactionStack.lastOrNull; + if (parentSpan == null) { + _hub.options.logger( + SentryLevel.warning, + 'Active Sentry transaction does not exist, could not finish span for Drift operation: Finish Transaction', + logger: loggerName, + ); + return execute(); + } + try { final result = await execute(); - transactionSpan?.status = SpanStatus.ok(); + parentSpan.status = SpanStatus.ok(); return result; } catch (exception) { - transactionSpan?.throwable = exception; - transactionSpan?.status = SpanStatus.internalError(); + parentSpan.throwable = exception; + parentSpan.status = SpanStatus.internalError(); rethrow; } finally { - await transactionSpan?.finish(); - transactionSpan = null; + await parentSpan.finish(); + _transactionStack.removeLast(); } } - /// @nodoc - @internal - Future abortTransaction( - Future Function() execute, { - String? dbName, - }) async { + Future abortTransaction(Future Function() execute) async { + final parentSpan = _transactionStack.lastOrNull; + if (parentSpan == null) { + _hub.options.logger( + SentryLevel.warning, + 'Active Sentry transaction does not exist, could not finish span for Drift operation: Abort Transaction', + logger: loggerName, + ); + return Future.value(); + } + try { final result = await execute(); - transactionSpan?.status = SpanStatus.aborted(); + parentSpan.status = SpanStatus.aborted(); return result; } catch (exception) { - transactionSpan?.throwable = exception; - transactionSpan?.status = SpanStatus.internalError(); + parentSpan.throwable = exception; + parentSpan.status = SpanStatus.internalError(); rethrow; } finally { - await transactionSpan?.finish(); - transactionSpan = null; + await parentSpan.finish(); + _transactionStack.removeLast(); } } } diff --git a/drift/lib/src/sentry_transaction_executor.dart b/drift/lib/src/sentry_transaction_executor.dart deleted file mode 100644 index dba05cd8cc..0000000000 --- a/drift/lib/src/sentry_transaction_executor.dart +++ /dev/null @@ -1,167 +0,0 @@ -import 'package:drift/backends.dart'; -import 'package:meta/meta.dart'; -import 'package:sentry/sentry.dart'; - -import 'sentry_span_helper.dart'; - -/// @nodoc -@internal -class SentryTransactionExecutor extends TransactionExecutor { - final TransactionExecutor _executor; - - final Hub _hub; - - final _spanHelper = SentrySpanHelper( - // ignore: invalid_use_of_internal_member - SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - - final String? _dbName; - - bool _isOpen = false; - - final _withinTransactionDescription = 'Within transaction: '; - - /// @nodoc - SentryTransactionExecutor(this._executor, Hub hub, {@internal String? dbName}) - : _hub = hub, - _dbName = dbName { - _spanHelper.setHub(_hub); - } - - @override - TransactionExecutor beginTransaction() { - return _spanHelper.beginTransaction( - 'transaction', - () { - return _executor.beginTransaction(); - }, - dbName: _dbName, - ); - } - - @override - Future rollback() { - return _spanHelper.abortTransaction(() async { - return await _executor.rollback(); - }); - } - - @override - Future send() { - return _spanHelper.finishTransaction(() async { - return await _executor.send(); - }); - } - - @override - SqlDialect get dialect => _executor.dialect; - - @override - Future ensureOpen(QueryExecutorUser user) { - if (_isOpen) { - return Future.value(true); - } - return _spanHelper.asyncWrapInSpan( - 'Open transaction', - () async { - final res = await _executor.ensureOpen(user); - _isOpen = true; - return res; - }, - dbName: _dbName, - ); - } - - @override - Future runBatched(BatchedStatements statements) { - return _spanHelper.asyncWrapInSpan( - 'batch', - () async { - return await _executor.runBatched(statements); - }, - dbName: _dbName, - ); - } - - @override - Future runCustom(String statement, [List? args]) { - return _spanHelper.asyncWrapInSpan( - _spanDescriptionForOperations(statement), - () async { - return _executor.runCustom(statement, args); - }, - dbName: _dbName, - useTransactionSpan: true, - ); - } - - @override - Future runDelete(String statement, List args) { - return _spanHelper.asyncWrapInSpan( - _spanDescriptionForOperations(statement), - () async { - return _executor.runDelete(statement, args); - }, - dbName: _dbName, - useTransactionSpan: true, - ); - } - - @override - Future runInsert(String statement, List args) { - return _spanHelper.asyncWrapInSpan( - _spanDescriptionForOperations(statement), - () async { - return _executor.runInsert(statement, args); - }, - dbName: _dbName, - useTransactionSpan: true, - ); - } - - @override - Future>> runSelect( - String statement, - List args, - ) { - return _spanHelper.asyncWrapInSpan( - _spanDescriptionForOperations(statement), - () async { - return _executor.runSelect(statement, args); - }, - dbName: _dbName, - useTransactionSpan: true, - ); - } - - @override - // ignore: override_on_non_overriding_member, public_member_api_docs - QueryExecutor beginExclusive() { - final dynamic uncheckedExecutor = _executor; - try { - return uncheckedExecutor.beginExclusive() as QueryExecutor; - } on NoSuchMethodError catch (_) { - throw Exception('This method is not supported in Drift versions <2.19.0'); - } - } - - @override - Future runUpdate(String statement, List args) { - return _spanHelper.asyncWrapInSpan( - _spanDescriptionForOperations(statement), - () async { - return _executor.runUpdate(statement, args); - }, - dbName: _dbName, - useTransactionSpan: true, - ); - } - - @override - bool get supportsNestedTransactions => _executor.supportsNestedTransactions; - - String _spanDescriptionForOperations(String operation) { - return '$_withinTransactionDescription$operation'; - } -} diff --git a/drift/pubspec.yaml b/drift/pubspec.yaml index 2d0c84ab80..4acb87201b 100644 --- a/drift/pubspec.yaml +++ b/drift/pubspec.yaml @@ -19,7 +19,7 @@ platforms: dependencies: sentry: 8.13.0 meta: ^1.3.0 - drift: ^2.13.0 + drift: ^2.24.0 dev_dependencies: lints: '>=2.0.0' diff --git a/drift/test/mocks/mocks.dart b/drift/test/mocks/mocks.dart index 4842f97455..98470693f6 100644 --- a/drift/test/mocks/mocks.dart +++ b/drift/test/mocks/mocks.dart @@ -5,6 +5,6 @@ import 'package:sentry/sentry.dart'; @GenerateMocks([ Hub, LazyDatabase, - TransactionExecutor, + QueryExecutor, ]) void main() {} diff --git a/drift/test/mocks/mocks.mocks.dart b/drift/test/mocks/mocks.mocks.dart index 25dcb269e5..2001b49951 100644 --- a/drift/test/mocks/mocks.mocks.dart +++ b/drift/test/mocks/mocks.mocks.dart @@ -484,41 +484,20 @@ class MockLazyDatabase extends _i1.Mock implements _i6.LazyDatabase { ) as _i5.Future); } -/// A class which mocks [TransactionExecutor]. +/// A class which mocks [QueryExecutor]. /// /// See the documentation for Mockito's code generation for more information. -class MockTransactionExecutor extends _i1.Mock - implements _i3.TransactionExecutor { - MockTransactionExecutor() { +class MockQueryExecutor extends _i1.Mock implements _i3.QueryExecutor { + MockQueryExecutor() { _i1.throwOnMissingStub(this); } - @override - bool get supportsNestedTransactions => (super.noSuchMethod( - Invocation.getter(#supportsNestedTransactions), - returnValue: false, - ) as bool); - @override _i3.SqlDialect get dialect => (super.noSuchMethod( Invocation.getter(#dialect), returnValue: _i3.SqlDialect.sqlite, ) as _i3.SqlDialect); - @override - _i5.Future send() => (super.noSuchMethod( - Invocation.method(#send, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - - @override - _i5.Future rollback() => (super.noSuchMethod( - Invocation.method(#rollback, []), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) as _i5.Future); - @override _i5.Future ensureOpen(_i3.QueryExecutorUser? user) => (super.noSuchMethod( diff --git a/drift/test/sentry_database_test.dart b/drift/test/sentry_database_test.dart deleted file mode 100644 index 4a3d4daa3c..0000000000 --- a/drift/test/sentry_database_test.dart +++ /dev/null @@ -1,689 +0,0 @@ -// ignore_for_file: invalid_use_of_internal_member, library_annotations - -@TestOn('vm') - -import 'package:drift/drift.dart'; -import 'package:drift/native.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:sentry/sentry.dart'; -import 'package:sentry/src/sentry_tracer.dart'; -import 'package:sentry_drift/src/sentry_query_executor.dart'; -import 'package:sentry_drift/src/sentry_transaction_executor.dart'; -import 'package:sentry_drift/src/version.dart'; -import 'package:sqlite3/open.dart'; - -import 'mocks/mocks.mocks.dart'; -import 'test_database.dart'; -import 'utils.dart'; -import 'utils/windows_helper.dart'; - -void main() { - open.overrideFor(OperatingSystem.windows, openOnWindows); - - final expectedInsertStatement = - 'INSERT INTO "todo_items" ("title", "body") VALUES (?, ?)'; - final expectedUpdateStatement = - 'UPDATE "todo_items" SET "title" = ?, "body" = ? WHERE "title" = ?;'; - final expectedSelectStatement = 'SELECT * FROM todo_items'; - final expectedDeleteStatement = 'DELETE FROM "todo_items";'; - final expectedCloseStatement = 'Close DB: ${Fixture.dbName}'; - final expectedOpenStatement = 'Open DB: ${Fixture.dbName}'; - final expectedTransactionStatement = 'transaction'; - final withinTransactionDescription = 'Within transaction: '; - - void verifySpan( - String description, - SentrySpan? span, { - String origin = SentryTraceOrigins.autoDbDriftQueryExecutor, - SpanStatus? status, - }) { - status ??= SpanStatus.ok(); - expect(span?.context.operation, SentryQueryExecutor.dbOp); - expect(span?.context.description, description); - expect(span?.status, status); - expect(span?.origin, origin); - expect( - span?.data[SentryQueryExecutor.dbSystemKey], - SentryQueryExecutor.dbSystem, - ); - expect( - span?.data[SentryQueryExecutor.dbNameKey], - Fixture.dbName, - ); - } - - void verifyErrorSpan( - String description, - Exception exception, - SentrySpan? span, { - String origin = SentryTraceOrigins.autoDbDriftQueryExecutor, - }) { - expect(span?.context.operation, SentryQueryExecutor.dbOp); - expect(span?.context.description, description); - expect(span?.status, SpanStatus.internalError()); - expect(span?.origin, origin); - expect( - span?.data[SentryQueryExecutor.dbSystemKey], - SentryQueryExecutor.dbSystem, - ); - expect( - span?.data[SentryQueryExecutor.dbNameKey], - Fixture.dbName, - ); - - expect(span?.throwable, exception); - } - - Future insertRow(AppDatabase sut, {bool withError = false}) { - if (withError) { - return sut.into(sut.todoItems).insert( - TodoItemsCompanion.insert( - title: '', - content: '', - ), - ); - } else { - return sut.into(sut.todoItems).insert( - TodoItemsCompanion.insert( - title: 'todo: finish drift setup', - content: 'We can now write queries and define our own tables.', - ), - ); - } - } - - Future updateRow(AppDatabase sut, {bool withError = false}) { - if (withError) { - return (sut.update(sut.todoItems) - ..where((tbl) => tbl.title.equals('doesnt exist'))) - .write( - TodoItemsCompanion( - title: Value('after update'), - content: Value('We can now write queries and define our own tables.'), - ), - ); - } else { - return (sut.update(sut.todoItems) - ..where((tbl) => tbl.title.equals('todo: finish drift setup'))) - .write( - TodoItemsCompanion( - title: Value('after update'), - content: Value('We can now write queries and define our own tables.'), - ), - ); - } - } - - group('adds span', () { - late Fixture fixture; - - setUp(() async { - fixture = Fixture(); - - when(fixture.hub.options).thenReturn(fixture.options); - when(fixture.hub.getSpan()).thenReturn(fixture.tracer); - - await fixture.setUp(); - }); - - tearDown(() async { - await fixture.tearDown(); - }); - - test('open span is only added once', () async { - final sut = fixture.sut; - - await insertRow(sut); - await insertRow(sut); - await insertRow(sut); - - final openSpansCount = fixture.tracer.children - .where( - (element) => element.context.description == expectedOpenStatement, - ) - .length; - - expect(openSpansCount, 1); - }); - - test('insert adds span', () async { - final sut = fixture.sut; - - await insertRow(sut); - - verifySpan( - expectedInsertStatement, - fixture.getCreatedSpan(), - ); - }); - - test('update adds span', () async { - final sut = fixture.sut; - - await insertRow(sut); - await updateRow(sut); - - verifySpan( - expectedUpdateStatement, - fixture.getCreatedSpan(), - ); - }); - - test('custom adds span', () async { - final sut = fixture.sut; - - await sut.customStatement('SELECT * FROM todo_items'); - - verifySpan( - expectedSelectStatement, - fixture.getCreatedSpan(), - ); - }); - - test('delete adds span', () async { - final sut = fixture.sut; - - await insertRow(sut); - await fixture.sut.delete(fixture.sut.todoItems).go(); - - verifySpan( - expectedDeleteStatement, - fixture.getCreatedSpan(), - ); - }); - - test('transaction adds insert spans', () async { - final sut = fixture.sut; - - await sut.transaction(() async { - await insertRow(sut); - await insertRow(sut); - }); - - final insertSpanCount = fixture.tracer.children - .where( - (element) => - element.context.description == - '$withinTransactionDescription$expectedInsertStatement', - ) - .length; - expect(insertSpanCount, 2); - - verifySpan( - '$withinTransactionDescription$expectedInsertStatement', - fixture.getCreatedSpan(), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - - verifySpan( - expectedTransactionStatement, - fixture.getCreatedSpanByDescription(expectedTransactionStatement), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - }); - - test('transaction adds update spans', () async { - final sut = fixture.sut; - - await sut.transaction(() async { - await insertRow(sut); - await updateRow(sut); - }); - - final updateSpanCount = fixture.tracer.children - .where( - (element) => - element.context.description == - '$withinTransactionDescription$expectedUpdateStatement', - ) - .length; - expect(updateSpanCount, 1); - - verifySpan( - '$withinTransactionDescription$expectedUpdateStatement', - fixture.getCreatedSpan(), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - - verifySpan( - expectedTransactionStatement, - fixture.getCreatedSpanByDescription(expectedTransactionStatement), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - }); - - test('transaction adds delete spans', () async { - final sut = fixture.sut; - - await sut.transaction(() async { - await insertRow(sut); - await fixture.sut.delete(fixture.sut.todoItems).go(); - }); - - final deleteSpanCount = fixture.tracer.children - .where( - (element) => - element.context.description == - '$withinTransactionDescription$expectedDeleteStatement', - ) - .length; - expect(deleteSpanCount, 1); - - verifySpan( - '$withinTransactionDescription$expectedDeleteStatement', - fixture.getCreatedSpan(), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - - verifySpan( - expectedTransactionStatement, - fixture.getCreatedSpanByDescription(expectedTransactionStatement), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - }); - - test('transaction adds custom spans', () async { - final sut = fixture.sut; - - await sut.transaction(() async { - await insertRow(sut); - await sut.customStatement('SELECT * FROM todo_items'); - }); - - final customSpanCount = fixture.tracer.children - .where( - (element) => - element.context.description == - '$withinTransactionDescription$expectedSelectStatement', - ) - .length; - expect(customSpanCount, 1); - - verifySpan( - '$withinTransactionDescription$expectedSelectStatement', - fixture.getCreatedSpan(), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - - verifySpan( - expectedTransactionStatement, - fixture.getCreatedSpanByDescription(expectedTransactionStatement), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - }); - - test('transaction rollback adds span', () async { - final sut = fixture.sut; - - await insertRow(sut); - await insertRow(sut); - - try { - await sut.transaction(() async { - await insertRow(sut, withError: true); - }); - } catch (_) {} - - final spans = fixture.tracer.children - .where((child) => child.status == SpanStatus.aborted()); - expect(spans.length, 1); - final abortedSpan = spans.first; - - verifySpan( - expectedTransactionStatement, - abortedSpan, - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - status: SpanStatus.aborted(), - ); - }); - - test('batch adds span', () async { - final sut = fixture.sut; - - await sut.batch((batch) async { - await insertRow(sut); - await insertRow(sut); - }); - - verifySpan( - 'batch', - fixture.getCreatedSpan(), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - }); - - test('close adds span', () async { - final sut = fixture.sut; - - await sut.close(); - - verifySpan( - 'Close DB: ${Fixture.dbName}', - fixture.getCreatedSpan(), - ); - }); - - test('open adds span', () async { - final sut = fixture.sut; - - // SentryDriftDatabase is by default lazily opened by default so it won't - // create a span until it is actually used. - await sut.select(sut.todoItems).get(); - - verifySpan( - expectedOpenStatement, - fixture.getCreatedSpanByDescription(expectedOpenStatement), - ); - }); - }); - - group('does not add span', () { - late Fixture fixture; - - setUp(() async { - fixture = Fixture(); - - when(fixture.hub.options).thenReturn(fixture.options); - when(fixture.hub.getSpan()).thenReturn(fixture.tracer); - - await fixture.setUp(); - }); - - tearDown(() async { - await fixture.tearDown(); - }); - - test('does not add open span if db is not used', () async { - fixture.sut; - - expect(fixture.tracer.children.isEmpty, true); - }); - - test('batch does not add span for failed operations', () async { - final sut = fixture.sut; - - try { - await sut.batch((batch) async { - await insertRow(sut, withError: true); - await insertRow(sut); - }); - } catch (_) {} - - expect(fixture.tracer.children.isEmpty, true); - }); - }); - - group('adds error span', () { - late Fixture fixture; - - setUp(() async { - fixture = Fixture(); - - when(fixture.hub.options).thenReturn(fixture.options); - when(fixture.hub.getSpan()).thenReturn(fixture.tracer); - when(fixture.mockLazyDatabase.ensureOpen(any)) - .thenAnswer((_) => Future.value(true)); - - await fixture.setUp(injectMock: true); - }); - - tearDown(() async { - await fixture.tearDown(); - }); - - test('throwing runInsert throws error span', () async { - when(fixture.mockLazyDatabase.runInsert(any, any)) - .thenThrow(fixture.exception); - - try { - await insertRow(fixture.sut); - } catch (exception) { - expect(exception, fixture.exception); - } - - verifyErrorSpan( - expectedInsertStatement, - fixture.exception, - fixture.getCreatedSpan(), - ); - }); - - test('throwing runUpdate throws error span', () async { - when(fixture.mockLazyDatabase.runUpdate(any, any)) - .thenThrow(fixture.exception); - - try { - await updateRow(fixture.sut); - } catch (exception) { - expect(exception, fixture.exception); - } - - verifyErrorSpan( - expectedUpdateStatement, - fixture.exception, - fixture.getCreatedSpan(), - ); - }); - - test('throwing runCustom throws error span', () async { - when(fixture.mockLazyDatabase.runCustom(any, any)) - .thenThrow(fixture.exception); - - try { - await fixture.sut.customStatement('SELECT * FROM todo_items'); - } catch (exception) { - expect(exception, fixture.exception); - } - - verifyErrorSpan( - expectedSelectStatement, - fixture.exception, - fixture.getCreatedSpan(), - ); - }); - - test('throwing transaction throws error span', () async { - final mockTransactionExecutor = MockTransactionExecutor(); - when(mockTransactionExecutor.beginTransaction()) - .thenThrow(fixture.exception); - - try { - // We need to move it inside the try/catch becaue SentryTransactionExecutor - // starts beginTransaction() directly after init - final SentryTransactionExecutor transactionExecutor = - SentryTransactionExecutor( - mockTransactionExecutor, - fixture.hub, - dbName: Fixture.dbName, - ); - - when(fixture.mockLazyDatabase.beginTransaction()) - .thenReturn(transactionExecutor); - - await fixture.sut.transaction(() async { - await insertRow(fixture.sut); - }); - } catch (exception) { - expect(exception, fixture.exception); - } - - verifyErrorSpan( - expectedTransactionStatement, - fixture.exception, - fixture.getCreatedSpan(), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - }); - - test('throwing batch throws error span', () async { - final mockTransactionExecutor = MockTransactionExecutor(); - when(mockTransactionExecutor.beginTransaction()) - .thenThrow(fixture.exception); - - // We need to move it inside the try/catch becaue SentryTransactionExecutor - // starts beginTransaction() directly after init - final SentryTransactionExecutor transactionExecutor = - SentryTransactionExecutor( - mockTransactionExecutor, - fixture.hub, - dbName: Fixture.dbName, - ); - - when(fixture.mockLazyDatabase.beginTransaction()) - .thenReturn(transactionExecutor); - - when(fixture.mockLazyDatabase.runInsert(any, any)) - .thenAnswer((realInvocation) => Future.value(1)); - - try { - await fixture.sut.batch((batch) async { - await insertRow(fixture.sut); - }); - } catch (exception) { - expect(exception, fixture.exception); - } - - verifyErrorSpan( - expectedTransactionStatement, - fixture.exception, - fixture.getCreatedSpan(), - origin: SentryTraceOrigins.autoDbDriftTransactionExecutor, - ); - }); - - test('throwing close throws error span', () async { - when(fixture.mockLazyDatabase.close()).thenThrow(fixture.exception); - when(fixture.mockLazyDatabase.runInsert(any, any)) - .thenAnswer((_) => Future.value(1)); - - try { - await insertRow(fixture.sut); - await fixture.sut.close(); - } catch (exception) { - expect(exception, fixture.exception); - } - - verifyErrorSpan( - expectedCloseStatement, - fixture.exception, - fixture.getCreatedSpan(), - ); - - when(fixture.mockLazyDatabase.close()).thenAnswer((_) => Future.value()); - }); - - test('throwing ensureOpen throws error span', () async { - when(fixture.mockLazyDatabase.ensureOpen(any)) - .thenThrow(fixture.exception); - - try { - await fixture.sut.select(fixture.sut.todoItems).get(); - } catch (exception) { - expect(exception, fixture.exception); - } - - verifyErrorSpan( - expectedOpenStatement, - fixture.exception, - fixture.getCreatedSpanByDescription(expectedOpenStatement), - ); - }); - - test('throwing runDelete throws error span', () async { - when(fixture.mockLazyDatabase.runDelete(any, any)) - .thenThrow(fixture.exception); - - try { - await fixture.sut.delete(fixture.sut.todoItems).go(); - } catch (exception) { - expect(exception, fixture.exception); - } - - verifyErrorSpan( - expectedDeleteStatement, - fixture.exception, - fixture.getCreatedSpan(), - ); - }); - }); - - group('integrations', () { - late Fixture fixture; - - setUp(() async { - fixture = Fixture(); - - when(fixture.hub.options).thenReturn(fixture.options); - when(fixture.hub.getSpan()).thenReturn(fixture.tracer); - - await fixture.setUp(); - }); - - tearDown(() async { - await fixture.tearDown(); - }); - - test('adds integration', () { - expect( - fixture.options.sdk.integrations.contains('SentryDriftTracing'), - true, - ); - }); - - test('adds package', () { - expect( - fixture.options.sdk.packages.any( - (element) => - element.name == packageName && element.version == sdkVersion, - ), - true, - ); - }); - }); -} - -class Fixture { - final options = defaultTestOptions(); - final hub = MockHub(); - static final dbName = 'people-drift-impl'; - final exception = Exception('fixture-exception'); - final _context = SentryTransactionContext('name', 'operation'); - late final tracer = SentryTracer(_context, hub); - late AppDatabase sut; - final mockLazyDatabase = MockLazyDatabase(); - - Future setUp({bool injectMock = false}) async { - sut = AppDatabase(openConnection(injectMock: injectMock)); - } - - Future tearDown() async { - await sut.close(); - } - - SentrySpan? getCreatedSpan() { - return tracer.children.last; - } - - SentrySpan? getCreatedSpanByDescription(String description) { - return tracer.children - .firstWhere((element) => element.context.description == description); - } - - SentryQueryExecutor openConnection({bool injectMock = false}) { - if (injectMock) { - final executor = - SentryQueryExecutor(() => mockLazyDatabase, databaseName: dbName); - executor.setHub(hub); - return executor; - } else { - return SentryQueryExecutor( - () { - return NativeDatabase.memory(); - }, - hub: hub, - databaseName: dbName, - ); - } - } -} diff --git a/drift/test/sentry_drift_test.dart b/drift/test/sentry_drift_test.dart new file mode 100644 index 0000000000..fbf42c43de --- /dev/null +++ b/drift/test/sentry_drift_test.dart @@ -0,0 +1,802 @@ +// ignore_for_file: invalid_use_of_internal_member, library_annotations + +@TestOn('vm') + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry_drift/src/constants.dart' as drift_constants; +import 'package:sentry_drift/src/sentry_query_interceptor.dart'; +import 'package:sentry_drift/src/version.dart'; +import 'package:sqlite3/open.dart'; + +import 'mocks/mocks.mocks.dart'; +import 'test_database.dart'; +import 'utils.dart'; +import 'utils/windows_helper.dart'; + +void main() { + open.overrideFor(OperatingSystem.windows, openOnWindows); + + final expectedInsertStatement = + 'INSERT INTO "todo_items" ("title", "body") VALUES (?, ?)'; + final expectedUpdateStatement = + 'UPDATE "todo_items" SET "title" = ?, "body" = ? WHERE "title" = ?;'; + final expectedSelectStatement = 'SELECT * FROM "todo_items";'; + final expectedDeleteStatement = 'DELETE FROM "todo_items";'; + + late Fixture fixture; + + setUp(() async { + fixture = Fixture(); + await Sentry.init( + (options) {}, + options: fixture.options, + ); + }); + + group('open operations', () { + test('successful adds span only once', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await _insertRow(db); + await _insertRow(db); + await _insertRow(db); + + final openSpans = tx.children.where( + (element) => + element.context.description == + SentrySpanDescriptions.dbOpen(dbName: Fixture.dbName), + ); + + expect(openSpans.length, 1); + _verifySpan( + operation: SentrySpanOperations.dbOpen, + SentrySpanDescriptions.dbOpen(dbName: Fixture.dbName), + openSpans.first, + ); + }); + + test('error case adds error span', () async { + final exception = Exception('test'); + final queryExecutor = MockQueryExecutor(); + when(queryExecutor.ensureOpen(any)).thenThrow(exception); + when(queryExecutor.runInsert(any, any)) + .thenAnswer((_) => Future.value(1)); + when(queryExecutor.dialect).thenReturn(SqlDialect.sqlite); + + final sut = fixture.getSut(); + final db = AppDatabase(queryExecutor.interceptWith(sut)); + + final tx = _startTransaction(); + try { + await _insertRow(db); + } catch (e) { + // making sure the thrown exception doesn't fail the test + } + + final openSpans = tx.children.where( + (element) => + element.context.description == + SentrySpanDescriptions.dbOpen(dbName: Fixture.dbName), + ); + + expect(openSpans.length, 1); + _verifyErrorSpan( + operation: SentrySpanOperations.dbOpen, + SentrySpanDescriptions.dbOpen(dbName: Fixture.dbName), + exception, + openSpans.first, + ); + }); + }); + + group('close operations', () { + test('successful adds close only once', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await _insertRow(db); + await db.close(); + + final closeSpans = tx.children.where( + (element) => + element.context.description == + SentrySpanDescriptions.dbClose(dbName: Fixture.dbName), + ); + + expect(closeSpans.length, 1); + _verifySpan( + operation: SentrySpanOperations.dbClose, + SentrySpanDescriptions.dbClose(dbName: Fixture.dbName), + closeSpans.first, + ); + }); + + test('error case adds error span', () async { + final exception = Exception('test'); + final queryExecutor = MockQueryExecutor(); + when(queryExecutor.ensureOpen(any)).thenAnswer((_) => Future.value(true)); + when(queryExecutor.runInsert(any, any)) + .thenAnswer((_) => Future.value(1)); + when(queryExecutor.close()).thenThrow(exception); + when(queryExecutor.dialect).thenReturn(SqlDialect.sqlite); + + final sut = fixture.getSut(); + final db = AppDatabase(queryExecutor.interceptWith(sut)); + + final tx = _startTransaction(); + try { + await _insertRow(db); + await db.close(); + } catch (e) { + // making sure the thrown exception doesn't fail the test + } + + final closeSpans = tx.children.where( + (element) => + element.context.description == + SentrySpanDescriptions.dbClose(dbName: Fixture.dbName), + ); + + expect(closeSpans.length, 1); + _verifyErrorSpan( + SentrySpanDescriptions.dbClose(dbName: Fixture.dbName), + exception, + closeSpans.first, + operation: SentrySpanOperations.dbClose, + ); + }); + }); + + group('insert operations', () { + test('successful adds span', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await _insertRow(db); + + _verifySpan( + expectedInsertStatement, + tx.children.last, + ); + }); + + test('error case adds error span', () async { + final exception = Exception('test'); + final queryExecutor = MockQueryExecutor(); + when(queryExecutor.ensureOpen(any)).thenAnswer((_) => Future.value(true)); + when(queryExecutor.runInsert(any, any)).thenThrow(exception); + when(queryExecutor.dialect).thenReturn(SqlDialect.sqlite); + + final sut = fixture.getSut(); + final db = AppDatabase(queryExecutor.interceptWith(sut)); + + final tx = _startTransaction(); + try { + await _insertRow(db); + } catch (e) { + // making sure the thrown exception doesn't fail the test + } + + _verifyErrorSpan( + expectedInsertStatement, + exception, + tx.children.last, + ); + }); + }); + + group('update operations', () { + test('successful adds span', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await _insertRow(db); + await _updateRow(db); + + _verifySpan( + expectedUpdateStatement, + tx.children.last, + ); + }); + + test('error case adds error span', () async { + final exception = Exception('test'); + final queryExecutor = MockQueryExecutor(); + when(queryExecutor.ensureOpen(any)).thenAnswer((_) => Future.value(true)); + when(queryExecutor.runInsert(any, any)) + .thenAnswer((_) => Future.value(1)); + when(queryExecutor.runUpdate(any, any)).thenThrow(exception); + when(queryExecutor.dialect).thenReturn(SqlDialect.sqlite); + + final sut = fixture.getSut(); + final db = AppDatabase(queryExecutor.interceptWith(sut)); + + final tx = _startTransaction(); + try { + await _insertRow(db); + await _updateRow(db); + } catch (e) { + // making sure the thrown exception doesn't fail the test + } + + _verifyErrorSpan( + expectedUpdateStatement, + exception, + tx.children.last, + ); + }); + }); + + group('delete operations', () { + test('successful adds span', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await _insertRow(db); + await db.delete(db.todoItems).go(); + + _verifySpan( + expectedDeleteStatement, + tx.children.last, + ); + }); + + test('error case adds error span', () async { + final exception = Exception('test'); + final queryExecutor = MockQueryExecutor(); + when(queryExecutor.ensureOpen(any)).thenAnswer((_) => Future.value(true)); + when(queryExecutor.runInsert(any, any)) + .thenAnswer((_) => Future.value(1)); + when(queryExecutor.runDelete(any, any)).thenThrow(exception); + when(queryExecutor.dialect).thenReturn(SqlDialect.sqlite); + + final sut = fixture.getSut(); + final db = AppDatabase(queryExecutor.interceptWith(sut)); + + final tx = _startTransaction(); + try { + await _insertRow(db); + await db.delete(db.todoItems).go(); + } catch (e) { + // making sure the thrown exception doesn't fail the test + } + + _verifyErrorSpan( + expectedDeleteStatement, + exception, + tx.children.last, + ); + }); + }); + + group('select operations', () { + test('successful adds span', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await _insertRow(db); + await db.select(db.todoItems).get(); + + _verifySpan( + expectedSelectStatement, + tx.children.last, + ); + }); + + test('error case adds error span', () async { + final exception = Exception('test'); + final queryExecutor = MockQueryExecutor(); + when(queryExecutor.ensureOpen(any)).thenAnswer((_) => Future.value(true)); + when(queryExecutor.runInsert(any, any)) + .thenAnswer((_) => Future.value(1)); + when(queryExecutor.runSelect(any, any)).thenThrow(exception); + when(queryExecutor.dialect).thenReturn(SqlDialect.sqlite); + + final sut = fixture.getSut(); + final db = AppDatabase(queryExecutor.interceptWith(sut)); + + final tx = _startTransaction(); + try { + await _insertRow(db); + await db.select(db.todoItems).get(); + } catch (e) { + // making sure the thrown exception doesn't fail the test + } + + _verifyErrorSpan( + expectedSelectStatement, + exception, + tx.children.last, + ); + }); + }); + + group('custom query operations', () { + test('successful adds span', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await db.customStatement(expectedSelectStatement); + + _verifySpan( + expectedSelectStatement, + tx.children.last, + ); + }); + + test('error case adds error span', () async { + final exception = Exception('test'); + final queryExecutor = MockQueryExecutor(); + when(queryExecutor.ensureOpen(any)).thenAnswer((_) => Future.value(true)); + when(queryExecutor.runCustom(any, any)).thenThrow(exception); + when(queryExecutor.dialect).thenReturn(SqlDialect.sqlite); + + final sut = fixture.getSut(); + final db = AppDatabase(queryExecutor.interceptWith(sut)); + + final tx = _startTransaction(); + try { + await db.customStatement(expectedSelectStatement); + } catch (e) { + // making sure the thrown exception doesn't fail the test + } + + _verifyErrorSpan( + expectedSelectStatement, + exception, + tx.children.last, + ); + }); + }); + + group('transaction operations', () { + test('without transaction, spans are added to active scope span', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await _insertRow(db); + + expect(tx.children.length, 2); + + final insertSpan = tx.children.last; + expect(insertSpan.context.parentSpanId, tx.context.spanId); + expect(sut.spanHelper.transactionStack, isEmpty); + }); + + // already tests nesting + test('commit successful adds spans', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await db.transaction(() async { + await db.into(db.todoItems).insert( + TodoItemsCompanion.insert( + title: 'first transaction insert', + content: 'test', + ), + ); + await db.transaction(() async { + await db.delete(db.todoItems).go(); + }); + }); + + // 5 spans = 1 db open + 2 tx + 1 insert + 1 delete + expect(tx.children.length, 5); + + final outerTxSpan = tx.children[1]; + final insertSpan = tx.children[2]; + final innerTxSpan = tx.children[3]; + final deleteSpan = tx.children[4]; + + // Verify parent relationships + expect(outerTxSpan.context.parentSpanId, tx.context.spanId); + expect(insertSpan.context.parentSpanId, outerTxSpan.context.spanId); + expect(innerTxSpan.context.parentSpanId, outerTxSpan.context.spanId); + expect(deleteSpan.context.parentSpanId, innerTxSpan.context.spanId); + }); + + test('successful commit adds insert spans', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await db.transaction(() async { + await _insertRow(db); + await _insertRow(db); + }); + + final insertSpanCount = tx.children + .where( + (element) => element.context.description == expectedInsertStatement, + ) + .length; + expect(insertSpanCount, 2); + + _verifySpan( + expectedInsertStatement, + tx.children.last, + ); + + _verifySpan( + SentrySpanDescriptions.dbTransaction, + tx.children[1], + operation: SentrySpanOperations.dbSqlTransaction, + ); + }); + + test('successful commit adds update spans', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await db.transaction(() async { + await _insertRow(db); + await _updateRow(db); + }); + + final insertSpanCount = tx.children + .where( + (element) => element.context.description == expectedInsertStatement, + ) + .length; + expect(insertSpanCount, 1); + + final updateSpanCount = tx.children + .where( + (element) => element.context.description == expectedInsertStatement, + ) + .length; + expect(updateSpanCount, 1); + + _verifySpan( + expectedUpdateStatement, + tx.children.last, + ); + + _verifySpan( + SentrySpanDescriptions.dbTransaction, + tx.children[1], + operation: SentrySpanOperations.dbSqlTransaction, + ); + }); + + test('successful commit adds delete spans', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await db.transaction(() async { + await _insertRow(db); + await db.delete(db.todoItems).go(); + }); + + final insertSpanCount = tx.children + .where( + (element) => element.context.description == expectedInsertStatement, + ) + .length; + expect(insertSpanCount, 1); + + final deleteSpanCount = tx.children + .where( + (element) => element.context.description == expectedDeleteStatement, + ) + .length; + expect(deleteSpanCount, 1); + + _verifySpan( + expectedDeleteStatement, + tx.children.last, + ); + + _verifySpan( + SentrySpanDescriptions.dbTransaction, + tx.children[1], + operation: SentrySpanOperations.dbSqlTransaction, + ); + }); + + test('successful commit adds custom query spans', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await db.transaction(() async { + await db.customStatement(expectedSelectStatement); + }); + + final customSpanCount = tx.children + .where( + (element) => element.context.description == expectedSelectStatement, + ) + .length; + expect(customSpanCount, 1); + + _verifySpan( + expectedSelectStatement, + tx.children.last, + ); + + _verifySpan( + SentrySpanDescriptions.dbTransaction, + tx.children[1], + operation: SentrySpanOperations.dbSqlTransaction, + ); + }); + + test('successful commit adds batch spans', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await db.transaction(() async { + await _insertIntoBatch(db); + }); + + _verifySpan( + SentrySpanDescriptions.dbBatch(statements: [expectedInsertStatement]), + tx.children.last, + operation: SentrySpanOperations.dbSqlBatch, + ); + }); + + test('batch creates transaction span', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await _insertIntoBatch(db); + + _verifySpan( + SentrySpanDescriptions.dbTransaction, + tx.children[1], + operation: SentrySpanOperations.dbSqlTransaction, + ); + + _verifySpan( + SentrySpanDescriptions.dbBatch(statements: [expectedInsertStatement]), + tx.children.last, + operation: SentrySpanOperations.dbSqlBatch, + ); + }); + + test('rollback case adds aborted span', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + await _insertRow(db); + await _insertRow(db); + + try { + await db.transaction(() async { + await _insertRow(db, withError: true); + }); + } catch (_) {} + + final spans = + tx.children.where((child) => child.status == SpanStatus.aborted()); + expect(spans.length, 1); + final abortedSpan = spans.first; + + expect(sut.spanHelper.transactionStack, isEmpty); + _verifySpan( + SentrySpanDescriptions.dbTransaction, + abortedSpan, + status: SpanStatus.aborted(), + operation: SentrySpanOperations.dbSqlTransaction, + ); + }); + + test('batch does not add span for failed operations', () async { + final sut = fixture.getSut(); + final db = AppDatabase(NativeDatabase.memory().interceptWith(sut)); + + final tx = _startTransaction(); + try { + await db.batch((batch) async { + await _insertRow(db, withError: true); + await _insertRow(db); + }); + } catch (_) {} + + expect(tx.children.isEmpty, true); + }); + + test('error case adds error span', () async { + final exception = Exception('test'); + final queryExecutor = MockQueryExecutor(); + when(queryExecutor.ensureOpen(any)).thenAnswer((_) => Future.value(true)); + when(queryExecutor.beginTransaction()).thenThrow(exception); + when(queryExecutor.dialect).thenReturn(SqlDialect.sqlite); + + final sut = fixture.getSut(); + final db = AppDatabase(queryExecutor.interceptWith(sut)); + + final tx = _startTransaction(); + try { + await db.transaction(() async { + await _insertRow(db); + }); + } catch (e) { + // making sure the thrown exception doesn't fail the test + } + + // when beginTransaction errored, we don't add it to the stack + expect(sut.spanHelper.transactionStack, isEmpty); + _verifyErrorSpan( + operation: SentrySpanOperations.dbSqlTransaction, + SentrySpanDescriptions.dbTransaction, + exception, + tx.children.last, + ); + }); + }); + + group('integrations', () { + setUp(() async { + // init the interceptor so the integrations are added + fixture.getSut(); + }); + + test('adds integration', () { + expect( + fixture.options.sdk.integrations + .contains(drift_constants.integrationName), + true, + ); + }); + + test('adds package', () { + expect( + fixture.options.sdk.packages.any( + (element) => + element.name == packageName && element.version == sdkVersion, + ), + true, + ); + }); + }); +} + +class Fixture { + static final dbName = 'test_db_name'; + final options = defaultTestOptions()..tracesSampleRate = 1.0; + + Future sentryInit() { + return Sentry.init( + (options) {}, + options: options, + ); + } + + SentryQueryInterceptor getSut() { + driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; + return SentryQueryInterceptor(databaseName: dbName); + } +} + +void _verifySpan( + String description, + SentrySpan? span, { + String? operation, + SpanStatus? status, +}) { + status ??= SpanStatus.ok(); + expect( + span?.context.operation, + operation ?? SentrySpanOperations.dbSqlQuery, + ); + expect(span?.context.description, description); + expect(span?.status, status); + expect(span?.origin, SentryTraceOrigins.autoDbDriftQueryInterceptor); + expect( + span?.data[SentrySpanData.dbSystemKey], + SentrySpanData.dbSystemSqlite, + ); + expect( + span?.data[SentrySpanData.dbNameKey], + Fixture.dbName, + ); +} + +void _verifyErrorSpan( + String description, + Exception exception, + SentrySpan? span, { + String? operation, + SpanStatus? status, +}) { + expect( + span?.context.operation, + operation ?? SentrySpanOperations.dbSqlQuery, + ); + expect(span?.context.description, description); + expect(span?.status, status ?? SpanStatus.internalError()); + expect(span?.origin, SentryTraceOrigins.autoDbDriftQueryInterceptor); + expect( + span?.data[SentrySpanData.dbSystemKey], + SentrySpanData.dbSystemSqlite, + ); + expect( + span?.data[SentrySpanData.dbNameKey], + Fixture.dbName, + ); + + expect(span?.throwable, exception); +} + +Future _insertRow(AppDatabase db, {bool withError = false}) { + if (withError) { + return db.into(db.todoItems).insert( + TodoItemsCompanion.insert( + title: '', + content: '', + ), + ); + } else { + return db.into(db.todoItems).insert( + TodoItemsCompanion.insert( + title: 'todo: finish drift setup', + content: 'We can now write queries and define our own tables.', + ), + ); + } +} + +Future _insertIntoBatch(AppDatabase sut) { + return sut.batch((batch) { + batch.insertAll(sut.todoItems, [ + TodoItemsCompanion.insert( + title: 'todo: finish drift setup #1', + content: 'We can now write queries and define our own tables.', + ), + TodoItemsCompanion.insert( + title: 'todo: finish drift setup #2', + content: 'We can now write queries and define our own tables.', + ), + ]); + }); +} + +Future _updateRow(AppDatabase sut, {bool withError = false}) { + if (withError) { + return (sut.update(sut.todoItems) + ..where((tbl) => tbl.title.equals('doesnt exist'))) + .write( + TodoItemsCompanion( + title: Value('after update'), + content: Value('We can now write queries and define our own tables.'), + ), + ); + } else { + return (sut.update(sut.todoItems) + ..where((tbl) => tbl.title.equals('todo: finish drift setup'))) + .write( + TodoItemsCompanion( + title: Value('after update'), + content: Value('We can now write queries and define our own tables.'), + ), + ); + } +} + +SentryTracer _startTransaction() { + return Sentry.startTransaction('drift', 'test op', bindToScope: true) + as SentryTracer; +} diff --git a/drift/test/test_database.g.dart b/drift/test/test_database.g.dart index 65430026da..45207ed14f 100644 --- a/drift/test/test_database.g.dart +++ b/drift/test/test_database.g.dart @@ -160,6 +160,15 @@ class TodoItem extends DataClass implements Insertable { content: content ?? this.content, category: category.present ? category.value : this.category, ); + TodoItem copyWithCompanion(TodoItemsCompanion data) { + return TodoItem( + id: data.id.present ? data.id.value : this.id, + title: data.title.present ? data.title.value : this.title, + content: data.content.present ? data.content.value : this.content, + category: data.category.present ? data.category.value : this.category, + ); + } + @override String toString() { return (StringBuffer('TodoItem(') @@ -260,6 +269,7 @@ class TodoItemsCompanion extends UpdateCompanion { abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); + $AppDatabaseManager get managers => $AppDatabaseManager(this); late final $TodoItemsTable todoItems = $TodoItemsTable(this); @override Iterable> get allTables => @@ -267,3 +277,155 @@ abstract class _$AppDatabase extends GeneratedDatabase { @override List get allSchemaEntities => [todoItems]; } + +typedef $$TodoItemsTableCreateCompanionBuilder = TodoItemsCompanion Function({ + Value id, + required String title, + required String content, + Value category, +}); +typedef $$TodoItemsTableUpdateCompanionBuilder = TodoItemsCompanion Function({ + Value id, + Value title, + Value content, + Value category, +}); + +class $$TodoItemsTableFilterComposer + extends Composer<_$AppDatabase, $TodoItemsTable> { + $$TodoItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get title => $composableBuilder( + column: $table.title, builder: (column) => ColumnFilters(column)); + + ColumnFilters get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnFilters(column)); + + ColumnFilters get category => $composableBuilder( + column: $table.category, builder: (column) => ColumnFilters(column)); +} + +class $$TodoItemsTableOrderingComposer + extends Composer<_$AppDatabase, $TodoItemsTable> { + $$TodoItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get title => $composableBuilder( + column: $table.title, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get content => $composableBuilder( + column: $table.content, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get category => $composableBuilder( + column: $table.category, builder: (column) => ColumnOrderings(column)); +} + +class $$TodoItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $TodoItemsTable> { + $$TodoItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get title => + $composableBuilder(column: $table.title, builder: (column) => column); + + GeneratedColumn get content => + $composableBuilder(column: $table.content, builder: (column) => column); + + GeneratedColumn get category => + $composableBuilder(column: $table.category, builder: (column) => column); +} + +class $$TodoItemsTableTableManager extends RootTableManager< + _$AppDatabase, + $TodoItemsTable, + TodoItem, + $$TodoItemsTableFilterComposer, + $$TodoItemsTableOrderingComposer, + $$TodoItemsTableAnnotationComposer, + $$TodoItemsTableCreateCompanionBuilder, + $$TodoItemsTableUpdateCompanionBuilder, + (TodoItem, BaseReferences<_$AppDatabase, $TodoItemsTable, TodoItem>), + TodoItem, + PrefetchHooks Function()> { + $$TodoItemsTableTableManager(_$AppDatabase db, $TodoItemsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$TodoItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$TodoItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$TodoItemsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value id = const Value.absent(), + Value title = const Value.absent(), + Value content = const Value.absent(), + Value category = const Value.absent(), + }) => + TodoItemsCompanion( + id: id, + title: title, + content: content, + category: category, + ), + createCompanionCallback: ({ + Value id = const Value.absent(), + required String title, + required String content, + Value category = const Value.absent(), + }) => + TodoItemsCompanion.insert( + id: id, + title: title, + content: content, + category: category, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$TodoItemsTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $TodoItemsTable, + TodoItem, + $$TodoItemsTableFilterComposer, + $$TodoItemsTableOrderingComposer, + $$TodoItemsTableAnnotationComposer, + $$TodoItemsTableCreateCompanionBuilder, + $$TodoItemsTableUpdateCompanionBuilder, + (TodoItem, BaseReferences<_$AppDatabase, $TodoItemsTable, TodoItem>), + TodoItem, + PrefetchHooks Function()>; + +class $AppDatabaseManager { + final _$AppDatabase _db; + $AppDatabaseManager(this._db); + $$TodoItemsTableTableManager get todoItems => + $$TodoItemsTableTableManager(_db, _db.todoItems); +} diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index a9496c1068..4d26747920 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:dio/dio.dart'; +import 'package:drift/drift.dart' show ApplyInterceptor; import 'package:feedback/feedback.dart' as feedback; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -654,10 +655,8 @@ class MainScaffold extends StatelessWidget { bindToScope: true, ); - final executor = SentryQueryExecutor( - () async => inMemoryExecutor(), - databaseName: 'sentry_in_memory_db', - ); + final executor = inMemoryExecutor().interceptWith( + SentryQueryInterceptor(databaseName: 'sentry_in_memory_db')); final db = AppDatabase(executor);