mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'simolus3:develop' into develop
This commit is contained in:
commit
78387a3610
|
@ -0,0 +1,17 @@
|
|||
// #docregion user
|
||||
class User {
|
||||
final int id;
|
||||
final String name;
|
||||
|
||||
User(this.id, this.name);
|
||||
}
|
||||
// #enddocregion user
|
||||
|
||||
// #docregion userwithfriends
|
||||
class UserWithFriends {
|
||||
final User user;
|
||||
final List<User> friends;
|
||||
|
||||
UserWithFriends(this.user, {this.friends = const []});
|
||||
}
|
||||
// #enddocregion userwithfriends
|
|
@ -0,0 +1,22 @@
|
|||
-- #docregion users
|
||||
import 'row_class.dart'; --import for where the row class is defined
|
||||
|
||||
CREATE TABLE users (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL
|
||||
) WITH User; -- This tells drift to use the existing Dart class
|
||||
-- #enddocregion users
|
||||
|
||||
-- #docregion friends
|
||||
-- table to demonstrate a more complex select query below.
|
||||
-- also, remember to add the import for `UserWithFriends` to your drift file.
|
||||
CREATE TABLE friends (
|
||||
user_a INTEGER NOT NULL REFERENCES users(id),
|
||||
user_b INTEGER NOT NULL REFERENCES users(id),
|
||||
PRIMARY KEY (user_a, user_b)
|
||||
);
|
||||
|
||||
allFriendsOf WITH UserWithFriends: SELECT users.** AS user, LIST(
|
||||
SELECT * FROM users a INNER JOIN friends ON user_a = a.id WHERE user_b = users.id OR user_a = users.id
|
||||
) AS friends FROM users WHERE id = :id;
|
||||
-- #enddocregion friends
|
|
@ -0,0 +1,346 @@
|
|||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:drift_docs/snippets/modular/drift/row_class.dart' as i1;
|
||||
import 'package:drift_docs/snippets/modular/drift/with_existing.drift.dart'
|
||||
as i2;
|
||||
import 'package:drift/internal/modular.dart' as i3;
|
||||
|
||||
class Users extends i0.Table with i0.TableInfo<Users, i1.User> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Users(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||
late final i0.GeneratedColumn<int> id = i0.GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: 'NOT NULL PRIMARY KEY');
|
||||
static const i0.VerificationMeta _nameMeta =
|
||||
const i0.VerificationMeta('name');
|
||||
late final i0.GeneratedColumn<String> name = i0.GeneratedColumn<String>(
|
||||
'name', aliasedName, false,
|
||||
type: i0.DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL');
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [id, name];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'users';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(i0.Insertable<i1.User> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
if (data.containsKey('name')) {
|
||||
context.handle(
|
||||
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_nameMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
i1.User map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i1.User(
|
||||
attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Users createAlias(String alias) {
|
||||
return Users(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class UsersCompanion extends i0.UpdateCompanion<i1.User> {
|
||||
final i0.Value<int> id;
|
||||
final i0.Value<String> name;
|
||||
const UsersCompanion({
|
||||
this.id = const i0.Value.absent(),
|
||||
this.name = const i0.Value.absent(),
|
||||
});
|
||||
UsersCompanion.insert({
|
||||
this.id = const i0.Value.absent(),
|
||||
required String name,
|
||||
}) : name = i0.Value(name);
|
||||
static i0.Insertable<i1.User> custom({
|
||||
i0.Expression<int>? id,
|
||||
i0.Expression<String>? name,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (name != null) 'name': name,
|
||||
});
|
||||
}
|
||||
|
||||
i2.UsersCompanion copyWith({i0.Value<int>? id, i0.Value<String>? name}) {
|
||||
return i2.UsersCompanion(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = i0.Variable<int>(id.value);
|
||||
}
|
||||
if (name.present) {
|
||||
map['name'] = i0.Variable<String>(name.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('UsersCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class Friends extends i0.Table with i0.TableInfo<Friends, i2.Friend> {
|
||||
@override
|
||||
final i0.GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Friends(this.attachedDatabase, [this._alias]);
|
||||
static const i0.VerificationMeta _userAMeta =
|
||||
const i0.VerificationMeta('userA');
|
||||
late final i0.GeneratedColumn<int> userA = i0.GeneratedColumn<int>(
|
||||
'user_a', aliasedName, false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL REFERENCES users(id)');
|
||||
static const i0.VerificationMeta _userBMeta =
|
||||
const i0.VerificationMeta('userB');
|
||||
late final i0.GeneratedColumn<int> userB = i0.GeneratedColumn<int>(
|
||||
'user_b', aliasedName, false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL REFERENCES users(id)');
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [userA, userB];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'friends';
|
||||
@override
|
||||
i0.VerificationContext validateIntegrity(i0.Insertable<i2.Friend> instance,
|
||||
{bool isInserting = false}) {
|
||||
final context = i0.VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('user_a')) {
|
||||
context.handle(
|
||||
_userAMeta, userA.isAcceptableOrUnknown(data['user_a']!, _userAMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_userAMeta);
|
||||
}
|
||||
if (data.containsKey('user_b')) {
|
||||
context.handle(
|
||||
_userBMeta, userB.isAcceptableOrUnknown(data['user_b']!, _userBMeta));
|
||||
} else if (isInserting) {
|
||||
context.missing(_userBMeta);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
@override
|
||||
Set<i0.GeneratedColumn> get $primaryKey => {userA, userB};
|
||||
@override
|
||||
i2.Friend map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return i2.Friend(
|
||||
userA: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}user_a'])!,
|
||||
userB: attachedDatabase.typeMapping
|
||||
.read(i0.DriftSqlType.int, data['${effectivePrefix}user_b'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Friends createAlias(String alias) {
|
||||
return Friends(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> get customConstraints => const ['PRIMARY KEY(user_a, user_b)'];
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class Friend extends i0.DataClass implements i0.Insertable<i2.Friend> {
|
||||
final int userA;
|
||||
final int userB;
|
||||
const Friend({required this.userA, required this.userB});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
map['user_a'] = i0.Variable<int>(userA);
|
||||
map['user_b'] = i0.Variable<int>(userB);
|
||||
return map;
|
||||
}
|
||||
|
||||
i2.FriendsCompanion toCompanion(bool nullToAbsent) {
|
||||
return i2.FriendsCompanion(
|
||||
userA: i0.Value(userA),
|
||||
userB: i0.Value(userB),
|
||||
);
|
||||
}
|
||||
|
||||
factory Friend.fromJson(Map<String, dynamic> json,
|
||||
{i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return Friend(
|
||||
userA: serializer.fromJson<int>(json['user_a']),
|
||||
userB: serializer.fromJson<int>(json['user_b']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'user_a': serializer.toJson<int>(userA),
|
||||
'user_b': serializer.toJson<int>(userB),
|
||||
};
|
||||
}
|
||||
|
||||
i2.Friend copyWith({int? userA, int? userB}) => i2.Friend(
|
||||
userA: userA ?? this.userA,
|
||||
userB: userB ?? this.userB,
|
||||
);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('Friend(')
|
||||
..write('userA: $userA, ')
|
||||
..write('userB: $userB')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(userA, userB);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is i2.Friend &&
|
||||
other.userA == this.userA &&
|
||||
other.userB == this.userB);
|
||||
}
|
||||
|
||||
class FriendsCompanion extends i0.UpdateCompanion<i2.Friend> {
|
||||
final i0.Value<int> userA;
|
||||
final i0.Value<int> userB;
|
||||
final i0.Value<int> rowid;
|
||||
const FriendsCompanion({
|
||||
this.userA = const i0.Value.absent(),
|
||||
this.userB = const i0.Value.absent(),
|
||||
this.rowid = const i0.Value.absent(),
|
||||
});
|
||||
FriendsCompanion.insert({
|
||||
required int userA,
|
||||
required int userB,
|
||||
this.rowid = const i0.Value.absent(),
|
||||
}) : userA = i0.Value(userA),
|
||||
userB = i0.Value(userB);
|
||||
static i0.Insertable<i2.Friend> custom({
|
||||
i0.Expression<int>? userA,
|
||||
i0.Expression<int>? userB,
|
||||
i0.Expression<int>? rowid,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (userA != null) 'user_a': userA,
|
||||
if (userB != null) 'user_b': userB,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
i2.FriendsCompanion copyWith(
|
||||
{i0.Value<int>? userA, i0.Value<int>? userB, i0.Value<int>? rowid}) {
|
||||
return i2.FriendsCompanion(
|
||||
userA: userA ?? this.userA,
|
||||
userB: userB ?? this.userB,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, i0.Expression>{};
|
||||
if (userA.present) {
|
||||
map['user_a'] = i0.Variable<int>(userA.value);
|
||||
}
|
||||
if (userB.present) {
|
||||
map['user_b'] = i0.Variable<int>(userB.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = i0.Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('FriendsCompanion(')
|
||||
..write('userA: $userA, ')
|
||||
..write('userB: $userB, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class WithExistingDrift extends i3.ModularAccessor {
|
||||
WithExistingDrift(i0.GeneratedDatabase db) : super(db);
|
||||
i0.Selectable<i1.UserWithFriends> allFriendsOf(int id) {
|
||||
return customSelect(
|
||||
'SELECT"users"."id" AS "nested_0.id", "users"."name" AS "nested_0.name", users.id AS "\$n_0", users.id AS "\$n_1" FROM users WHERE id = ?1',
|
||||
variables: [
|
||||
i0.Variable<int>(id)
|
||||
],
|
||||
readsFrom: {
|
||||
users,
|
||||
friends,
|
||||
}).asyncMap((i0.QueryRow row) async => i1.UserWithFriends(
|
||||
await users.mapFromRow(row, tablePrefix: 'nested_0'),
|
||||
friends: await customSelect(
|
||||
'SELECT * FROM users AS a INNER JOIN friends ON user_a = a.id WHERE user_b = ?1 OR user_a = ?2',
|
||||
variables: [
|
||||
i0.Variable<int>(row.read('\$n_0')),
|
||||
i0.Variable<int>(row.read('\$n_1'))
|
||||
],
|
||||
readsFrom: {
|
||||
users,
|
||||
friends,
|
||||
})
|
||||
.map((i0.QueryRow row) => i1.User(
|
||||
row.read<int>('id'),
|
||||
row.read<String>('name'),
|
||||
))
|
||||
.get(),
|
||||
));
|
||||
}
|
||||
|
||||
i2.Users get users => this.resultSet<i2.Users>('users');
|
||||
i2.Friends get friends => this.resultSet<i2.Friends>('friends');
|
||||
}
|
|
@ -25,6 +25,36 @@ extension FindById<Table extends HasResultSet, Row>
|
|||
}
|
||||
// #enddocregion findById
|
||||
|
||||
// #docregion updateTitle
|
||||
extension UpdateTitle on DatabaseConnectionUser {
|
||||
Future<Row?> updateTitle<T extends TableInfo<Table, Row>, Row>(
|
||||
T table, int id, String newTitle) async {
|
||||
final columnsByName = table.columnsByName;
|
||||
final stmt = update(table)
|
||||
..where((tbl) {
|
||||
final idColumn = columnsByName['id'];
|
||||
|
||||
if (idColumn == null) {
|
||||
throw ArgumentError.value(
|
||||
this, 'this', 'Must be a table with an id column');
|
||||
}
|
||||
|
||||
if (idColumn.type != DriftSqlType.int) {
|
||||
throw ArgumentError('Column `id` is not an integer');
|
||||
}
|
||||
|
||||
return idColumn.equals(id);
|
||||
});
|
||||
|
||||
final rows = await stmt.writeReturning(RawValuesInsertable({
|
||||
'title': Variable<String>(newTitle),
|
||||
}));
|
||||
|
||||
return rows.singleOrNull;
|
||||
}
|
||||
}
|
||||
// #enddocregion updateTitle
|
||||
|
||||
extension FindTodoEntryById on GeneratedDatabase {
|
||||
Todos get todos => Todos(this);
|
||||
|
||||
|
@ -33,4 +63,10 @@ extension FindTodoEntryById on GeneratedDatabase {
|
|||
return select(todos)..where((row) => row.id.equals(id));
|
||||
}
|
||||
// #enddocregion findTodoEntryById
|
||||
|
||||
// #docregion updateTodo
|
||||
Future<Todo?> updateTodoTitle(int id, String newTitle) {
|
||||
return updateTitle(todos, id, newTitle);
|
||||
}
|
||||
// #enddocregion updateTodo
|
||||
}
|
||||
|
|
|
@ -31,7 +31,12 @@ class TodoItems extends Table {
|
|||
|
||||
@DriftDatabase(tables: [TodoItems])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
// #enddocregion open
|
||||
// After generating code, this class needs to define a `schemaVersion` getter
|
||||
// and a constructor telling drift where the database should be stored.
|
||||
// These are described in the getting started guide: https://drift.simonbinder.eu/getting-started/#open
|
||||
// #enddocregion before_generation
|
||||
// #docregion open
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
|
|
|
@ -49,8 +49,24 @@ To call this extension, `await myDatabase.todos.findById(3).getSingle()` could b
|
|||
A nice thing about defining the method as an extension is that type inference works really well - calling `findById` on `todos`
|
||||
returns a `Todo` instance, the generated data class for this table.
|
||||
|
||||
## Updates and inserts
|
||||
|
||||
The same approach also works to construct update, delete and insert statements (although those require a [TableInfo] instead of a [ResultSetImplementation]
|
||||
as views are read-only).
|
||||
Also, updates and inserts use an `Insertable` object which represents a partial row of updated or
|
||||
inserted columns, respectively.
|
||||
With a known table, one would use the generated typed `Companion` objects for that.
|
||||
But this can also be done with schema introspection thanks to the `RawValuesInsertable`, which
|
||||
can be used as a generic `Insertable` backed by a map of column names to values.
|
||||
|
||||
This example builds on the previous one to update the `title` column of a generic table based on a filter
|
||||
of the `id` column:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'updateTitle' %}
|
||||
|
||||
In a database or database accessor class, the method can then be called like this:
|
||||
|
||||
{% include "blocks/snippet" snippets = snippets name = 'updateTodo' %}
|
||||
|
||||
Hopefully, this page gives you some pointers to start reflectively inspecting your drift databases.
|
||||
The linked Dart documentation also expains the concepts in more detail.
|
||||
|
@ -59,4 +75,4 @@ If you have questions about this, or have a suggestion for more examples to incl
|
|||
[ResultSetImplementation]: https://drift.simonbinder.eu/api/drift/resultsetimplementation-class
|
||||
[TableInfo]: https://drift.simonbinder.eu/api/drift/tableinfo-mixin
|
||||
[ViewInfo]: https://drift.simonbinder.eu/api/drift/viewinfo-class
|
||||
[GeneratedColumn]: https://drift.simonbinder.eu/api/drift/generatedcolumn-class
|
||||
[GeneratedColumn]: https://drift.simonbinder.eu/api/drift/generatedcolumn-class
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
data:
|
||||
title: "Selects"
|
||||
description: "Select rows or invidiual columns from tables in Dart"
|
||||
description: "Select rows or individual columns from tables in Dart"
|
||||
weight: 2
|
||||
template: layouts/docs/single
|
||||
|
||||
|
|
|
@ -54,6 +54,8 @@ At the moment, drift supports these options:
|
|||
(so a column named `user_name` would also use `user_name` as a json key instead of `userName`).
|
||||
You can always override the json key by using a `JSON KEY` column constraint
|
||||
(e.g. `user_name VARCHAR NOT NULL JSON KEY userName`).
|
||||
* `use_sql_column_name_as_json_key` (defaults to false): Uses the column name in SQL as the JSON key for serialization,
|
||||
regardless of whether the table was defined in a drift file or not.
|
||||
* `generate_connect_constructor` (deprecated): Generates a named `connect()` constructor on database classes
|
||||
that takes a `DatabaseConnection` instead of a `QueryExecutor`.
|
||||
This option was deprecated in drift 2.5 because `DatabaseConnection` now implements `QueryExecutor`.
|
||||
|
|
|
@ -167,6 +167,8 @@ statement before it runs it.
|
|||
a defined query by appending `WITH YourDartClass` to a `CREATE TABLE` statement.
|
||||
- Alternatively, you may use `AS DesiredRowClassName` to change the name of the
|
||||
row class generated by drift.
|
||||
- Both custom row classes and custom table names also work for views, e.g. with
|
||||
`CREATE VIEW my_view AS DartName AS SELECT ...;`.
|
||||
- In a column definition, `MAPPED BY` can be used to [apply a converter](#type-converters)
|
||||
to that column.
|
||||
- Similarly, a `JSON KEY` constraint can be used to define the key drift will
|
||||
|
@ -356,28 +358,17 @@ With that option, the variable will be inferred to `Preferences` instead of `Str
|
|||
|
||||
### Existing row classes
|
||||
|
||||
{% assign existingDrift = "package:drift_docs/snippets/modular/drift/with_existing.drift.excerpt.json" | readString | json_decode %}
|
||||
{% assign rowClassDart = "package:drift_docs/snippets/modular/drift/row_class.dart.excerpt.json" | readString | json_decode %}
|
||||
|
||||
You can use custom row classes instead of having drift generate one for you.
|
||||
For instance, let's say you had a Dart class defined as
|
||||
|
||||
```dart
|
||||
class User {
|
||||
final int id;
|
||||
final String name;
|
||||
|
||||
User(this.id, this.name);
|
||||
}
|
||||
```
|
||||
{% include "blocks/snippet" snippets = rowClassDart name = "user" %}
|
||||
|
||||
Then, you can instruct drift to use that class as a row class as follows:
|
||||
|
||||
```sql
|
||||
import 'row_class.dart'; --import for where the row class is defined
|
||||
|
||||
CREATE TABLE users (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
) WITH User; -- This tells drift to use the existing Dart class
|
||||
```
|
||||
{% include "blocks/snippet" snippets = existingDrift name = "users" %}
|
||||
|
||||
When using custom row classes defined in another Dart file, you also need to import that file into the file where you define
|
||||
the database.
|
||||
|
@ -388,32 +379,11 @@ can be added after the name of the query.
|
|||
|
||||
For instance, let's say we expand the existing Dart code in `row_class.dart` by adding another class:
|
||||
|
||||
```dart
|
||||
class UserWithFriends {
|
||||
final User user;
|
||||
final List<User> friends;
|
||||
|
||||
UserWithFriends(this.user, {this.friends = const []});
|
||||
}
|
||||
```
|
||||
{% include "blocks/snippet" snippets = rowClassDart name = "userwithfriends" %}
|
||||
|
||||
Now, we can add a corresponding query using the new class for its rows:
|
||||
|
||||
```sql
|
||||
-- table to demonstrate a more complex select query below.
|
||||
-- also, remember to add the import for `UserWithFriends` to your drift file.
|
||||
CREATE TABLE friends (
|
||||
user_a INTEGER NOT NULL REFERENCES users(id),
|
||||
user_b INTEGER NOT NULL REFERENCES users(id),
|
||||
PRIMARY KEY (user_a, user_b)
|
||||
);
|
||||
|
||||
allFriendsOf WITH UserWithFriends: SELECT users.**, LIST(
|
||||
SELECT * FROM users a INNER JOIN friends ON user_a = a.id WHERE user_b = users.id
|
||||
UNION ALL
|
||||
SELECT * FROM users b INNER JOIN friends ON user_b = b.id WHERE user_a = users.id
|
||||
) AS friends FROM users WHERE id = :id;
|
||||
```
|
||||
{% include "blocks/snippet" snippets = existingDrift name = "friends" %}
|
||||
|
||||
The `WITH UserWithFriends` syntax will make drift consider the `UserWithFriends` class.
|
||||
For every field in the constructor, drift will check the column from the query and
|
||||
|
|
|
@ -99,7 +99,7 @@ You will now see errors related to missing overrides and a missing constructor.
|
|||
is responsible for telling drift how to open the database. The `schemaVersion` getter is relevant
|
||||
for migrations after changing the database, we can leave it at `1` for now. The database class
|
||||
now looks like this:
|
||||
|
||||
<a name="open">
|
||||
{% include "blocks/snippet" snippets = snippets name = 'open' %}
|
||||
|
||||
The Android-specific workarounds are necessary because sqlite3 attempts to use `/tmp` to store
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
## 2.16.0-dev
|
||||
## 2.17.0-dev
|
||||
|
||||
- Adds `companion` entry to `DataClassName` to override the name of the
|
||||
generated companion class.
|
||||
- Add the `TypeConverter.extensionType` factory to create type converters for
|
||||
extension types.
|
||||
- Fix invalid SQL syntax being generated for `BLOB` literals on postgres.
|
||||
|
||||
## 2.16.0
|
||||
|
||||
- When a migration throws, the database will now block subsequent operations
|
||||
instead of potentially allowing them to operate on a database in an
|
||||
|
@ -12,6 +20,9 @@
|
|||
- Improve stack traces for errors happening on drift isolates (which includes
|
||||
usages of `NativeDatabase.createInBackground`).
|
||||
- Don't cache `EXPLAIN` statements, avoiding schema locks.
|
||||
- Deprecate `Value.ofNullable` in favor of `Value.absentIfNull`, which is more
|
||||
explicit about its behavior and allows nullable types too.
|
||||
- Migrate `WasmDatabase` to `dart:js_interop` and `package:web`.
|
||||
|
||||
## 2.15.0
|
||||
|
||||
|
|
|
@ -308,7 +308,10 @@ final class TableIndex {
|
|||
class DataClassName {
|
||||
/// The overridden name to use when generating the data class for a table.
|
||||
/// {@macro drift_custom_data_class}
|
||||
final String name;
|
||||
final String? name;
|
||||
|
||||
/// The overridden name to use when generating the companion class for a table.
|
||||
final String? companion;
|
||||
|
||||
/// The parent type of the data class generated by drift.
|
||||
///
|
||||
|
@ -345,7 +348,11 @@ class DataClassName {
|
|||
|
||||
/// Customize the data class name for a given table.
|
||||
/// {@macro drift_custom_data_class}
|
||||
const DataClassName(this.name, {this.extending});
|
||||
const DataClassName(this.name, {this.extending, this.companion});
|
||||
|
||||
/// Customize the data class name for a given table.
|
||||
/// {@macro drift_custom_data_class}
|
||||
const DataClassName.custom({this.name, this.extending, this.companion});
|
||||
}
|
||||
|
||||
/// An annotation specifying an existing class to be used as a data class.
|
||||
|
|
|
@ -145,7 +145,9 @@ class DriftProtocol {
|
|||
|
||||
result.add(rows.length);
|
||||
for (final row in rows) {
|
||||
result.addAll(row.values);
|
||||
for (final value in row.values) {
|
||||
result.add(_encodeDbValue(value));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -234,7 +236,7 @@ class DriftProtocol {
|
|||
|
||||
result.add({
|
||||
for (var c = 0; c < columnCount; c++)
|
||||
columns[c]: fullMessage[rowOffset + c]
|
||||
columns[c]: _decodeDbValue(fullMessage[rowOffset + c])
|
||||
});
|
||||
}
|
||||
return SelectResult(result);
|
||||
|
|
|
@ -158,6 +158,7 @@ class Value<T> {
|
|||
/// This constructor should only be used when [T] is not nullable. If [T] were
|
||||
/// nullable, there wouldn't be a clear interpretation for a `null` [value].
|
||||
/// See the overall documentation on [Value] for details.
|
||||
@Deprecated('Use Value.absentIfNull instead')
|
||||
const Value.ofNullable(T? value)
|
||||
: assert(
|
||||
value != null || null is! T,
|
||||
|
@ -167,6 +168,15 @@ class Value<T> {
|
|||
_value = value,
|
||||
present = value != null;
|
||||
|
||||
/// Create a value that is absent if [value] is `null` and [present] if it's
|
||||
/// not.
|
||||
///
|
||||
/// The functionality is equiavalent to the following:
|
||||
/// `x != null ? Value(x) : Value.absent()`.
|
||||
const Value.absentIfNull(T? value)
|
||||
: _value = value,
|
||||
present = value != null;
|
||||
|
||||
@override
|
||||
String toString() => present ? 'Value($value)' : 'Value.absent()';
|
||||
|
||||
|
|
|
@ -24,14 +24,14 @@ class CustomExpression<D extends Object> extends Expression<D> {
|
|||
@override
|
||||
final Precedence precedence;
|
||||
|
||||
final CustomSqlType<D>? _customSqlType;
|
||||
final UserDefinedSqlType<D>? _customSqlType;
|
||||
|
||||
/// Constructs a custom expression by providing the raw sql [content].
|
||||
const CustomExpression(
|
||||
this.content, {
|
||||
this.watchedTables = const [],
|
||||
this.precedence = Precedence.unknown,
|
||||
CustomSqlType<D>? customType,
|
||||
UserDefinedSqlType<D>? customType,
|
||||
}) : _dialectSpecificContent = null,
|
||||
_customSqlType = customType;
|
||||
|
||||
|
@ -41,7 +41,7 @@ class CustomExpression<D extends Object> extends Expression<D> {
|
|||
Map<SqlDialect, String> content, {
|
||||
this.watchedTables = const [],
|
||||
this.precedence = Precedence.unknown,
|
||||
CustomSqlType<D>? customType,
|
||||
UserDefinedSqlType<D>? customType,
|
||||
}) : _dialectSpecificContent = content,
|
||||
content = '',
|
||||
_customSqlType = customType;
|
||||
|
|
|
@ -636,7 +636,7 @@ class _SubqueryExpression<R extends Object> extends Expression<R> {
|
|||
int get hashCode => statement.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object? other) {
|
||||
bool operator ==(Object other) {
|
||||
return other is _SubqueryExpression && other.statement == statement;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,10 +137,31 @@ extension StringExpressionOperators on Expression<String> {
|
|||
/// and [length] can be negative to return a section of the string before
|
||||
/// [start].
|
||||
Expression<String> substr(int start, [int? length]) {
|
||||
return substrExpr(
|
||||
Constant(start), length != null ? Constant(length) : null);
|
||||
}
|
||||
|
||||
/// Calls the [`substr`](https://sqlite.org/lang_corefunc.html#substr)
|
||||
/// function with arbitrary expressions as arguments.
|
||||
///
|
||||
/// For instance, this call uses [substrExpr] to remove the last 5 characters
|
||||
/// from a column. As this depends on its [StringExpressionOperators.length],
|
||||
/// it needs to use expressions:
|
||||
///
|
||||
/// ```dart
|
||||
/// update(table).write(TableCompanion.custom(
|
||||
/// column: column.substrExpr(Variable(1), column.length - Variable(5))
|
||||
/// ));
|
||||
/// ```
|
||||
///
|
||||
/// When both [start] and [length] are Dart values (e.g. [Variable]s or
|
||||
/// [Constant]s), consider using [substr] instead.
|
||||
Expression<String> substrExpr(Expression<int> start,
|
||||
[Expression<int>? length]) {
|
||||
return FunctionCallExpression('SUBSTR', [
|
||||
this,
|
||||
Constant<int>(start),
|
||||
if (length != null) Constant<int>(length),
|
||||
start,
|
||||
if (length != null) length,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ extension WithTypes<T extends Object> on Expression<T> {
|
|||
/// Creates a variable with a matching [driftSqlType].
|
||||
Variable<T> variable(T? value) {
|
||||
return switch (driftSqlType) {
|
||||
CustomSqlType<T> custom => Variable(value, custom),
|
||||
UserDefinedSqlType<T> custom => Variable(value, custom),
|
||||
_ => Variable(value),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -132,7 +132,7 @@ class Migrator {
|
|||
return _issueCustomQuery(context.sql, context.boundVariables);
|
||||
}
|
||||
|
||||
/// Alter columns of an existing tabe.
|
||||
/// Alter columns of an existing table.
|
||||
///
|
||||
/// Since sqlite does not provide a way to alter the type or constraint of an
|
||||
/// individual column, one needs to write a fairly complex migration procedure
|
||||
|
|
|
@ -162,7 +162,7 @@ class GeneratedColumn<T extends Object> extends Column<T> {
|
|||
|
||||
// these custom constraints refer to builtin constraints from drift
|
||||
if (!isSerial && _defaultConstraints != null) {
|
||||
_defaultConstraints!(into);
|
||||
_defaultConstraints(into);
|
||||
}
|
||||
} else if ($customConstraints?.isNotEmpty == true) {
|
||||
into.buffer
|
||||
|
|
|
@ -276,8 +276,15 @@ class InsertStatement<T extends Table, D> {
|
|||
if (ctx.dialect == SqlDialect.mariadb) {
|
||||
ctx.buffer.write(' ON DUPLICATE');
|
||||
} else {
|
||||
ctx.buffer.write(' ON CONFLICT(');
|
||||
ctx.buffer.write(' ON CONFLICT');
|
||||
|
||||
if (target != null && target.isEmpty) {
|
||||
// An empty list indicates that no explicit target should be generated
|
||||
// by drift, the default rules by the database will apply instead.
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.buffer.write('(');
|
||||
final conflictTarget = target ?? table.$primaryKey.toList();
|
||||
|
||||
if (conflictTarget.isEmpty) {
|
||||
|
@ -348,7 +355,7 @@ class InsertStatement<T extends Table, D> {
|
|||
|
||||
if (onConflict._where != null) {
|
||||
ctx.writeWhitespace();
|
||||
final where = onConflict._where!(
|
||||
final where = onConflict._where(
|
||||
table.asDslTable, table.createAlias('excluded').asDslTable);
|
||||
where.writeInto(ctx);
|
||||
}
|
||||
|
@ -473,6 +480,8 @@ class DoUpdate<T extends Table, D> extends UpsertClause<T, D> {
|
|||
/// specifies the uniqueness constraint that will trigger the upsert.
|
||||
///
|
||||
/// By default, the primary key of the table will be used.
|
||||
/// This can be set to an empty list, in which case no explicit conflict
|
||||
/// target will be generated by drift.
|
||||
final List<Column>? target;
|
||||
|
||||
/// Creates a `DO UPDATE` clause.
|
||||
|
|
|
@ -47,6 +47,35 @@ abstract class TypeConverter<D, S> {
|
|||
json: json,
|
||||
);
|
||||
}
|
||||
|
||||
/// A type converter mapping [extension types] to their underlying
|
||||
/// representation to store them in databases.
|
||||
///
|
||||
/// Here, [ExtType] is the extension type to use in Dart classes, and [Inner]
|
||||
/// is the underlying type stored in the database. For instance, if you had
|
||||
/// a type to represent ids in a database:
|
||||
///
|
||||
/// ```dart
|
||||
/// extension type IdNumber(int id) {}
|
||||
/// ```
|
||||
///
|
||||
/// You could use `TypeConverter.extensionType<IdNumber, int>()` in a column
|
||||
/// definition:
|
||||
///
|
||||
/// ```dart
|
||||
/// class Users extends Table {
|
||||
/// IntColumn get id => integer()
|
||||
/// .autoIncrement()
|
||||
/// .map(TypeConverter.extensionType<IdNumber, int>())();
|
||||
/// TextColumn get name => text()();
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// [extension types]: https://dart.dev/language/extension-types
|
||||
static JsonTypeConverter<ExtType, Inner>
|
||||
extensionType<ExtType, Inner extends Object>() {
|
||||
return _ExtensionTypeConverter();
|
||||
}
|
||||
}
|
||||
|
||||
/// A mixin for [TypeConverter]s that should also apply to drift's builtin
|
||||
|
@ -264,3 +293,17 @@ class _NullWrappingTypeConverterWithJson<D, S extends Object, J extends Object>
|
|||
return value == null ? null : requireToJson(value);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExtensionTypeConverter<ExtType, Inner extends Object>
|
||||
extends TypeConverter<ExtType, Inner>
|
||||
with JsonTypeConverter<ExtType, Inner> {
|
||||
const _ExtensionTypeConverter();
|
||||
|
||||
@override
|
||||
ExtType fromSql(Inner fromDb) {
|
||||
return fromDb as ExtType;
|
||||
}
|
||||
|
||||
@override
|
||||
Inner toSql(ExtType value) => value as Inner;
|
||||
}
|
||||
|
|
|
@ -124,9 +124,17 @@ final class SqlTypes {
|
|||
return (dart.millisecondsSinceEpoch ~/ 1000).toString();
|
||||
}
|
||||
} else if (dart is Uint8List) {
|
||||
// BLOB literals are string literals containing hexadecimal data and
|
||||
// preceded by a single "x" or "X" character. Example: X'53514C697465'
|
||||
return "x'${hex.encode(dart)}'";
|
||||
final String hexString = hex.encode(dart);
|
||||
|
||||
if (dialect == SqlDialect.postgres) {
|
||||
// Postgres BYTEA hex format
|
||||
// https://www.postgresql.org/docs/current/datatype-binary.html#DATATYPE-BINARY-BYTEA-HEX-FORMAT
|
||||
return "'\\x$hexString'::bytea";
|
||||
} else {
|
||||
// BLOB literals are string literals containing hexadecimal data and
|
||||
// preceded by a single "x" or "X" character. Example: X'53514C697465'
|
||||
return "x'$hexString'";
|
||||
}
|
||||
} else if (dart is DriftAny) {
|
||||
return mapToSqlLiteral(dart.rawSqlValue);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,47 @@
|
|||
@JS()
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:js_interop_unsafe';
|
||||
|
||||
import 'package:drift/src/runtime/api/runtime_api.dart';
|
||||
import 'package:drift/src/runtime/executor/stream_queries.dart';
|
||||
import 'package:js/js.dart';
|
||||
import 'package:js/js_util.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
@JS('Array')
|
||||
extension type _ArrayWrapper._(JSArray _) implements JSObject {
|
||||
external static JSBoolean isArray(JSAny? value);
|
||||
}
|
||||
|
||||
/// A [StreamQueryStore] using [web broadcast] APIs
|
||||
///
|
||||
/// [web broadcast]: https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API
|
||||
class BroadcastStreamQueryStore extends StreamQueryStore {
|
||||
final BroadcastChannel _channel;
|
||||
StreamSubscription<MessageEvent>? _messageFromChannel;
|
||||
final web.BroadcastChannel _channel;
|
||||
StreamSubscription<web.MessageEvent>? _messageFromChannel;
|
||||
|
||||
/// Constructs a broadcast query store with the given [identifier].
|
||||
///
|
||||
/// All query stores with the same identifier will share stream query updates.
|
||||
BroadcastStreamQueryStore(String identifier)
|
||||
: _channel = BroadcastChannel('drift_updates_$identifier') {
|
||||
_messageFromChannel = _channel.onMessage.listen(_handleMessage);
|
||||
: _channel = web.BroadcastChannel('drift_updates_$identifier') {
|
||||
_messageFromChannel = web.EventStreamProviders.messageEvent
|
||||
.forTarget(_channel)
|
||||
.listen(_handleMessage);
|
||||
}
|
||||
|
||||
void _handleMessage(MessageEvent message) {
|
||||
// Using getProperty to avoid dart2js structured clone that turns the
|
||||
// anonymous object into a map.
|
||||
final data = getProperty<Object?>(message, 'data');
|
||||
if (data is! List || data.isEmpty) return;
|
||||
void _handleMessage(web.MessageEvent message) {
|
||||
final data = message.data;
|
||||
if (!_ArrayWrapper.isArray(data).toDart) {
|
||||
return;
|
||||
}
|
||||
|
||||
final asList = (data as JSArray).toDart;
|
||||
if (asList.isEmpty) return;
|
||||
|
||||
super.handleTableUpdates({
|
||||
for (final entry in data.cast<_SerializedTableUpdate>())
|
||||
for (final entry in asList.cast<_SerializedTableUpdate>())
|
||||
entry.toTableUpdate,
|
||||
});
|
||||
}
|
||||
|
@ -39,7 +52,7 @@ class BroadcastStreamQueryStore extends StreamQueryStore {
|
|||
|
||||
_channel.postMessage([
|
||||
for (final update in updates) _SerializedTableUpdate.of(update),
|
||||
]);
|
||||
].toJS);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -50,34 +63,31 @@ class BroadcastStreamQueryStore extends StreamQueryStore {
|
|||
}
|
||||
|
||||
/// Whether the current JavaScript context supports broadcast channels.
|
||||
static bool get supported => hasProperty(globalThis, 'BroadcastChannel');
|
||||
static bool get supported => globalContext.has('BroadcastChannel');
|
||||
}
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
@staticInterop
|
||||
class _SerializedTableUpdate {
|
||||
extension type _SerializedTableUpdate._(JSObject _) implements JSObject {
|
||||
external factory _SerializedTableUpdate({
|
||||
required String? kind,
|
||||
required String table,
|
||||
required JSString? kind,
|
||||
required JSString table,
|
||||
});
|
||||
|
||||
factory _SerializedTableUpdate.of(TableUpdate update) {
|
||||
return _SerializedTableUpdate(kind: update.kind?.name, table: update.table);
|
||||
return _SerializedTableUpdate(
|
||||
kind: update.kind?.name.toJS,
|
||||
table: update.table.toJS,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension on _SerializedTableUpdate {
|
||||
@JS()
|
||||
external String? get kind;
|
||||
|
||||
@JS()
|
||||
external String get table;
|
||||
external JSString? get kind;
|
||||
external JSString get table;
|
||||
|
||||
TableUpdate get toTableUpdate {
|
||||
final updateKind = _updateKindByName[kind];
|
||||
final updateKind = _updateKindByName[kind?.toDart];
|
||||
|
||||
return TableUpdate(table, kind: updateKind);
|
||||
return TableUpdate(table.toDart, kind: updateKind);
|
||||
}
|
||||
|
||||
static final _updateKindByName = UpdateKind.values.asNameMap();
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import 'dart:js_interop';
|
||||
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
/// Extension to transform a raw [MessagePort] from web workers into a Dart
|
||||
/// [StreamChannel].
|
||||
extension WebPortToChannel on web.MessagePort {
|
||||
static const _disconnectMessage = '_disconnect';
|
||||
|
||||
/// Converts this port to a two-way communication channel, exposed as a
|
||||
/// [StreamChannel].
|
||||
///
|
||||
/// This can be used to implement a remote database connection over service
|
||||
/// workers.
|
||||
///
|
||||
/// The [explicitClose] parameter can be used to control whether a close
|
||||
/// message should be sent through the channel when it is closed. This will
|
||||
/// cause it to be closed on the other end as well. Note that this is not a
|
||||
/// reliable way of determining channel closures though, as there is no event
|
||||
/// for channels being closed due to a tab or worker being closed.
|
||||
/// Both "ends" of a JS channel calling [channel] on their part must use the
|
||||
/// value for [explicitClose].
|
||||
StreamChannel<Object?> channel({bool explicitClose = false}) {
|
||||
final controller = StreamChannelController<Object?>();
|
||||
|
||||
onmessage = (web.MessageEvent event) {
|
||||
final message = event.data;
|
||||
|
||||
if (explicitClose && message == _disconnectMessage.toJS) {
|
||||
// Other end has closed the connection
|
||||
controller.local.sink.close();
|
||||
} else {
|
||||
controller.local.sink.add(message.dartify());
|
||||
}
|
||||
}.toJS;
|
||||
|
||||
controller.local.stream.listen((e) => postMessage(e.jsify()), onDone: () {
|
||||
// Closed locally, inform the other end.
|
||||
if (explicitClose) {
|
||||
postMessage(_disconnectMessage.toJS);
|
||||
}
|
||||
|
||||
close();
|
||||
});
|
||||
|
||||
return controller.foreign;
|
||||
}
|
||||
}
|
|
@ -7,22 +7,23 @@
|
|||
/// asynchronous
|
||||
// ignore_for_file: public_member_api_docs
|
||||
@internal
|
||||
@JS()
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:js_interop_unsafe';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/remote.dart';
|
||||
import 'package:drift/wasm.dart';
|
||||
import 'package:js/js.dart';
|
||||
import 'package:js/js_util.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
import 'package:web/web.dart' as web;
|
||||
|
||||
import 'broadcast_stream_queries.dart';
|
||||
import 'channel.dart';
|
||||
import 'new_channel.dart';
|
||||
import 'wasm_setup/shared.dart';
|
||||
import 'wasm_setup/protocol.dart';
|
||||
|
||||
|
@ -32,10 +33,10 @@ import 'wasm_setup/protocol.dart';
|
|||
external bool get crossOriginIsolated;
|
||||
|
||||
/// Whether shared workers can be constructed in the current context.
|
||||
bool get supportsSharedWorkers => hasProperty(globalThis, 'SharedWorker');
|
||||
bool get supportsSharedWorkers => globalContext.has('SharedWorker');
|
||||
|
||||
/// Whether dedicated workers can be constructed in the current context.
|
||||
bool get supportsWorkers => hasProperty(globalThis, 'Worker');
|
||||
bool get supportsWorkers => globalContext.has('Worker');
|
||||
|
||||
class WasmDatabaseOpener {
|
||||
final Uri sqlite3WasmUri;
|
||||
|
@ -107,7 +108,7 @@ class WasmDatabaseOpener {
|
|||
Future<void> _probeDedicated() async {
|
||||
if (supportsWorkers) {
|
||||
final dedicatedWorker = _dedicatedWorker =
|
||||
_DriftWorker.dedicated(Worker(driftWorkerUri.toString()));
|
||||
_DriftWorker.dedicated(web.Worker(driftWorkerUri.toString()));
|
||||
_createCompatibilityCheck().sendTo(dedicatedWorker.send);
|
||||
|
||||
final status = await dedicatedWorker.workerMessages.nextNoError
|
||||
|
@ -133,8 +134,8 @@ class WasmDatabaseOpener {
|
|||
Future<void> _probeShared() async {
|
||||
if (supportsSharedWorkers) {
|
||||
final sharedWorker =
|
||||
SharedWorker(driftWorkerUri.toString(), 'drift worker');
|
||||
final port = sharedWorker.port!;
|
||||
web.SharedWorker(driftWorkerUri.toString(), 'drift worker'.toJS);
|
||||
final port = sharedWorker.port;
|
||||
final shared = _sharedWorker = _DriftWorker.shared(sharedWorker, port);
|
||||
|
||||
// First, the shared worker will tell us which features it supports.
|
||||
|
@ -161,40 +162,38 @@ class WasmDatabaseOpener {
|
|||
}
|
||||
|
||||
final class _DriftWorker {
|
||||
final AbstractWorker worker;
|
||||
/// Either a [web.SharedWorker] or a [web.Worker].
|
||||
final JSObject worker;
|
||||
ProtocolVersion version = ProtocolVersion.legacy;
|
||||
|
||||
/// The message port to communicate with the worker, if it's a shared worker.
|
||||
final MessagePort? portForShared;
|
||||
final web.MessagePort? portForShared;
|
||||
|
||||
final StreamQueue<WasmInitializationMessage> workerMessages;
|
||||
|
||||
_DriftWorker.dedicated(Worker this.worker)
|
||||
_DriftWorker.dedicated(web.Worker this.worker)
|
||||
: portForShared = null,
|
||||
workerMessages =
|
||||
StreamQueue(_readMessages(worker.onMessage, worker.onError));
|
||||
workerMessages = StreamQueue(_readMessages(worker, worker));
|
||||
|
||||
_DriftWorker.shared(SharedWorker this.worker, this.portForShared)
|
||||
: workerMessages =
|
||||
StreamQueue(_readMessages(worker.port!.onMessage, worker.onError));
|
||||
_DriftWorker.shared(web.SharedWorker this.worker, this.portForShared)
|
||||
: workerMessages = StreamQueue(_readMessages(worker.port, worker)) {
|
||||
(worker as web.SharedWorker).port.start();
|
||||
}
|
||||
|
||||
void send(Object? msg, [List<Object>? transfer]) {
|
||||
switch (worker) {
|
||||
case final Worker worker:
|
||||
worker.postMessage(msg, transfer);
|
||||
case SharedWorker():
|
||||
portForShared!.postMessage(msg, transfer);
|
||||
void send(JSAny? msg, List<JSObject>? transfer) {
|
||||
if (portForShared case final port?) {
|
||||
port.postMessage(msg, (transfer ?? const []).toJS);
|
||||
} else {
|
||||
(worker as web.Worker).postMessage(msg, (transfer ?? const []).toJS);
|
||||
}
|
||||
}
|
||||
|
||||
void close() {
|
||||
workerMessages.cancel();
|
||||
|
||||
switch (worker) {
|
||||
case final Worker dedicated:
|
||||
dedicated.terminate();
|
||||
case SharedWorker():
|
||||
portForShared!.close();
|
||||
if (portForShared case final port?) {
|
||||
port.close();
|
||||
} else {
|
||||
(worker as web.Worker).terminate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,9 +224,9 @@ final class _ProbeResult implements WasmProbeResult {
|
|||
FutureOr<Uint8List?> Function()? initializeDatabase,
|
||||
WasmDatabaseSetup? localSetup,
|
||||
}) async {
|
||||
final channel = MessageChannel();
|
||||
final channel = web.MessageChannel();
|
||||
final initializer = initializeDatabase;
|
||||
final initChannel = initializer != null ? MessageChannel() : null;
|
||||
final initChannel = initializer != null ? web.MessageChannel() : null;
|
||||
|
||||
ServeDriftDatabase message;
|
||||
final sharedWorker = opener._sharedWorker;
|
||||
|
@ -276,18 +275,24 @@ final class _ProbeResult implements WasmProbeResult {
|
|||
initializeDatabase, localSetup);
|
||||
}
|
||||
|
||||
initChannel?.port1.onMessage.listen((event) async {
|
||||
// The worker hosting the database is asking for the initial blob because
|
||||
// the database doesn't exist.
|
||||
Uint8List? result;
|
||||
try {
|
||||
result = await initializer?.call();
|
||||
} finally {
|
||||
initChannel.port1
|
||||
..postMessage(result, [if (result != null) result.buffer])
|
||||
..close();
|
||||
}
|
||||
});
|
||||
if (initChannel != null) {
|
||||
initChannel.port1.start();
|
||||
web.EventStreamProviders.messageEvent
|
||||
.forTarget(initChannel.port1)
|
||||
.listen((event) async {
|
||||
// The worker hosting the database is asking for the initial blob because
|
||||
// the database doesn't exist.
|
||||
Uint8List? result;
|
||||
try {
|
||||
result = await initializer?.call();
|
||||
} finally {
|
||||
initChannel.port1
|
||||
..postMessage(
|
||||
result?.toJS, [if (result != null) result.buffer.toJS].toJS)
|
||||
..close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final local = channel.port1
|
||||
.channel(explicitClose: message.protocolVersion >= ProtocolVersion.v1);
|
||||
|
@ -350,7 +355,13 @@ final class _ProbeResult implements WasmProbeResult {
|
|||
}
|
||||
|
||||
Stream<WasmInitializationMessage> _readMessages(
|
||||
Stream<MessageEvent> messages, Stream<Event> errors) {
|
||||
web.EventTarget messageTarget,
|
||||
web.EventTarget errorTarget,
|
||||
) {
|
||||
final messages =
|
||||
web.EventStreamProviders.messageEvent.forTarget(messageTarget);
|
||||
final errors = web.EventStreamProviders.errorEvent.forTarget(errorTarget);
|
||||
|
||||
final mappedMessages = messages.map(WasmInitializationMessage.read);
|
||||
|
||||
return Stream.multi((listener) {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:js_interop_unsafe';
|
||||
|
||||
import 'package:js/js_util.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
import 'package:web/web.dart'
|
||||
show DedicatedWorkerGlobalScope, EventStreamProviders;
|
||||
|
||||
import '../../utils/synchronized.dart';
|
||||
import 'protocol.dart';
|
||||
|
@ -22,7 +24,7 @@ class DedicatedDriftWorker {
|
|||
: _servers = DriftServerController(setup);
|
||||
|
||||
void start() {
|
||||
self.onMessage.listen((event) {
|
||||
EventStreamProviders.messageEvent.forTarget(self).listen((event) {
|
||||
final message = WasmInitializationMessage.read(event);
|
||||
_handleMessage(message);
|
||||
});
|
||||
|
@ -69,11 +71,10 @@ class DedicatedDriftWorker {
|
|||
}
|
||||
|
||||
DedicatedWorkerCompatibilityResult(
|
||||
supportsNestedWorkers: hasProperty(globalThis, 'Worker'),
|
||||
supportsNestedWorkers: globalContext.has('Worker'),
|
||||
canAccessOpfs: supportsOpfs,
|
||||
supportsIndexedDb: supportsIndexedDb,
|
||||
supportsSharedArrayBuffers:
|
||||
hasProperty(globalThis, 'SharedArrayBuffer'),
|
||||
supportsSharedArrayBuffers: globalContext.has('SharedArrayBuffer'),
|
||||
opfsExists: opfsExists,
|
||||
indexedDbExists: indexedDbExists,
|
||||
existingDatabases: existingDatabases,
|
||||
|
@ -83,7 +84,7 @@ class DedicatedDriftWorker {
|
|||
_servers.serve(message);
|
||||
case StartFileSystemServer(sqlite3Options: final options):
|
||||
final worker = await VfsWorker.create(options);
|
||||
self.postMessage(true);
|
||||
self.postMessage(true.toJS);
|
||||
await worker.start();
|
||||
case DeleteDatabase(database: (final storage, final name)):
|
||||
try {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
// ignore_for_file: public_member_api_docs
|
||||
|
||||
import 'dart:html';
|
||||
import 'dart:js';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:js_interop_unsafe';
|
||||
|
||||
import 'package:js/js_util.dart';
|
||||
import 'package:web/web.dart' hide WorkerOptions;
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
|
||||
import 'types.dart';
|
||||
|
@ -18,8 +18,8 @@ class ProtocolVersion {
|
|||
|
||||
const ProtocolVersion._(this.versionCode);
|
||||
|
||||
void writeToJs(Object object) {
|
||||
setProperty(object, 'v', versionCode);
|
||||
void writeToJs(JSObject object) {
|
||||
object['v'] = versionCode.toJS;
|
||||
}
|
||||
|
||||
bool operator >=(ProtocolVersion other) {
|
||||
|
@ -36,9 +36,9 @@ class ProtocolVersion {
|
|||
};
|
||||
}
|
||||
|
||||
static ProtocolVersion fromJsObject(Object object) {
|
||||
if (hasProperty(object, 'v')) {
|
||||
return negotiate(getProperty<int>(object, 'v'));
|
||||
static ProtocolVersion fromJsObject(JSObject object) {
|
||||
if (object.has('v')) {
|
||||
return negotiate((object['v'] as JSNumber).toDartInt);
|
||||
} else {
|
||||
return legacy;
|
||||
}
|
||||
|
@ -58,52 +58,56 @@ class ProtocolVersion {
|
|||
static const current = v1;
|
||||
}
|
||||
|
||||
typedef PostMessage = void Function(Object? msg, [List<Object>? transfer]);
|
||||
typedef PostMessage = void Function(JSObject? msg, List<JSObject>? transfer);
|
||||
|
||||
/// Sealed superclass for JavaScript objects exchanged between the UI tab and
|
||||
/// workers spawned by drift to find a suitable database implementation.
|
||||
sealed class WasmInitializationMessage {
|
||||
WasmInitializationMessage();
|
||||
|
||||
factory WasmInitializationMessage.fromJs(Object jsObject) {
|
||||
final type = getProperty<String>(jsObject, 'type');
|
||||
final payload = getProperty<Object?>(jsObject, 'payload');
|
||||
factory WasmInitializationMessage.fromJs(JSObject jsObject) {
|
||||
final type = (jsObject['type'] as JSString).toDart;
|
||||
final payload = jsObject['payload'];
|
||||
|
||||
return switch (type) {
|
||||
WorkerError.type => WorkerError.fromJsPayload(payload!),
|
||||
ServeDriftDatabase.type => ServeDriftDatabase.fromJsPayload(payload!),
|
||||
WorkerError.type => WorkerError.fromJsPayload(payload as JSObject),
|
||||
ServeDriftDatabase.type =>
|
||||
ServeDriftDatabase.fromJsPayload(payload as JSObject),
|
||||
StartFileSystemServer.type =>
|
||||
StartFileSystemServer.fromJsPayload(payload!),
|
||||
StartFileSystemServer.fromJsPayload(payload as JSObject),
|
||||
RequestCompatibilityCheck.type =>
|
||||
RequestCompatibilityCheck.fromJsPayload(payload),
|
||||
DedicatedWorkerCompatibilityResult.type =>
|
||||
DedicatedWorkerCompatibilityResult.fromJsPayload(payload!),
|
||||
DedicatedWorkerCompatibilityResult.fromJsPayload(payload as JSObject),
|
||||
SharedWorkerCompatibilityResult.type =>
|
||||
SharedWorkerCompatibilityResult.fromJsPayload(payload!),
|
||||
DeleteDatabase.type => DeleteDatabase.fromJsPayload(payload!),
|
||||
SharedWorkerCompatibilityResult.fromJsPayload(payload as JSArray),
|
||||
DeleteDatabase.type => DeleteDatabase.fromJsPayload(payload as JSAny),
|
||||
_ => throw ArgumentError('Unknown type $type'),
|
||||
};
|
||||
}
|
||||
|
||||
factory WasmInitializationMessage.read(MessageEvent event) {
|
||||
// Not using event.data because we don't want the SDK to dartify the raw JS
|
||||
// object we're passing around.
|
||||
final rawData = getProperty<Object>(event, 'data');
|
||||
return WasmInitializationMessage.fromJs(rawData);
|
||||
return WasmInitializationMessage.fromJs(event.data as JSObject);
|
||||
}
|
||||
|
||||
void sendTo(PostMessage sender);
|
||||
|
||||
void sendToWorker(Worker worker) {
|
||||
sendTo(worker.postMessage);
|
||||
sendTo((msg, transfer) {
|
||||
worker.postMessage(msg, (transfer ?? const []).toJS);
|
||||
});
|
||||
}
|
||||
|
||||
void sendToPort(MessagePort port) {
|
||||
sendTo(port.postMessage);
|
||||
sendTo((msg, transfer) {
|
||||
port.postMessage(msg, (transfer ?? const []).toJS);
|
||||
});
|
||||
}
|
||||
|
||||
void sendToClient(DedicatedWorkerGlobalScope worker) {
|
||||
sendTo(worker.postMessage);
|
||||
sendTo((msg, transfer) {
|
||||
worker.postMessage(msg, (transfer ?? const []).toJS);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,16 +160,15 @@ final class SharedWorkerCompatibilityResult extends CompatibilityResult {
|
|||
required super.version,
|
||||
});
|
||||
|
||||
factory SharedWorkerCompatibilityResult.fromJsPayload(Object payload) {
|
||||
final asList = payload as List;
|
||||
factory SharedWorkerCompatibilityResult.fromJsPayload(JSArray payload) {
|
||||
final asList = payload.toDart;
|
||||
final asBooleans = asList.cast<bool>();
|
||||
|
||||
final List<ExistingDatabase> existingDatabases;
|
||||
var version = ProtocolVersion.legacy;
|
||||
|
||||
if (asList.length > 5) {
|
||||
existingDatabases =
|
||||
EncodeLocations.readFromJs(asList[5] as List<dynamic>);
|
||||
existingDatabases = EncodeLocations.readFromJs(asList[5] as JSArray);
|
||||
|
||||
if (asList.length > 6) {
|
||||
version = ProtocolVersion.negotiate(asList[6] as int);
|
||||
|
@ -187,15 +190,17 @@ final class SharedWorkerCompatibilityResult extends CompatibilityResult {
|
|||
|
||||
@override
|
||||
void sendTo(PostMessage sender) {
|
||||
sender.sendTyped(type, [
|
||||
canSpawnDedicatedWorkers,
|
||||
dedicatedWorkersCanUseOpfs,
|
||||
canUseIndexedDb,
|
||||
indexedDbExists,
|
||||
opfsExists,
|
||||
existingDatabases.encodeToJs(),
|
||||
version.versionCode,
|
||||
]);
|
||||
sender.sendTyped(
|
||||
type,
|
||||
[
|
||||
canSpawnDedicatedWorkers.toJS,
|
||||
dedicatedWorkersCanUseOpfs.toJS,
|
||||
canUseIndexedDb.toJS,
|
||||
indexedDbExists.toJS,
|
||||
opfsExists.toJS,
|
||||
existingDatabases.encodeToJs(),
|
||||
version.versionCode.toJS,
|
||||
].toJS);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -216,13 +221,13 @@ final class WorkerError extends WasmInitializationMessage implements Exception {
|
|||
|
||||
WorkerError(this.error);
|
||||
|
||||
factory WorkerError.fromJsPayload(Object payload) {
|
||||
return WorkerError(payload as String);
|
||||
factory WorkerError.fromJsPayload(JSObject payload) {
|
||||
return WorkerError((payload as JSString).toDart);
|
||||
}
|
||||
|
||||
@override
|
||||
void sendTo(PostMessage sender) {
|
||||
sender.sendTyped(type, error);
|
||||
sender.sendTyped(type, error.toJS);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -252,32 +257,32 @@ final class ServeDriftDatabase extends WasmInitializationMessage {
|
|||
required this.protocolVersion,
|
||||
});
|
||||
|
||||
factory ServeDriftDatabase.fromJsPayload(Object payload) {
|
||||
factory ServeDriftDatabase.fromJsPayload(JSObject payload) {
|
||||
return ServeDriftDatabase(
|
||||
sqlite3WasmUri: Uri.parse(getProperty(payload, 'sqlite')),
|
||||
port: getProperty(payload, 'port'),
|
||||
sqlite3WasmUri: Uri.parse((payload['sqlite'] as JSString).toDart),
|
||||
port: payload['port'] as MessagePort,
|
||||
storage: WasmStorageImplementation.values
|
||||
.byName(getProperty(payload, 'storage')),
|
||||
databaseName: getProperty(payload, 'database'),
|
||||
initializationPort: getProperty(payload, 'initPort'),
|
||||
.byName((payload['storage'] as JSString).toDart),
|
||||
databaseName: (payload['database'] as JSString).toDart,
|
||||
initializationPort: payload['initPort'] as MessagePort?,
|
||||
protocolVersion: ProtocolVersion.fromJsObject(payload),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void sendTo(PostMessage sender) {
|
||||
final object = newObject<Object>();
|
||||
setProperty(object, 'sqlite', sqlite3WasmUri.toString());
|
||||
setProperty(object, 'port', port);
|
||||
setProperty(object, 'storage', storage.name);
|
||||
setProperty(object, 'database', databaseName);
|
||||
final initPort = initializationPort;
|
||||
setProperty(object, 'initPort', initPort);
|
||||
final object = JSObject()
|
||||
..['sqlite'] = sqlite3WasmUri.toString().toJS
|
||||
..['port'] = port
|
||||
..['storage'] = storage.name.toJS
|
||||
..['database'] = databaseName.toJS
|
||||
..['initPort'] = initializationPort;
|
||||
|
||||
protocolVersion.writeToJs(object);
|
||||
|
||||
sender.sendTyped(type, object, [
|
||||
port,
|
||||
if (initPort != null) initPort,
|
||||
if (initializationPort != null) initializationPort!,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -293,13 +298,13 @@ final class RequestCompatibilityCheck extends WasmInitializationMessage {
|
|||
|
||||
RequestCompatibilityCheck(this.databaseName);
|
||||
|
||||
factory RequestCompatibilityCheck.fromJsPayload(Object? payload) {
|
||||
return RequestCompatibilityCheck(payload as String);
|
||||
factory RequestCompatibilityCheck.fromJsPayload(JSAny? payload) {
|
||||
return RequestCompatibilityCheck((payload as JSString).toDart);
|
||||
}
|
||||
|
||||
@override
|
||||
void sendTo(PostMessage sender) {
|
||||
sender.sendTyped(type, databaseName);
|
||||
sender.sendTyped(type, databaseName.toJS);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -322,22 +327,23 @@ final class DedicatedWorkerCompatibilityResult extends CompatibilityResult {
|
|||
required super.version,
|
||||
});
|
||||
|
||||
factory DedicatedWorkerCompatibilityResult.fromJsPayload(Object payload) {
|
||||
factory DedicatedWorkerCompatibilityResult.fromJsPayload(JSObject payload) {
|
||||
final existingDatabases = <ExistingDatabase>[];
|
||||
|
||||
if (hasProperty(payload, 'existing')) {
|
||||
if (payload.has('existing')) {
|
||||
existingDatabases
|
||||
.addAll(EncodeLocations.readFromJs(getProperty(payload, 'existing')));
|
||||
.addAll(EncodeLocations.readFromJs(payload['existing'] as JSArray));
|
||||
}
|
||||
|
||||
return DedicatedWorkerCompatibilityResult(
|
||||
supportsNestedWorkers: getProperty(payload, 'supportsNestedWorkers'),
|
||||
canAccessOpfs: getProperty(payload, 'canAccessOpfs'),
|
||||
supportsNestedWorkers:
|
||||
(payload['supportsNestedWorkers'] as JSBoolean).toDart,
|
||||
canAccessOpfs: (payload['canAccessOpfs'] as JSBoolean).toDart,
|
||||
supportsSharedArrayBuffers:
|
||||
getProperty(payload, 'supportsSharedArrayBuffers'),
|
||||
supportsIndexedDb: getProperty(payload, 'supportsIndexedDb'),
|
||||
indexedDbExists: getProperty(payload, 'indexedDbExists'),
|
||||
opfsExists: getProperty(payload, 'opfsExists'),
|
||||
(payload['supportsSharedArrayBuffers'] as JSBoolean).toDart,
|
||||
supportsIndexedDb: (payload['supportsIndexedDb'] as JSBoolean).toDart,
|
||||
indexedDbExists: (payload['indexedDbExists'] as JSBoolean).toDart,
|
||||
opfsExists: (payload['opfsExists'] as JSBoolean).toDart,
|
||||
existingDatabases: existingDatabases,
|
||||
version: ProtocolVersion.fromJsObject(payload),
|
||||
);
|
||||
|
@ -345,16 +351,14 @@ final class DedicatedWorkerCompatibilityResult extends CompatibilityResult {
|
|||
|
||||
@override
|
||||
void sendTo(PostMessage sender) {
|
||||
final object = newObject<Object>();
|
||||
|
||||
setProperty(object, 'supportsNestedWorkers', supportsNestedWorkers);
|
||||
setProperty(object, 'canAccessOpfs', canAccessOpfs);
|
||||
setProperty(object, 'supportsIndexedDb', supportsIndexedDb);
|
||||
setProperty(
|
||||
object, 'supportsSharedArrayBuffers', supportsSharedArrayBuffers);
|
||||
setProperty(object, 'indexedDbExists', indexedDbExists);
|
||||
setProperty(object, 'opfsExists', opfsExists);
|
||||
setProperty(object, 'existing', existingDatabases.encodeToJs());
|
||||
final object = JSObject()
|
||||
..['supportsNestedWorkers'] = supportsNestedWorkers.toJS
|
||||
..['canAccessOpfs'] = canAccessOpfs.toJS
|
||||
..['supportsIndexedDb'] = supportsIndexedDb.toJS
|
||||
..['supportsSharedArrayBuffers'] = supportsSharedArrayBuffers.toJS
|
||||
..['indexedDbExists'] = indexedDbExists.toJS
|
||||
..['opfsExists'] = opfsExists.toJS
|
||||
..['existing'] = existingDatabases.encodeToJs();
|
||||
version.writeToJs(object);
|
||||
|
||||
sender.sendTyped(type, object);
|
||||
|
@ -381,13 +385,13 @@ final class StartFileSystemServer extends WasmInitializationMessage {
|
|||
|
||||
StartFileSystemServer(this.sqlite3Options);
|
||||
|
||||
factory StartFileSystemServer.fromJsPayload(Object payload) {
|
||||
factory StartFileSystemServer.fromJsPayload(JSObject payload) {
|
||||
return StartFileSystemServer(payload as WorkerOptions);
|
||||
}
|
||||
|
||||
@override
|
||||
void sendTo(PostMessage sender) {
|
||||
sender.sendTyped(type, sqlite3Options);
|
||||
sender.sendTyped(type, sqlite3Options as JSObject);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,53 +402,51 @@ final class DeleteDatabase extends WasmInitializationMessage {
|
|||
|
||||
DeleteDatabase(this.database);
|
||||
|
||||
factory DeleteDatabase.fromJsPayload(Object payload) {
|
||||
final asList = payload as List<Object?>;
|
||||
factory DeleteDatabase.fromJsPayload(JSAny payload) {
|
||||
final asList = (payload as JSArray).toDart;
|
||||
return DeleteDatabase((
|
||||
WebStorageApi.byName[asList[0] as String]!,
|
||||
asList[1] as String,
|
||||
WebStorageApi.byName[(asList[0] as JSString).toDart]!,
|
||||
(asList[1] as JSString).toDart,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
void sendTo(PostMessage sender) {
|
||||
sender.sendTyped(type, [database.$1.name, database.$2]);
|
||||
sender.sendTyped(type, [database.$1.name.toJS, database.$2.toJS].toJS);
|
||||
}
|
||||
}
|
||||
|
||||
extension EncodeLocations on List<ExistingDatabase> {
|
||||
static List<ExistingDatabase> readFromJs(List<Object?> object) {
|
||||
static List<ExistingDatabase> readFromJs(JSArray object) {
|
||||
final existing = <ExistingDatabase>[];
|
||||
|
||||
for (final entry in object) {
|
||||
for (final entry in object.toDart.cast<JSObject>()) {
|
||||
existing.add((
|
||||
WebStorageApi.byName[getProperty(entry as Object, 'l')]!,
|
||||
getProperty(entry, 'n'),
|
||||
WebStorageApi.byName[(entry['l'] as JSString).toDart]!,
|
||||
(entry['n'] as JSString).toDart,
|
||||
));
|
||||
}
|
||||
|
||||
return existing;
|
||||
}
|
||||
|
||||
Object encodeToJs() {
|
||||
final existing = JsArray<Object>();
|
||||
JSObject encodeToJs() {
|
||||
final existing = <JSObject>[];
|
||||
for (final entry in this) {
|
||||
final object = newObject<Object>();
|
||||
setProperty(object, 'l', entry.$1.name);
|
||||
setProperty(object, 'n', entry.$2);
|
||||
|
||||
existing.add(object);
|
||||
existing.add(JSObject()
|
||||
..['l'] = entry.$1.name.toJS
|
||||
..['n'] = entry.$2.toJS);
|
||||
}
|
||||
|
||||
return existing;
|
||||
return existing.toJS;
|
||||
}
|
||||
}
|
||||
|
||||
extension on PostMessage {
|
||||
void sendTyped(String type, Object? payload, [List<Object>? transfer]) {
|
||||
final object = newObject<Object>();
|
||||
setProperty(object, 'type', type);
|
||||
setProperty(object, 'payload', payload);
|
||||
void sendTyped(String type, JSAny? payload, [List<JSObject>? transfer]) {
|
||||
final object = JSObject()
|
||||
..['type'] = type.toJS
|
||||
..['payload'] = payload;
|
||||
|
||||
call(object, transfer);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:indexed_db';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:js_interop_unsafe';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/remote.dart';
|
||||
import 'package:drift/wasm.dart';
|
||||
import 'package:js/js_util.dart';
|
||||
import 'package:web/web.dart'
|
||||
show
|
||||
Worker,
|
||||
IDBFactory,
|
||||
IDBRequest,
|
||||
IDBDatabase,
|
||||
IDBVersionChangeEvent,
|
||||
EventStreamProviders,
|
||||
MessageEvent;
|
||||
// ignore: implementation_imports
|
||||
import 'package:sqlite3/src/wasm/js_interop/file_system_access.dart';
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
import '../channel.dart';
|
||||
import '../new_channel.dart';
|
||||
import 'protocol.dart';
|
||||
|
||||
/// Checks whether the OPFS API is likely to be correctly implemented in the
|
||||
|
@ -38,10 +46,10 @@ Future<bool> checkOpfsSupport() async {
|
|||
// In earlier versions of the OPFS standard, some methods like `getSize()`
|
||||
// on a sync file handle have actually been asynchronous. We don't support
|
||||
// Browsers that implement the outdated spec.
|
||||
final getSizeResult = callMethod<Object?>(openedFile, 'getSize', []);
|
||||
if (typeofEquals<Object?>(getSizeResult, 'object')) {
|
||||
final getSizeResult = (openedFile as JSObject).callMethod('getSize'.toJS);
|
||||
if (getSizeResult.typeofEquals('object')) {
|
||||
// Returned a promise, that's no good.
|
||||
await promiseToFuture<Object?>(getSizeResult!);
|
||||
await (getSizeResult as JSPromise).toDart;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -61,18 +69,18 @@ Future<bool> checkOpfsSupport() async {
|
|||
|
||||
/// Checks whether IndexedDB is working in the current browser.
|
||||
Future<bool> checkIndexedDbSupport() async {
|
||||
if (!hasProperty(globalThis, 'indexedDB') ||
|
||||
if (!globalContext.has('indexedDB') ||
|
||||
// FileReader needed to read and write blobs efficiently
|
||||
!hasProperty(globalThis, 'FileReader')) {
|
||||
!globalContext.has('FileReader')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final idb = getProperty<IdbFactory>(globalThis, 'indexedDB');
|
||||
final idb = globalContext['indexedDB'] as IDBFactory;
|
||||
|
||||
try {
|
||||
const name = 'drift_mock_db';
|
||||
|
||||
final mockDb = await idb.open(name);
|
||||
final mockDb = await idb.open(name).complete<IDBDatabase>();
|
||||
mockDb.close();
|
||||
idb.deleteDatabase(name);
|
||||
} catch (error) {
|
||||
|
@ -87,19 +95,16 @@ Future<bool> checkIndexedDbExists(String databaseName) async {
|
|||
bool? indexedDbExists;
|
||||
|
||||
try {
|
||||
final idb = getProperty<IdbFactory>(globalThis, 'indexedDB');
|
||||
final idb = globalContext['indexedDB'] as IDBFactory;
|
||||
|
||||
final database = await idb.open(
|
||||
databaseName,
|
||||
// Current schema version used by the [IndexedDbFileSystem]
|
||||
version: 1,
|
||||
onUpgradeNeeded: (event) {
|
||||
// If there's an upgrade, we're going from 0 to 1 - the database doesn't
|
||||
// exist! Abort the transaction so that we don't create it here.
|
||||
event.target.transaction!.abort();
|
||||
indexedDbExists = false;
|
||||
},
|
||||
);
|
||||
final openRequest = idb.open(databaseName, 1);
|
||||
openRequest.onupgradeneeded = (IDBVersionChangeEvent event) {
|
||||
// If there's an upgrade, we're going from 0 to 1 - the database doesn't
|
||||
// exist! Abort the transaction so that we don't create it here.
|
||||
openRequest.transaction!.abort();
|
||||
indexedDbExists = false;
|
||||
}.toJS;
|
||||
final database = await openRequest.complete<IDBDatabase>();
|
||||
|
||||
indexedDbExists ??= true;
|
||||
database.close();
|
||||
|
@ -112,9 +117,9 @@ Future<bool> checkIndexedDbExists(String databaseName) async {
|
|||
|
||||
/// Deletes a database from IndexedDb if supported.
|
||||
Future<void> deleteDatabaseInIndexedDb(String databaseName) async {
|
||||
final idb = window.indexedDB;
|
||||
if (idb != null) {
|
||||
await idb.deleteDatabase(databaseName);
|
||||
if (globalContext.has('indexedDB')) {
|
||||
final idb = globalContext['indexedDB'] as IDBFactory;
|
||||
await idb.deleteDatabase(databaseName).complete<JSAny?>();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -181,12 +186,16 @@ class DriftServerController {
|
|||
final initPort = message.initializationPort;
|
||||
|
||||
final initializer = initPort != null
|
||||
? () async {
|
||||
initPort.postMessage(true);
|
||||
? () {
|
||||
final completer = Completer<Uint8List?>();
|
||||
initPort.postMessage(true.toJS);
|
||||
|
||||
return await initPort.onMessage
|
||||
.map((e) => e.data as Uint8List?)
|
||||
.first;
|
||||
initPort.onmessage = (MessageEvent e) {
|
||||
final data = (e.data as JSUint8Array?);
|
||||
completer.complete(data?.toDart);
|
||||
}.toJS;
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
: null;
|
||||
|
||||
|
@ -269,7 +278,7 @@ class DriftServerController {
|
|||
StartFileSystemServer(options).sendToWorker(worker);
|
||||
|
||||
// Wait for the server worker to report that it's ready
|
||||
await worker.onMessage.first;
|
||||
await EventStreamProviders.messageEvent.forTarget(worker).first;
|
||||
|
||||
return WasmVfs(workerOptions: options);
|
||||
}
|
||||
|
@ -349,3 +358,21 @@ extension StorageClassification on WasmStorageImplementation {
|
|||
this == WasmStorageImplementation.sharedIndexedDb ||
|
||||
this == WasmStorageImplementation.unsafeIndexedDb;
|
||||
}
|
||||
|
||||
/// Utilities to complete an IndexedDB request.
|
||||
extension CompleteIdbRequest on IDBRequest {
|
||||
/// Turns this request into a Dart future that completes with the first
|
||||
/// success or error event.
|
||||
Future<T> complete<T extends JSAny?>() {
|
||||
final completer = Completer<T>.sync();
|
||||
|
||||
EventStreamProviders.successEvent.forTarget(this).listen((event) {
|
||||
completer.complete(result as T);
|
||||
});
|
||||
EventStreamProviders.errorEvent.forTarget(this).listen((event) {
|
||||
completer.completeError(error ?? event);
|
||||
});
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
// ignore_for_file: public_member_api_docs
|
||||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:js/js_util.dart';
|
||||
import 'package:web/web.dart';
|
||||
|
||||
import '../wasm_setup.dart';
|
||||
import 'protocol.dart';
|
||||
|
@ -22,13 +22,15 @@ class SharedDriftWorker {
|
|||
: _servers = DriftServerController(setup);
|
||||
|
||||
void start() {
|
||||
const event = EventStreamProvider<MessageEvent>('connect');
|
||||
event.forTarget(self).listen(_newConnection);
|
||||
const event = EventStreamProviders.connectEvent;
|
||||
event.forTarget(self).listen((e) => _newConnection(e as MessageEvent));
|
||||
}
|
||||
|
||||
void _newConnection(MessageEvent event) async {
|
||||
final clientPort = event.ports[0];
|
||||
clientPort.onMessage
|
||||
final clientPort = event.ports.toDart[0];
|
||||
clientPort.start();
|
||||
EventStreamProviders.messageEvent
|
||||
.forTarget(clientPort)
|
||||
.listen((event) => _messageFromClient(clientPort, event));
|
||||
}
|
||||
|
||||
|
@ -111,9 +113,9 @@ class SharedDriftWorker {
|
|||
}
|
||||
}
|
||||
|
||||
messageSubscription = worker.onMessage.listen((event) {
|
||||
final data =
|
||||
WasmInitializationMessage.fromJs(getProperty(event, 'data'));
|
||||
messageSubscription =
|
||||
EventStreamProviders.messageEvent.forTarget(worker).listen((event) {
|
||||
final data = WasmInitializationMessage.read(event);
|
||||
final compatibilityResult = data as DedicatedWorkerCompatibilityResult;
|
||||
|
||||
result(
|
||||
|
@ -124,7 +126,8 @@ class SharedDriftWorker {
|
|||
);
|
||||
});
|
||||
|
||||
errorSubscription = worker.onError.listen((event) {
|
||||
errorSubscription =
|
||||
EventStreamProviders.errorEvent.forTarget(worker).listen((event) {
|
||||
result(false, false, false, const []);
|
||||
worker.terminate();
|
||||
_dedicatedWorker = null;
|
||||
|
|
|
@ -7,15 +7,17 @@
|
|||
library drift.wasm;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:html';
|
||||
import 'dart:js_interop';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/src/web/wasm_setup.dart';
|
||||
import 'package:web/web.dart'
|
||||
show DedicatedWorkerGlobalScope, SharedWorkerGlobalScope;
|
||||
import 'package:sqlite3/wasm.dart';
|
||||
|
||||
import 'backends.dart';
|
||||
import 'src/sqlite3/database.dart';
|
||||
import 'src/web/wasm_setup.dart';
|
||||
import 'src/web/wasm_setup/dedicated_worker.dart';
|
||||
import 'src/web/wasm_setup/shared_worker.dart';
|
||||
import 'src/web/wasm_setup/types.dart';
|
||||
|
@ -205,12 +207,15 @@ class WasmDatabase extends DelegatedDatabase {
|
|||
static void workerMainForOpen({
|
||||
WasmDatabaseSetup? setupAllDatabases,
|
||||
}) {
|
||||
final self = WorkerGlobalScope.instance;
|
||||
final self = globalContext;
|
||||
|
||||
if (self is DedicatedWorkerGlobalScope) {
|
||||
DedicatedDriftWorker(self, setupAllDatabases).start();
|
||||
} else if (self is SharedWorkerGlobalScope) {
|
||||
SharedDriftWorker(self, setupAllDatabases).start();
|
||||
if (self.instanceOfString('DedicatedWorkerGlobalScope')) {
|
||||
DedicatedDriftWorker(
|
||||
self as DedicatedWorkerGlobalScope, setupAllDatabases)
|
||||
.start();
|
||||
} else if (self.instanceOfString('SharedWorkerGlobalScope')) {
|
||||
SharedDriftWorker(self as SharedWorkerGlobalScope, setupAllDatabases)
|
||||
.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,4 +10,4 @@ import 'package:meta/meta.dart';
|
|||
export 'src/web/sql_js.dart';
|
||||
export 'src/web/storage.dart' hide CustomSchemaVersionSave;
|
||||
export 'src/web/web_db.dart';
|
||||
export 'src/web/channel.dart';
|
||||
export 'src/web/channel.dart' show PortToChannel;
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
name: drift
|
||||
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
|
||||
version: 2.15.0
|
||||
version: 2.16.0
|
||||
repository: https://github.com/simolus3/drift
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
async: ^2.5.0
|
||||
convert: ^3.0.0
|
||||
collection: ^1.15.0
|
||||
js: ^0.6.3
|
||||
js: '>=0.6.3 <0.8.0'
|
||||
meta: ^1.3.0
|
||||
stream_channel: ^2.1.0
|
||||
sqlite3: ^2.4.0
|
||||
path: ^1.8.0
|
||||
stack_trace: ^1.11.1
|
||||
web: ^0.5.0
|
||||
|
||||
dev_dependencies:
|
||||
archive: ^3.3.1
|
||||
analyzer: ^6.4.1
|
||||
build_test: ^2.0.0
|
||||
build_runner_core: ^7.0.0
|
||||
build_verify: ^3.0.0
|
||||
|
|
|
@ -35,14 +35,14 @@ void main() {
|
|||
);
|
||||
|
||||
b.replaceAll(db.categories, const [
|
||||
CategoriesCompanion(id: Value(1), description: Value('new1')),
|
||||
CategoriesCompanion(id: Value(2), description: Value('new2')),
|
||||
CategoriesCompanion(id: Value(RowId(1)), description: Value('new1')),
|
||||
CategoriesCompanion(id: Value(RowId(2)), description: Value('new2')),
|
||||
]);
|
||||
|
||||
b.deleteWhere<$CategoriesTable, Category>(
|
||||
db.categories, (tbl) => tbl.id.equals(1));
|
||||
b.deleteAll(db.categories);
|
||||
b.delete(db.todosTable, const TodosTableCompanion(id: Value(3)));
|
||||
b.delete(db.todosTable, const TodosTableCompanion(id: Value(RowId(3))));
|
||||
|
||||
b.update(db.users, const UsersCompanion(name: Value('new name 2')));
|
||||
|
||||
|
@ -97,7 +97,7 @@ void main() {
|
|||
db.categories,
|
||||
CategoriesCompanion.insert(description: 'description'),
|
||||
onConflict: DoUpdate((old) {
|
||||
return const CategoriesCompanion(id: Value(42));
|
||||
return const CategoriesCompanion(id: Value(RowId(42)));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -203,16 +203,17 @@ void main() {
|
|||
|
||||
test('updates stream queries', () async {
|
||||
await db.batch((b) {
|
||||
b.insert(db.todosTable, const TodoEntry(id: 3, content: 'content'));
|
||||
b.insert(
|
||||
db.todosTable, const TodoEntry(id: RowId(3), content: 'content'));
|
||||
|
||||
b.update(db.users, const UsersCompanion(name: Value('new user name')));
|
||||
b.replace(
|
||||
db.todosTable,
|
||||
const TodosTableCompanion(id: Value(3), content: Value('new')),
|
||||
const TodosTableCompanion(id: Value(RowId(3)), content: Value('new')),
|
||||
);
|
||||
|
||||
b.deleteWhere(db.todosTable, (TodosTable row) => row.id.equals(3));
|
||||
b.delete(db.todosTable, const TodosTableCompanion(id: Value(3)));
|
||||
b.delete(db.todosTable, const TodosTableCompanion(id: Value(RowId(3))));
|
||||
});
|
||||
|
||||
verify(
|
||||
|
|
|
@ -6,7 +6,7 @@ import '../generated/todos.dart';
|
|||
void main() {
|
||||
test('data classes can be serialized', () {
|
||||
final entry = TodoEntry(
|
||||
id: 13,
|
||||
id: RowId(13),
|
||||
title: 'Title',
|
||||
content: 'Content',
|
||||
targetDate: DateTime.now(),
|
||||
|
@ -36,7 +36,7 @@ void main() {
|
|||
driftRuntimeOptions.defaultSerializer = _MySerializer();
|
||||
|
||||
final entry = TodoEntry(
|
||||
id: 13,
|
||||
id: RowId(13),
|
||||
title: 'Title',
|
||||
content: 'Content',
|
||||
category: 3,
|
||||
|
@ -59,7 +59,7 @@ void main() {
|
|||
|
||||
test('can serialize and deserialize blob columns', () {
|
||||
final user = User(
|
||||
id: 3,
|
||||
id: RowId(3),
|
||||
name: 'Username',
|
||||
isAwesome: true,
|
||||
profilePicture: Uint8List.fromList(const [1, 2, 3, 4]),
|
||||
|
@ -79,7 +79,7 @@ void main() {
|
|||
|
||||
test('generated data classes can be converted to companions', () {
|
||||
const entry = Category(
|
||||
id: 3,
|
||||
id: RowId(3),
|
||||
description: 'description',
|
||||
priority: CategoryPriority.low,
|
||||
descriptionInUpperCase: 'ignored',
|
||||
|
@ -91,7 +91,7 @@ void main() {
|
|||
companion,
|
||||
equals(CategoriesCompanion.insert(
|
||||
description: 'description',
|
||||
id: const Value(3),
|
||||
id: const Value(RowId(3)),
|
||||
priority: const Value(CategoryPriority.low),
|
||||
)),
|
||||
);
|
||||
|
@ -105,15 +105,16 @@ void main() {
|
|||
expect(entry.toCompanion(true), const PureDefaultsCompanion());
|
||||
});
|
||||
|
||||
test('nullable values cannot be used with nullOrAbsent', () {
|
||||
test('utilities to wrap nullable values', () {
|
||||
expect(
|
||||
// ignore: prefer_const_constructors
|
||||
// ignore: prefer_const_constructors, deprecated_member_use_from_same_package
|
||||
() => Value<int?>.ofNullable(null),
|
||||
throwsA(isA<AssertionError>()));
|
||||
|
||||
expect(const Value<int>.ofNullable(null).present, isFalse);
|
||||
expect(const Value<int?>.ofNullable(12).present, isTrue);
|
||||
expect(const Value<int>.ofNullable(23).present, isTrue);
|
||||
expect(const Value<int?>.absentIfNull(null).present, isFalse);
|
||||
expect(const Value<int>.absentIfNull(null).present, isFalse);
|
||||
expect(const Value<int?>.absentIfNull(12).present, isTrue);
|
||||
expect(const Value<int>.absentIfNull(23).present, isTrue);
|
||||
});
|
||||
|
||||
test('companions support hash and equals', () {
|
||||
|
|
|
@ -72,7 +72,7 @@ void main() {
|
|||
final executor = MockExecutor();
|
||||
final db = TodoDb(executor);
|
||||
|
||||
await db.someDao.todosForUser(user: 1).get();
|
||||
await db.someDao.todosForUser(user: RowId(1)).get();
|
||||
|
||||
verify(executor.runSelect(argThat(contains('SELECT t.* FROM todos')), [1]));
|
||||
});
|
||||
|
|
|
@ -44,6 +44,23 @@ void main() {
|
|||
expect(exp, generates('?', [10]));
|
||||
expect(exp.driftSqlType, isA<_NegatedIntType>());
|
||||
});
|
||||
|
||||
test('also supports dialect-aware types', () {
|
||||
const b = CustomExpression(
|
||||
'b',
|
||||
customType: DialectAwareSqlType<int>.via(
|
||||
fallback: _NegatedIntType(),
|
||||
overrides: {SqlDialect.postgres: DriftSqlType.int},
|
||||
),
|
||||
precedence: Precedence.primary,
|
||||
);
|
||||
|
||||
expect(b.equals(3), generates('b = ?', [-3]));
|
||||
expect(
|
||||
b.equals(3),
|
||||
generatesWithOptions('b = \$1',
|
||||
variables: [3], dialect: SqlDialect.postgres));
|
||||
});
|
||||
}
|
||||
|
||||
class _NegatedIntType implements CustomSqlType<int> {
|
||||
|
|
|
@ -125,7 +125,11 @@ void main() {
|
|||
});
|
||||
|
||||
test('substring', () {
|
||||
expect(eval(Constant('hello world').substr(7)), completion('world'));
|
||||
final input = Constant('hello world');
|
||||
expect(eval(input.substr(7)), completion('world'));
|
||||
|
||||
expect(eval(input.substrExpr(Variable(1), input.length - Variable(6))),
|
||||
completion('hello'));
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -52,5 +52,8 @@ void main() {
|
|||
test('substr', () {
|
||||
expect(expression.substr(10), generates('SUBSTR(col, 10)'));
|
||||
expect(expression.substr(10, 2), generates('SUBSTR(col, 10, 2)'));
|
||||
|
||||
expect(expression.substrExpr(Variable(1), expression.length - Variable(5)),
|
||||
generates('SUBSTR(col, ?, LENGTH(col) - ?)', [1, 5]));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ void main() {
|
|||
group('compiled custom queries', () {
|
||||
// defined query: SELECT * FROM todos WHERE title = ?2 OR id IN ? OR title = ?1
|
||||
test('work with arrays', () async {
|
||||
await db.withIn('one', 'two', [1, 2, 3]).get();
|
||||
await db.withIn('one', 'two', [RowId(1), RowId(2), RowId(3)]).get();
|
||||
|
||||
verify(
|
||||
executor.runSelect(
|
||||
|
|
|
@ -58,13 +58,14 @@ void main() {
|
|||
|
||||
final returnedValue = await db
|
||||
.delete(db.todosTable)
|
||||
.deleteReturning(const TodosTableCompanion(id: Value(10)));
|
||||
.deleteReturning(const TodosTableCompanion(id: Value(RowId(10))));
|
||||
|
||||
verify(executor.runSelect(
|
||||
'DELETE FROM "todos" WHERE "id" = ? RETURNING *;', [10]));
|
||||
verify(streamQueries.handleTableUpdates(
|
||||
{TableUpdate.onTable(db.todosTable, kind: UpdateKind.delete)}));
|
||||
expect(returnedValue, const TodoEntry(id: 10, content: 'Content'));
|
||||
expect(
|
||||
returnedValue, const TodoEntry(id: RowId(10), content: 'Content'));
|
||||
});
|
||||
|
||||
test('for multiple rows', () async {
|
||||
|
@ -112,7 +113,7 @@ void main() {
|
|||
});
|
||||
|
||||
test('deleteOne()', () async {
|
||||
await db.users.deleteOne(const UsersCompanion(id: Value(3)));
|
||||
await db.users.deleteOne(const UsersCompanion(id: Value(RowId(3))));
|
||||
|
||||
verify(
|
||||
executor.runDelete('DELETE FROM "users" WHERE "id" = ?;', const [3]));
|
||||
|
|
|
@ -56,7 +56,7 @@ void main() {
|
|||
test('generates insert or replace statements', () async {
|
||||
await db.into(db.todosTable).insert(
|
||||
const TodoEntry(
|
||||
id: 113,
|
||||
id: RowId(113),
|
||||
content: 'Done',
|
||||
),
|
||||
mode: InsertMode.insertOrReplace);
|
||||
|
@ -263,6 +263,22 @@ void main() {
|
|||
));
|
||||
});
|
||||
|
||||
test('can ignore conflict target', () async {
|
||||
await db.into(db.todosTable).insert(
|
||||
TodosTableCompanion.insert(content: 'my content'),
|
||||
onConflict: DoUpdate((old) {
|
||||
return TodosTableCompanion.custom(
|
||||
content: const Variable('important: ') + old.content);
|
||||
}, target: []),
|
||||
);
|
||||
|
||||
verify(executor.runInsert(
|
||||
'INSERT INTO "todos" ("content") VALUES (?) '
|
||||
'ON CONFLICT DO UPDATE SET "content" = ? || "content"',
|
||||
argThat(equals(['my content', 'important: '])),
|
||||
));
|
||||
});
|
||||
|
||||
test(
|
||||
'can use multiple upsert targets',
|
||||
() async {
|
||||
|
@ -389,7 +405,8 @@ void main() {
|
|||
when(executor.runInsert(any, any)).thenAnswer((_) => Future.value(3));
|
||||
|
||||
final id = await db.into(db.todosTable).insertOnConflictUpdate(
|
||||
TodosTableCompanion.insert(content: 'content', id: const Value(3)));
|
||||
TodosTableCompanion.insert(
|
||||
content: 'content', id: const Value(RowId(3))));
|
||||
|
||||
verify(executor.runInsert(
|
||||
'INSERT INTO "todos" ("id", "content") VALUES (?, ?) '
|
||||
|
@ -599,7 +616,7 @@ void main() {
|
|||
expect(
|
||||
row,
|
||||
const Category(
|
||||
id: 1,
|
||||
id: RowId(1),
|
||||
description: 'description',
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
priority: CategoryPriority.medium,
|
||||
|
|
|
@ -81,7 +81,7 @@ void main() {
|
|||
expect(
|
||||
row.readTable(todos),
|
||||
TodoEntry(
|
||||
id: 5,
|
||||
id: RowId(5),
|
||||
title: 'title',
|
||||
content: 'content',
|
||||
targetDate: date,
|
||||
|
@ -92,7 +92,7 @@ void main() {
|
|||
expect(
|
||||
row.readTable(categories),
|
||||
const Category(
|
||||
id: 3,
|
||||
id: RowId(3),
|
||||
description: 'description',
|
||||
priority: CategoryPriority.high,
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
|
@ -134,7 +134,7 @@ void main() {
|
|||
expect(
|
||||
row.readTable(db.todosTable),
|
||||
const TodoEntry(
|
||||
id: 5,
|
||||
id: RowId(5),
|
||||
title: 'title',
|
||||
content: 'content',
|
||||
));
|
||||
|
@ -256,7 +256,7 @@ void main() {
|
|||
result.readTable(categories),
|
||||
equals(
|
||||
const Category(
|
||||
id: 3,
|
||||
id: RowId(3),
|
||||
description: 'Description',
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
priority: CategoryPriority.medium,
|
||||
|
@ -306,7 +306,7 @@ void main() {
|
|||
result.readTable(categories),
|
||||
equals(
|
||||
const Category(
|
||||
id: 3,
|
||||
id: RowId(3),
|
||||
description: 'Description',
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
priority: CategoryPriority.medium,
|
||||
|
@ -362,7 +362,7 @@ void main() {
|
|||
expect(
|
||||
result.readTable(categories),
|
||||
const Category(
|
||||
id: 3,
|
||||
id: RowId(3),
|
||||
description: 'desc',
|
||||
descriptionInUpperCase: 'DESC',
|
||||
priority: CategoryPriority.low,
|
||||
|
|
|
@ -16,7 +16,7 @@ final _dataOfTodoEntry = {
|
|||
};
|
||||
|
||||
const _todoEntry = TodoEntry(
|
||||
id: 10,
|
||||
id: RowId(10),
|
||||
title: 'A todo title',
|
||||
content: 'Content',
|
||||
category: 3,
|
||||
|
@ -126,7 +126,7 @@ void main() {
|
|||
}
|
||||
];
|
||||
const resolved = TodoEntry(
|
||||
id: 10,
|
||||
id: RowId(10),
|
||||
title: null,
|
||||
content: 'Content',
|
||||
category: null,
|
||||
|
@ -198,7 +198,7 @@ void main() {
|
|||
expect(
|
||||
category,
|
||||
const Category(
|
||||
id: 1,
|
||||
id: RowId(1),
|
||||
description: 'description',
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
priority: CategoryPriority.high,
|
||||
|
@ -232,7 +232,7 @@ void main() {
|
|||
|
||||
expect(rows, [
|
||||
TodoEntry(
|
||||
id: 10,
|
||||
id: RowId(10),
|
||||
title: null,
|
||||
content: 'Content',
|
||||
category: null,
|
||||
|
|
|
@ -55,7 +55,7 @@ void main() {
|
|||
group('generates replace statements', () {
|
||||
test('regular', () async {
|
||||
await db.update(db.todosTable).replace(const TodoEntry(
|
||||
id: 3,
|
||||
id: RowId(3),
|
||||
title: 'Title',
|
||||
content: 'Updated content',
|
||||
status: TodoStatus.workInProgress,
|
||||
|
@ -71,7 +71,7 @@ void main() {
|
|||
test('applies default values', () async {
|
||||
await db.update(db.users).replace(
|
||||
UsersCompanion(
|
||||
id: const Value(3),
|
||||
id: const Value(RowId(3)),
|
||||
name: const Value('Hummingbird'),
|
||||
profilePicture: Value(Uint8List(0)),
|
||||
),
|
||||
|
@ -167,14 +167,14 @@ void main() {
|
|||
|
||||
group('update on table instances', () {
|
||||
test('update()', () async {
|
||||
await db.users.update().write(const UsersCompanion(id: Value(3)));
|
||||
await db.users.update().write(const UsersCompanion(id: Value(RowId(3))));
|
||||
|
||||
verify(executor.runUpdate('UPDATE "users" SET "id" = ?;', [3]));
|
||||
});
|
||||
|
||||
test('replace', () async {
|
||||
await db.categories.replaceOne(const CategoriesCompanion(
|
||||
id: Value(3), description: Value('new name')));
|
||||
id: Value(RowId(3)), description: Value('new name')));
|
||||
|
||||
verify(executor.runUpdate(
|
||||
'UPDATE "categories" SET "desc" = ?, "priority" = 0 WHERE "id" = ?;',
|
||||
|
@ -205,7 +205,7 @@ void main() {
|
|||
|
||||
expect(rows, const [
|
||||
Category(
|
||||
id: 3,
|
||||
id: RowId(3),
|
||||
description: 'test',
|
||||
priority: CategoryPriority.low,
|
||||
descriptionInUpperCase: 'TEST',
|
||||
|
|
|
@ -59,7 +59,7 @@ void main() {
|
|||
expect(
|
||||
todo,
|
||||
const TodoEntry(
|
||||
id: 1,
|
||||
id: RowId(1),
|
||||
title: 'some title',
|
||||
content: 'do this',
|
||||
targetDate: null,
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:drift/drift.dart';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import '../../generated/converter.dart';
|
||||
import '../../generated/todos.dart';
|
||||
|
||||
enum _MyEnum { one, two, three }
|
||||
|
||||
|
@ -34,6 +35,16 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
test('TypeConverter.extensionType', () {
|
||||
final converter = TypeConverter.extensionType<RowId, int>();
|
||||
|
||||
expect(converter.toSql(RowId(123)), 123);
|
||||
expect(converter.fromSql(15), RowId(15));
|
||||
expect(converter.fromSql(15), 15);
|
||||
expect(converter.fromJson(16), RowId(16));
|
||||
expect(converter.toJson(RowId(124)), 124);
|
||||
});
|
||||
|
||||
group('enum name', () {
|
||||
const converter = EnumNameConverter(_MyEnum.values);
|
||||
const values = {
|
||||
|
|
|
@ -4,8 +4,14 @@ import 'package:uuid/uuid.dart';
|
|||
|
||||
part 'todos.g.dart';
|
||||
|
||||
extension type RowId._(int id) {
|
||||
const RowId(this.id);
|
||||
}
|
||||
|
||||
mixin AutoIncrement on Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get id => integer()
|
||||
.autoIncrement()
|
||||
.map(TypeConverter.extensionType<RowId, int>())();
|
||||
}
|
||||
|
||||
@DataClassName('TodoEntry')
|
||||
|
|
|
@ -11,13 +11,14 @@ class $CategoriesTable extends Categories
|
|||
$CategoriesTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
late final GeneratedColumnWithTypeConverter<RowId, int> id = GeneratedColumn<
|
||||
int>('id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'))
|
||||
.withConverter<RowId>($CategoriesTable.$converterid);
|
||||
static const VerificationMeta _descriptionMeta =
|
||||
const VerificationMeta('description');
|
||||
@override
|
||||
|
@ -56,9 +57,7 @@ class $CategoriesTable extends Categories
|
|||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
context.handle(_idMeta, const VerificationResult.success());
|
||||
if (data.containsKey('desc')) {
|
||||
context.handle(_descriptionMeta,
|
||||
description.isAcceptableOrUnknown(data['desc']!, _descriptionMeta));
|
||||
|
@ -81,8 +80,8 @@ class $CategoriesTable extends Categories
|
|||
Category map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return Category(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
id: $CategoriesTable.$converterid.fromSql(attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!),
|
||||
description: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}desc'])!,
|
||||
priority: $CategoriesTable.$converterpriority.fromSql(attachedDatabase
|
||||
|
@ -99,12 +98,14 @@ class $CategoriesTable extends Categories
|
|||
return $CategoriesTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static JsonTypeConverter2<RowId, int, int> $converterid =
|
||||
TypeConverter.extensionType<RowId, int>();
|
||||
static JsonTypeConverter2<CategoryPriority, int, int> $converterpriority =
|
||||
const EnumIndexConverter<CategoryPriority>(CategoryPriority.values);
|
||||
}
|
||||
|
||||
class Category extends DataClass implements Insertable<Category> {
|
||||
final int id;
|
||||
final RowId id;
|
||||
final String description;
|
||||
final CategoryPriority priority;
|
||||
final String descriptionInUpperCase;
|
||||
|
@ -116,7 +117,9 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
{
|
||||
map['id'] = Variable<int>($CategoriesTable.$converterid.toSql(id));
|
||||
}
|
||||
map['desc'] = Variable<String>(description);
|
||||
{
|
||||
map['priority'] =
|
||||
|
@ -137,7 +140,8 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return Category(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
id: $CategoriesTable.$converterid
|
||||
.fromJson(serializer.fromJson<int>(json['id'])),
|
||||
description: serializer.fromJson<String>(json['description']),
|
||||
priority: $CategoriesTable.$converterpriority
|
||||
.fromJson(serializer.fromJson<int>(json['priority'])),
|
||||
|
@ -154,7 +158,7 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'id': serializer.toJson<int>($CategoriesTable.$converterid.toJson(id)),
|
||||
'description': serializer.toJson<String>(description),
|
||||
'priority': serializer
|
||||
.toJson<int>($CategoriesTable.$converterpriority.toJson(priority)),
|
||||
|
@ -164,7 +168,7 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
}
|
||||
|
||||
Category copyWith(
|
||||
{int? id,
|
||||
{RowId? id,
|
||||
String? description,
|
||||
CategoryPriority? priority,
|
||||
String? descriptionInUpperCase}) =>
|
||||
|
@ -200,7 +204,7 @@ class Category extends DataClass implements Insertable<Category> {
|
|||
}
|
||||
|
||||
class CategoriesCompanion extends UpdateCompanion<Category> {
|
||||
final Value<int> id;
|
||||
final Value<RowId> id;
|
||||
final Value<String> description;
|
||||
final Value<CategoryPriority> priority;
|
||||
const CategoriesCompanion({
|
||||
|
@ -226,7 +230,7 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
|
|||
}
|
||||
|
||||
CategoriesCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
{Value<RowId>? id,
|
||||
Value<String>? description,
|
||||
Value<CategoryPriority>? priority}) {
|
||||
return CategoriesCompanion(
|
||||
|
@ -240,7 +244,7 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
|
|||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
map['id'] = Variable<int>($CategoriesTable.$converterid.toSql(id.value));
|
||||
}
|
||||
if (description.present) {
|
||||
map['desc'] = Variable<String>(description.value);
|
||||
|
@ -271,13 +275,14 @@ class $TodosTableTable extends TodosTable
|
|||
$TodosTableTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
late final GeneratedColumnWithTypeConverter<RowId, int> id = GeneratedColumn<
|
||||
int>('id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'))
|
||||
.withConverter<RowId>($TodosTableTable.$converterid);
|
||||
static const VerificationMeta _titleMeta = const VerificationMeta('title');
|
||||
@override
|
||||
late final GeneratedColumn<String> title = GeneratedColumn<String>(
|
||||
|
@ -328,9 +333,7 @@ class $TodosTableTable extends TodosTable
|
|||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
context.handle(_idMeta, const VerificationResult.success());
|
||||
if (data.containsKey('title')) {
|
||||
context.handle(
|
||||
_titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta));
|
||||
|
@ -366,8 +369,8 @@ class $TodosTableTable extends TodosTable
|
|||
TodoEntry map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return TodoEntry(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
id: $TodosTableTable.$converterid.fromSql(attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!),
|
||||
title: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}title']),
|
||||
content: attachedDatabase.typeMapping
|
||||
|
@ -387,6 +390,8 @@ class $TodosTableTable extends TodosTable
|
|||
return $TodosTableTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static JsonTypeConverter2<RowId, int, int> $converterid =
|
||||
TypeConverter.extensionType<RowId, int>();
|
||||
static JsonTypeConverter2<TodoStatus, String, String> $converterstatus =
|
||||
const EnumNameConverter<TodoStatus>(TodoStatus.values);
|
||||
static JsonTypeConverter2<TodoStatus?, String?, String?> $converterstatusn =
|
||||
|
@ -394,7 +399,7 @@ class $TodosTableTable extends TodosTable
|
|||
}
|
||||
|
||||
class TodoEntry extends DataClass implements Insertable<TodoEntry> {
|
||||
final int id;
|
||||
final RowId id;
|
||||
final String? title;
|
||||
final String content;
|
||||
final DateTime? targetDate;
|
||||
|
@ -410,7 +415,9 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
|
|||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
{
|
||||
map['id'] = Variable<int>($TodosTableTable.$converterid.toSql(id));
|
||||
}
|
||||
if (!nullToAbsent || title != null) {
|
||||
map['title'] = Variable<String>(title);
|
||||
}
|
||||
|
@ -449,7 +456,8 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
|
|||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return TodoEntry(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
id: $TodosTableTable.$converterid
|
||||
.fromJson(serializer.fromJson<int>(json['id'])),
|
||||
title: serializer.fromJson<String?>(json['title']),
|
||||
content: serializer.fromJson<String>(json['content']),
|
||||
targetDate: serializer.fromJson<DateTime?>(json['target_date']),
|
||||
|
@ -467,7 +475,7 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
|
|||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'id': serializer.toJson<int>($TodosTableTable.$converterid.toJson(id)),
|
||||
'title': serializer.toJson<String?>(title),
|
||||
'content': serializer.toJson<String>(content),
|
||||
'target_date': serializer.toJson<DateTime?>(targetDate),
|
||||
|
@ -478,7 +486,7 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
|
|||
}
|
||||
|
||||
TodoEntry copyWith(
|
||||
{int? id,
|
||||
{RowId? id,
|
||||
Value<String?> title = const Value.absent(),
|
||||
String? content,
|
||||
Value<DateTime?> targetDate = const Value.absent(),
|
||||
|
@ -521,7 +529,7 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
|
|||
}
|
||||
|
||||
class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
|
||||
final Value<int> id;
|
||||
final Value<RowId> id;
|
||||
final Value<String?> title;
|
||||
final Value<String> content;
|
||||
final Value<DateTime?> targetDate;
|
||||
|
@ -562,7 +570,7 @@ class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
|
|||
}
|
||||
|
||||
TodosTableCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
{Value<RowId>? id,
|
||||
Value<String?>? title,
|
||||
Value<String>? content,
|
||||
Value<DateTime?>? targetDate,
|
||||
|
@ -582,7 +590,7 @@ class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
|
|||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
map['id'] = Variable<int>($TodosTableTable.$converterid.toSql(id.value));
|
||||
}
|
||||
if (title.present) {
|
||||
map['title'] = Variable<String>(title.value);
|
||||
|
@ -624,13 +632,14 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
$UsersTable(this.attachedDatabase, [this._alias]);
|
||||
static const VerificationMeta _idMeta = const VerificationMeta('id');
|
||||
@override
|
||||
late final GeneratedColumn<int> id = GeneratedColumn<int>(
|
||||
'id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
|
||||
late final GeneratedColumnWithTypeConverter<RowId, int> id = GeneratedColumn<
|
||||
int>('id', aliasedName, false,
|
||||
hasAutoIncrement: true,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'))
|
||||
.withConverter<RowId>($UsersTable.$converterid);
|
||||
static const VerificationMeta _nameMeta = const VerificationMeta('name');
|
||||
@override
|
||||
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||
|
@ -678,9 +687,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
{bool isInserting = false}) {
|
||||
final context = VerificationContext();
|
||||
final data = instance.toColumns(true);
|
||||
if (data.containsKey('id')) {
|
||||
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||
}
|
||||
context.handle(_idMeta, const VerificationResult.success());
|
||||
if (data.containsKey('name')) {
|
||||
context.handle(
|
||||
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta));
|
||||
|
@ -714,8 +721,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
User map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return User(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
|
||||
id: $UsersTable.$converterid.fromSql(attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}id'])!),
|
||||
name: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}name'])!,
|
||||
isAwesome: attachedDatabase.typeMapping
|
||||
|
@ -731,10 +738,13 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
$UsersTable createAlias(String alias) {
|
||||
return $UsersTable(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static JsonTypeConverter2<RowId, int, int> $converterid =
|
||||
TypeConverter.extensionType<RowId, int>();
|
||||
}
|
||||
|
||||
class User extends DataClass implements Insertable<User> {
|
||||
final int id;
|
||||
final RowId id;
|
||||
final String name;
|
||||
final bool isAwesome;
|
||||
final Uint8List profilePicture;
|
||||
|
@ -748,7 +758,9 @@ class User extends DataClass implements Insertable<User> {
|
|||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<int>(id);
|
||||
{
|
||||
map['id'] = Variable<int>($UsersTable.$converterid.toSql(id));
|
||||
}
|
||||
map['name'] = Variable<String>(name);
|
||||
map['is_awesome'] = Variable<bool>(isAwesome);
|
||||
map['profile_picture'] = Variable<Uint8List>(profilePicture);
|
||||
|
@ -770,7 +782,8 @@ class User extends DataClass implements Insertable<User> {
|
|||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return User(
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
id: $UsersTable.$converterid
|
||||
.fromJson(serializer.fromJson<int>(json['id'])),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
isAwesome: serializer.fromJson<bool>(json['isAwesome']),
|
||||
profilePicture: serializer.fromJson<Uint8List>(json['profilePicture']),
|
||||
|
@ -785,7 +798,7 @@ class User extends DataClass implements Insertable<User> {
|
|||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<int>(id),
|
||||
'id': serializer.toJson<int>($UsersTable.$converterid.toJson(id)),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'isAwesome': serializer.toJson<bool>(isAwesome),
|
||||
'profilePicture': serializer.toJson<Uint8List>(profilePicture),
|
||||
|
@ -794,7 +807,7 @@ class User extends DataClass implements Insertable<User> {
|
|||
}
|
||||
|
||||
User copyWith(
|
||||
{int? id,
|
||||
{RowId? id,
|
||||
String? name,
|
||||
bool? isAwesome,
|
||||
Uint8List? profilePicture,
|
||||
|
@ -834,7 +847,7 @@ class User extends DataClass implements Insertable<User> {
|
|||
}
|
||||
|
||||
class UsersCompanion extends UpdateCompanion<User> {
|
||||
final Value<int> id;
|
||||
final Value<RowId> id;
|
||||
final Value<String> name;
|
||||
final Value<bool> isAwesome;
|
||||
final Value<Uint8List> profilePicture;
|
||||
|
@ -871,7 +884,7 @@ class UsersCompanion extends UpdateCompanion<User> {
|
|||
}
|
||||
|
||||
UsersCompanion copyWith(
|
||||
{Value<int>? id,
|
||||
{Value<RowId>? id,
|
||||
Value<String>? name,
|
||||
Value<bool>? isAwesome,
|
||||
Value<Uint8List>? profilePicture,
|
||||
|
@ -889,7 +902,7 @@ class UsersCompanion extends UpdateCompanion<User> {
|
|||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<int>(id.value);
|
||||
map['id'] = Variable<int>($UsersTable.$converterid.toSql(id.value));
|
||||
}
|
||||
if (name.present) {
|
||||
map['name'] = Variable<String>(name.value);
|
||||
|
@ -1872,7 +1885,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
todosTable,
|
||||
}).map((QueryRow row) => AllTodosWithCategoryResult(
|
||||
row: row,
|
||||
id: row.read<int>('id'),
|
||||
id: $TodosTableTable.$converterid.fromSql(row.read<int>('id')),
|
||||
title: row.readNullable<String>('title'),
|
||||
content: row.read<String>('content'),
|
||||
targetDate: row.readNullable<DateTime>('target_date'),
|
||||
|
@ -1880,21 +1893,21 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
status: NullAwareTypeConverter.wrapFromSql(
|
||||
$TodosTableTable.$converterstatus,
|
||||
row.readNullable<String>('status')),
|
||||
catId: row.read<int>('catId'),
|
||||
catId: $CategoriesTable.$converterid.fromSql(row.read<int>('catId')),
|
||||
catDesc: row.read<String>('catDesc'),
|
||||
));
|
||||
}
|
||||
|
||||
Future<int> deleteTodoById(int var1) {
|
||||
Future<int> deleteTodoById(RowId var1) {
|
||||
return customUpdate(
|
||||
'DELETE FROM todos WHERE id = ?1',
|
||||
variables: [Variable<int>(var1)],
|
||||
variables: [Variable<int>($TodosTableTable.$converterid.toSql(var1))],
|
||||
updates: {todosTable},
|
||||
updateKind: UpdateKind.delete,
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<TodoEntry> withIn(String? var1, String? var2, List<int> var3) {
|
||||
Selectable<TodoEntry> withIn(String? var1, String? var2, List<RowId> var3) {
|
||||
var $arrayStartIndex = 3;
|
||||
final expandedvar3 = $expandVar($arrayStartIndex, var3.length);
|
||||
$arrayStartIndex += var3.length;
|
||||
|
@ -1903,18 +1916,19 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
variables: [
|
||||
Variable<String>(var1),
|
||||
Variable<String>(var2),
|
||||
for (var $ in var3) Variable<int>($)
|
||||
for (var $ in var3)
|
||||
Variable<int>($TodosTableTable.$converterid.toSql($))
|
||||
],
|
||||
readsFrom: {
|
||||
todosTable,
|
||||
}).asyncMap(todosTable.mapFromRow);
|
||||
}
|
||||
|
||||
Selectable<TodoEntry> search({required int id}) {
|
||||
Selectable<TodoEntry> search({required RowId id}) {
|
||||
return customSelect(
|
||||
'SELECT * FROM todos WHERE CASE WHEN -1 = ?1 THEN 1 ELSE id = ?1 END',
|
||||
variables: [
|
||||
Variable<int>(id)
|
||||
Variable<int>($TodosTableTable.$converterid.toSql(id))
|
||||
],
|
||||
readsFrom: {
|
||||
todosTable,
|
||||
|
@ -1949,13 +1963,13 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
}
|
||||
|
||||
class AllTodosWithCategoryResult extends CustomResultSet {
|
||||
final int id;
|
||||
final RowId id;
|
||||
final String? title;
|
||||
final String content;
|
||||
final DateTime? targetDate;
|
||||
final int? category;
|
||||
final TodoStatus? status;
|
||||
final int catId;
|
||||
final RowId catId;
|
||||
final String catDesc;
|
||||
AllTodosWithCategoryResult({
|
||||
required QueryRow row,
|
||||
|
@ -2006,11 +2020,11 @@ mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
|
|||
$SharedTodosTable get sharedTodos => attachedDatabase.sharedTodos;
|
||||
$TodoWithCategoryViewView get todoWithCategoryView =>
|
||||
attachedDatabase.todoWithCategoryView;
|
||||
Selectable<TodoEntry> todosForUser({required int user}) {
|
||||
Selectable<TodoEntry> todosForUser({required RowId user}) {
|
||||
return customSelect(
|
||||
'SELECT t.* FROM todos AS t INNER JOIN shared_todos AS st ON st.todo = t.id INNER JOIN users AS u ON u.id = st.user WHERE u.id = ?1',
|
||||
variables: [
|
||||
Variable<int>(user)
|
||||
Variable<int>($UsersTable.$converterid.toSql(user))
|
||||
],
|
||||
readsFrom: {
|
||||
todosTable,
|
||||
|
|
|
@ -109,7 +109,7 @@ void main() {
|
|||
expect(
|
||||
entry,
|
||||
const Category(
|
||||
id: 1,
|
||||
id: RowId(1),
|
||||
description: 'Description',
|
||||
priority: CategoryPriority.low,
|
||||
descriptionInUpperCase: 'DESCRIPTION',
|
||||
|
|
|
@ -26,7 +26,7 @@ void main() {
|
|||
final rows = await (db.select(db.users)
|
||||
..orderBy([(_) => OrderingTerm.random()]))
|
||||
.get();
|
||||
expect(rows.isSorted((a, b) => a.id.compareTo(b.id)), isFalse);
|
||||
expect(rows.isSorted((a, b) => a.id.id.compareTo(b.id.id)), isFalse);
|
||||
});
|
||||
|
||||
test('can select view', () async {
|
||||
|
@ -35,7 +35,7 @@ void main() {
|
|||
await db.todosTable.insertOne(TodosTableCompanion.insert(
|
||||
content: 'some content',
|
||||
title: const Value('title'),
|
||||
category: Value(category.id)));
|
||||
category: Value(category.id.id)));
|
||||
|
||||
final result = await db.todoWithCategoryView.select().getSingle();
|
||||
expect(
|
||||
|
|
|
@ -190,7 +190,7 @@ void main() {
|
|||
stream,
|
||||
emits([
|
||||
Category(
|
||||
id: 1,
|
||||
id: RowId(1),
|
||||
description: 'From remote isolate!',
|
||||
priority: CategoryPriority.low,
|
||||
descriptionInUpperCase: 'FROM REMOTE ISOLATE!',
|
||||
|
@ -240,6 +240,34 @@ void main() {
|
|||
await testWith(DatabaseConnection(NativeDatabase.memory()));
|
||||
});
|
||||
});
|
||||
|
||||
test('uses correct dialect', () async {
|
||||
// Regression test for https://github.com/simolus3/drift/issues/2894
|
||||
final isolate = await DriftIsolate.spawn(() {
|
||||
return NativeDatabase.memory()
|
||||
.interceptWith(PretendDialectInterceptor(SqlDialect.postgres));
|
||||
});
|
||||
final database = TodoDb(await isolate.connect(singleClientMode: true));
|
||||
addTearDown(database.close);
|
||||
|
||||
await database.transaction(() async {
|
||||
await expectLater(
|
||||
database.into(database.users).insertReturning(UsersCompanion.insert(
|
||||
name: 'test user', profilePicture: Uint8List(0))),
|
||||
throwsA(
|
||||
isA<DriftRemoteException>().having(
|
||||
(e) => e.remoteCause,
|
||||
'remoteCause',
|
||||
isA<SqliteException>().having(
|
||||
(e) => e.causingStatement,
|
||||
'causingStatement',
|
||||
contains(r'VALUES ($1, $2)'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void _runTests(FutureOr<DriftIsolate> Function() spawner, bool terminateIsolate,
|
||||
|
@ -290,7 +318,7 @@ void _runTests(FutureOr<DriftIsolate> Function() spawner, bool terminateIsolate,
|
|||
await database.into(database.todosTable).insert(initialCompanion);
|
||||
await expectLater(
|
||||
stream,
|
||||
emits(const TodoEntry(id: 1, content: 'my content')),
|
||||
emits(const TodoEntry(id: RowId(1), content: 'my content')),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ void main() {
|
|||
ExecuteQuery(StatementMethod.select, 'SELECT ?', [BigInt.one]),
|
||||
);
|
||||
|
||||
final mapped = protocol.deserialize(protocol.serialize(request)!);
|
||||
final mapped = _checkSimpleRoundtrip(protocol, request);
|
||||
expect(
|
||||
mapped,
|
||||
isA<Request>().having((e) => e.id, 'id', 1).having(
|
||||
|
@ -109,6 +109,27 @@ void main() {
|
|||
.having((e) => e.args, 'args', [isA<BigInt>()]),
|
||||
),
|
||||
);
|
||||
|
||||
final response = SuccessResponse(
|
||||
1,
|
||||
SelectResult([
|
||||
{'col': BigInt.one}
|
||||
]));
|
||||
final mappedResponse = _checkSimpleRoundtrip(protocol, response);
|
||||
expect(
|
||||
mappedResponse,
|
||||
isA<SuccessResponse>().having((e) => e.requestId, 'requestId', 1).having(
|
||||
(e) => e.response,
|
||||
'response',
|
||||
isA<SelectResult>().having(
|
||||
(e) => e.rows,
|
||||
'rows',
|
||||
([
|
||||
{'col': BigInt.one}
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('can run protocol without using complex types', () async {
|
||||
|
@ -244,6 +265,12 @@ void _checkSimple(Object? object) {
|
|||
}
|
||||
}
|
||||
|
||||
Message _checkSimpleRoundtrip(DriftProtocol protocol, Message source) {
|
||||
final serialized = protocol.serialize(source);
|
||||
_checkSimple(serialized);
|
||||
return protocol.deserialize(serialized!);
|
||||
}
|
||||
|
||||
extension<T> on StreamChannel<T> {
|
||||
StreamChannel<T> get expectedToClose {
|
||||
return transformStream(StreamTransformer.fromHandlers(
|
||||
|
|
|
@ -6,7 +6,7 @@ import 'generated/todos.dart';
|
|||
final DateTime _someDate = DateTime(2019, 06, 08);
|
||||
|
||||
final TodoEntry _someTodoEntry = TodoEntry(
|
||||
id: 3,
|
||||
id: RowId(3),
|
||||
title: null,
|
||||
content: 'content',
|
||||
targetDate: _someDate,
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
@TestOn('vm')
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:analyzer/dart/ast/ast.dart';
|
||||
import 'package:analyzer/dart/analysis/utilities.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('drift does not import legacy JS interop files', () {
|
||||
// The old web APIs can't be used in dart2wasm, so we shouldn't use them in
|
||||
// web-specific drift code.
|
||||
// Legacy APIs (involving `WebDatabase`) are excempt from this.
|
||||
const allowedLegacyCode = [
|
||||
'lib/web/worker.dart', // Wasm uses a different worker
|
||||
'lib/src/web/channel.dart',
|
||||
'lib/src/web/storage.dart',
|
||||
'lib/src/web/sql_js.dart',
|
||||
];
|
||||
|
||||
final failures = <(String, String)>[];
|
||||
|
||||
void check(FileSystemEntity e) {
|
||||
switch (e) {
|
||||
case File():
|
||||
if (allowedLegacyCode.contains(e.path)) return;
|
||||
|
||||
final text = e.readAsStringSync();
|
||||
final parsed = parseString(content: text).unit;
|
||||
|
||||
for (final directive in parsed.directives) {
|
||||
if (directive is ImportDirective) {
|
||||
final uri = directive.uri.stringValue!;
|
||||
if (uri.contains('package:js') ||
|
||||
uri == 'dart:js' ||
|
||||
uri == 'dart:js_util' ||
|
||||
uri == 'dart:html' ||
|
||||
uri == 'dart:indexeddb') {
|
||||
failures.add((e.path, directive.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case Directory():
|
||||
for (final entry in e.listSync()) {
|
||||
check(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final root = Directory('lib/');
|
||||
check(root);
|
||||
|
||||
expect(failures, isEmpty,
|
||||
reason: 'Drift should not import legacy JS code.');
|
||||
});
|
||||
}
|
|
@ -109,7 +109,7 @@ class _GeneratesSqlMatcher extends Matcher {
|
|||
|
||||
final argsMatchState = <String, Object?>{};
|
||||
if (_matchVariables != null &&
|
||||
!_matchVariables!.matches(ctx.boundVariables, argsMatchState)) {
|
||||
!_matchVariables.matches(ctx.boundVariables, argsMatchState)) {
|
||||
matchState['vars'] = ctx.boundVariables;
|
||||
matchState['vars_match'] = argsMatchState;
|
||||
matches = false;
|
||||
|
|
|
@ -100,3 +100,14 @@ class CustomTable extends Table with TableInfo<CustomTable, void> {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class PretendDialectInterceptor extends QueryInterceptor {
|
||||
final SqlDialect _dialect;
|
||||
|
||||
PretendDialectInterceptor(this._dialect);
|
||||
|
||||
@override
|
||||
SqlDialect dialect(QueryExecutor executor) {
|
||||
return _dialect;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Mocks generated by Mockito 5.4.4 from annotations
|
||||
// Mocks generated by Mockito 5.4.3 from annotations
|
||||
// in drift/test/test_utils/test_utils.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
|
|
|
@ -1,4 +1,12 @@
|
|||
## 2.15.1-dev
|
||||
## 2.17.0-dev
|
||||
|
||||
- Fix drift using the wrong import alias in generated part files.
|
||||
- Add the `use_sql_column_name_as_json_key` builder option.
|
||||
- Add a `setup` parameter to `SchemaVerifier`. It is called when the verifier
|
||||
creates database connections (similar to the callback on `NativeDatabase`)
|
||||
and can be used to register custom functions.
|
||||
|
||||
## 2.16.0
|
||||
|
||||
- Keep import alias when referencing existing elements in generated code
|
||||
([#2845](https://github.com/simolus3/drift/issues/2845)).
|
||||
|
|
|
@ -11,8 +11,18 @@ export 'package:drift_dev/src/services/schema/verifier_common.dart'
|
|||
show SchemaMismatch;
|
||||
|
||||
abstract class SchemaVerifier {
|
||||
factory SchemaVerifier(SchemaInstantiationHelper helper) =
|
||||
VerifierImplementation;
|
||||
/// Creates a schema verifier for the drift-generated [helper].
|
||||
///
|
||||
/// See [tests] for more information.
|
||||
/// The optional [setup] parameter is used internally by the verifier for
|
||||
/// every database connection it opens. This can be used to, for instance,
|
||||
/// register custom functions expected by your database.
|
||||
///
|
||||
/// [tests]: https://drift.simonbinder.eu/docs/migrations/tests/
|
||||
factory SchemaVerifier(
|
||||
SchemaInstantiationHelper helper, {
|
||||
void Function(Database raw)? setup,
|
||||
}) = VerifierImplementation;
|
||||
|
||||
/// Creates a [DatabaseConnection] that contains empty tables created for the
|
||||
/// known schema [version].
|
||||
|
|
|
@ -45,13 +45,19 @@ class DriftOptions {
|
|||
defaultValue: true)
|
||||
final bool useColumnNameAsJsonKeyWhenDefinedInMoorFile;
|
||||
|
||||
/// Uses the sql column name as the json key instead of the name in dart.
|
||||
///
|
||||
/// Overrides [useColumnNameAsJsonKeyWhenDefinedInMoorFile] when set to `true`.
|
||||
@JsonKey(name: 'use_sql_column_name_as_json_key', defaultValue: false)
|
||||
final bool useSqlColumnNameAsJsonKey;
|
||||
|
||||
/// Generate a `connect` constructor in database superclasses.
|
||||
///
|
||||
/// This makes drift generate a constructor for database classes that takes a
|
||||
/// `DatabaseConnection` instead of just a `QueryExecutor` - meaning that
|
||||
/// stream queries can also be shared across multiple database instances.
|
||||
/// Starting from drift 2.5, the database connection class implements the
|
||||
/// `QueryExecutor` interface, making this option unecessary.
|
||||
/// `QueryExecutor` interface, making this option unnecessary.
|
||||
@JsonKey(name: 'generate_connect_constructor', defaultValue: false)
|
||||
final bool generateConnectConstructor;
|
||||
|
||||
|
@ -120,6 +126,7 @@ class DriftOptions {
|
|||
this.skipVerificationCode = false,
|
||||
this.useDataClassNameForCompanions = false,
|
||||
this.useColumnNameAsJsonKeyWhenDefinedInMoorFile = true,
|
||||
this.useSqlColumnNameAsJsonKey = false,
|
||||
this.generateConnectConstructor = false,
|
||||
this.dataClassToCompanions = true,
|
||||
this.generateMutableClasses = false,
|
||||
|
@ -147,6 +154,7 @@ class DriftOptions {
|
|||
required this.skipVerificationCode,
|
||||
required this.useDataClassNameForCompanions,
|
||||
required this.useColumnNameAsJsonKeyWhenDefinedInMoorFile,
|
||||
required this.useSqlColumnNameAsJsonKey,
|
||||
required this.generateConnectConstructor,
|
||||
required this.dataClassToCompanions,
|
||||
required this.generateMutableClasses,
|
||||
|
|
|
@ -197,12 +197,14 @@ extension TypeUtils on DartType {
|
|||
}
|
||||
|
||||
class DataClassInformation {
|
||||
final String enforcedName;
|
||||
final String? enforcedName;
|
||||
final String? companionName;
|
||||
final CustomParentClass? extending;
|
||||
final ExistingRowClass? existingClass;
|
||||
|
||||
DataClassInformation(
|
||||
this.enforcedName,
|
||||
this.companionName,
|
||||
this.extending,
|
||||
this.existingClass,
|
||||
);
|
||||
|
@ -233,16 +235,15 @@ class DataClassInformation {
|
|||
));
|
||||
}
|
||||
|
||||
String name;
|
||||
var name = dataClassName?.getField('name')!.toStringValue();
|
||||
final companionName =
|
||||
dataClassName?.getField('companionName')?.toStringValue();
|
||||
CustomParentClass? customParentClass;
|
||||
ExistingRowClass? existingClass;
|
||||
|
||||
if (dataClassName != null) {
|
||||
name = dataClassName.getField('name')!.toStringValue()!;
|
||||
customParentClass =
|
||||
parseCustomParentClass(name, dataClassName, element, resolver);
|
||||
} else {
|
||||
name = dataClassNameForClassName(element.name);
|
||||
}
|
||||
|
||||
if (useRowClass != null) {
|
||||
|
@ -277,7 +278,12 @@ class DataClassInformation {
|
|||
}
|
||||
}
|
||||
|
||||
return DataClassInformation(name, customParentClass, existingClass);
|
||||
return DataClassInformation(
|
||||
name,
|
||||
companionName,
|
||||
customParentClass,
|
||||
existingClass,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:analyzer/dart/ast/ast.dart';
|
|||
import 'package:analyzer/dart/ast/syntactic_entity.dart';
|
||||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift_dev/src/analysis/resolver/shared/data_class.dart';
|
||||
import 'package:sqlparser/sqlparser.dart' as sql;
|
||||
|
||||
import '../../driver/error.dart';
|
||||
|
@ -54,7 +55,9 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
DriftDeclaration.dartElement(element),
|
||||
columns: columns,
|
||||
references: references.toList(),
|
||||
nameOfRowClass: dataClassInfo.enforcedName,
|
||||
nameOfRowClass:
|
||||
dataClassInfo.enforcedName ?? dataClassNameForClassName(element.name),
|
||||
nameOfCompanionClass: dataClassInfo.companionName,
|
||||
existingRowClass: dataClassInfo.existingClass,
|
||||
customParentClass: dataClassInfo.extending,
|
||||
baseDartName: element.name,
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:recase/recase.dart';
|
|||
import '../../results/results.dart';
|
||||
import '../intermediate_state.dart';
|
||||
import '../resolver.dart';
|
||||
import '../shared/data_class.dart';
|
||||
import 'helper.dart';
|
||||
|
||||
class DartViewResolver extends LocalElementResolver<DiscoveredDartView> {
|
||||
|
@ -26,7 +27,8 @@ class DartViewResolver extends LocalElementResolver<DiscoveredDartView> {
|
|||
discovered.ownId,
|
||||
DriftDeclaration.dartElement(discovered.dartElement),
|
||||
columns: columns,
|
||||
nameOfRowClass: dataClassInfo.enforcedName,
|
||||
nameOfRowClass: dataClassInfo.enforcedName ??
|
||||
dataClassNameForClassName(discovered.dartElement.name),
|
||||
existingRowClass: dataClassInfo.existingClass,
|
||||
customParentClass: dataClassInfo.extending,
|
||||
entityInfoName: '\$${discovered.dartElement.name}View',
|
||||
|
|
|
@ -31,7 +31,7 @@ String dataClassNameForClassName(String tableName) {
|
|||
}
|
||||
|
||||
CustomParentClass? parseCustomParentClass(
|
||||
String dartTypeName,
|
||||
String? dartTypeName,
|
||||
DartObject dataClassName,
|
||||
ClassElement element,
|
||||
LocalElementResolver resolver,
|
||||
|
@ -87,7 +87,10 @@ CustomParentClass? parseCustomParentClass(
|
|||
code = AnnotatedDartCode([
|
||||
DartTopLevelSymbol.topLevelElement(extendingType.element),
|
||||
'<',
|
||||
DartTopLevelSymbol(dartTypeName, null),
|
||||
DartTopLevelSymbol(
|
||||
dartTypeName ?? dataClassNameForClassName(element.name),
|
||||
null,
|
||||
),
|
||||
'>',
|
||||
]);
|
||||
} else {
|
||||
|
|
|
@ -108,12 +108,13 @@ class DriftColumn implements HasType {
|
|||
/// The actual json key to use when serializing a data class of this table
|
||||
/// to json.
|
||||
///
|
||||
/// This respectts the [overriddenJsonName], if any, as well as [options].
|
||||
/// This respects the [overriddenJsonName], if any, as well as [options].
|
||||
String getJsonKey([DriftOptions options = const DriftOptions.defaults()]) {
|
||||
if (overriddenJsonName != null) return overriddenJsonName!;
|
||||
|
||||
final useColumnName = options.useColumnNameAsJsonKeyWhenDefinedInMoorFile &&
|
||||
declaredInDriftFile;
|
||||
final useColumnName = options.useSqlColumnNameAsJsonKey ||
|
||||
(options.useColumnNameAsJsonKeyWhenDefinedInMoorFile &&
|
||||
declaredInDriftFile);
|
||||
return useColumnName ? nameInSql : nameInDart;
|
||||
}
|
||||
|
||||
|
|
|
@ -559,4 +559,11 @@ class _AddFromAst extends GeneralizingAstVisitor<void> {
|
|||
_visitCommaSeparated(node.elements);
|
||||
_childEntities([node.rightBracket]);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitTypeArgumentList(TypeArgumentList node) {
|
||||
_builder.addText('<');
|
||||
_visitCommaSeparated(node.arguments);
|
||||
_builder.addText('>');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,9 @@ abstract class DriftElementWithResultSet extends DriftSchemaElement {
|
|||
/// The name for the data class associated with this table or view.
|
||||
String get nameOfRowClass;
|
||||
|
||||
/// The name for the companion class associated with this table or view.
|
||||
String? get nameOfCompanionClass;
|
||||
|
||||
/// All [columns] of this table, indexed by their name in SQL.
|
||||
late final Map<String, DriftColumn> columnBySqlName = CaseInsensitiveMap.of({
|
||||
for (final column in columns) column.nameInSql: column,
|
||||
|
|
|
@ -32,6 +32,9 @@ class DriftTable extends DriftElementWithResultSet {
|
|||
@override
|
||||
final String nameOfRowClass;
|
||||
|
||||
@override
|
||||
final String? nameOfCompanionClass;
|
||||
|
||||
final bool withoutRowId;
|
||||
|
||||
/// Information about the virtual table creating statement backing this table,
|
||||
|
@ -69,6 +72,7 @@ class DriftTable extends DriftElementWithResultSet {
|
|||
required this.columns,
|
||||
required this.baseDartName,
|
||||
required this.nameOfRowClass,
|
||||
this.nameOfCompanionClass,
|
||||
this.references = const [],
|
||||
this.existingRowClass,
|
||||
this.customParentClass,
|
||||
|
|
|
@ -25,6 +25,9 @@ class DriftView extends DriftElementWithResultSet {
|
|||
@override
|
||||
final String nameOfRowClass;
|
||||
|
||||
@override
|
||||
final String? nameOfCompanionClass;
|
||||
|
||||
@override
|
||||
List<DriftElement> references;
|
||||
|
||||
|
@ -38,6 +41,7 @@ class DriftView extends DriftElementWithResultSet {
|
|||
required this.existingRowClass,
|
||||
required this.nameOfRowClass,
|
||||
required this.references,
|
||||
this.nameOfCompanionClass,
|
||||
});
|
||||
|
||||
@override
|
||||
|
|
|
@ -57,6 +57,7 @@ class ElementSerializer {
|
|||
'fixed_entity_info_name': element.fixedEntityInfoName,
|
||||
'base_dart_name': element.baseDartName,
|
||||
'row_class_name': element.nameOfRowClass,
|
||||
'companion_class_name': element.nameOfCompanionClass,
|
||||
'without_rowid': element.withoutRowId,
|
||||
'strict': element.strict,
|
||||
if (element.isVirtual)
|
||||
|
@ -146,6 +147,7 @@ class ElementSerializer {
|
|||
'custom_parent_class':
|
||||
_serializeCustomParentClass(element.customParentClass),
|
||||
'name_of_row_class': element.nameOfRowClass,
|
||||
'name_of_companion_class': element.nameOfCompanionClass,
|
||||
'source': serializedSource,
|
||||
};
|
||||
} else if (element is BaseDriftAccessor) {
|
||||
|
@ -539,6 +541,7 @@ class ElementDeserializer {
|
|||
fixedEntityInfoName: json['fixed_entity_info_name'] as String?,
|
||||
baseDartName: json['base_dart_name'] as String,
|
||||
nameOfRowClass: json['row_class_name'] as String,
|
||||
nameOfCompanionClass: json['companion_class_name'] as String?,
|
||||
withoutRowId: json['without_rowid'] as bool,
|
||||
strict: json['strict'] as bool,
|
||||
virtualTableData: virtualTableData,
|
||||
|
@ -678,6 +681,7 @@ class ElementDeserializer {
|
|||
customParentClass:
|
||||
_readCustomParentClass(json['custom_parent_class'] as Map?),
|
||||
nameOfRowClass: json['name_of_row_class'] as String,
|
||||
nameOfCompanionClass: json['name_of_companion_class'] as String,
|
||||
existingRowClass: json['existing_data_class'] != null
|
||||
? await _readExistingRowClass(
|
||||
id.libraryUri, json['existing_data_class'] as Map)
|
||||
|
|
|
@ -18,6 +18,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate(
|
|||
'skip_verification_code',
|
||||
'use_data_class_name_for_companions',
|
||||
'use_column_name_as_json_key_when_defined_in_moor_file',
|
||||
'use_sql_column_name_as_json_key',
|
||||
'generate_connect_constructor',
|
||||
'sqlite_modules',
|
||||
'sqlite',
|
||||
|
@ -52,6 +53,8 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate(
|
|||
useColumnNameAsJsonKeyWhenDefinedInMoorFile: $checkedConvert(
|
||||
'use_column_name_as_json_key_when_defined_in_moor_file',
|
||||
(v) => v as bool? ?? true),
|
||||
useSqlColumnNameAsJsonKey: $checkedConvert(
|
||||
'use_sql_column_name_as_json_key', (v) => v as bool? ?? false),
|
||||
generateConnectConstructor: $checkedConvert(
|
||||
'generate_connect_constructor', (v) => v as bool? ?? false),
|
||||
dataClassToCompanions: $checkedConvert(
|
||||
|
@ -111,6 +114,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate(
|
|||
'useDataClassNameForCompanions': 'use_data_class_name_for_companions',
|
||||
'useColumnNameAsJsonKeyWhenDefinedInMoorFile':
|
||||
'use_column_name_as_json_key_when_defined_in_moor_file',
|
||||
'useSqlColumnNameAsJsonKey': 'use_sql_column_name_as_json_key',
|
||||
'generateConnectConstructor': 'generate_connect_constructor',
|
||||
'dataClassToCompanions': 'data_class_to_companions',
|
||||
'generateMutableClasses': 'mutable_classes',
|
||||
|
@ -143,6 +147,7 @@ Map<String, dynamic> _$DriftOptionsToJson(DriftOptions instance) =>
|
|||
instance.useDataClassNameForCompanions,
|
||||
'use_column_name_as_json_key_when_defined_in_moor_file':
|
||||
instance.useColumnNameAsJsonKeyWhenDefinedInMoorFile,
|
||||
'use_sql_column_name_as_json_key': instance.useSqlColumnNameAsJsonKey,
|
||||
'generate_connect_constructor': instance.generateConnectConstructor,
|
||||
'sqlite_modules':
|
||||
instance.modules.map((e) => _$SqlModuleEnumMap[e]!).toList(),
|
||||
|
|
|
@ -13,8 +13,9 @@ Expando<List<Input>> expectedSchema = Expando();
|
|||
class VerifierImplementation implements SchemaVerifier {
|
||||
final SchemaInstantiationHelper helper;
|
||||
final Random _random = Random();
|
||||
final void Function(Database)? setup;
|
||||
|
||||
VerifierImplementation(this.helper);
|
||||
VerifierImplementation(this.helper, {this.setup});
|
||||
|
||||
@override
|
||||
Future<void> migrateAndValidate(GeneratedDatabase db, int expectedVersion,
|
||||
|
@ -57,14 +58,20 @@ class VerifierImplementation implements SchemaVerifier {
|
|||
return buffer.toString();
|
||||
}
|
||||
|
||||
Database _setupDatabase(String uri) {
|
||||
final database = sqlite3.open(uri, uri: true);
|
||||
setup?.call(database);
|
||||
return database;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<InitializedSchema> schemaAt(int version) async {
|
||||
// Use distinct executors for setup and use, allowing us to close the helper
|
||||
// db here and avoid creating it twice.
|
||||
// https://www.sqlite.org/inmemorydb.html#sharedmemdb
|
||||
final uri = 'file:mem${_randomString()}?mode=memory&cache=shared';
|
||||
final dbForSetup = sqlite3.open(uri, uri: true);
|
||||
final dbForUse = sqlite3.open(uri, uri: true);
|
||||
final dbForSetup = _setupDatabase(uri);
|
||||
final dbForUse = _setupDatabase(uri);
|
||||
|
||||
final executor = NativeDatabase.opened(dbForSetup);
|
||||
final db = helper.databaseForVersion(executor, version);
|
||||
|
@ -74,7 +81,7 @@ class VerifierImplementation implements SchemaVerifier {
|
|||
await db.close();
|
||||
|
||||
return InitializedSchema(dbForUse, () {
|
||||
final db = sqlite3.open(uri, uri: true);
|
||||
final db = _setupDatabase(uri);
|
||||
return DatabaseConnection(NativeDatabase.opened(db));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -31,13 +31,23 @@ class ImportManagerForPartFiles extends ImportManager {
|
|||
// Part files can't add their own imports, so try to find the element in an
|
||||
// existing import.
|
||||
for (final MapEntry(:key, :value) in _namedImports.entries) {
|
||||
if (value.containsKey(elementName)) {
|
||||
final foundHere = value[elementName];
|
||||
if (foundHere != null && _matchingUrl(definitionUri, foundHere)) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool _matchingUrl(Uri wanted, Element target) {
|
||||
final targetUri = target.librarySource?.uri;
|
||||
if (targetUri == null || targetUri.scheme != wanted.scheme) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class NullImportManager extends ImportManager {
|
||||
|
|
|
@ -74,9 +74,10 @@ abstract class _NodeOrWriter {
|
|||
}
|
||||
|
||||
AnnotatedDartCode companionType(DriftTable table) {
|
||||
final baseName = writer.options.useDataClassNameForCompanions
|
||||
? table.nameOfRowClass
|
||||
: table.baseDartName;
|
||||
final baseName = table.nameOfCompanionClass ??
|
||||
(writer.options.useDataClassNameForCompanions
|
||||
? table.nameOfRowClass
|
||||
: table.baseDartName);
|
||||
|
||||
return generatedElement(table, '${baseName}Companion');
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: drift_dev
|
||||
description: Dev-dependency for users of drift. Contains the generator and development tools.
|
||||
version: 2.15.0
|
||||
version: 2.16.0
|
||||
repository: https://github.com/simolus3/drift
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
|
@ -30,7 +30,7 @@ dependencies:
|
|||
io: ^1.0.3
|
||||
|
||||
# Drift-specific analysis and apis
|
||||
drift: '>=2.15.0 <2.16.0'
|
||||
drift: '>=2.16.0 <2.17.0'
|
||||
sqlite3: '>=0.1.6 <3.0.0'
|
||||
sqlparser: '^0.34.0'
|
||||
|
||||
|
|
|
@ -38,16 +38,17 @@ CREATE INDEX b_idx /* comment should be stripped */ ON b (foo, upper(foo));
|
|||
final result = await emulateDriftBuild(
|
||||
inputs: {
|
||||
'a|lib/main.dart': r'''
|
||||
import 'dart:io' as io;
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'tables.dart' as tables;
|
||||
|
||||
@drift.DriftDatabase(tables: [tables.Texts])
|
||||
@drift.DriftDatabase(tables: [tables.Files])
|
||||
class MyDatabase extends _$MyDatabase {}
|
||||
''',
|
||||
'a|lib/tables.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Texts extends Table {
|
||||
class Files extends Table {
|
||||
TextColumn get content => text()();
|
||||
}
|
||||
''',
|
||||
|
@ -59,12 +60,12 @@ class Texts extends Table {
|
|||
'a|lib/main.drift.dart': decodedMatches(
|
||||
allOf(
|
||||
contains(
|
||||
r'class $TextsTable extends tables.Texts with '
|
||||
r'drift.TableInfo<$TextsTable, Text>',
|
||||
r'class $FilesTable extends tables.Files with '
|
||||
r'drift.TableInfo<$FilesTable, File>',
|
||||
),
|
||||
contains(
|
||||
'class Text extends drift.DataClass implements '
|
||||
'drift.Insertable<Text>',
|
||||
'class File extends drift.DataClass implements '
|
||||
'drift.Insertable<File>',
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -5,7 +5,11 @@ import 'package:drift_dev/src/services/schema/verifier_impl.dart';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
final verifier = SchemaVerifier(_TestHelper());
|
||||
final verifier = SchemaVerifier(
|
||||
_TestHelper(),
|
||||
setup: (rawDb) => rawDb.createFunction(
|
||||
functionName: 'test_function', function: (args) => 1),
|
||||
);
|
||||
|
||||
group('startAt', () {
|
||||
test('starts at the requested version', () async {
|
||||
|
@ -15,6 +19,12 @@ void main() {
|
|||
expect(details.hadUpgrade, isFalse, reason: 'no upgrade expected');
|
||||
}));
|
||||
});
|
||||
|
||||
test('registers custom functions', () async {
|
||||
final db = (await verifier.startAt(17)).executor;
|
||||
await db.ensureOpen(_DelegatedUser(17, (_, details) async {}));
|
||||
await db.runSelect('select test_function()', []);
|
||||
});
|
||||
});
|
||||
|
||||
group('migrateAndValidate', () {
|
||||
|
|
|
@ -267,6 +267,120 @@ extension ItemToInsertable on i1.Item {
|
|||
result.writer,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'generates fromJson and toJson with the sql column names as json keys',
|
||||
() async {
|
||||
final writer = await emulateDriftBuild(
|
||||
options: const BuilderOptions({
|
||||
'use_sql_column_name_as_json_key': true,
|
||||
}),
|
||||
inputs: const {
|
||||
'a|lib/main.dart': r'''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
part 'main.drift.dart';
|
||||
|
||||
class MyTable extends Table {
|
||||
TextColumn get myFirstColumn => text()();
|
||||
IntColumn get mySecondColumn => integer()();
|
||||
}
|
||||
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [MyTable],
|
||||
)
|
||||
class Database extends _$Database {}
|
||||
'''
|
||||
},
|
||||
);
|
||||
|
||||
checkOutputs({
|
||||
'a|lib/main.drift.dart': decodedMatches(contains(r'''
|
||||
factory MyTableData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return MyTableData(
|
||||
myFirstColumn: serializer.fromJson<String>(json['my_first_column']),
|
||||
mySecondColumn: serializer.fromJson<int>(json['my_second_column']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'my_first_column': serializer.toJson<String>(myFirstColumn),
|
||||
'my_second_column': serializer.toJson<int>(mySecondColumn),
|
||||
};
|
||||
}
|
||||
|
||||
''')),
|
||||
}, writer.dartOutputs, writer.writer);
|
||||
},
|
||||
tags: 'analyzer',
|
||||
);
|
||||
|
||||
test(
|
||||
'It should use the provided names for the data classes and the companion class',
|
||||
() async {
|
||||
final writer = await emulateDriftBuild(
|
||||
inputs: const {
|
||||
'a|lib/main.dart': r'''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
part 'main.drift.dart';
|
||||
|
||||
@DataClassName('FirstDataClass', companion: 'FirstCompanionClass')
|
||||
class FirstTable extends Table {
|
||||
TextColumn get foo => text()();
|
||||
IntColumn get bar => integer()();
|
||||
}
|
||||
|
||||
@DataClassName.custom(name: 'SecondDataClass', companion: 'SecondCompanionClass')
|
||||
class SecondTable extends Table {
|
||||
TextColumn get foo => text()();
|
||||
IntColumn get bar => integer()();
|
||||
}
|
||||
|
||||
@DataClassName.custom(companion: 'ThirdCompanionClass')
|
||||
class ThirdTable extends Table {
|
||||
TextColumn get foo => text()();
|
||||
IntColumn get bar => integer()();
|
||||
}
|
||||
|
||||
@DriftDatabase(
|
||||
tables: [FirstTable, SecondTable, ThirdTable],
|
||||
)
|
||||
class Database extends _$Database {}
|
||||
'''
|
||||
},
|
||||
);
|
||||
|
||||
checkOutputs({
|
||||
'a|lib/main.drift.dart': decodedMatches(allOf([
|
||||
contains(
|
||||
'class FirstDataClass extends DataClass implements Insertable<FirstDataClass> {',
|
||||
),
|
||||
contains(
|
||||
'class FirstTableCompanion extends UpdateCompanion<FirstDataClass> {',
|
||||
),
|
||||
contains(
|
||||
'class SecondDataClass extends DataClass implements Insertable<SecondDataClass> {',
|
||||
),
|
||||
contains(
|
||||
'class SecondTableCompanion extends UpdateCompanion<SecondDataClass> {',
|
||||
),
|
||||
contains(
|
||||
'class ThirdTableData extends DataClass implements Insertable<ThirdTableData> {',
|
||||
),
|
||||
contains(
|
||||
'class ThirdTableCompanion extends UpdateCompanion<ThirdTableData> {',
|
||||
),
|
||||
])),
|
||||
}, writer.dartOutputs, writer.writer);
|
||||
},
|
||||
tags: 'analyzer',
|
||||
);
|
||||
}
|
||||
|
||||
class _GeneratesConstDataClasses extends Matcher {
|
||||
|
|
|
@ -3,7 +3,7 @@ version: 1.0.0
|
|||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: '>=2.18.0 <3.0.0'
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
drift:
|
||||
|
|
|
@ -3,7 +3,7 @@ publish_to: none
|
|||
version: 1.0.0
|
||||
|
||||
environment:
|
||||
sdk: '>=2.14.0 <4.0.0'
|
||||
sdk: '>=3.3.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
drift: ^2.11.0
|
||||
|
|
|
@ -120,7 +120,8 @@ class RemoteDatabase {
|
|||
isAlive: isAlive,
|
||||
scope: {'db': database.database.id!},
|
||||
);
|
||||
final value = await eval.retrieveFullValueAsString(stringVal);
|
||||
final value = await eval.service
|
||||
.retrieveFullStringValue(eval.isolateRef!.id!, stringVal);
|
||||
|
||||
final description = DatabaseDescription.fromJson(json.decode(value!));
|
||||
|
||||
|
|
|
@ -12,11 +12,11 @@ dependencies:
|
|||
sdk: flutter
|
||||
|
||||
devtools_extensions: ^0.0.8
|
||||
devtools_app_shared: '>=0.0.5 <0.0.6' # 0.0.6 requires unstable Flutter
|
||||
devtools_app_shared: ^0.0.9
|
||||
db_viewer: ^1.0.3
|
||||
rxdart: ^0.27.7
|
||||
flutter_riverpod: ^3.0.0-dev.0
|
||||
vm_service: ^11.10.0
|
||||
vm_service: ^13.0.0
|
||||
path: ^1.8.3
|
||||
drift: ^2.12.1
|
||||
logging: ^1.2.0
|
||||
|
@ -29,5 +29,8 @@ dev_dependencies:
|
|||
sdk: flutter
|
||||
flutter_lints: ^3.0.0
|
||||
|
||||
dependency_overrides:
|
||||
web: ^0.5.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
|
|
@ -4,8 +4,10 @@ talking to PostgreSQL databases by using the `postgres` package.
|
|||
## Using this
|
||||
|
||||
For general notes on using drift, see [this guide](https://drift.simonbinder.eu/getting-started/).
|
||||
Detailed docs on getting started with `drift_postgres` are available [here](https://drift.simonbinder.eu/docs/platforms/postgres/#setup).
|
||||
|
||||
To use drift_postgres, add this to your `pubspec.yaml`
|
||||
|
||||
```yaml
|
||||
dependencies:
|
||||
drift: "$latest version"
|
||||
|
@ -25,6 +27,21 @@ final database = AppDatabase(PgDatabase(
|
|||
));
|
||||
```
|
||||
|
||||
Also, consider adding builder options to make drift generate postgres-specific code:
|
||||
|
||||
```yaml
|
||||
# build.yaml
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
drift_dev:
|
||||
options:
|
||||
sql:
|
||||
dialects:
|
||||
- sqlite # remove this line if you only need postgres
|
||||
- postgres
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
To test this package, first run
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
targets:
|
||||
$default:
|
||||
builders:
|
||||
drift_dev:
|
||||
options:
|
||||
sql:
|
||||
dialects:
|
||||
- sqlite # remove this line if you only need postgres
|
||||
- postgres
|
|
@ -54,7 +54,8 @@ class _PgDelegate extends DatabaseDelegate {
|
|||
late DbVersionDelegate versionDelegate;
|
||||
|
||||
@override
|
||||
Future<bool> get isOpen => Future.value(_openedSession != null);
|
||||
Future<bool> get isOpen =>
|
||||
Future.value(_openedSession != null && _openedSession!.isOpen);
|
||||
|
||||
@override
|
||||
Future<void> open(QueryExecutorUser user) async {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: drift_postgres
|
||||
description: Postgres implementation and APIs for the drift database package.
|
||||
version: 1.2.0-dev
|
||||
version: 1.2.0
|
||||
repository: https://github.com/simolus3/drift
|
||||
homepage: https://drift.simonbinder.eu/docs/platforms/postgres/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
|
|
|
@ -26,19 +26,19 @@ void main() {
|
|||
return row.read(expression)!;
|
||||
}
|
||||
|
||||
void testWith<T extends Object>(CustomSqlType<T>? type, T value) {
|
||||
test('with variable', () async {
|
||||
final variable = Variable(value, type);
|
||||
expect(await eval(variable), value);
|
||||
});
|
||||
|
||||
test('with constant', () async {
|
||||
final constant = Constant(value, type);
|
||||
expect(await eval(constant), value);
|
||||
});
|
||||
}
|
||||
|
||||
group('custom types pass through', () {
|
||||
void testWith<T extends Object>(CustomSqlType<T> type, T value) {
|
||||
test('with variable', () async {
|
||||
final variable = Variable(value, type);
|
||||
expect(await eval(variable), value);
|
||||
});
|
||||
|
||||
test('with constant', () async {
|
||||
final constant = Constant(value, type);
|
||||
expect(await eval(constant), value);
|
||||
});
|
||||
}
|
||||
|
||||
group('uuid', () => testWith(PgTypes.uuid, Uuid().v4obj()));
|
||||
group(
|
||||
'interval',
|
||||
|
@ -60,6 +60,8 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
group('bytea', () => testWith(null, Uint8List.fromList([1, 2, 3, 4, 5])));
|
||||
|
||||
test('compare datetimes', () async {
|
||||
final time = DateTime.now();
|
||||
final before = Variable(
|
||||
|
|
|
@ -162,6 +162,17 @@ class DriftWebDriver {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> setSchemaVersion(int version) async {
|
||||
final result = await driver.executeAsync(
|
||||
'set_schema_version(arguments[0], arguments[1])',
|
||||
[version.toString()],
|
||||
);
|
||||
|
||||
if (result != true) {
|
||||
throw 'Could not set schema version';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteDatabase(WebStorageApi storageApi, String name) async {
|
||||
await driver.executeAsync('delete_database(arguments[0], arguments[1])', [
|
||||
json.encode([storageApi.name, name]),
|
||||
|
|
|
@ -12,5 +12,13 @@ class TestDatabase extends _$TestDatabase {
|
|||
TestDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onUpgrade: (m, from, to) async {
|
||||
await into(testTable).insert(
|
||||
TestTableCompanion.insert(content: 'from onUpgrade migration'));
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
int schemaVersion = 1;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ dependencies:
|
|||
shelf: ^1.4.1
|
||||
shelf_proxy: ^1.0.4
|
||||
path: ^1.8.3
|
||||
js: ^0.6.7
|
||||
js: ^0.7.0
|
||||
package_config: ^2.1.0
|
||||
async: ^2.11.0
|
||||
http: ^1.0.0
|
||||
|
|
|
@ -40,7 +40,11 @@ enum Browser {
|
|||
|
||||
Future<Process> spawnDriver() async {
|
||||
return switch (this) {
|
||||
firefox => Process.start('geckodriver', []),
|
||||
firefox => Process.start('geckodriver', []).then((result) async {
|
||||
// geckodriver seems to take a while to initialize
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
return result;
|
||||
}),
|
||||
chrome =>
|
||||
Process.start('chromedriver', ['--port=4444', '--url-base=/wd/hub']),
|
||||
};
|
||||
|
@ -156,6 +160,20 @@ void main() {
|
|||
final finalImpls = await driver.probeImplementations();
|
||||
expect(finalImpls.existing, isEmpty);
|
||||
});
|
||||
|
||||
test('migrations', () async {
|
||||
await driver.openDatabase(entry);
|
||||
await driver.insertIntoDatabase();
|
||||
await driver.waitForTableUpdate();
|
||||
|
||||
await driver.closeDatabase();
|
||||
await driver.driver.refresh();
|
||||
|
||||
await driver.setSchemaVersion(2);
|
||||
await driver.openDatabase(entry);
|
||||
// The migration adds a row
|
||||
expect(await driver.amountOfRows, 2);
|
||||
});
|
||||
}
|
||||
|
||||
group(
|
||||
|
|
|
@ -18,6 +18,7 @@ TestDatabase? openedDatabase;
|
|||
StreamQueue<void>? tableUpdates;
|
||||
|
||||
InitializationMode initializationMode = InitializationMode.none;
|
||||
int schemaVersion = 1;
|
||||
|
||||
void main() {
|
||||
_addCallbackForWebDriver('detectImplementations', _detectImplementations);
|
||||
|
@ -32,6 +33,10 @@ void main() {
|
|||
initializationMode = InitializationMode.values.byName(arg!);
|
||||
return true;
|
||||
});
|
||||
_addCallbackForWebDriver('set_schema_version', (arg) async {
|
||||
schemaVersion = int.parse(arg!);
|
||||
return true;
|
||||
});
|
||||
_addCallbackForWebDriver('delete_database', (arg) async {
|
||||
final result = await WasmDatabase.probe(
|
||||
sqlite3Uri: sqlite3WasmUri,
|
||||
|
@ -85,7 +90,7 @@ Future<Uint8List?> _initializeDatabase() async {
|
|||
|
||||
// Let's first open a custom WasmDatabase, the way it would have been
|
||||
// done before WasmDatabase.open.
|
||||
final sqlite3 = await WasmSqlite3.loadFromUrl(Uri.parse('sqlite3.wasm'));
|
||||
final sqlite3 = await WasmSqlite3.loadFromUrl(sqlite3WasmUri);
|
||||
final fs = await IndexedDbFileSystem.open(dbName: dbName);
|
||||
sqlite3.registerVirtualFileSystem(fs, makeDefault: true);
|
||||
|
||||
|
@ -150,7 +155,7 @@ Future<void> _open(String? implementationName) async {
|
|||
db.createFunction(
|
||||
functionName: 'database_host',
|
||||
function: (args) => 'document',
|
||||
argumentCount: const AllowedArgumentCount(1),
|
||||
argumentCount: const AllowedArgumentCount(0),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
@ -158,7 +163,8 @@ Future<void> _open(String? implementationName) async {
|
|||
connection = result.resolvedExecutor;
|
||||
}
|
||||
|
||||
final db = openedDatabase = TestDatabase(connection);
|
||||
final db =
|
||||
openedDatabase = TestDatabase(connection)..schemaVersion = schemaVersion;
|
||||
|
||||
// Make sure it works!
|
||||
await db.customSelect('SELECT database_host()').get();
|
||||
|
|
|
@ -6,7 +6,7 @@ void main() {
|
|||
db.createFunction(
|
||||
functionName: 'database_host',
|
||||
function: (args) => 'worker',
|
||||
argumentCount: const AllowedArgumentCount(1),
|
||||
argumentCount: const AllowedArgumentCount(0),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
## 0.34.1-dev
|
||||
## 3.35.0-dev
|
||||
|
||||
- Fix parsing binary literals.
|
||||
- Drift extensions: Allow custom class names for `CREATE VIEW` statements.
|
||||
|
||||
## 0.34.1
|
||||
|
||||
- Allow selecting from virtual tables using the table-valued function
|
||||
syntax.
|
||||
|
|
|
@ -82,7 +82,7 @@ class ResolvedType {
|
|||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is ResolvedType &&
|
||||
other.type == type &&
|
||||
|
@ -114,7 +114,7 @@ abstract class TypeHint {
|
|||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) => other.runtimeType == runtimeType;
|
||||
bool operator ==(Object other) => other.runtimeType == runtimeType;
|
||||
}
|
||||
|
||||
/// Type hint to mark that this type will contain a boolean value.
|
||||
|
@ -191,7 +191,7 @@ class ResolveResult {
|
|||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
other is ResolveResult &&
|
||||
other.type == type &&
|
||||
|
|
|
@ -64,7 +64,7 @@ class SimpleName extends DeclaredStatementIdentifier {
|
|||
int get hashCode => name.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is SimpleName && other.name == name);
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ class SpecialStatementIdentifier extends DeclaredStatementIdentifier {
|
|||
String get name => specialName;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other is SpecialStatementIdentifier &&
|
||||
other.specialName == specialName);
|
||||
|
|
|
@ -241,7 +241,7 @@ class FrameBoundary {
|
|||
}
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ abstract class TriggerTarget extends AstNode {
|
|||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) => other.runtimeType == runtimeType;
|
||||
bool operator ==(Object other) => other.runtimeType == runtimeType;
|
||||
|
||||
@override
|
||||
Iterable<AstNode> get childNodes => const Iterable.empty();
|
||||
|
|
|
@ -2294,27 +2294,30 @@ class Parser {
|
|||
supportAs ? const [TokenType.as, TokenType.$with] : [TokenType.$with];
|
||||
|
||||
if (enableDriftExtensions && (_match(types))) {
|
||||
final first = _previous;
|
||||
final useExisting = _previous.type == TokenType.$with;
|
||||
final name =
|
||||
_consumeIdentifier('Expected the name for the data class').identifier;
|
||||
String? constructorName;
|
||||
|
||||
if (_matchOne(TokenType.dot)) {
|
||||
constructorName = _consumeIdentifier(
|
||||
'Expected name of the constructor to use after the dot')
|
||||
.identifier;
|
||||
}
|
||||
|
||||
return DriftTableName(
|
||||
useExistingDartClass: useExisting,
|
||||
overriddenDataClassName: name,
|
||||
constructorName: constructorName,
|
||||
)..setSpan(first, _previous);
|
||||
return _startedDriftTableName(_previous);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DriftTableName _startedDriftTableName(Token first) {
|
||||
final useExisting = _previous.type == TokenType.$with;
|
||||
final name =
|
||||
_consumeIdentifier('Expected the name for the data class').identifier;
|
||||
String? constructorName;
|
||||
|
||||
if (_matchOne(TokenType.dot)) {
|
||||
constructorName = _consumeIdentifier(
|
||||
'Expected name of the constructor to use after the dot')
|
||||
.identifier;
|
||||
}
|
||||
|
||||
return DriftTableName(
|
||||
useExistingDartClass: useExisting,
|
||||
overriddenDataClassName: name,
|
||||
constructorName: constructorName,
|
||||
)..setSpan(first, _previous);
|
||||
}
|
||||
|
||||
/// Parses a "CREATE TRIGGER" statement, assuming that the create token has
|
||||
/// already been consumed.
|
||||
CreateTriggerStatement? _createTrigger() {
|
||||
|
@ -2409,17 +2412,33 @@ class Parser {
|
|||
final ifNotExists = _ifNotExists();
|
||||
final name = _consumeIdentifier('Expected a name for this view');
|
||||
|
||||
// Don't allow the "AS ClassName" syntax for views since it causes an
|
||||
// ambiguity with the regular view syntax.
|
||||
final driftTableName = _driftTableName(supportAs: false);
|
||||
DriftTableName? driftTableName;
|
||||
var skippedToSelect = false;
|
||||
|
||||
List<String>? columnNames;
|
||||
if (_matchOne(TokenType.leftParen)) {
|
||||
columnNames = _columnNames();
|
||||
_consume(TokenType.rightParen, 'Expected closing bracket');
|
||||
if (enableDriftExtensions) {
|
||||
if (_check(TokenType.$with)) {
|
||||
driftTableName = _driftTableName();
|
||||
} else if (_matchOne(TokenType.as)) {
|
||||
// This can either be a data class name or the beginning of the select
|
||||
if (_check(TokenType.identifier)) {
|
||||
// It's a data class name
|
||||
driftTableName = _startedDriftTableName(_previous);
|
||||
} else {
|
||||
// No, we'll expect the SELECT next.
|
||||
skippedToSelect = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_consume(TokenType.as, 'Expected AS SELECT');
|
||||
List<String>? columnNames;
|
||||
if (!skippedToSelect) {
|
||||
if (_matchOne(TokenType.leftParen)) {
|
||||
columnNames = _columnNames();
|
||||
_consume(TokenType.rightParen, 'Expected closing bracket');
|
||||
}
|
||||
|
||||
_consume(TokenType.as, 'Expected AS SELECT');
|
||||
}
|
||||
|
||||
final query = _fullSelect();
|
||||
if (query == null) {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue