diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml new file mode 100644 index 0000000..40be1c9 --- /dev/null +++ b/.github/workflows/packages.yml @@ -0,0 +1,48 @@ +name: Packages check + +on: + push: + branches: + - "**" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.x" + channel: "stable" + + - name: Install Melos + run: flutter pub global activate melos + - name: Install dependencies + run: melos bootstrap + - name: Check formatting + run: melos format:check:packages + - name: Lint + run: melos analyze:packages + - name: Publish dry-run + run: melos publish --dry-run --yes + - name: Check publish score + run: | + flutter pub global activate pana + ./.github/workflows/scripts/run-pana.sh + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.x" + channel: "stable" + - name: Install melos + run: flutter pub global activate melos + - name: Install dependencies + run: melos bootstrap + - name: Run tests + run: melos test diff --git a/.github/workflows/scripts/run-pana.sh b/.github/workflows/scripts/run-pana.sh new file mode 100755 index 0000000..8590577 --- /dev/null +++ b/.github/workflows/scripts/run-pana.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +# Get the root directory of your project +ROOT_DIR=$(pwd) + +# Specify the path to the packages folder +PACKAGES_DIR="$ROOT_DIR/packages" + +# Iterate over each package folder +for PACKAGE in "$PACKAGES_DIR"/*; do + # Check if it's a directory + if [ -d "$PACKAGE" ]; then + echo "Analyzing package in: $PACKAGE" + + # Change into the package directory + cd "$PACKAGE" || exit + + # Run the pana command + flutter pub global run pana --no-warning --exit-code-threshold 10 + + # Return to the root directory + cd "$ROOT_DIR" || exit + fi +done diff --git a/.gitignore b/.gitignore index 3a83c2f..4562763 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,6 @@ .dart_tool/ .packages build/ -# If you're building an application, you may want to check-in your pubspec.lock -pubspec.lock # Directory created by dartdoc # If you don't generate documentation locally you can remove this line. @@ -25,3 +23,10 @@ doc/api/ .flutter-plugins .flutter-plugins-dependencies + +# IDE files +*.iml +.idea + +# Test databases +*.db \ No newline at end of file diff --git a/LICENSE b/LICENSE index 84bfb2b..2d3030a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 PowerSync +Copyright (c) 2024 Journey Mobile, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index e370693..7f7b256 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ # drift_sqlite_async -sqlite_async executor for drift + +`drift_sqlite_async` allows using drift on an sqlite_async database - the APIs from both can be seamlessly used together in the same application. + +See [./packages/drift_sqlite_async](./packages/drift_sqlite_async) for details. diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 0000000..a4f9ea3 --- /dev/null +++ b/melos.yaml @@ -0,0 +1,31 @@ +name: drift_sqlite_async_monorepo + +packages: + - demos/** + - packages/** + +scripts: + format: + description: Format Dart code. + run: dart format . + + format:check:packages: + description: Check formatting of Dart code in packages. + run: dart format --output none --set-exit-if-changed packages + + analyze:packages: + description: Analyze Dart code in packages. + run: dart analyze packages --fatal-infos + + test: + description: Run tests in a specific package. + run: dart test + exec: + concurrency: 1 + packageFilters: + dirExists: + - test + # This tells Melos tests to ignore env variables passed to tests from `melos run test` + # as they could change the behaviour of how tests filter packages. + env: + MELOS_TEST: true diff --git a/packages/drift_sqlite_async/CHANGELOG.md b/packages/drift_sqlite_async/CHANGELOG.md new file mode 100644 index 0000000..a998fef --- /dev/null +++ b/packages/drift_sqlite_async/CHANGELOG.md @@ -0,0 +1,4 @@ + +## 0.1.0-alpha.1 + +Initial release. diff --git a/packages/drift_sqlite_async/LICENSE b/packages/drift_sqlite_async/LICENSE new file mode 100644 index 0000000..2d3030a --- /dev/null +++ b/packages/drift_sqlite_async/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Journey Mobile, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/drift_sqlite_async/README.md b/packages/drift_sqlite_async/README.md new file mode 100644 index 0000000..c732652 --- /dev/null +++ b/packages/drift_sqlite_async/README.md @@ -0,0 +1,62 @@ +# drift_sqlite_async + +`drift_sqlite_async` allows using drift on an sqlite_async database - the APIs from both can be seamlessly used together in the same application. + +Supported functionality: +1. All queries including select, insert, update, delete. +2. Transactions and nested transactions. +3. Table updates are propagated between sqlite_async and Drift - watching queries works using either API. +4. Select queries can run concurrently with writes and other select statements. + + +## Usage + +Use `SqliteAsyncDriftConnection` to create a DatabaseConnection / QueryExecutor for Drift from the sqlite_async SqliteDatabase: + +```dart +@DriftDatabase(tables: [TodoItems]) +class AppDatabase extends _$AppDatabase { + AppDatabase(SqliteConnection db) : super(SqliteAsyncDriftConnection(db)); + + @override + int get schemaVersion => 1; +} + +Future main() async { + // The sqlite_async db + final db = SqliteDatabase(path: 'example.db'); + // The Drift db + final appdb = AppDatabase(db); +} +``` + +A full example is in the `examples/` folder. + +For details on table definitions and using the database, see the [Drift documentation](https://drift.simonbinder.eu/). + +## Transactions and concurrency + +sqlite_async uses WAL mode and multiple read connections by default, and this +is also exposed when using the database with Drift. + +Drift's transactions use sqlite_async's `writeTransaction`. The same locks are used +for both, preventing conflicts. + +Read-only transactions are not currently supported in Drift. + +Drift's nested transactions are supported, implemented using SAVEPOINT. + +Select statements in Drift use read operations (`getAll()`) in sqlite_async, +and can run concurrently with writes. + +## Update notifications + +sqlite_async uses SQLite's update_hook to detect changes for watching queries, +and will automatically pick up changes made using Drift. This also includes any updates from custom queries in Drift. + +Changes from sqlite_async are automatically propagated to Drift when using SqliteAsyncDriftConnection. +These events are only sent while no write transaction is active. + +Within Drift's transactions, Drift's own update notifications will still apply for watching queries within that transaction. + +Note: There is a possibility of events being duplicated. This should not have a significant impact on most applications. \ No newline at end of file diff --git a/packages/drift_sqlite_async/build.yaml b/packages/drift_sqlite_async/build.yaml new file mode 100644 index 0000000..e1151bf --- /dev/null +++ b/packages/drift_sqlite_async/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + builders: + drift_dev: + options: + fatal_warnings: true diff --git a/packages/drift_sqlite_async/example/main.dart b/packages/drift_sqlite_async/example/main.dart new file mode 100644 index 0000000..6972643 --- /dev/null +++ b/packages/drift_sqlite_async/example/main.dart @@ -0,0 +1,49 @@ +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/drift_sqlite_async.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +part 'main.g.dart'; + +class TodoItems extends Table { + @override + String get tableName => 'todos'; + + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); +} + +@DriftDatabase(tables: [TodoItems]) +class AppDatabase extends _$AppDatabase { + AppDatabase(SqliteConnection db) : super(SqliteAsyncDriftConnection(db)); + + @override + int get schemaVersion => 1; +} + +Future main() async { + final db = SqliteDatabase(path: 'example.db'); + + // Example where the schema is managed manually + await db.execute( + 'CREATE TABLE IF NOT EXISTS todos(id integer primary key, description text)'); + + final appdb = AppDatabase(db); + + // Watch a query on the Drift database + appdb.select(appdb.todoItems).watch().listen((todos) { + print('Todos: $todos'); + }); + + // Insert using the Drift database + await appdb + .into(appdb.todoItems) + .insert(TodoItemsCompanion.insert(description: 'Test Drift')); + + // Insert using the sqlite_async database + await db.execute('INSERT INTO todos(description) VALUES(?)', ['Test Direct']); + + await Future.delayed(const Duration(milliseconds: 100)); + + await appdb.close(); + await db.close(); +} diff --git a/packages/drift_sqlite_async/example/main.g.dart b/packages/drift_sqlite_async/example/main.g.dart new file mode 100644 index 0000000..576157c --- /dev/null +++ b/packages/drift_sqlite_async/example/main.g.dart @@ -0,0 +1,189 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'main.dart'; + +// ignore_for_file: type=lint +class $TodoItemsTable extends TodoItems + with TableInfo<$TodoItemsTable, TodoItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TodoItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, description]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'todos'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } else if (isInserting) { + context.missing(_descriptionMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + TodoItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodoItem( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + ); + } + + @override + $TodoItemsTable createAlias(String alias) { + return $TodoItemsTable(attachedDatabase, alias); + } +} + +class TodoItem extends DataClass implements Insertable { + final int id; + final String description; + const TodoItem({required this.id, required this.description}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['description'] = Variable(description); + return map; + } + + TodoItemsCompanion toCompanion(bool nullToAbsent) { + return TodoItemsCompanion( + id: Value(id), + description: Value(description), + ); + } + + factory TodoItem.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoItem( + id: serializer.fromJson(json['id']), + description: serializer.fromJson(json['description']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'description': serializer.toJson(description), + }; + } + + TodoItem copyWith({int? id, String? description}) => TodoItem( + id: id ?? this.id, + description: description ?? this.description, + ); + @override + String toString() { + return (StringBuffer('TodoItem(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, description); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodoItem && + other.id == this.id && + other.description == this.description); +} + +class TodoItemsCompanion extends UpdateCompanion { + final Value id; + final Value description; + const TodoItemsCompanion({ + this.id = const Value.absent(), + this.description = const Value.absent(), + }); + TodoItemsCompanion.insert({ + this.id = const Value.absent(), + required String description, + }) : description = Value(description); + static Insertable custom({ + Expression? id, + Expression? description, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (description != null) 'description': description, + }); + } + + TodoItemsCompanion copyWith({Value? id, Value? description}) { + return TodoItemsCompanion( + id: id ?? this.id, + description: description ?? this.description, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TodoItemsCompanion(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + late final $TodoItemsTable todoItems = $TodoItemsTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [todoItems]; +} diff --git a/packages/drift_sqlite_async/example/with_migrations.dart b/packages/drift_sqlite_async/example/with_migrations.dart new file mode 100644 index 0000000..2bd4a87 --- /dev/null +++ b/packages/drift_sqlite_async/example/with_migrations.dart @@ -0,0 +1,58 @@ +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/drift_sqlite_async.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +part 'with_migrations.g.dart'; + +class TodoItems extends Table { + @override + String get tableName => 'todos'; + + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); +} + +@DriftDatabase(tables: [TodoItems]) +class AppDatabase extends _$AppDatabase { + AppDatabase(SqliteConnection db) : super(SqliteAsyncDriftConnection(db)); + + @override + int get schemaVersion => 1; + + @override + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (m) async { + // In this example, the schema is managed by Drift + await m.createAll(); + }, + ); + } +} + +Future main() async { + final db = SqliteDatabase(path: 'with_migrations.db'); + + await db.execute( + 'CREATE TABLE IF NOT EXISTS todos(id integer primary key, description text)'); + + final appdb = AppDatabase(db); + + // Watch a query on the Drift database + appdb.select(appdb.todoItems).watch().listen((todos) { + print('Todos: $todos'); + }); + + // Insert using the Drift database + await appdb + .into(appdb.todoItems) + .insert(TodoItemsCompanion.insert(description: 'Test Drift')); + + // Insert using the sqlite_async database + await db.execute('INSERT INTO todos(description) VALUES(?)', ['Test Direct']); + + await Future.delayed(const Duration(milliseconds: 100)); + + await appdb.close(); + await db.close(); +} diff --git a/packages/drift_sqlite_async/example/with_migrations.g.dart b/packages/drift_sqlite_async/example/with_migrations.g.dart new file mode 100644 index 0000000..67ce020 --- /dev/null +++ b/packages/drift_sqlite_async/example/with_migrations.g.dart @@ -0,0 +1,189 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'with_migrations.dart'; + +// ignore_for_file: type=lint +class $TodoItemsTable extends TodoItems + with TableInfo<$TodoItemsTable, TodoItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TodoItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, description]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'todos'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } else if (isInserting) { + context.missing(_descriptionMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + TodoItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodoItem( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + ); + } + + @override + $TodoItemsTable createAlias(String alias) { + return $TodoItemsTable(attachedDatabase, alias); + } +} + +class TodoItem extends DataClass implements Insertable { + final int id; + final String description; + const TodoItem({required this.id, required this.description}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['description'] = Variable(description); + return map; + } + + TodoItemsCompanion toCompanion(bool nullToAbsent) { + return TodoItemsCompanion( + id: Value(id), + description: Value(description), + ); + } + + factory TodoItem.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoItem( + id: serializer.fromJson(json['id']), + description: serializer.fromJson(json['description']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'description': serializer.toJson(description), + }; + } + + TodoItem copyWith({int? id, String? description}) => TodoItem( + id: id ?? this.id, + description: description ?? this.description, + ); + @override + String toString() { + return (StringBuffer('TodoItem(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, description); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodoItem && + other.id == this.id && + other.description == this.description); +} + +class TodoItemsCompanion extends UpdateCompanion { + final Value id; + final Value description; + const TodoItemsCompanion({ + this.id = const Value.absent(), + this.description = const Value.absent(), + }); + TodoItemsCompanion.insert({ + this.id = const Value.absent(), + required String description, + }) : description = Value(description); + static Insertable custom({ + Expression? id, + Expression? description, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (description != null) 'description': description, + }); + } + + TodoItemsCompanion copyWith({Value? id, Value? description}) { + return TodoItemsCompanion( + id: id ?? this.id, + description: description ?? this.description, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TodoItemsCompanion(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + late final $TodoItemsTable todoItems = $TodoItemsTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [todoItems]; +} diff --git a/packages/drift_sqlite_async/lib/drift_sqlite_async.dart b/packages/drift_sqlite_async/lib/drift_sqlite_async.dart new file mode 100644 index 0000000..f842c5b --- /dev/null +++ b/packages/drift_sqlite_async/lib/drift_sqlite_async.dart @@ -0,0 +1,4 @@ +library drift_sqlite_async; + +export './src/connection.dart'; +export './src/executor.dart'; diff --git a/packages/drift_sqlite_async/lib/src/connection.dart b/packages/drift_sqlite_async/lib/src/connection.dart new file mode 100644 index 0000000..e375795 --- /dev/null +++ b/packages/drift_sqlite_async/lib/src/connection.dart @@ -0,0 +1,34 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/src/executor.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +/// Wraps a sqlite_async [SqliteConnection] as a Drift [DatabaseConnection]. +/// +/// The SqliteConnection must be instantiated before constructing this, and +/// is not closed when [SqliteAsyncDriftConnection.close] is called. +/// +/// This class handles delegating Drift's queries and transactions to the +/// [SqliteConnection], and passes on any table updates from the +/// [SqliteConnection] to Drift. +class SqliteAsyncDriftConnection extends DatabaseConnection { + late StreamSubscription _updateSubscription; + + SqliteAsyncDriftConnection(SqliteConnection db) + : super(SqliteAsyncQueryExecutor(db)) { + _updateSubscription = (db as SqliteQueries).updates!.listen((event) { + var setUpdates = {}; + for (var tableName in event.tables) { + setUpdates.add(TableUpdate(tableName)); + } + super.streamQueries.handleTableUpdates(setUpdates); + }); + } + + @override + Future close() async { + await _updateSubscription.cancel(); + await super.close(); + } +} diff --git a/packages/drift_sqlite_async/lib/src/executor.dart b/packages/drift_sqlite_async/lib/src/executor.dart new file mode 100644 index 0000000..a106b91 --- /dev/null +++ b/packages/drift_sqlite_async/lib/src/executor.dart @@ -0,0 +1,147 @@ +import 'dart:async'; + +import 'package:drift/backends.dart'; +import 'package:drift_sqlite_async/src/transaction_executor.dart'; +import 'package:sqlite_async/sqlite3.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +class _SqliteAsyncDelegate extends DatabaseDelegate { + final SqliteConnection db; + bool _closed = false; + + _SqliteAsyncDelegate(this.db); + + @override + late final DbVersionDelegate versionDelegate = + _SqliteAsyncVersionDelegate(db); + + // Not used - we override beginTransaction() with SqliteAsyncTransactionExecutor for more control. + @override + late final TransactionDelegate transactionDelegate = + const NoTransactionDelegate(); + + @override + bool get isOpen => !db.closed && !_closed; + + // Ends with " RETURNING *", or starts with insert/update/delete. + // Drift-generated queries will always have the RETURNING *. + // The INSERT/UPDATE/DELETE check is for custom queries, and is not exhaustive. + final _returningCheck = RegExp( + r'( RETURNING \*;?$)|(^(INSERT|UPDATE|DELETE))', + caseSensitive: false); + + @override + Future open(QueryExecutorUser user) async { + // Workaround - this ensures the db is open + await db.get('SELECT 1'); + } + + @override + Future close() async { + // We don't own the underlying SqliteConnection - don't close it. + _closed = true; + } + + @override + Future runBatched(BatchedStatements statements) async { + return db.writeLock((tx) async { + // sqlite_async's batch functionality doesn't have enough flexibility to support + // this with prepared statements yet. + for (final arg in statements.arguments) { + await tx.execute( + statements.statements[arg.statementIndex], arg.arguments); + } + }); + } + + @override + Future runCustom(String statement, List args) { + return db.execute(statement, args); + } + + @override + Future runInsert(String statement, List args) async { + return db.writeLock((tx) async { + await tx.execute(statement, args); + final row = await tx.get('SELECT last_insert_rowid() as row_id'); + return row['row_id']; + }); + } + + @override + Future runSelect(String statement, List args) async { + ResultSet result; + if (_returningCheck.hasMatch(statement)) { + // Could be "INSERT INTO ... RETURNING *" (or update or delete), + // so we need to use execute() instead of getAll(). + // This takes write lock, so we want to avoid it for plain select statements. + // This is not an exhaustive check, but should cover all Drift-generated queries using + // `runSelect()`. + result = await db.execute(statement, args); + } else { + // Plain SELECT statement - use getAll() to avoid using a write lock. + result = await db.getAll(statement, args); + } + return QueryResult(result.columnNames, result.rows); + } + + @override + Future runUpdate(String statement, List args) { + return db.writeLock((tx) async { + await tx.execute(statement, args); + final row = await tx.get('SELECT changes() as changes'); + return row['changes']; + }); + } +} + +class _SqliteAsyncVersionDelegate extends DynamicVersionDelegate { + final SqliteConnection _db; + + _SqliteAsyncVersionDelegate(this._db); + + @override + Future get schemaVersion async { + final result = await _db.get('PRAGMA user_version;'); + return result['user_version']; + } + + @override + Future setSchemaVersion(int version) async { + await _db.execute('PRAGMA user_version = $version;'); + } +} + +/// A query executor that uses sqlite_async internally. +/// In most cases, SqliteAsyncConnection should be used instead, as it handles +/// stream queries automatically. +/// +/// Wraps a sqlite_async [SqliteConnection] as a Drift [QueryExecutor]. +/// +/// The SqliteConnection must be instantiated before constructing this, and +/// is not closed when [SqliteAsyncQueryExecutor.close] is called. +/// +/// This class handles delegating Drift's queries and transactions to the +/// [SqliteConnection]. +/// +/// Extnral update notifications from the [SqliteConnection] are _not_ forwarded +/// automatically - use [SqliteAsyncDriftConnection] for that. +class SqliteAsyncQueryExecutor extends DelegatedDatabase { + SqliteAsyncQueryExecutor(SqliteConnection db) + : super( + _SqliteAsyncDelegate(db), + ); + + /// The underlying SqliteConnection used by drift to send queries. + SqliteConnection get db { + return (delegate as _SqliteAsyncDelegate).db; + } + + @override + bool get isSequential => false; + + @override + TransactionExecutor beginTransaction() { + return SqliteAsyncTransactionExecutor(db); + } +} diff --git a/packages/drift_sqlite_async/lib/src/transaction_executor.dart b/packages/drift_sqlite_async/lib/src/transaction_executor.dart new file mode 100644 index 0000000..ee4db9c --- /dev/null +++ b/packages/drift_sqlite_async/lib/src/transaction_executor.dart @@ -0,0 +1,193 @@ +import 'dart:async'; + +import 'package:drift/backends.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +/// Based on Drift's _WrappingTransactionExecutor, which is private. +/// Extended to support nested transactions. +/// +/// The outer SqliteAsyncTransactionExecutor uses sqlite_async's writeTransaction, which +/// does BEGIN/COMMIT/ROLLBACK. +/// +/// Nested transactions use SqliteAsyncNestedTransactionExecutor to implement SAVEPOINT / ROLLBACK. +class SqliteAsyncTransactionExecutor extends TransactionExecutor + with _TransactionQueryMixin { + final SqliteConnection _db; + static final _artificialRollback = + Exception('artificial exception to rollback the transaction'); + final Zone _createdIn = Zone.current; + final Completer _completerForCallback = Completer(); + Completer? _opened, _finished; + + /// Whether this executor has explicitly been closed. + bool _closed = false; + + @override + late SqliteWriteContext ctx; + + SqliteAsyncTransactionExecutor(this._db); + + void _checkCanOpen() { + if (_closed) { + throw StateError( + "A tranaction was used after being closed. Please check that you're " + 'awaiting all database operations inside a `transaction` block.'); + } + } + + @override + Future ensureOpen(QueryExecutorUser user) { + _checkCanOpen(); + var opened = _opened; + + if (opened == null) { + _opened = opened = Completer(); + _createdIn.run(() async { + final result = _db.writeTransaction((innerCtx) async { + opened!.complete(); + ctx = innerCtx; + await _completerForCallback.future; + }); + + _finished = Completer() + ..complete( + // ignore: void_checks + result + // Ignore the exception caused by [rollback] which may be + // rethrown by startTransaction + .onError((error, stackTrace) => null, + test: (e) => e == _artificialRollback) + // Consider this transaction closed after the call completes + // This may happen without send/rollback being called in + // case there's an exception when opening the transaction. + .whenComplete(() => _closed = true), + ); + }); + } + + // The opened completer is never completed if `startTransaction` throws + // before our callback is invoked (probably becaue `BEGIN` threw an + // exception). In that case, _finished will complete with that error though. + return Future.any([opened.future, if (_finished != null) _finished!.future]) + .then((value) => true); + } + + @override + Future send() async { + // don't do anything if the transaction completes before it was opened + if (_opened == null || _closed) return; + + _completerForCallback.complete(); + _closed = true; + await _finished?.future; + } + + @override + Future rollback() async { + // Note: This may be called after send() if send() throws (that is, the + // transaction can't be completed). But if completing fails, we assume that + // the transaction will implicitly be rolled back the underlying connection + // (it's not like we could explicitly roll it back, we only have one + // callback to implement). + if (_opened == null || _closed) return; + + _completerForCallback.completeError(_artificialRollback); + _closed = true; + await _finished?.future; + } + + @override + TransactionExecutor beginTransaction() { + return SqliteAsyncNestedTransactionExecutor(ctx, 1); + } + + @override + SqlDialect get dialect => SqlDialect.sqlite; + + @override + bool get supportsNestedTransactions => true; +} + +class SqliteAsyncNestedTransactionExecutor extends TransactionExecutor + with _TransactionQueryMixin { + @override + final SqliteWriteContext ctx; + + int depth; + + SqliteAsyncNestedTransactionExecutor(this.ctx, this.depth); + + @override + Future ensureOpen(QueryExecutorUser user) async { + await ctx.execute('SAVEPOINT tx$depth'); + return true; + } + + @override + Future send() async { + await ctx.execute('RELEASE SAVEPOINT tx$depth'); + } + + @override + Future rollback() async { + await ctx.execute('ROLLBACK TO SAVEPOINT tx$depth'); + } + + @override + TransactionExecutor beginTransaction() { + return SqliteAsyncNestedTransactionExecutor(ctx, depth + 1); + } + + @override + SqlDialect get dialect => SqlDialect.sqlite; + + @override + bool get supportsNestedTransactions => true; +} + +abstract class _QueryDelegate { + SqliteWriteContext get ctx; +} + +mixin _TransactionQueryMixin implements QueryExecutor, _QueryDelegate { + @override + Future runBatched(BatchedStatements statements) async { + // sqlite_async's batch functionality doesn't have enough flexibility to support + // this with prepared statements yet. + for (final arg in statements.arguments) { + await ctx.execute( + statements.statements[arg.statementIndex], arg.arguments); + } + } + + @override + Future runCustom(String statement, [List? args]) { + return ctx.execute(statement, args ?? const []); + } + + @override + Future runInsert(String statement, List args) async { + await ctx.execute(statement, args); + final row = await ctx.get('SELECT last_insert_rowid() as row_id'); + return row['row_id']; + } + + @override + Future>> runSelect( + String statement, List args) async { + final result = await ctx.execute(statement, args); + return QueryResult(result.columnNames, result.rows).asMap.toList(); + } + + @override + Future runUpdate(String statement, List args) async { + await ctx.execute(statement, args); + final row = await ctx.get('SELECT changes() as changes'); + return row['changes']; + } + + @override + Future runDelete(String statement, List args) { + return runUpdate(statement, args); + } +} diff --git a/packages/drift_sqlite_async/pubspec.lock b/packages/drift_sqlite_async/pubspec.lock new file mode 100644 index 0000000..fccfa8f --- /dev/null +++ b/packages/drift_sqlite_async/pubspec.lock @@ -0,0 +1,597 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" + url: "https://pub.dev" + source: hosted + version: "67.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + url: "https://pub.dev" + source: hosted + version: "0.11.3" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + url: "https://pub.dev" + source: hosted + version: "2.4.8" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + url: "https://pub.dev" + source: hosted + version: "8.9.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: "8acabb8306b57a409bf4c83522065672ee13179297a6bb0cb9ead73948df7c76" + url: "https://pub.dev" + source: hosted + version: "1.7.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + url: "https://pub.dev" + source: hosted + version: "2.3.4" + drift: + dependency: "direct main" + description: + name: drift + sha256: b50a8342c6ddf05be53bda1d246404cbad101b64dc73e8d6d1ac1090d119b4e2 + url: "https://pub.dev" + source: hosted + version: "2.15.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: c037d9431b6f8dc633652b1469e5f53aaec6e4eb405ed29dd232fa888ef10d88 + url: "https://pub.dev" + source: hosted + version: "2.15.0" + ffi: + dependency: transitive + description: + name: ffi + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: "direct dev" + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + meta: + dependency: transitive + description: + name: meta + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e + url: "https://pub.dev" + source: hosted + version: "1.1.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + sqlite3: + dependency: "direct dev" + description: + name: sqlite3 + sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqlite_async: + dependency: "direct main" + description: + name: sqlite_async + sha256: "609a8405b8b608ac396dd7f478ed42e230c496eb38fe53dd97e9c592e1cd5cda" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: dc384bb1f56d1384ce078edb5ff8247976abdab79d0c83e437210c85f06ecb61 + url: "https://pub.dev" + source: hosted + version: "0.34.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: "direct dev" + description: + name: test + sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + url: "https://pub.dev" + source: hosted + version: "1.25.2" + test_api: + dependency: "direct dev" + description: + name: test_api + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: a2662fb1f114f4296cf3f5a50786a2d888268d7776cf681aa17d660ffa23b246 + url: "https://pub.dev" + source: hosted + version: "14.0.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + url: "https://pub.dev" + source: hosted + version: "0.4.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "939ab60734a4f8fa95feacb55804fa278de28bdeef38e616dc08e44a84adea23" + url: "https://pub.dev" + source: hosted + version: "2.4.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.2.0 <4.0.0" diff --git a/packages/drift_sqlite_async/pubspec.yaml b/packages/drift_sqlite_async/pubspec.yaml new file mode 100644 index 0000000..9851396 --- /dev/null +++ b/packages/drift_sqlite_async/pubspec.yaml @@ -0,0 +1,18 @@ +name: drift_sqlite_async +version: 0.1.0-alpha.1 +homepage: https://github.com/powersync-ja/drift_sqlite_async/ +repository: https://github.com/powersync-ja/drift_sqlite_async/ +description: Use Drift with a sqlite_async database, allowing both to be used in the same application. + +environment: + sdk: ">=3.0.0 <4.0.0" +dependencies: + drift: ^2.15.0 + sqlite_async: ^0.6.0 +dev_dependencies: + build_runner: ^2.4.8 + drift_dev: ^2.15.0 + glob: ^2.1.2 + sqlite3: ^2.4.0 + test: ^1.25.2 + test_api: ^0.7.0 diff --git a/packages/drift_sqlite_async/test/basic_test.dart b/packages/drift_sqlite_async/test/basic_test.dart new file mode 100644 index 0000000..d3d35f1 --- /dev/null +++ b/packages/drift_sqlite_async/test/basic_test.dart @@ -0,0 +1,212 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/drift_sqlite_async.dart'; +import 'package:sqlite_async/sqlite_async.dart'; +import 'package:test/test.dart'; + +import './utils/test_utils.dart'; + +class EmptyDatabase extends GeneratedDatabase { + EmptyDatabase(super.executor); + + @override + Iterable> get allTables => []; + + @override + int get schemaVersion => 1; +} + +void main() { + group('Basic Tests', () { + late String path; + late SqliteDatabase db; + late SqliteAsyncDriftConnection connection; + late EmptyDatabase dbu; + + createTables(SqliteDatabase db) async { + await db.writeTransaction((tx) async { + await tx.execute( + 'CREATE TABLE test_data(id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT)'); + }); + } + + setUp(() async { + path = dbPath(); + await cleanDb(path: path); + + db = await setupDatabase(path: path); + connection = SqliteAsyncDriftConnection(db); + dbu = EmptyDatabase(connection); + await createTables(db); + }); + + tearDown(() async { + await dbu.close(); + await db.close(); + + await cleanDb(path: path); + }); + + test('INSERT/SELECT', () async { + final insertRowId = await dbu.customInsert( + 'INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test Data')]); + expect(insertRowId, greaterThanOrEqualTo(1)); + + final result = await dbu + .customSelect('SELECT description FROM test_data') + .getSingle(); + expect(result.data, equals({'description': 'Test Data'})); + }); + + test('INSERT RETURNING', () async { + final row = await dbu.customSelect( + 'INSERT INTO test_data(description) VALUES(?) RETURNING *', + variables: [Variable('Test Data')]).getSingle(); + expect(row.data['description'], equals('Test Data')); + }); + + test('Flat transaction', () async { + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test Data')]); + + // This runs outside the transaction - should not see the insert + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + + // This runs in the transaction - should see the insert + expect( + (await dbu + .customSelect('select count(*) as count from test_data') + .getSingle()) + .data, + equals({'count': 1})); + }); + + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 1})); + }); + + test('Flat transaction rollback', () async { + final testException = Exception('abort'); + + try { + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test Data')]); + + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + + throw testException; + }); + + // ignore: dead_code + throw Exception('Exception expected'); + } catch (e) { + expect(e, equals(testException)); + } + + // Rolled back - no data persisted + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + }); + + test('Nested transaction', () async { + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 1')]); + + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 2')]); + }); + + // This runs outside the transaction + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + }); + + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 2})); + }); + + test('Nested transaction rollback', () async { + final testException = Exception('abort'); + + await dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 1')]); + + try { + await dbu.transaction(() async { + await dbu.customInsert( + 'INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 2')]); + + throw testException; + }); + + // ignore: dead_code + throw Exception('Exception expected'); + } catch (e) { + expect(e, equals(testException)); + } + + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test 3')]); + + // This runs outside the transaction + expect(await db.get('select count(*) as count from test_data'), + equals({'count': 0})); + }); + + expect( + await db + .getAll('select description from test_data order by description'), + equals([ + {'description': 'Test 1'}, + {'description': 'Test 3'} + ])); + }); + + test('Concurrent select', () async { + var completer1 = Completer(); + var completer2 = Completer(); + + final tx1 = dbu.transaction(() async { + await dbu.customInsert('INSERT INTO test_data(description) VALUES(?)', + variables: [Variable('Test Data')]); + + completer2.complete(); + + // Stay in the transaction until the check below completed. + await completer1.future; + }); + + await completer2.future; + try { + // This times out if concurrent select is not supported + expect( + (await dbu + .customSelect('select count(*) as count from test_data') + .getSingle() + .timeout(const Duration(milliseconds: 500))) + .data, + equals({'count': 0})); + } finally { + completer1.complete(); + } + await tx1; + + expect( + (await dbu + .customSelect('select count(*) as count from test_data') + .getSingle()) + .data, + equals({'count': 1})); + }); + }); +} diff --git a/packages/drift_sqlite_async/test/db_test.dart b/packages/drift_sqlite_async/test/db_test.dart new file mode 100644 index 0000000..0b3dfef --- /dev/null +++ b/packages/drift_sqlite_async/test/db_test.dart @@ -0,0 +1,95 @@ +import 'package:drift/drift.dart'; +import 'package:sqlite_async/sqlite_async.dart'; +import 'package:test/test.dart'; + +import './utils/test_utils.dart'; +import 'generated/database.dart'; + +void main() { + group('Generated DB tests', () { + late String path; + late SqliteDatabase db; + late TodoDatabase dbu; + + createTables(SqliteDatabase db) async { + await db.writeTransaction((tx) async { + await tx.execute( + 'CREATE TABLE todos(id INTEGER PRIMARY KEY AUTOINCREMENT, description TEXT)'); + }); + } + + setUp(() async { + path = dbPath(); + await cleanDb(path: path); + + db = await setupDatabase(path: path); + dbu = TodoDatabase(db); + await createTables(db); + }); + + tearDown(() async { + await dbu.close(); + await db.close(); + + await cleanDb(path: path); + }); + + test('INSERT/SELECT', () async { + var insertRowId = await dbu + .into(dbu.todoItems) + .insert(TodoItemsCompanion.insert(description: 'Test 1')); + expect(insertRowId, greaterThanOrEqualTo(1)); + + final result = await dbu.select(dbu.todoItems).getSingle(); + expect(result.description, equals('Test 1')); + }); + + test('watch', () async { + var stream = dbu.select(dbu.todoItems).watch(); + var resultsPromise = + stream.distinct().skipWhile((e) => e.isEmpty).take(3).toList(); + + await dbu.into(dbu.todoItems).insert( + TodoItemsCompanion.insert(id: Value(1), description: 'Test 1')); + + await Future.delayed(Duration(milliseconds: 100)); + await (dbu.update(dbu.todoItems)) + .write(TodoItemsCompanion(description: Value('Test 1B'))); + + await Future.delayed(Duration(milliseconds: 100)); + await (dbu.delete(dbu.todoItems).go()); + + var results = await resultsPromise.timeout(Duration(milliseconds: 500)); + expect( + results, + equals([ + [TodoItem(id: 1, description: 'Test 1')], + [TodoItem(id: 1, description: 'Test 1B')], + [] + ])); + }); + + test('watch with external updates', () async { + var stream = dbu.select(dbu.todoItems).watch(); + var resultsPromise = + stream.distinct().skipWhile((e) => e.isEmpty).take(3).toList(); + + await db.execute( + 'INSERT INTO todos(id, description) VALUES(?, ?)', [1, 'Test 1']); + await Future.delayed(Duration(milliseconds: 100)); + await db.execute( + 'UPDATE todos SET description = ? WHERE id = ?', ['Test 1B', 1]); + await Future.delayed(Duration(milliseconds: 100)); + await db.execute('DELETE FROM todos WHERE id = 1'); + + var results = await resultsPromise.timeout(Duration(milliseconds: 500)); + expect( + results, + equals([ + [TodoItem(id: 1, description: 'Test 1')], + [TodoItem(id: 1, description: 'Test 1B')], + [] + ])); + }); + }); +} diff --git a/packages/drift_sqlite_async/test/generated/database.dart b/packages/drift_sqlite_async/test/generated/database.dart new file mode 100644 index 0000000..e955c3d --- /dev/null +++ b/packages/drift_sqlite_async/test/generated/database.dart @@ -0,0 +1,21 @@ +import 'package:drift/drift.dart'; +import 'package:drift_sqlite_async/drift_sqlite_async.dart'; +import 'package:sqlite_async/sqlite_async.dart'; + +part 'database.g.dart'; + +class TodoItems extends Table { + @override + String get tableName => 'todos'; + + IntColumn get id => integer().autoIncrement()(); + TextColumn get description => text()(); +} + +@DriftDatabase(tables: [TodoItems]) +class TodoDatabase extends _$TodoDatabase { + TodoDatabase(SqliteConnection db) : super(SqliteAsyncDriftConnection(db)); + + @override + int get schemaVersion => 1; +} diff --git a/packages/drift_sqlite_async/test/generated/database.g.dart b/packages/drift_sqlite_async/test/generated/database.g.dart new file mode 100644 index 0000000..2572c32 --- /dev/null +++ b/packages/drift_sqlite_async/test/generated/database.g.dart @@ -0,0 +1,189 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class $TodoItemsTable extends TodoItems + with TableInfo<$TodoItemsTable, TodoItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $TodoItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', aliasedName, false, + hasAutoIncrement: true, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultConstraints: + GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); + static const VerificationMeta _descriptionMeta = + const VerificationMeta('description'); + @override + late final GeneratedColumn description = GeneratedColumn( + 'description', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + @override + List get $columns => [id, description]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'todos'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } else if (isInserting) { + context.missing(_descriptionMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + TodoItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return TodoItem( + id: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}id'])!, + description: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}description'])!, + ); + } + + @override + $TodoItemsTable createAlias(String alias) { + return $TodoItemsTable(attachedDatabase, alias); + } +} + +class TodoItem extends DataClass implements Insertable { + final int id; + final String description; + const TodoItem({required this.id, required this.description}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['description'] = Variable(description); + return map; + } + + TodoItemsCompanion toCompanion(bool nullToAbsent) { + return TodoItemsCompanion( + id: Value(id), + description: Value(description), + ); + } + + factory TodoItem.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return TodoItem( + id: serializer.fromJson(json['id']), + description: serializer.fromJson(json['description']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'description': serializer.toJson(description), + }; + } + + TodoItem copyWith({int? id, String? description}) => TodoItem( + id: id ?? this.id, + description: description ?? this.description, + ); + @override + String toString() { + return (StringBuffer('TodoItem(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, description); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TodoItem && + other.id == this.id && + other.description == this.description); +} + +class TodoItemsCompanion extends UpdateCompanion { + final Value id; + final Value description; + const TodoItemsCompanion({ + this.id = const Value.absent(), + this.description = const Value.absent(), + }); + TodoItemsCompanion.insert({ + this.id = const Value.absent(), + required String description, + }) : description = Value(description); + static Insertable custom({ + Expression? id, + Expression? description, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (description != null) 'description': description, + }); + } + + TodoItemsCompanion copyWith({Value? id, Value? description}) { + return TodoItemsCompanion( + id: id ?? this.id, + description: description ?? this.description, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (description.present) { + map['description'] = Variable(description.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('TodoItemsCompanion(') + ..write('id: $id, ') + ..write('description: $description') + ..write(')')) + .toString(); + } +} + +abstract class _$TodoDatabase extends GeneratedDatabase { + _$TodoDatabase(QueryExecutor e) : super(e); + late final $TodoItemsTable todoItems = $TodoItemsTable(this); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [todoItems]; +} diff --git a/packages/drift_sqlite_async/test/utils/test_utils.dart b/packages/drift_sqlite_async/test/utils/test_utils.dart new file mode 100644 index 0000000..1016f8d --- /dev/null +++ b/packages/drift_sqlite_async/test/utils/test_utils.dart @@ -0,0 +1,97 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:glob/glob.dart'; +import 'package:glob/list_local_fs.dart'; +import 'package:sqlite3/open.dart' as sqlite_open; +import 'package:sqlite_async/sqlite3.dart' as sqlite; +import 'package:sqlite_async/sqlite_async.dart'; +import 'package:test_api/src/backend/invoker.dart'; + +const defaultSqlitePath = 'libsqlite3.so.0'; +// const defaultSqlitePath = './sqlite-autoconf-3410100/.libs/libsqlite3.so.0'; + +class TestSqliteOpenFactory extends DefaultSqliteOpenFactory { + String sqlitePath; + + TestSqliteOpenFactory( + {required super.path, + super.sqliteOptions, + this.sqlitePath = defaultSqlitePath}); + + @override + sqlite.Database open(SqliteOpenOptions options) { + sqlite_open.open.overrideFor(sqlite_open.OperatingSystem.linux, () { + return DynamicLibrary.open(sqlitePath); + }); + final db = super.open(options); + + db.createFunction( + functionName: 'test_sleep', + argumentCount: const sqlite.AllowedArgumentCount(1), + function: (args) { + final millis = args[0] as int; + sleep(Duration(milliseconds: millis)); + return millis; + }, + ); + + db.createFunction( + functionName: 'test_connection_name', + argumentCount: const sqlite.AllowedArgumentCount(0), + function: (args) { + return Isolate.current.debugName; + }, + ); + + return db; + } +} + +SqliteOpenFactory testFactory({String? path}) { + return TestSqliteOpenFactory(path: path ?? dbPath()); +} + +Future setupDatabase({String? path}) async { + final db = SqliteDatabase.withFactory(testFactory(path: path)); + await db.initialize(); + return db; +} + +Future cleanDb({required String path}) async { + try { + await File(path).delete(); + } on PathNotFoundException { + // Not an issue + } + try { + await File("$path-shm").delete(); + } on PathNotFoundException { + // Not an issue + } + try { + await File("$path-wal").delete(); + } on PathNotFoundException { + // Not an issue + } +} + +List findSqliteLibraries() { + var glob = Glob('sqlite-*/.libs/libsqlite3.so'); + List sqlites = [ + 'libsqlite3.so.0', + for (var sqlite in glob.listSync()) sqlite.path + ]; + return sqlites; +} + +String dbPath() { + final test = Invoker.current!.liveTest; + var testName = test.test.name; + var testShortName = + testName.replaceAll(RegExp(r'[\s\./]'), '_').toLowerCase(); + var dbName = "test-db/$testShortName.db"; + Directory("test-db").createSync(recursive: false); + return dbName; +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..96dc219 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,325 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + ansi_styles: + dependency: transitive + description: + name: ansi_styles + sha256: "9c656cc12b3c27b17dd982b2cc5c0cfdfbdabd7bc8f3ae5e8542d9867b47ce8a" + url: "https://pub.dev" + source: hosted + version: "0.3.2+1" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + cli_launcher: + dependency: transitive + description: + name: cli_launcher + sha256: "5e7e0282b79e8642edd6510ee468ae2976d847a0a29b3916e85f5fa1bfe24005" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + conventional_commit: + dependency: transitive + description: + name: conventional_commit + sha256: dec15ad1118f029c618651a4359eb9135d8b88f761aa24e4016d061cd45948f2 + url: "https://pub.dev" + source: hosted + version: "0.6.0+1" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + http: + dependency: transitive + description: + name: http + sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + url: "https://pub.dev" + source: hosted + version: "1.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + melos: + dependency: "direct dev" + description: + name: melos + sha256: "7266e9fc9fee5f4a0c075e5cec375c00736dfc944358f533b740b93b3d8d681e" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + meta: + dependency: transitive + description: + name: meta + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" + source: hosted + version: "1.12.0" + mustache_template: + dependency: transitive + description: + name: mustache_template + sha256: a46e26f91445bfb0b60519be280555b06792460b27b19e2b19ad5b9740df5d1c + url: "https://pub.dev" + source: hosted + version: "2.0.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + prompts: + dependency: transitive + description: + name: prompts + sha256: "3773b845e85a849f01e793c4fc18a45d52d7783b4cb6c0569fad19f9d0a774a1" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pub_updater: + dependency: transitive + description: + name: pub_updater + sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + pubspec: + dependency: transitive + description: + name: pubspec + sha256: f534a50a2b4d48dc3bc0ec147c8bd7c304280fff23b153f3f11803c4d49d927e + url: "https://pub.dev" + source: hosted + version: "2.3.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" + source: hosted + version: "0.7.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uri: + dependency: transitive + description: + name: uri + sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + web: + dependency: transitive + description: + name: web + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + url: "https://pub.dev" + source: hosted + version: "0.4.2" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: "1579d4a0340a83cf9e4d580ea51a16329c916973bffd5bd4b45e911b25d46bfd" + url: "https://pub.dev" + source: hosted + version: "2.1.1" +sdks: + dart: ">=3.2.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..e467d25 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,6 @@ +name: drift_sqlite_async_monorepo + +environment: + sdk: ">=3.0.0 <4.0.0" +dev_dependencies: + melos: ^4.1.0