mirror of https://github.com/AMT-Cheif/drift.git
Integrate db viewer into devtools
This commit is contained in:
parent
90db860f03
commit
026fae935c
|
@ -18,4 +18,5 @@ flutter_export_environment.sh
|
|||
docs/**/*.g.dart
|
||||
|
||||
*/build/
|
||||
drift/extension/devtools/build
|
||||
**/pubspec_overrides.yaml
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
!./build
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -34,7 +34,7 @@ class DriftServiceExtension {
|
|||
});
|
||||
});
|
||||
|
||||
return id.toString();
|
||||
return id;
|
||||
case 'unsubscribe-from-tables':
|
||||
_activeSubscriptions.remove(int.parse(parameters['id']!))?.cancel();
|
||||
return null;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
extensions:
|
||||
- drift: true
|
|
@ -0,0 +1,3 @@
|
|||
dart run devtools_extensions build_and_copy \
|
||||
--source=. \
|
||||
--dest=../../drift/extension/devtools
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue