mirror of https://github.com/AMT-Cheif/drift.git
Validate schema in DevTools extension
This commit is contained in:
parent
f9012fc05c
commit
a9379a85b1
|
@ -606,3 +606,15 @@ extension on TransactionExecutor {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Exposes the private `_runConnectionZoned` method for other parts of drift.
|
||||
///
|
||||
/// This is only used by the DevTools extension.
|
||||
@internal
|
||||
extension RunWithEngine on DatabaseConnectionUser {
|
||||
/// Call the private [_runConnectionZoned] method.
|
||||
Future<T> runConnectionZoned<T>(
|
||||
DatabaseConnectionUser user, Future<T> Function() calculation) {
|
||||
return _runConnectionZoned(user, calculation);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ import 'dart:async';
|
|||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/src/remote/protocol.dart';
|
||||
import 'package:drift/src/runtime/executor/transactions.dart';
|
||||
|
||||
import '../query_builder/query_builder.dart';
|
||||
import 'devtools.dart';
|
||||
|
||||
/// A service extension making asynchronous requests on drift databases
|
||||
|
@ -26,7 +27,7 @@ class DriftServiceExtension {
|
|||
final stream = tracked.database.tableUpdates();
|
||||
final id = _subscriptionId++;
|
||||
|
||||
stream.listen((event) {
|
||||
_activeSubscriptions[id] = stream.listen((event) {
|
||||
postEvent('event', {
|
||||
'subscription': id,
|
||||
'payload':
|
||||
|
@ -60,6 +61,16 @@ class DriftServiceExtension {
|
|||
};
|
||||
|
||||
return _protocol.encodePayload(result);
|
||||
case 'collect-expected-schema':
|
||||
final executor = _CollectCreateStatements();
|
||||
await tracked.database.runConnectionZoned(
|
||||
BeforeOpenRunner(tracked.database, executor), () async {
|
||||
// ignore: invalid_use_of_protected_member, invalid_use_of_visible_for_testing_member
|
||||
final migrator = tracked.database.createMigrator();
|
||||
await migrator.createAll();
|
||||
});
|
||||
|
||||
return executor.statements;
|
||||
default:
|
||||
throw UnsupportedError('Method $action');
|
||||
}
|
||||
|
@ -96,3 +107,52 @@ class DriftServiceExtension {
|
|||
|
||||
static const _protocol = DriftProtocol();
|
||||
}
|
||||
|
||||
class _CollectCreateStatements extends QueryExecutor {
|
||||
final List<String> statements = [];
|
||||
|
||||
@override
|
||||
TransactionExecutor beginTransaction() {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
SqlDialect get dialect => SqlDialect.sqlite;
|
||||
|
||||
@override
|
||||
Future<bool> ensureOpen(QueryExecutorUser user) {
|
||||
return Future.value(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runBatched(BatchedStatements statements) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> runCustom(String statement, [List<Object?>? args]) {
|
||||
statements.add(statement);
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runDelete(String statement, List<Object?> args) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runInsert(String statement, List<Object?> args) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Map<String, Object?>>> runSelect(
|
||||
String statement, List<Object?> args) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<int> runUpdate(String statement, List<Object?> args) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@ class EntityDescription {
|
|||
return EntityDescription(
|
||||
name: entity.entityName,
|
||||
type: switch (entity) {
|
||||
VirtualTableInfo() => 'virtual_table',
|
||||
TableInfo() => 'table',
|
||||
ViewInfo() => 'view',
|
||||
Index() => 'index',
|
||||
|
|
|
@ -37,3 +37,4 @@ dev_dependencies:
|
|||
shelf: ^1.3.0
|
||||
stack_trace: ^1.10.0
|
||||
test_descriptor: ^2.0.1
|
||||
vm_service: ^13.0.0
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:drift/native.dart';
|
||||
|
||||
import '../../generated/todos.dart';
|
||||
|
||||
void main() {
|
||||
TodoDb(NativeDatabase.memory());
|
||||
print('database created');
|
||||
|
||||
// Keep the process alive
|
||||
Stream<void>.periodic(const Duration(seconds: 10)).listen(null);
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:vm_service/vm_service_io.dart';
|
||||
|
||||
void main() {
|
||||
late Process child;
|
||||
late VmService vm;
|
||||
late String isolateId;
|
||||
|
||||
setUpAll(() async {
|
||||
final socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
|
||||
final port = socket.port;
|
||||
await socket.close();
|
||||
|
||||
String sdk = p.dirname(p.dirname(Platform.resolvedExecutable));
|
||||
child = await Process.start(p.join(sdk, 'bin', 'dart'), [
|
||||
'run',
|
||||
'--enable-vm-service=$port',
|
||||
'--disable-service-auth-codes',
|
||||
'--enable-asserts',
|
||||
'test/integration_tests/devtools/app.dart',
|
||||
]);
|
||||
|
||||
final vmServiceListening = Completer<void>();
|
||||
final databaseOpened = Completer<void>();
|
||||
|
||||
child.stdout
|
||||
.map(utf8.decode)
|
||||
.transform(const LineSplitter())
|
||||
.listen((line) {
|
||||
if (line.startsWith('The Dart VM service is listening')) {
|
||||
vmServiceListening.complete();
|
||||
} else if (line == 'database created') {
|
||||
databaseOpened.complete();
|
||||
} else if (!line.startsWith('The Dart DevTools')) {
|
||||
print('[child]: $line');
|
||||
}
|
||||
});
|
||||
|
||||
await vmServiceListening.future;
|
||||
vm = await vmServiceConnectUri('ws://localhost:$port/ws');
|
||||
|
||||
final state = await vm.getVM();
|
||||
isolateId = state.isolates!.single.id!;
|
||||
|
||||
await databaseOpened.future;
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
child.kill();
|
||||
});
|
||||
|
||||
test('can list create statements', () async {
|
||||
final response = await vm.callServiceExtension(
|
||||
'ext.drift.database',
|
||||
args: {'action': 'collect-expected-schema', 'db': '0'},
|
||||
isolateId: isolateId,
|
||||
);
|
||||
|
||||
expect(
|
||||
response.json!['r'],
|
||||
containsAll([
|
||||
'CREATE TABLE IF NOT EXISTS "categories" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "desc" TEXT NOT NULL UNIQUE, "priority" INTEGER NOT NULL DEFAULT 0, "description_in_upper_case" TEXT NOT NULL GENERATED ALWAYS AS (UPPER("desc")) VIRTUAL);',
|
||||
'CREATE TABLE IF NOT EXISTS "todos" ("id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "title" TEXT NULL, "content" TEXT NOT NULL, "target_date" INTEGER NULL UNIQUE, "category" INTEGER NULL REFERENCES categories (id), "status" TEXT NULL, UNIQUE ("title", "category"), UNIQUE ("title", "target_date"));',
|
||||
'CREATE TABLE IF NOT EXISTS "shared_todos" ("todo" INTEGER NOT NULL, "user" INTEGER NOT NULL, PRIMARY KEY ("todo", "user"), FOREIGN KEY (todo) REFERENCES todos(id), FOREIGN KEY (user) REFERENCES users(id));'
|
||||
]));
|
||||
});
|
||||
}
|
|
@ -2,10 +2,13 @@ import 'package:drift/drift.dart';
|
|||
import 'package:drift/internal/migrations.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:drift_dev/src/services/schema/verifier_impl.dart';
|
||||
import 'package:drift_dev/src/services/schema/verifier_common.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
|
||||
export 'package:drift/internal/migrations.dart';
|
||||
export 'package:drift_dev/src/services/schema/verifier_common.dart'
|
||||
show SchemaMismatch;
|
||||
|
||||
abstract class SchemaVerifier {
|
||||
factory SchemaVerifier(SchemaInstantiationHelper helper) =
|
||||
|
@ -130,18 +133,6 @@ class _GenerateFromScratch extends GeneratedDatabase {
|
|||
int get schemaVersion => 1;
|
||||
}
|
||||
|
||||
/// Thrown when the actual schema differs from the expected schema.
|
||||
class SchemaMismatch implements Exception {
|
||||
final String explanation;
|
||||
|
||||
SchemaMismatch(this.explanation);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Schema does not match\n$explanation';
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an initialized schema with all tables, views, triggers and indices.
|
||||
///
|
||||
/// You can use the [newConnection] for your database class and the
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:drift_dev/src/analysis/options.dart';
|
||||
import 'package:drift_dev/src/services/schema/verifier_impl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:sqlite3/common.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
@ -8,6 +7,7 @@ import 'package:sqlparser/sqlparser.dart';
|
|||
import '../../analysis/backend.dart';
|
||||
import '../../analysis/driver/driver.dart';
|
||||
import '../../analysis/results/results.dart';
|
||||
import 'verifier_common.dart';
|
||||
|
||||
/// Extracts drift elements from the schema of an existing database.
|
||||
///
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import 'find_differences.dart';
|
||||
|
||||
/// Attempts to recognize whether [name] is likely the name of an internal
|
||||
/// sqlite3 table (like `sqlite3_sequence`) that we should not consider when
|
||||
/// comparing schemas.
|
||||
bool isInternalElement(String name, List<String> virtualTables) {
|
||||
// Skip sqlite-internal tables, https://www.sqlite.org/fileformat2.html#intschema
|
||||
if (name.startsWith('sqlite_')) return true;
|
||||
if (virtualTables.any((v) => name.startsWith('${v}_'))) return true;
|
||||
|
||||
// This file is added on some Android versions when using the native Android
|
||||
// database APIs, https://github.com/simolus3/drift/discussions/2042
|
||||
if (name == 'android_metadata') return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void verify(List<Input> referenceSchema, List<Input> actualSchema,
|
||||
bool validateDropped) {
|
||||
final result =
|
||||
FindSchemaDifferences(referenceSchema, actualSchema, validateDropped)
|
||||
.compare();
|
||||
|
||||
if (!result.noChanges) {
|
||||
throw SchemaMismatch(result.describe());
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown when the actual schema differs from the expected schema.
|
||||
class SchemaMismatch implements Exception {
|
||||
final String explanation;
|
||||
|
||||
SchemaMismatch(this.explanation);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Schema does not match\n$explanation';
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import 'package:drift_dev/api/migrations.dart';
|
|||
import 'package:sqlite3/sqlite3.dart';
|
||||
|
||||
import 'find_differences.dart';
|
||||
import 'verifier_common.dart';
|
||||
|
||||
Expando<List<Input>> expectedSchema = Expando();
|
||||
|
||||
|
@ -94,21 +95,6 @@ Input? _parseInputFromSchemaRow(
|
|||
return Input(name, row['sql'] as String);
|
||||
}
|
||||
|
||||
/// Attempts to recognize whether [name] is likely the name of an internal
|
||||
/// sqlite3 table (like `sqlite3_sequence`) that we should not consider when
|
||||
/// comparing schemas.
|
||||
bool isInternalElement(String name, List<String> virtualTables) {
|
||||
// Skip sqlite-internal tables, https://www.sqlite.org/fileformat2.html#intschema
|
||||
if (name.startsWith('sqlite_')) return true;
|
||||
if (virtualTables.any((v) => name.startsWith('${v}_'))) return true;
|
||||
|
||||
// This file is added on some Android versions when using the native Android
|
||||
// database APIs, https://github.com/simolus3/drift/discussions/2042
|
||||
if (name == 'android_metadata') return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
extension CollectSchemaDb on DatabaseConnectionUser {
|
||||
Future<List<Input>> collectSchemaInput(List<String> virtualTables) async {
|
||||
final result = await customSelect('SELECT * FROM sqlite_master;').get();
|
||||
|
@ -141,17 +127,6 @@ extension CollectSchema on QueryExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
void verify(List<Input> referenceSchema, List<Input> actualSchema,
|
||||
bool validateDropped) {
|
||||
final result =
|
||||
FindSchemaDifferences(referenceSchema, actualSchema, validateDropped)
|
||||
.compare();
|
||||
|
||||
if (!result.noChanges) {
|
||||
throw SchemaMismatch(result.describe());
|
||||
}
|
||||
}
|
||||
|
||||
class _DelegatingUser extends QueryExecutorUser {
|
||||
@override
|
||||
final int schemaVersion;
|
||||
|
|
|
@ -52,7 +52,10 @@ class ViewerDatabase implements DbViewerDatabase {
|
|||
@override
|
||||
List<String> get entityNames => [
|
||||
for (final entity in database.description.entities)
|
||||
if (entity.type == 'table') entity.name,
|
||||
if (entity.type == 'table' ||
|
||||
entity.type == 'virtual_table' ||
|
||||
entity.type == 'view')
|
||||
entity.name,
|
||||
];
|
||||
|
||||
@override
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:devtools_app_shared/service.dart';
|
||||
import 'package:drift_devtools_extension/src/schema_validator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
|
@ -55,6 +56,10 @@ class _DatabaseDetailsState extends ConsumerState<DatabaseDetails> {
|
|||
child: ListView(
|
||||
controller: controller,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: DatabaseSchemaCheck(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
|
|
|
@ -71,6 +71,11 @@ class RemoteDatabase {
|
|||
await _executeQuery<void>(ExecuteQuery(StatementMethod.custom, sql, args));
|
||||
}
|
||||
|
||||
Future<List<String>> get createStatements async {
|
||||
final res = await _driftRequest('collect-expected-schema');
|
||||
return (res as List).cast();
|
||||
}
|
||||
|
||||
Future<int> _newTableSubscription() async {
|
||||
final result = await _driftRequest('subscribe-to-tables');
|
||||
return result as int;
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
import 'package:devtools_app_shared/ui.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:drift_dev/src/services/schema/find_differences.dart';
|
||||
// ignore: implementation_imports
|
||||
import 'package:drift_dev/src/services/schema/verifier_common.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sqlite3/wasm.dart' hide Row;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'details.dart';
|
||||
import 'remote_database.dart';
|
||||
import 'service.dart';
|
||||
|
||||
sealed class SchemaStatus {}
|
||||
|
||||
final class DidNotValidateYet implements SchemaStatus {
|
||||
const DidNotValidateYet();
|
||||
}
|
||||
|
||||
final class SchemaComparisonResult implements SchemaStatus {
|
||||
final bool schemaValid;
|
||||
final String message;
|
||||
|
||||
SchemaComparisonResult({required this.schemaValid, required this.message});
|
||||
}
|
||||
|
||||
final schemaStateProvider =
|
||||
AsyncNotifierProvider.autoDispose<SchemaVerifier, SchemaStatus>(
|
||||
SchemaVerifier._);
|
||||
|
||||
class SchemaVerifier extends AutoDisposeAsyncNotifier<SchemaStatus> {
|
||||
RemoteDatabase? _database;
|
||||
CommonSqlite3? _sqlite3;
|
||||
|
||||
SchemaVerifier._();
|
||||
|
||||
@override
|
||||
Future<SchemaStatus> build() async {
|
||||
_database = await ref.read(loadedDatabase.future);
|
||||
_sqlite3 = await ref.read(sqliteProvider.future);
|
||||
|
||||
return const DidNotValidateYet();
|
||||
}
|
||||
|
||||
Future<void> validate() async {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard<SchemaStatus>(() async {
|
||||
final database = _database!;
|
||||
|
||||
final virtualTables = database.description.entities
|
||||
.where((e) => e.type == 'virtual_table')
|
||||
.map((e) => e.name)
|
||||
.toList();
|
||||
|
||||
final expected = await _inputFromNewDatabase(virtualTables);
|
||||
final actual = <Input>[];
|
||||
|
||||
for (final row in await database
|
||||
.select('SELECT name, sql FROM sqlite_schema;', [])) {
|
||||
final name = row['name'] as String;
|
||||
final sql = row['sql'] as String;
|
||||
|
||||
if (!isInternalElement(name, virtualTables)) {
|
||||
actual.add(Input(name, sql));
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
verify(expected, actual, true);
|
||||
return SchemaComparisonResult(
|
||||
schemaValid: true,
|
||||
message: 'The schema of the database matches its Dart and .drift '
|
||||
'definitions, meaning that migrations are likely correct.',
|
||||
);
|
||||
} on SchemaMismatch catch (e) {
|
||||
return SchemaComparisonResult(
|
||||
schemaValid: false,
|
||||
message: e.toString(),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<Input>> _inputFromNewDatabase(List<String> virtuals) async {
|
||||
final expectedStatements = await _database!.createStatements;
|
||||
final newDatabase = _sqlite3!.openInMemory();
|
||||
final inputs = <Input>[];
|
||||
|
||||
for (var statement in expectedStatements) {
|
||||
newDatabase.execute(statement);
|
||||
}
|
||||
|
||||
for (final row
|
||||
in newDatabase.select('SELECT name, sql FROM sqlite_schema;', [])) {
|
||||
final name = row['name'] as String;
|
||||
final sql = row['sql'] as String;
|
||||
|
||||
if (!isInternalElement(name, virtuals)) {
|
||||
inputs.add(Input(name, sql));
|
||||
}
|
||||
}
|
||||
|
||||
newDatabase.dispose();
|
||||
return inputs;
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseSchemaCheck extends ConsumerWidget {
|
||||
const DatabaseSchemaCheck({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(schemaStateProvider);
|
||||
|
||||
final description = switch (state) {
|
||||
AsyncData(
|
||||
value: SchemaComparisonResult(schemaValid: true, :var message)
|
||||
) =>
|
||||
Text.rich(TextSpan(
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: 'Success! ', style: TextStyle(color: Colors.green)),
|
||||
TextSpan(text: message),
|
||||
],
|
||||
)),
|
||||
AsyncData(
|
||||
value: SchemaComparisonResult(schemaValid: false, :var message)
|
||||
) =>
|
||||
Text.rich(TextSpan(
|
||||
children: [
|
||||
const TextSpan(
|
||||
text: 'Mismatch detected! ',
|
||||
style: TextStyle(color: Colors.red)),
|
||||
TextSpan(text: message),
|
||||
],
|
||||
)),
|
||||
AsyncError(:var error) =>
|
||||
Text('The schema could not be validated due to an error: $error'),
|
||||
_ => Text.rich(TextSpan(
|
||||
text: 'By validating your schema, you can ensure that the current '
|
||||
'state of the database in your app (after migrations ran) '
|
||||
'matches the expected state of tables as defined in your sources. ',
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Learn more',
|
||||
style: const TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
await launchUrl(Uri.parse(
|
||||
'https://drift.simonbinder.eu/docs/migrations/#verifying-a-database-schema-at-runtime'));
|
||||
},
|
||||
),
|
||||
],
|
||||
)),
|
||||
};
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: description,
|
||||
),
|
||||
DevToolsButton(
|
||||
label: switch (state) {
|
||||
AsyncError() ||
|
||||
AsyncData(value: SchemaComparisonResult()) =>
|
||||
'Validate again',
|
||||
_ => 'Validate schema',
|
||||
},
|
||||
onPressed: () {
|
||||
if (state is! AsyncLoading) {
|
||||
ref.read(schemaStateProvider.notifier).validate();
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import 'package:devtools_extensions/devtools_extensions.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:rxdart/transformers.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
import 'package:vm_service/vm_service.dart';
|
||||
|
||||
final _serviceConnection = StreamController<VmService>.broadcast();
|
||||
|
@ -48,3 +49,9 @@ final hotRestartEventProvider =
|
|||
|
||||
return notifier;
|
||||
});
|
||||
|
||||
final sqliteProvider = FutureProvider((ref) async {
|
||||
final sqlite = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm'));
|
||||
sqlite.registerVirtualFileSystem(InMemoryFileSystem(), makeDefault: true);
|
||||
return sqlite;
|
||||
});
|
||||
|
|
|
@ -15,12 +15,14 @@ dependencies:
|
|||
devtools_app_shared: '>=0.0.5 <0.0.6' # 0.0.6 requires unstable Flutter
|
||||
db_viewer: ^1.0.3
|
||||
rxdart: ^0.27.7
|
||||
flutter_riverpod: ^2.4.4
|
||||
flutter_riverpod: ^3.0.0-dev.0
|
||||
vm_service: ^11.10.0
|
||||
path: ^1.8.3
|
||||
drift: ^2.12.1
|
||||
logging: ^1.2.0
|
||||
url_launcher: ^6.1.14
|
||||
drift_dev: ^2.13.1
|
||||
sqlite3: ^2.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
../../assets/sqlite3.wasm
|
Loading…
Reference in New Issue