diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7a098a3..356835d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,11 +21,11 @@ jobs: - uses: dart-lang/setup-dart@v1 - - name: Install dependencies - run: dart pub get + - name: Install Melos + run: dart pub global activate melos - - name: Compile WebWorker - run: dart compile js -o assets/db_worker.js -O0 lib/src/web/worker/worker.dart + - name: Install dependencies + run: melos prepare - name: Set tag name id: tag diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index f805f55..85c9c02 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,18 +12,20 @@ jobs: - uses: actions/checkout@v3 - uses: dart-lang/setup-dart@v1 + - name: Install Melos + run: dart pub global activate melos - name: Install dependencies - run: dart pub get + run: melos prepare - name: Check formatting - run: dart format --output=none --set-exit-if-changed . + run: melos format:check:packages - name: Lint - run: dart analyze + run: melos analyze:packages - name: Publish dry-run - run: dart pub publish --dry-run + run: melos publish --dry-run --yes - name: Check publish score run: | dart pub global activate pana - dart pub global run pana --no-warning --exit-code-threshold 0 + melos analyze:packages:pana test: runs-on: ubuntu-latest @@ -51,18 +53,17 @@ jobs: with: sdk: ${{ matrix.dart_sdk }} + - name: Install Melos + run: dart pub global activate melos + - name: Install dependencies - run: dart pub get + run: melos prepare - name: Install SQLite run: | ./scripts/install_sqlite.sh ${{ matrix.sqlite_version }} ${{ matrix.sqlite_url }} - mkdir -p assets && curl -LJ https://github.com/simolus3/sqlite3.dart/releases/download/sqlite3-2.4.3/sqlite3.wasm -o assets/sqlite3.wasm - - - name: Compile WebWorker - run: dart compile js -o assets/db_worker.js -O0 lib/src/web/worker/worker.dart - name: Run Tests run: | - export LD_LIBRARY_PATH=./sqlite-autoconf-${{ matrix.sqlite_version }}/.libs - dart test -p vm,chrome + export LD_LIBRARY_PATH=$(pwd)/sqlite-autoconf-${{ matrix.sqlite_version }}/.libs + melos test diff --git a/.gitignore b/.gitignore index 271119a..71ce878 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ assets test-db sqlite-autoconf-* doc +*.iml build diff --git a/DEVELOPING.md b/DEVELOPING.md index d66a664..ff12dec 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -1,13 +1,5 @@ # Developing Instructions -## Testing - -Running tests for the `web` platform requires some preparation to be executed. The `sqlite3.wasm` and `db_worker.js` files need to be available in the Git ignored `./assets` folder. - -See the [test action](./.github/workflows/test.yaml) for the latest steps. - -On your local machine run the commands from the `Install SQLite`, `Compile WebWorker` and `Run Tests` steps. - ## Releases Web worker files are compiled and uploaded to draft Github releases whenever tags matching `v*` are pushed. These tags are created when versioning. Releases should be manually finalized and published when releasing new package versions. diff --git a/README.md b/README.md index 2649914..dbb27aa 100644 --- a/README.md +++ b/README.md @@ -2,100 +2,15 @@ High-performance asynchronous interface for SQLite on Dart & Flutter. -[SQLite](https://www.sqlite.org/) is small, fast, has a lot of built-in functionality, and works -great as an in-app database. However, SQLite is designed for many different use cases, and requires -some configuration for optimal performance as an in-app database. - -The [sqlite3](https://pub.dev/packages/sqlite3) Dart bindings are great for direct synchronous access -to a SQLite database, but leaves the configuration up to the developer. - -This library wraps the bindings and configures the database with a good set of defaults, with -all database calls being asynchronous to avoid blocking the UI, while still providing direct SQL -query access. - -## Features - -- All operations are asynchronous by default - does not block the main isolate. -- Watch a query to automatically re-run on changes to the underlying data. -- Concurrent transactions supported by default - one write transaction and many multiple read transactions. -- Uses WAL mode for fast writes and running read transactions concurrently with a write transaction. -- Direct synchronous access in an isolate is supported for performance-sensitive use cases. -- Automatically convert query args to JSON where applicable, making JSON1 operations simple. -- Direct SQL queries - no wrapper classes or code generation required. - -See this [blog post](https://www.powersync.co/blog/sqlite-optimizations-for-ultra-high-performance), -explaining why these features are important for using SQLite. - -## Installation - -```sh -dart pub add sqlite_async -``` - -For flutter applications, additionally add `sqlite3_flutter_libs` to include the native SQLite -library. - -For other platforms, see the [sqlite3 package docs](https://pub.dev/packages/sqlite3#supported-platforms). - -Web is currently not supported. +| package | build | pub | likes | popularity | pub points | +|----------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------| ------- | ------- | +| sqlite_async | [![build](https://github.com/powersync-ja/sqlite_async.dart/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/powersync-ja/sqlite_async.dart/actions?query=workflow%3Atest) | [![pub package](https://img.shields.io/pub/v/sqlite_async.svg)](https://pub.dev/packages/sqlite_async) | [![likes](https://img.shields.io/pub/likes/powersync?logo=dart)](https://pub.dev/packages/sqlite_async/score) | [![popularity](https://img.shields.io/pub/popularity/sqlite_async?logo=dart)](https://pub.dev/packages/sqlite_async/score) | [![pub points](https://img.shields.io/pub/points/sqlite_async?logo=dart)](https://pub.dev/packages/sqlite_async/score) +| drift_sqlite_async | [![build](https://github.com/powersync-ja/sqlite_async.dart/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/powersync-ja/sqlite_async/actions?query=workflow%3Atest) | [![pub package](https://img.shields.io/pub/v/drift_sqlite_async.svg)](https://pub.dev/packages/drift_sqlite_async) | [![likes](https://img.shields.io/pub/likes/drift_sqlite_async?logo=dart)](https://pub.dev/packages/drift_sqlite_async/score) | [![popularity](https://img.shields.io/pub/popularity/drift_sqlite_async?logo=dart)](https://pub.dev/packages/drift_sqlite_async/score) | [![pub points](https://img.shields.io/pub/points/drift_sqlite_async?logo=dart)](https://pub.dev/packages/drift_sqlite_async/score) ## Getting Started -```dart -import 'package:sqlite_async/sqlite_async.dart'; - -final migrations = SqliteMigrations() - ..add(SqliteMigration(1, (tx) async { - await tx.execute( - 'CREATE TABLE test_data(id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT)'); - })); - -void main() async { - final db = SqliteDatabase(path: 'test.db'); - await migrations.migrate(db); - - // Use execute() or executeBatch() for INSERT/UPDATE/DELETE statements - await db.executeBatch('INSERT INTO test_data(data) values(?)', [ - ['Test1'], - ['Test2'] - ]); - - // Use getAll(), get() or getOptional() for SELECT statements - var results = await db.getAll('SELECT * FROM test_data'); - print('Results: $results'); - - // Combine multiple statements into a single write transaction for: - // 1. Atomic persistence (all updates are either applied or rolled back). - // 2. Improved throughput. - await db.writeTransaction((tx) async { - await tx.execute('INSERT INTO test_data(data) values(?)', ['Test3']); - await tx.execute('INSERT INTO test_data(data) values(?)', ['Test4']); - }); - - await db.close(); -} -``` - -# Web - -Note: Web support is currently in Beta. - -Web support requires Sqlite3 WASM and web worker Javascript files to be accessible via configurable URIs. - -Default URIs are shown in the example below. URIs only need to be specified if they differ from default values. - -The compiled web worker files can be found in our Github [releases](https://github.com/powersync-ja/sqlite_async.dart/releases) -The `sqlite3.wasm` asset can be found [here](https://github.com/simolus3/sqlite3.dart/releases) - -Setup - -```Dart -import 'package:sqlite_async/sqlite_async.dart'; +This monorepo uses [melos](https://melos.invertase.dev/) to handle command and package management. -final db = SqliteDatabase( - path: 'test.db', - options: SqliteOptions( - webSqliteOptions: WebSqliteOptions( - wasmUri: 'sqlite3.wasm', workerUri: 'db_worker.js'))); +To configure the monorepo for development run `melos prepare` after cloning. -``` +For detailed usage, check out the inner [sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/sqlite_async) and [drift_sqlite_async](https://github.com/powersync-ja/sqlite_async.dart/tree/main/packages/drift_sqlite_async) packages. diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 0000000..19d45ad --- /dev/null +++ b/melos.yaml @@ -0,0 +1,46 @@ +name: sqlite_async_monorepo + +packages: + - packages/** + +scripts: + prepare: melos bootstrap && melos prepare:compile:webworker && melos prepare:sqlite:wasm + + prepare:compile:webworker: + description: Compile Javascript web worker distributable + run: dart compile js -o assets/db_worker.js -O0 packages/sqlite_async/lib/src/web/worker/worker.dart + + prepare:sqlite:wasm: + description: Download SQLite3 WASM binary + run: dart run ./scripts/sqlite3_wasm_download.dart + + 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 + + analyze:packages:pana: + description: Analyze Dart packages with Pana + exec: dart pub global run pana --no-warning --exit-code-threshold 0 + packageFilters: + noPrivate: true + + test: + description: Run tests in a specific package. + run: dart test -p chrome,vm + 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.yaml b/packages/drift_sqlite_async/pubspec.yaml new file mode 100644 index 0000000..f6a780f --- /dev/null +++ b/packages/drift_sqlite_async/pubspec.yaml @@ -0,0 +1,25 @@ +name: drift_sqlite_async +version: 0.1.0-alpha.1 +homepage: https://github.com/powersync-ja/sqlite_async.dart +repository: https://github.com/powersync-ja/sqlite_async.dart +description: Use Drift with a sqlite_async database, allowing both to be used in the same application. + +topics: + - drift + - sqlite + - async + - sql + - flutter + +environment: + sdk: ">=3.0.0 <4.0.0" +dependencies: + drift: ^2.15.0 + sqlite_async: ^0.8.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/pubspec_overrides.yaml b/packages/drift_sqlite_async/pubspec_overrides.yaml new file mode 100644 index 0000000..6048d57 --- /dev/null +++ b/packages/drift_sqlite_async/pubspec_overrides.yaml @@ -0,0 +1,4 @@ +# melos_managed_dependency_overrides: sqlite_async +dependency_overrides: + sqlite_async: + path: ../sqlite_async 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..503604f --- /dev/null +++ b/packages/drift_sqlite_async/test/basic_test.dart @@ -0,0 +1,214 @@ +// TODO +@TestOn('!browser') +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..ed901cf --- /dev/null +++ b/packages/drift_sqlite_async/test/db_test.dart @@ -0,0 +1,97 @@ +// TODO +@TestOn('!browser') +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..1128c91 --- /dev/null +++ b/packages/drift_sqlite_async/test/utils/test_utils.dart @@ -0,0 +1,98 @@ +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/sqlite3_common.dart'; +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 + CommonDatabase 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; + } +} + +DefaultSqliteOpenFactory 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/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/sqlite_async/CHANGELOG.md diff --git a/packages/sqlite_async/LICENSE b/packages/sqlite_async/LICENSE new file mode 100644 index 0000000..53316b9 --- /dev/null +++ b/packages/sqlite_async/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 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/sqlite_async/README.md b/packages/sqlite_async/README.md new file mode 100644 index 0000000..2649914 --- /dev/null +++ b/packages/sqlite_async/README.md @@ -0,0 +1,101 @@ +# sqlite_async + +High-performance asynchronous interface for SQLite on Dart & Flutter. + +[SQLite](https://www.sqlite.org/) is small, fast, has a lot of built-in functionality, and works +great as an in-app database. However, SQLite is designed for many different use cases, and requires +some configuration for optimal performance as an in-app database. + +The [sqlite3](https://pub.dev/packages/sqlite3) Dart bindings are great for direct synchronous access +to a SQLite database, but leaves the configuration up to the developer. + +This library wraps the bindings and configures the database with a good set of defaults, with +all database calls being asynchronous to avoid blocking the UI, while still providing direct SQL +query access. + +## Features + +- All operations are asynchronous by default - does not block the main isolate. +- Watch a query to automatically re-run on changes to the underlying data. +- Concurrent transactions supported by default - one write transaction and many multiple read transactions. +- Uses WAL mode for fast writes and running read transactions concurrently with a write transaction. +- Direct synchronous access in an isolate is supported for performance-sensitive use cases. +- Automatically convert query args to JSON where applicable, making JSON1 operations simple. +- Direct SQL queries - no wrapper classes or code generation required. + +See this [blog post](https://www.powersync.co/blog/sqlite-optimizations-for-ultra-high-performance), +explaining why these features are important for using SQLite. + +## Installation + +```sh +dart pub add sqlite_async +``` + +For flutter applications, additionally add `sqlite3_flutter_libs` to include the native SQLite +library. + +For other platforms, see the [sqlite3 package docs](https://pub.dev/packages/sqlite3#supported-platforms). + +Web is currently not supported. + +## Getting Started + +```dart +import 'package:sqlite_async/sqlite_async.dart'; + +final migrations = SqliteMigrations() + ..add(SqliteMigration(1, (tx) async { + await tx.execute( + 'CREATE TABLE test_data(id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT)'); + })); + +void main() async { + final db = SqliteDatabase(path: 'test.db'); + await migrations.migrate(db); + + // Use execute() or executeBatch() for INSERT/UPDATE/DELETE statements + await db.executeBatch('INSERT INTO test_data(data) values(?)', [ + ['Test1'], + ['Test2'] + ]); + + // Use getAll(), get() or getOptional() for SELECT statements + var results = await db.getAll('SELECT * FROM test_data'); + print('Results: $results'); + + // Combine multiple statements into a single write transaction for: + // 1. Atomic persistence (all updates are either applied or rolled back). + // 2. Improved throughput. + await db.writeTransaction((tx) async { + await tx.execute('INSERT INTO test_data(data) values(?)', ['Test3']); + await tx.execute('INSERT INTO test_data(data) values(?)', ['Test4']); + }); + + await db.close(); +} +``` + +# Web + +Note: Web support is currently in Beta. + +Web support requires Sqlite3 WASM and web worker Javascript files to be accessible via configurable URIs. + +Default URIs are shown in the example below. URIs only need to be specified if they differ from default values. + +The compiled web worker files can be found in our Github [releases](https://github.com/powersync-ja/sqlite_async.dart/releases) +The `sqlite3.wasm` asset can be found [here](https://github.com/simolus3/sqlite3.dart/releases) + +Setup + +```Dart +import 'package:sqlite_async/sqlite_async.dart'; + +final db = SqliteDatabase( + path: 'test.db', + options: SqliteOptions( + webSqliteOptions: WebSqliteOptions( + wasmUri: 'sqlite3.wasm', workerUri: 'db_worker.js'))); + +``` diff --git a/example/README.md b/packages/sqlite_async/example/README.md similarity index 100% rename from example/README.md rename to packages/sqlite_async/example/README.md diff --git a/example/basic_example.dart b/packages/sqlite_async/example/basic_example.dart similarity index 100% rename from example/basic_example.dart rename to packages/sqlite_async/example/basic_example.dart diff --git a/example/custom_functions_example.dart b/packages/sqlite_async/example/custom_functions_example.dart similarity index 100% rename from example/custom_functions_example.dart rename to packages/sqlite_async/example/custom_functions_example.dart diff --git a/example/json_example.dart b/packages/sqlite_async/example/json_example.dart similarity index 100% rename from example/json_example.dart rename to packages/sqlite_async/example/json_example.dart diff --git a/example/linux_cli_example.dart b/packages/sqlite_async/example/linux_cli_example.dart similarity index 100% rename from example/linux_cli_example.dart rename to packages/sqlite_async/example/linux_cli_example.dart diff --git a/example/migration_example.dart b/packages/sqlite_async/example/migration_example.dart similarity index 100% rename from example/migration_example.dart rename to packages/sqlite_async/example/migration_example.dart diff --git a/example/watch_example.dart b/packages/sqlite_async/example/watch_example.dart similarity index 100% rename from example/watch_example.dart rename to packages/sqlite_async/example/watch_example.dart diff --git a/lib/mutex.dart b/packages/sqlite_async/lib/mutex.dart similarity index 100% rename from lib/mutex.dart rename to packages/sqlite_async/lib/mutex.dart diff --git a/lib/sqlite3.dart b/packages/sqlite_async/lib/sqlite3.dart similarity index 100% rename from lib/sqlite3.dart rename to packages/sqlite_async/lib/sqlite3.dart diff --git a/lib/sqlite3_common.dart b/packages/sqlite_async/lib/sqlite3_common.dart similarity index 100% rename from lib/sqlite3_common.dart rename to packages/sqlite_async/lib/sqlite3_common.dart diff --git a/lib/sqlite3_wasm.dart b/packages/sqlite_async/lib/sqlite3_wasm.dart similarity index 100% rename from lib/sqlite3_wasm.dart rename to packages/sqlite_async/lib/sqlite3_wasm.dart diff --git a/lib/sqlite3_web.dart b/packages/sqlite_async/lib/sqlite3_web.dart similarity index 100% rename from lib/sqlite3_web.dart rename to packages/sqlite_async/lib/sqlite3_web.dart diff --git a/lib/sqlite3_web_worker.dart b/packages/sqlite_async/lib/sqlite3_web_worker.dart similarity index 100% rename from lib/sqlite3_web_worker.dart rename to packages/sqlite_async/lib/sqlite3_web_worker.dart diff --git a/lib/sqlite_async.dart b/packages/sqlite_async/lib/sqlite_async.dart similarity index 100% rename from lib/sqlite_async.dart rename to packages/sqlite_async/lib/sqlite_async.dart diff --git a/lib/src/common/abstract_open_factory.dart b/packages/sqlite_async/lib/src/common/abstract_open_factory.dart similarity index 100% rename from lib/src/common/abstract_open_factory.dart rename to packages/sqlite_async/lib/src/common/abstract_open_factory.dart diff --git a/lib/src/common/connection/sync_sqlite_connection.dart b/packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart similarity index 100% rename from lib/src/common/connection/sync_sqlite_connection.dart rename to packages/sqlite_async/lib/src/common/connection/sync_sqlite_connection.dart diff --git a/lib/src/common/isolate_connection_factory.dart b/packages/sqlite_async/lib/src/common/isolate_connection_factory.dart similarity index 100% rename from lib/src/common/isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/common/isolate_connection_factory.dart diff --git a/lib/src/common/mutex.dart b/packages/sqlite_async/lib/src/common/mutex.dart similarity index 100% rename from lib/src/common/mutex.dart rename to packages/sqlite_async/lib/src/common/mutex.dart diff --git a/lib/src/common/port_channel.dart b/packages/sqlite_async/lib/src/common/port_channel.dart similarity index 100% rename from lib/src/common/port_channel.dart rename to packages/sqlite_async/lib/src/common/port_channel.dart diff --git a/lib/src/common/sqlite_database.dart b/packages/sqlite_async/lib/src/common/sqlite_database.dart similarity index 100% rename from lib/src/common/sqlite_database.dart rename to packages/sqlite_async/lib/src/common/sqlite_database.dart diff --git a/lib/src/impl/isolate_connection_factory_impl.dart b/packages/sqlite_async/lib/src/impl/isolate_connection_factory_impl.dart similarity index 100% rename from lib/src/impl/isolate_connection_factory_impl.dart rename to packages/sqlite_async/lib/src/impl/isolate_connection_factory_impl.dart diff --git a/lib/src/impl/mutex_impl.dart b/packages/sqlite_async/lib/src/impl/mutex_impl.dart similarity index 100% rename from lib/src/impl/mutex_impl.dart rename to packages/sqlite_async/lib/src/impl/mutex_impl.dart diff --git a/lib/src/impl/open_factory_impl.dart b/packages/sqlite_async/lib/src/impl/open_factory_impl.dart similarity index 100% rename from lib/src/impl/open_factory_impl.dart rename to packages/sqlite_async/lib/src/impl/open_factory_impl.dart diff --git a/lib/src/impl/sqlite_database_impl.dart b/packages/sqlite_async/lib/src/impl/sqlite_database_impl.dart similarity index 100% rename from lib/src/impl/sqlite_database_impl.dart rename to packages/sqlite_async/lib/src/impl/sqlite_database_impl.dart diff --git a/lib/src/impl/stub_isolate_connection_factory.dart b/packages/sqlite_async/lib/src/impl/stub_isolate_connection_factory.dart similarity index 100% rename from lib/src/impl/stub_isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/impl/stub_isolate_connection_factory.dart diff --git a/lib/src/impl/stub_mutex.dart b/packages/sqlite_async/lib/src/impl/stub_mutex.dart similarity index 100% rename from lib/src/impl/stub_mutex.dart rename to packages/sqlite_async/lib/src/impl/stub_mutex.dart diff --git a/lib/src/impl/stub_sqlite_database.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart similarity index 100% rename from lib/src/impl/stub_sqlite_database.dart rename to packages/sqlite_async/lib/src/impl/stub_sqlite_database.dart diff --git a/lib/src/impl/stub_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/impl/stub_sqlite_open_factory.dart similarity index 100% rename from lib/src/impl/stub_sqlite_open_factory.dart rename to packages/sqlite_async/lib/src/impl/stub_sqlite_open_factory.dart diff --git a/lib/src/isolate_connection_factory.dart b/packages/sqlite_async/lib/src/isolate_connection_factory.dart similarity index 100% rename from lib/src/isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/isolate_connection_factory.dart diff --git a/lib/src/mutex.dart b/packages/sqlite_async/lib/src/mutex.dart similarity index 100% rename from lib/src/mutex.dart rename to packages/sqlite_async/lib/src/mutex.dart diff --git a/lib/src/native/database/connection_pool.dart b/packages/sqlite_async/lib/src/native/database/connection_pool.dart similarity index 100% rename from lib/src/native/database/connection_pool.dart rename to packages/sqlite_async/lib/src/native/database/connection_pool.dart diff --git a/lib/src/native/database/native_sqlite_connection_impl.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart similarity index 100% rename from lib/src/native/database/native_sqlite_connection_impl.dart rename to packages/sqlite_async/lib/src/native/database/native_sqlite_connection_impl.dart diff --git a/lib/src/native/database/native_sqlite_database.dart b/packages/sqlite_async/lib/src/native/database/native_sqlite_database.dart similarity index 100% rename from lib/src/native/database/native_sqlite_database.dart rename to packages/sqlite_async/lib/src/native/database/native_sqlite_database.dart diff --git a/lib/src/native/database/upstream_updates.dart b/packages/sqlite_async/lib/src/native/database/upstream_updates.dart similarity index 100% rename from lib/src/native/database/upstream_updates.dart rename to packages/sqlite_async/lib/src/native/database/upstream_updates.dart diff --git a/lib/src/native/native_isolate_connection_factory.dart b/packages/sqlite_async/lib/src/native/native_isolate_connection_factory.dart similarity index 100% rename from lib/src/native/native_isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/native/native_isolate_connection_factory.dart diff --git a/lib/src/native/native_isolate_mutex.dart b/packages/sqlite_async/lib/src/native/native_isolate_mutex.dart similarity index 100% rename from lib/src/native/native_isolate_mutex.dart rename to packages/sqlite_async/lib/src/native/native_isolate_mutex.dart diff --git a/lib/src/native/native_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/native/native_sqlite_open_factory.dart similarity index 100% rename from lib/src/native/native_sqlite_open_factory.dart rename to packages/sqlite_async/lib/src/native/native_sqlite_open_factory.dart diff --git a/lib/src/sqlite_connection.dart b/packages/sqlite_async/lib/src/sqlite_connection.dart similarity index 100% rename from lib/src/sqlite_connection.dart rename to packages/sqlite_async/lib/src/sqlite_connection.dart diff --git a/lib/src/sqlite_database.dart b/packages/sqlite_async/lib/src/sqlite_database.dart similarity index 100% rename from lib/src/sqlite_database.dart rename to packages/sqlite_async/lib/src/sqlite_database.dart diff --git a/lib/src/sqlite_migrations.dart b/packages/sqlite_async/lib/src/sqlite_migrations.dart similarity index 100% rename from lib/src/sqlite_migrations.dart rename to packages/sqlite_async/lib/src/sqlite_migrations.dart diff --git a/lib/src/sqlite_open_factory.dart b/packages/sqlite_async/lib/src/sqlite_open_factory.dart similarity index 100% rename from lib/src/sqlite_open_factory.dart rename to packages/sqlite_async/lib/src/sqlite_open_factory.dart diff --git a/lib/src/sqlite_options.dart b/packages/sqlite_async/lib/src/sqlite_options.dart similarity index 100% rename from lib/src/sqlite_options.dart rename to packages/sqlite_async/lib/src/sqlite_options.dart diff --git a/lib/src/sqlite_queries.dart b/packages/sqlite_async/lib/src/sqlite_queries.dart similarity index 100% rename from lib/src/sqlite_queries.dart rename to packages/sqlite_async/lib/src/sqlite_queries.dart diff --git a/lib/src/update_notification.dart b/packages/sqlite_async/lib/src/update_notification.dart similarity index 100% rename from lib/src/update_notification.dart rename to packages/sqlite_async/lib/src/update_notification.dart diff --git a/lib/src/utils.dart b/packages/sqlite_async/lib/src/utils.dart similarity index 100% rename from lib/src/utils.dart rename to packages/sqlite_async/lib/src/utils.dart diff --git a/lib/src/utils/database_utils.dart b/packages/sqlite_async/lib/src/utils/database_utils.dart similarity index 100% rename from lib/src/utils/database_utils.dart rename to packages/sqlite_async/lib/src/utils/database_utils.dart diff --git a/lib/src/utils/native_database_utils.dart b/packages/sqlite_async/lib/src/utils/native_database_utils.dart similarity index 100% rename from lib/src/utils/native_database_utils.dart rename to packages/sqlite_async/lib/src/utils/native_database_utils.dart diff --git a/lib/src/utils/shared_utils.dart b/packages/sqlite_async/lib/src/utils/shared_utils.dart similarity index 100% rename from lib/src/utils/shared_utils.dart rename to packages/sqlite_async/lib/src/utils/shared_utils.dart diff --git a/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart similarity index 100% rename from lib/src/web/database.dart rename to packages/sqlite_async/lib/src/web/database.dart diff --git a/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart similarity index 100% rename from lib/src/web/database/web_sqlite_database.dart rename to packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart diff --git a/lib/src/web/protocol.dart b/packages/sqlite_async/lib/src/web/protocol.dart similarity index 100% rename from lib/src/web/protocol.dart rename to packages/sqlite_async/lib/src/web/protocol.dart diff --git a/lib/src/web/web_isolate_connection_factory.dart b/packages/sqlite_async/lib/src/web/web_isolate_connection_factory.dart similarity index 100% rename from lib/src/web/web_isolate_connection_factory.dart rename to packages/sqlite_async/lib/src/web/web_isolate_connection_factory.dart diff --git a/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart similarity index 100% rename from lib/src/web/web_mutex.dart rename to packages/sqlite_async/lib/src/web/web_mutex.dart diff --git a/lib/src/web/web_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart similarity index 100% rename from lib/src/web/web_sqlite_open_factory.dart rename to packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart diff --git a/lib/src/web/worker/worker.dart b/packages/sqlite_async/lib/src/web/worker/worker.dart similarity index 100% rename from lib/src/web/worker/worker.dart rename to packages/sqlite_async/lib/src/web/worker/worker.dart diff --git a/lib/src/web/worker/worker_utils.dart b/packages/sqlite_async/lib/src/web/worker/worker_utils.dart similarity index 100% rename from lib/src/web/worker/worker_utils.dart rename to packages/sqlite_async/lib/src/web/worker/worker_utils.dart diff --git a/lib/utils.dart b/packages/sqlite_async/lib/utils.dart similarity index 100% rename from lib/utils.dart rename to packages/sqlite_async/lib/utils.dart diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml new file mode 100644 index 0000000..4523ae7 --- /dev/null +++ b/packages/sqlite_async/pubspec.yaml @@ -0,0 +1,41 @@ +name: sqlite_async +description: High-performance asynchronous interface for SQLite on Dart and Flutter. +version: 0.8.0 +repository: https://github.com/powersync-ja/sqlite_async.dart +environment: + sdk: ">=3.4.0 <4.0.0" + +topics: + - sqlite + - async + - sql + - flutter + +dependencies: + sqlite3: "^2.4.4" + sqlite3_web: ^0.1.2-wip + async: ^2.10.0 + collection: ^1.17.0 + mutex: ^3.1.0 + meta: ^1.10.0 + +dev_dependencies: + dcli: ^4.0.0 + js: ^0.6.7 + lints: ^3.0.0 + test: ^1.21.0 + test_api: ^0.7.0 + glob: ^2.1.1 + benchmarking: ^0.6.1 + shelf: ^1.4.1 + shelf_static: ^1.1.2 + stream_channel: ^2.1.2 + path: ^1.9.0 + +platforms: + android: + ios: + linux: + macos: + windows: + web: diff --git a/scripts/benchmark.dart b/packages/sqlite_async/scripts/benchmark.dart similarity index 100% rename from scripts/benchmark.dart rename to packages/sqlite_async/scripts/benchmark.dart diff --git a/test/basic_test.dart b/packages/sqlite_async/test/basic_test.dart similarity index 100% rename from test/basic_test.dart rename to packages/sqlite_async/test/basic_test.dart diff --git a/test/close_test.dart b/packages/sqlite_async/test/close_test.dart similarity index 100% rename from test/close_test.dart rename to packages/sqlite_async/test/close_test.dart diff --git a/test/isolate_test.dart b/packages/sqlite_async/test/isolate_test.dart similarity index 100% rename from test/isolate_test.dart rename to packages/sqlite_async/test/isolate_test.dart diff --git a/test/json1_test.dart b/packages/sqlite_async/test/json1_test.dart similarity index 100% rename from test/json1_test.dart rename to packages/sqlite_async/test/json1_test.dart diff --git a/test/migration_test.dart b/packages/sqlite_async/test/migration_test.dart similarity index 100% rename from test/migration_test.dart rename to packages/sqlite_async/test/migration_test.dart diff --git a/test/mutex_test.dart b/packages/sqlite_async/test/mutex_test.dart similarity index 100% rename from test/mutex_test.dart rename to packages/sqlite_async/test/mutex_test.dart diff --git a/test/native/basic_test.dart b/packages/sqlite_async/test/native/basic_test.dart similarity index 100% rename from test/native/basic_test.dart rename to packages/sqlite_async/test/native/basic_test.dart diff --git a/test/native/watch_test.dart b/packages/sqlite_async/test/native/watch_test.dart similarity index 100% rename from test/native/watch_test.dart rename to packages/sqlite_async/test/native/watch_test.dart diff --git a/test/server/asset_server.dart b/packages/sqlite_async/test/server/asset_server.dart similarity index 100% rename from test/server/asset_server.dart rename to packages/sqlite_async/test/server/asset_server.dart diff --git a/test/server/worker_server.dart b/packages/sqlite_async/test/server/worker_server.dart similarity index 58% rename from test/server/worker_server.dart rename to packages/sqlite_async/test/server/worker_server.dart index 313c414..30cffe9 100644 --- a/test/server/worker_server.dart +++ b/packages/sqlite_async/test/server/worker_server.dart @@ -1,44 +1,30 @@ import 'dart:io'; +import 'package:dcli/dcli.dart'; import 'package:path/path.dart' as p; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart' as io; import 'package:shelf_static/shelf_static.dart'; import 'package:stream_channel/stream_channel.dart'; -import 'package:test/test.dart'; import 'asset_server.dart'; Future hybridMain(StreamChannel channel) async { - final directory = Directory('./assets'); + final directory = p.normalize( + p.join(DartScript.self.pathToScriptDirectory, '../../../../assets')); - final sqliteOutputPath = p.join(directory.path, 'sqlite3.wasm'); + final sqliteOutputPath = p.join(directory, 'sqlite3.wasm'); if (!(await File(sqliteOutputPath).exists())) { throw AssertionError( 'sqlite3.wasm file should be present in the ./assets folder'); } - final workerPath = p.join(directory.path, 'db_worker.js'); - if (!(await File(workerPath).exists())) { - final process = await Process.run(Platform.executable, [ - 'compile', - 'js', - '-o', - workerPath, - '-O0', - 'lib/src/web/worker/worker.dart', - ]); - if (process.exitCode != 0) { - fail('Could not compile worker'); - } - } - final server = await HttpServer.bind('localhost', 0); final handler = const Pipeline() .addMiddleware(cors()) - .addHandler(createStaticHandler(directory.path)); + .addHandler(createStaticHandler(directory)); io.serveRequests(server, handler); channel.sink.add(server.port); diff --git a/test/utils/abstract_test_utils.dart b/packages/sqlite_async/test/utils/abstract_test_utils.dart similarity index 100% rename from test/utils/abstract_test_utils.dart rename to packages/sqlite_async/test/utils/abstract_test_utils.dart diff --git a/test/utils/native_test_utils.dart b/packages/sqlite_async/test/utils/native_test_utils.dart similarity index 100% rename from test/utils/native_test_utils.dart rename to packages/sqlite_async/test/utils/native_test_utils.dart diff --git a/test/utils/stub_test_utils.dart b/packages/sqlite_async/test/utils/stub_test_utils.dart similarity index 100% rename from test/utils/stub_test_utils.dart rename to packages/sqlite_async/test/utils/stub_test_utils.dart diff --git a/test/utils/test_utils_impl.dart b/packages/sqlite_async/test/utils/test_utils_impl.dart similarity index 100% rename from test/utils/test_utils_impl.dart rename to packages/sqlite_async/test/utils/test_utils_impl.dart diff --git a/test/utils/web_test_utils.dart b/packages/sqlite_async/test/utils/web_test_utils.dart similarity index 100% rename from test/utils/web_test_utils.dart rename to packages/sqlite_async/test/utils/web_test_utils.dart diff --git a/test/watch_test.dart b/packages/sqlite_async/test/watch_test.dart similarity index 100% rename from test/watch_test.dart rename to packages/sqlite_async/test/watch_test.dart diff --git a/test/web/watch_test.dart b/packages/sqlite_async/test/web/watch_test.dart similarity index 100% rename from test/web/watch_test.dart rename to packages/sqlite_async/test/web/watch_test.dart diff --git a/pubspec.yaml b/pubspec.yaml index 2e041ba..b00adf8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,40 +1,6 @@ -name: sqlite_async -description: High-performance asynchronous interface for SQLite on Dart and Flutter. -version: 0.8.0 -repository: https://github.com/powersync-ja/sqlite_async.dart +name: drift_sqlite_async_monorepo + environment: sdk: ">=3.4.0 <4.0.0" - -topics: - - sqlite - - async - - sql - - flutter - -dependencies: - sqlite3: "^2.4.4" - sqlite3_web: ^0.1.2-wip - async: ^2.10.0 - collection: ^1.17.0 - mutex: ^3.1.0 - meta: ^1.10.0 - dev_dependencies: - js: ^0.6.7 - lints: ^3.0.0 - test: ^1.21.0 - test_api: ^0.7.0 - glob: ^2.1.1 - benchmarking: ^0.6.1 - shelf: ^1.4.1 - shelf_static: ^1.1.2 - stream_channel: ^2.1.2 - path: ^1.9.0 - -platforms: - android: - ios: - linux: - macos: - windows: - web: + melos: ^4.1.0 diff --git a/scripts/sqlite3_wasm_download.dart b/scripts/sqlite3_wasm_download.dart new file mode 100644 index 0000000..28d91b4 --- /dev/null +++ b/scripts/sqlite3_wasm_download.dart @@ -0,0 +1,33 @@ +/// Downloads sqlite3.wasm +import 'dart:io'; + +final sqliteUrl = + 'https://github.com/simolus3/sqlite3.dart/releases/download/sqlite3-2.4.3/sqlite3.wasm'; + +void main() async { + // Create assets directory if it doesn't exist + final assetsDir = Directory('assets'); + if (!await assetsDir.exists()) { + await assetsDir.create(); + } + + final sqliteFilename = 'sqlite3.wasm'; + final sqlitePath = 'assets/$sqliteFilename'; + + // Download sqlite3.wasm + await downloadFile(sqliteUrl, sqlitePath); +} + +Future downloadFile(String url, String savePath) async { + print('Downloading: $url'); + var httpClient = HttpClient(); + var request = await httpClient.getUrl(Uri.parse(url)); + var response = await request.close(); + if (response.statusCode == HttpStatus.ok) { + var file = File(savePath); + await response.pipe(file.openWrite()); + } else { + print( + 'Failed to download file: ${response.statusCode} ${response.reasonPhrase}'); + } +}