Integrate db viewer into devtools

This commit is contained in:
Simon Binder 2023-10-17 15:28:07 +02:00
parent 90db860f03
commit 026fae935c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
16 changed files with 246 additions and 93 deletions

1
.gitignore vendored
View File

@ -18,4 +18,5 @@ flutter_export_environment.sh
docs/**/*.g.dart
*/build/
drift/extension/devtools/build
**/pubspec_overrides.yaml

View File

@ -0,0 +1 @@
!./build

View File

@ -2,3 +2,7 @@ name: drift
issue_tracker: https://github.com/simolus3/drift/issues
version: 0.0.1
material_icon_code_point: '0xf41e'
# ??? https://github.com/flutter/devtools/issues/6539
issueTracker: https://github.com/simolus3/drift/issues
materialIconCodePoint: "0xf41e"

View File

@ -143,6 +143,7 @@ abstract class GeneratedDatabase extends DatabaseConnectionUser
@override
Future<void> close() async {
await super.close();
devtools.handleClosed(this);
assert(() {
if (_openedDbCount[runtimeType] != null) {

View File

@ -50,6 +50,14 @@ void handleCreated(GeneratedDatabase database) {
}
}
void handleClosed(GeneratedDatabase database) {
if (_enable) {
final tracked = TrackedDatabase.byDatabase[database];
TrackedDatabase.all.remove(tracked);
_postChangedEvent();
}
}
String describe(GeneratedDatabase database) {
return json.encode(DatabaseDescription.fromDrift(database));
}

View File

@ -34,7 +34,7 @@ class DriftServiceExtension {
});
});
return id.toString();
return id;
case 'unsubscribe-from-tables':
_activeSubscriptions.remove(int.parse(parameters['id']!))?.cancel();
return null;

View File

@ -64,6 +64,11 @@ class EntityDescription {
final String type;
final List<ColumnDescription>? columns;
late Map<String, ColumnDescription> columnsByName = {
for (final column in columns ?? const <ColumnDescription>[])
column.name: column,
};
EntityDescription(
{required this.name, required this.type, required this.columns});
@ -99,6 +104,10 @@ class DatabaseDescription {
final bool dateTimeAsText;
final List<EntityDescription> entities;
late Map<String, EntityDescription> entitiesByName = {
for (final entity in entities) entity.name: entity,
};
DatabaseDescription({required this.dateTimeAsText, required this.entities});
factory DatabaseDescription.fromDrift(GeneratedDatabase database) {

View File

@ -0,0 +1,2 @@
extensions:
- drift: true

View File

@ -0,0 +1,3 @@
dart run devtools_extensions build_and_copy \
--source=. \
--dest=../../drift/extension/devtools

View File

@ -59,7 +59,7 @@ class DriftDevtoolsBody extends ConsumerWidget {
roundedTopBorder: false,
includeTopBorder: false,
title: selected != null
? Text(selected.typeName)
? Text('Inspecting ${selected.typeName}')
: const Text('No database selected'),
),
if (selected != null) const Expanded(child: DatabaseDetails())

View File

@ -0,0 +1,154 @@
import 'package:db_viewer/db_viewer.dart';
import 'package:drift/drift.dart';
// ignore: invalid_use_of_internal_member, implementation_imports
import 'package:drift/src/runtime/devtools/shared.dart';
import 'package:flutter/widgets.dart';
import 'package:rxdart/streams.dart';
import '../remote_database.dart';
class ViewerDatabase implements DbViewerDatabase {
final RemoteDatabase database;
final Map<String, FilterData> _cachedFilters = {};
ViewerDatabase({required this.database});
@override
Widget buildWhereWidget(
{required VoidCallback onAddClicked,
required List<WhereClause> whereClauses}) {
return Container();
}
@override
Stream<int> count(String entityName) {
return customSelectStream('SELECT COUNT(*) AS r FROM "$entityName"')
.map((rows) => rows.first['r'] as int);
}
@override
Future<List<Map<String, dynamic>>> customSelect(String query,
{Set<String>? fromEntityNames}) {
return database.select(query, const []);
}
@override
Stream<List<Map<String, dynamic>>> customSelectStream(String query,
{Set<String>? fromEntityNames}) {
fromEntityNames ??= const {};
final updates = database.tableUpdates
.where(
(e) => e.any((updated) => fromEntityNames!.contains(updated.table)))
.asyncMap((event) => customSelect(query));
return ConcatStream([
Stream.fromFuture(customSelect(query)),
updates,
]);
}
@override
List<String> get entityNames => [
for (final entity in database.description.entities)
if (entity.type == 'table') entity.name,
];
@override
FilterData getCachedFilterData(String entityName) {
return _cachedFilters.putIfAbsent(
entityName, () => getFilterData(entityName));
}
@override
List<String> getColumnNamesByEntityName(String entityName) {
return database.description.entitiesByName[entityName]!.columnsByName.keys
.toList();
}
@override
FilterData getFilterData(String entityName) {
return DriftFilterData(
entity: database.description.entitiesByName[entityName]!);
}
@override
String getType(String entityName, String columnName) {
final type = database.description.entitiesByName[entityName]!
.columnsByName[columnName]!.type!;
final genContext = GenerationContext(
DriftDatabaseOptions(
storeDateTimeAsText: database.description.dateTimeAsText),
null,
);
return type.type?.sqlTypeName(genContext) ?? type.customTypeName!;
}
@override
List<Map<String, dynamic>> remapData(
String entityName, List<Map<String, dynamic>> data) {
// ignore: invalid_use_of_internal_member
final types = SqlTypes(database.description.dateTimeAsText);
final mapped = <Map<String, dynamic>>[];
final entity = database.description.entitiesByName[entityName]!;
for (final row in data) {
final mappedRow = <String, dynamic>{};
for (var MapEntry(key: column, :value) in row.entries) {
final resolvedColumn = entity.columnsByName[column];
if (resolvedColumn != null) {
final type = resolvedColumn.type?.type ?? DriftSqlType.any;
mappedRow[column] = types.read(type, value);
} else {
mappedRow[column] = value;
}
}
mapped.add(mappedRow);
}
return mapped;
}
@override
Future<void> runCustomStatement(String query) {
return database.execute(query, const []);
}
@override
void updateFilterData(String entityName, FilterData filterData) {
_cachedFilters[entityName] = filterData;
}
}
class DriftFilterData extends FilterData {
final EntityDescription entity;
DriftFilterData({required this.entity});
@override
DriftFilterData copy() {
return DriftFilterData(entity: entity);
}
@override
Map<String, bool> getSelectedColumns() {
return {
for (final column in entity.columns ?? const <ColumnDescription>[])
column.name: false,
};
}
@override
WhereClause? getWhereClause(String columnName) {
return null;
}
@override
String get tableName => entity.name;
}

View File

@ -0,0 +1,30 @@
import 'package:db_viewer/db_viewer.dart';
import 'package:flutter/material.dart';
import '../remote_database.dart';
import 'database.dart';
class DatabaseViewer extends StatefulWidget {
final RemoteDatabase database;
const DatabaseViewer({super.key, required this.database});
@override
State<DatabaseViewer> createState() => _DatabaseViewerState();
}
class _DatabaseViewerState extends State<DatabaseViewer> {
@override
void initState() {
DbViewerDatabase.initDb(ViewerDatabase(database: widget.database));
super.initState();
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 800),
child: const DbViewerNavigator(),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:devtools_app_shared/service.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'db_viewer/viewer.dart';
import 'list.dart';
import 'remote_database.dart';
import 'service.dart';
@ -20,16 +21,6 @@ final loadedDatabase = AutoDisposeFutureProvider((ref) async {
return null;
});
final _testQuery = AutoDisposeFutureProvider((ref) async {
final database = await ref.watch(loadedDatabase.future);
if (database != null) {
return await database.select('SELECT 1, 2, 3', []);
} else {
return null;
}
});
class DatabaseDetails extends ConsumerStatefulWidget {
const DatabaseDetails({super.key});
@ -51,21 +42,28 @@ class _DatabaseDetailsState extends ConsumerState<DatabaseDetails> {
@override
Widget build(BuildContext context) {
final database = ref.watch(loadedDatabase);
final query = ref.watch(_testQuery);
return database.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, stack) => Text('unknown error: $err\n$stack'),
data: (database) {
if (database != null) {
final textTheme = Theme.of(context).textTheme;
return Scrollbar(
controller: controller,
child: ListView(
controller: controller,
children: [
for (final entity in database.description.entities)
Text('${entity.name}: ${entity.type}'),
Text(query.toString()),
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text('Database viewer', style: textTheme.headlineMedium),
],
),
),
DatabaseViewer(database: database),
],
),
);

View File

@ -155,7 +155,9 @@ class _DatabaseEntry extends ConsumerWidget {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {},
onTap: () {
ref.read(selectedDatabase.notifier).state = database;
},
child: Container(
color: isSelected ? colorScheme.selectedRowBackgroundColor : null,
padding: _DatabaseListState._tilePadding,

View File

@ -62,12 +62,13 @@ class RemoteDatabase {
Future<List<Map<String, Object?>>> select(
String sql, List<Object?> args) async {
final result = await _driftRequest('execute-query', payload: {
'query': json.encode(_protocol
.encodePayload(ExecuteQuery(StatementMethod.select, sql, args)))
});
final result = await _executeQuery<SelectResult>(
ExecuteQuery(StatementMethod.select, sql, args));
return result.rows;
}
return (_protocol.decodePayload(result) as SelectResult).rows;
Future<void> execute(String sql, List<Object?> args) async {
await _executeQuery<void>(ExecuteQuery(StatementMethod.custom, sql, args));
}
Future<int> _newTableSubscription() async {
@ -82,6 +83,14 @@ class RemoteDatabase {
_tableNotifications = null;
}
Future<T> _executeQuery<T>(ExecuteQuery e) async {
final result = await _driftRequest('execute-query', payload: {
'query': json.encode(_protocol.encodePayload(e)),
});
return _protocol.decodePayload(result) as T;
}
Future<Object?> _driftRequest(String method,
{Map<String, String> payload = const {}}) async {
final response = await serviceManager.callServiceExtensionOnMainIsolate(

View File

@ -1,40 +1,16 @@
name: drift_devtools_extension
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
description: A Flutter web app contribution drift tools to DevTools
publish_to: 'none'
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=3.1.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
devtools_extensions: ^0.0.8
devtools_app_shared: ^0.0.5
db_viewer: ^1.0.3
@ -48,52 +24,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/assets-and-images/#from-packages
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/custom-fonts/#from-packages