Merge branch 'simolus3:develop' into develop

This commit is contained in:
Nikita Dauhashei 2024-04-10 18:33:06 +02:00 committed by GitHub
commit 78387a3610
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
109 changed files with 1716 additions and 516 deletions

View File

@ -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

View File

@ -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

View File

@ -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');
}

View File

@ -25,6 +25,36 @@ extension FindById<Table extends HasResultSet, Row>
} }
// #enddocregion findById // #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 { extension FindTodoEntryById on GeneratedDatabase {
Todos get todos => Todos(this); Todos get todos => Todos(this);
@ -33,4 +63,10 @@ extension FindTodoEntryById on GeneratedDatabase {
return select(todos)..where((row) => row.id.equals(id)); return select(todos)..where((row) => row.id.equals(id));
} }
// #enddocregion findTodoEntryById // #enddocregion findTodoEntryById
// #docregion updateTodo
Future<Todo?> updateTodoTitle(int id, String newTitle) {
return updateTitle(todos, id, newTitle);
}
// #enddocregion updateTodo
} }

View File

@ -31,7 +31,12 @@ class TodoItems extends Table {
@DriftDatabase(tables: [TodoItems]) @DriftDatabase(tables: [TodoItems])
class AppDatabase extends _$AppDatabase { 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 // #enddocregion before_generation
// #docregion open
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override

View File

@ -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` 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. 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] 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). 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. 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. 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 [ResultSetImplementation]: https://drift.simonbinder.eu/api/drift/resultsetimplementation-class
[TableInfo]: https://drift.simonbinder.eu/api/drift/tableinfo-mixin [TableInfo]: https://drift.simonbinder.eu/api/drift/tableinfo-mixin
[ViewInfo]: https://drift.simonbinder.eu/api/drift/viewinfo-class [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

View File

@ -1,7 +1,7 @@
--- ---
data: data:
title: "Selects" title: "Selects"
description: "Select rows or invidiual columns from tables in Dart" description: "Select rows or individual columns from tables in Dart"
weight: 2 weight: 2
template: layouts/docs/single template: layouts/docs/single

View File

@ -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`). (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 You can always override the json key by using a `JSON KEY` column constraint
(e.g. `user_name VARCHAR NOT NULL JSON KEY userName`). (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 * `generate_connect_constructor` (deprecated): Generates a named `connect()` constructor on database classes
that takes a `DatabaseConnection` instead of a `QueryExecutor`. that takes a `DatabaseConnection` instead of a `QueryExecutor`.
This option was deprecated in drift 2.5 because `DatabaseConnection` now implements `QueryExecutor`. This option was deprecated in drift 2.5 because `DatabaseConnection` now implements `QueryExecutor`.

View File

@ -167,6 +167,8 @@ statement before it runs it.
a defined query by appending `WITH YourDartClass` to a `CREATE TABLE` statement. a defined query by appending `WITH YourDartClass` to a `CREATE TABLE` statement.
- Alternatively, you may use `AS DesiredRowClassName` to change the name of the - Alternatively, you may use `AS DesiredRowClassName` to change the name of the
row class generated by drift. 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) - In a column definition, `MAPPED BY` can be used to [apply a converter](#type-converters)
to that column. to that column.
- Similarly, a `JSON KEY` constraint can be used to define the key drift will - 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 ### 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. 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 For instance, let's say you had a Dart class defined as
```dart {% include "blocks/snippet" snippets = rowClassDart name = "user" %}
class User {
final int id;
final String name;
User(this.id, this.name);
}
```
Then, you can instruct drift to use that class as a row class as follows: Then, you can instruct drift to use that class as a row class as follows:
```sql {% include "blocks/snippet" snippets = existingDrift name = "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
```
When using custom row classes defined in another Dart file, you also need to import that file into the file where you define 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. 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: For instance, let's say we expand the existing Dart code in `row_class.dart` by adding another class:
```dart {% include "blocks/snippet" snippets = rowClassDart name = "userwithfriends" %}
class UserWithFriends {
final User user;
final List<User> friends;
UserWithFriends(this.user, {this.friends = const []});
}
```
Now, we can add a corresponding query using the new class for its rows: Now, we can add a corresponding query using the new class for its rows:
```sql {% include "blocks/snippet" snippets = existingDrift name = "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.**, 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;
```
The `WITH UserWithFriends` syntax will make drift consider the `UserWithFriends` class. 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 For every field in the constructor, drift will check the column from the query and

View File

@ -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 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 for migrations after changing the database, we can leave it at `1` for now. The database class
now looks like this: now looks like this:
<a name="open">
{% include "blocks/snippet" snippets = snippets name = 'open' %} {% include "blocks/snippet" snippets = snippets name = 'open' %}
The Android-specific workarounds are necessary because sqlite3 attempts to use `/tmp` to store The Android-specific workarounds are necessary because sqlite3 attempts to use `/tmp` to store

View File

@ -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 - When a migration throws, the database will now block subsequent operations
instead of potentially allowing them to operate on a database in an 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 - Improve stack traces for errors happening on drift isolates (which includes
usages of `NativeDatabase.createInBackground`). usages of `NativeDatabase.createInBackground`).
- Don't cache `EXPLAIN` statements, avoiding schema locks. - 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 ## 2.15.0

View File

@ -308,7 +308,10 @@ final class TableIndex {
class DataClassName { class DataClassName {
/// The overridden name to use when generating the data class for a table. /// The overridden name to use when generating the data class for a table.
/// {@macro drift_custom_data_class} /// {@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. /// 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. /// Customize the data class name for a given table.
/// {@macro drift_custom_data_class} /// {@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. /// An annotation specifying an existing class to be used as a data class.

View File

@ -145,7 +145,9 @@ class DriftProtocol {
result.add(rows.length); result.add(rows.length);
for (final row in rows) { for (final row in rows) {
result.addAll(row.values); for (final value in row.values) {
result.add(_encodeDbValue(value));
}
} }
return result; return result;
} }
@ -234,7 +236,7 @@ class DriftProtocol {
result.add({ result.add({
for (var c = 0; c < columnCount; c++) for (var c = 0; c < columnCount; c++)
columns[c]: fullMessage[rowOffset + c] columns[c]: _decodeDbValue(fullMessage[rowOffset + c])
}); });
} }
return SelectResult(result); return SelectResult(result);

View File

@ -158,6 +158,7 @@ class Value<T> {
/// This constructor should only be used when [T] is not nullable. If [T] were /// 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]. /// nullable, there wouldn't be a clear interpretation for a `null` [value].
/// See the overall documentation on [Value] for details. /// See the overall documentation on [Value] for details.
@Deprecated('Use Value.absentIfNull instead')
const Value.ofNullable(T? value) const Value.ofNullable(T? value)
: assert( : assert(
value != null || null is! T, value != null || null is! T,
@ -167,6 +168,15 @@ class Value<T> {
_value = value, _value = value,
present = value != null; 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 @override
String toString() => present ? 'Value($value)' : 'Value.absent()'; String toString() => present ? 'Value($value)' : 'Value.absent()';

View File

@ -24,14 +24,14 @@ class CustomExpression<D extends Object> extends Expression<D> {
@override @override
final Precedence precedence; final Precedence precedence;
final CustomSqlType<D>? _customSqlType; final UserDefinedSqlType<D>? _customSqlType;
/// Constructs a custom expression by providing the raw sql [content]. /// Constructs a custom expression by providing the raw sql [content].
const CustomExpression( const CustomExpression(
this.content, { this.content, {
this.watchedTables = const [], this.watchedTables = const [],
this.precedence = Precedence.unknown, this.precedence = Precedence.unknown,
CustomSqlType<D>? customType, UserDefinedSqlType<D>? customType,
}) : _dialectSpecificContent = null, }) : _dialectSpecificContent = null,
_customSqlType = customType; _customSqlType = customType;
@ -41,7 +41,7 @@ class CustomExpression<D extends Object> extends Expression<D> {
Map<SqlDialect, String> content, { Map<SqlDialect, String> content, {
this.watchedTables = const [], this.watchedTables = const [],
this.precedence = Precedence.unknown, this.precedence = Precedence.unknown,
CustomSqlType<D>? customType, UserDefinedSqlType<D>? customType,
}) : _dialectSpecificContent = content, }) : _dialectSpecificContent = content,
content = '', content = '',
_customSqlType = customType; _customSqlType = customType;

View File

@ -636,7 +636,7 @@ class _SubqueryExpression<R extends Object> extends Expression<R> {
int get hashCode => statement.hashCode; int get hashCode => statement.hashCode;
@override @override
bool operator ==(Object? other) { bool operator ==(Object other) {
return other is _SubqueryExpression && other.statement == statement; return other is _SubqueryExpression && other.statement == statement;
} }
} }

View File

@ -137,10 +137,31 @@ extension StringExpressionOperators on Expression<String> {
/// and [length] can be negative to return a section of the string before /// and [length] can be negative to return a section of the string before
/// [start]. /// [start].
Expression<String> substr(int start, [int? length]) { 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', [ return FunctionCallExpression('SUBSTR', [
this, this,
Constant<int>(start), start,
if (length != null) Constant<int>(length), if (length != null) length,
]); ]);
} }
} }

View File

@ -54,7 +54,7 @@ extension WithTypes<T extends Object> on Expression<T> {
/// Creates a variable with a matching [driftSqlType]. /// Creates a variable with a matching [driftSqlType].
Variable<T> variable(T? value) { Variable<T> variable(T? value) {
return switch (driftSqlType) { return switch (driftSqlType) {
CustomSqlType<T> custom => Variable(value, custom), UserDefinedSqlType<T> custom => Variable(value, custom),
_ => Variable(value), _ => Variable(value),
}; };
} }

View File

@ -132,7 +132,7 @@ class Migrator {
return _issueCustomQuery(context.sql, context.boundVariables); 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 /// 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 /// individual column, one needs to write a fairly complex migration procedure

View File

@ -162,7 +162,7 @@ class GeneratedColumn<T extends Object> extends Column<T> {
// these custom constraints refer to builtin constraints from drift // these custom constraints refer to builtin constraints from drift
if (!isSerial && _defaultConstraints != null) { if (!isSerial && _defaultConstraints != null) {
_defaultConstraints!(into); _defaultConstraints(into);
} }
} else if ($customConstraints?.isNotEmpty == true) { } else if ($customConstraints?.isNotEmpty == true) {
into.buffer into.buffer

View File

@ -276,8 +276,15 @@ class InsertStatement<T extends Table, D> {
if (ctx.dialect == SqlDialect.mariadb) { if (ctx.dialect == SqlDialect.mariadb) {
ctx.buffer.write(' ON DUPLICATE'); ctx.buffer.write(' ON DUPLICATE');
} else { } 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(); final conflictTarget = target ?? table.$primaryKey.toList();
if (conflictTarget.isEmpty) { if (conflictTarget.isEmpty) {
@ -348,7 +355,7 @@ class InsertStatement<T extends Table, D> {
if (onConflict._where != null) { if (onConflict._where != null) {
ctx.writeWhitespace(); ctx.writeWhitespace();
final where = onConflict._where!( final where = onConflict._where(
table.asDslTable, table.createAlias('excluded').asDslTable); table.asDslTable, table.createAlias('excluded').asDslTable);
where.writeInto(ctx); 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. /// specifies the uniqueness constraint that will trigger the upsert.
/// ///
/// By default, the primary key of the table will be used. /// 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; final List<Column>? target;
/// Creates a `DO UPDATE` clause. /// Creates a `DO UPDATE` clause.

View File

@ -47,6 +47,35 @@ abstract class TypeConverter<D, S> {
json: json, 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 /// 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); 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;
}

View File

@ -124,9 +124,17 @@ final class SqlTypes {
return (dart.millisecondsSinceEpoch ~/ 1000).toString(); return (dart.millisecondsSinceEpoch ~/ 1000).toString();
} }
} else if (dart is Uint8List) { } else if (dart is Uint8List) {
// BLOB literals are string literals containing hexadecimal data and final String hexString = hex.encode(dart);
// preceded by a single "x" or "X" character. Example: X'53514C697465'
return "x'${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) { } else if (dart is DriftAny) {
return mapToSqlLiteral(dart.rawSqlValue); return mapToSqlLiteral(dart.rawSqlValue);
} }

View File

@ -1,34 +1,47 @@
@JS()
library;
import 'dart:async'; 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/api/runtime_api.dart';
import 'package:drift/src/runtime/executor/stream_queries.dart'; import 'package:drift/src/runtime/executor/stream_queries.dart';
import 'package:js/js.dart'; import 'package:web/web.dart' as web;
import 'package:js/js_util.dart';
@JS('Array')
extension type _ArrayWrapper._(JSArray _) implements JSObject {
external static JSBoolean isArray(JSAny? value);
}
/// A [StreamQueryStore] using [web broadcast] APIs /// A [StreamQueryStore] using [web broadcast] APIs
/// ///
/// [web broadcast]: https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API /// [web broadcast]: https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API
class BroadcastStreamQueryStore extends StreamQueryStore { class BroadcastStreamQueryStore extends StreamQueryStore {
final BroadcastChannel _channel; final web.BroadcastChannel _channel;
StreamSubscription<MessageEvent>? _messageFromChannel; StreamSubscription<web.MessageEvent>? _messageFromChannel;
/// Constructs a broadcast query store with the given [identifier]. /// Constructs a broadcast query store with the given [identifier].
/// ///
/// All query stores with the same identifier will share stream query updates. /// All query stores with the same identifier will share stream query updates.
BroadcastStreamQueryStore(String identifier) BroadcastStreamQueryStore(String identifier)
: _channel = BroadcastChannel('drift_updates_$identifier') { : _channel = web.BroadcastChannel('drift_updates_$identifier') {
_messageFromChannel = _channel.onMessage.listen(_handleMessage); _messageFromChannel = web.EventStreamProviders.messageEvent
.forTarget(_channel)
.listen(_handleMessage);
} }
void _handleMessage(MessageEvent message) { void _handleMessage(web.MessageEvent message) {
// Using getProperty to avoid dart2js structured clone that turns the final data = message.data;
// anonymous object into a map. if (!_ArrayWrapper.isArray(data).toDart) {
final data = getProperty<Object?>(message, 'data'); return;
if (data is! List || data.isEmpty) return; }
final asList = (data as JSArray).toDart;
if (asList.isEmpty) return;
super.handleTableUpdates({ super.handleTableUpdates({
for (final entry in data.cast<_SerializedTableUpdate>()) for (final entry in asList.cast<_SerializedTableUpdate>())
entry.toTableUpdate, entry.toTableUpdate,
}); });
} }
@ -39,7 +52,7 @@ class BroadcastStreamQueryStore extends StreamQueryStore {
_channel.postMessage([ _channel.postMessage([
for (final update in updates) _SerializedTableUpdate.of(update), for (final update in updates) _SerializedTableUpdate.of(update),
]); ].toJS);
} }
@override @override
@ -50,34 +63,31 @@ class BroadcastStreamQueryStore extends StreamQueryStore {
} }
/// Whether the current JavaScript context supports broadcast channels. /// Whether the current JavaScript context supports broadcast channels.
static bool get supported => hasProperty(globalThis, 'BroadcastChannel'); static bool get supported => globalContext.has('BroadcastChannel');
} }
@JS() @JS()
@anonymous @anonymous
@staticInterop extension type _SerializedTableUpdate._(JSObject _) implements JSObject {
class _SerializedTableUpdate {
external factory _SerializedTableUpdate({ external factory _SerializedTableUpdate({
required String? kind, required JSString? kind,
required String table, required JSString table,
}); });
factory _SerializedTableUpdate.of(TableUpdate update) { 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 { external JSString? get kind;
@JS() external JSString get table;
external String? get kind;
@JS()
external String get table;
TableUpdate get toTableUpdate { 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(); static final _updateKindByName = UpdateKind.values.asNameMap();

View File

@ -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;
}
}

View File

@ -7,22 +7,23 @@
/// asynchronous /// asynchronous
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
@internal @internal
@JS()
library; library;
import 'dart:async'; import 'dart:async';
import 'dart:html'; import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:async/async.dart'; import 'package:async/async.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift/remote.dart'; import 'package:drift/remote.dart';
import 'package:drift/wasm.dart'; import 'package:drift/wasm.dart';
import 'package:js/js.dart';
import 'package:js/js_util.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:sqlite3/wasm.dart'; import 'package:sqlite3/wasm.dart';
import 'package:web/web.dart' as web;
import 'broadcast_stream_queries.dart'; import 'broadcast_stream_queries.dart';
import 'channel.dart'; import 'new_channel.dart';
import 'wasm_setup/shared.dart'; import 'wasm_setup/shared.dart';
import 'wasm_setup/protocol.dart'; import 'wasm_setup/protocol.dart';
@ -32,10 +33,10 @@ import 'wasm_setup/protocol.dart';
external bool get crossOriginIsolated; external bool get crossOriginIsolated;
/// Whether shared workers can be constructed in the current context. /// 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. /// Whether dedicated workers can be constructed in the current context.
bool get supportsWorkers => hasProperty(globalThis, 'Worker'); bool get supportsWorkers => globalContext.has('Worker');
class WasmDatabaseOpener { class WasmDatabaseOpener {
final Uri sqlite3WasmUri; final Uri sqlite3WasmUri;
@ -107,7 +108,7 @@ class WasmDatabaseOpener {
Future<void> _probeDedicated() async { Future<void> _probeDedicated() async {
if (supportsWorkers) { if (supportsWorkers) {
final dedicatedWorker = _dedicatedWorker = final dedicatedWorker = _dedicatedWorker =
_DriftWorker.dedicated(Worker(driftWorkerUri.toString())); _DriftWorker.dedicated(web.Worker(driftWorkerUri.toString()));
_createCompatibilityCheck().sendTo(dedicatedWorker.send); _createCompatibilityCheck().sendTo(dedicatedWorker.send);
final status = await dedicatedWorker.workerMessages.nextNoError final status = await dedicatedWorker.workerMessages.nextNoError
@ -133,8 +134,8 @@ class WasmDatabaseOpener {
Future<void> _probeShared() async { Future<void> _probeShared() async {
if (supportsSharedWorkers) { if (supportsSharedWorkers) {
final sharedWorker = final sharedWorker =
SharedWorker(driftWorkerUri.toString(), 'drift worker'); web.SharedWorker(driftWorkerUri.toString(), 'drift worker'.toJS);
final port = sharedWorker.port!; final port = sharedWorker.port;
final shared = _sharedWorker = _DriftWorker.shared(sharedWorker, port); final shared = _sharedWorker = _DriftWorker.shared(sharedWorker, port);
// First, the shared worker will tell us which features it supports. // First, the shared worker will tell us which features it supports.
@ -161,40 +162,38 @@ class WasmDatabaseOpener {
} }
final class _DriftWorker { final class _DriftWorker {
final AbstractWorker worker; /// Either a [web.SharedWorker] or a [web.Worker].
final JSObject worker;
ProtocolVersion version = ProtocolVersion.legacy; ProtocolVersion version = ProtocolVersion.legacy;
/// The message port to communicate with the worker, if it's a shared worker. /// 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; final StreamQueue<WasmInitializationMessage> workerMessages;
_DriftWorker.dedicated(Worker this.worker) _DriftWorker.dedicated(web.Worker this.worker)
: portForShared = null, : portForShared = null,
workerMessages = workerMessages = StreamQueue(_readMessages(worker, worker));
StreamQueue(_readMessages(worker.onMessage, worker.onError));
_DriftWorker.shared(SharedWorker this.worker, this.portForShared) _DriftWorker.shared(web.SharedWorker this.worker, this.portForShared)
: workerMessages = : workerMessages = StreamQueue(_readMessages(worker.port, worker)) {
StreamQueue(_readMessages(worker.port!.onMessage, worker.onError)); (worker as web.SharedWorker).port.start();
}
void send(Object? msg, [List<Object>? transfer]) { void send(JSAny? msg, List<JSObject>? transfer) {
switch (worker) { if (portForShared case final port?) {
case final Worker worker: port.postMessage(msg, (transfer ?? const []).toJS);
worker.postMessage(msg, transfer); } else {
case SharedWorker(): (worker as web.Worker).postMessage(msg, (transfer ?? const []).toJS);
portForShared!.postMessage(msg, transfer);
} }
} }
void close() { void close() {
workerMessages.cancel(); workerMessages.cancel();
if (portForShared case final port?) {
switch (worker) { port.close();
case final Worker dedicated: } else {
dedicated.terminate(); (worker as web.Worker).terminate();
case SharedWorker():
portForShared!.close();
} }
} }
} }
@ -225,9 +224,9 @@ final class _ProbeResult implements WasmProbeResult {
FutureOr<Uint8List?> Function()? initializeDatabase, FutureOr<Uint8List?> Function()? initializeDatabase,
WasmDatabaseSetup? localSetup, WasmDatabaseSetup? localSetup,
}) async { }) async {
final channel = MessageChannel(); final channel = web.MessageChannel();
final initializer = initializeDatabase; final initializer = initializeDatabase;
final initChannel = initializer != null ? MessageChannel() : null; final initChannel = initializer != null ? web.MessageChannel() : null;
ServeDriftDatabase message; ServeDriftDatabase message;
final sharedWorker = opener._sharedWorker; final sharedWorker = opener._sharedWorker;
@ -276,18 +275,24 @@ final class _ProbeResult implements WasmProbeResult {
initializeDatabase, localSetup); initializeDatabase, localSetup);
} }
initChannel?.port1.onMessage.listen((event) async { if (initChannel != null) {
// The worker hosting the database is asking for the initial blob because initChannel.port1.start();
// the database doesn't exist. web.EventStreamProviders.messageEvent
Uint8List? result; .forTarget(initChannel.port1)
try { .listen((event) async {
result = await initializer?.call(); // The worker hosting the database is asking for the initial blob because
} finally { // the database doesn't exist.
initChannel.port1 Uint8List? result;
..postMessage(result, [if (result != null) result.buffer]) try {
..close(); result = await initializer?.call();
} } finally {
}); initChannel.port1
..postMessage(
result?.toJS, [if (result != null) result.buffer.toJS].toJS)
..close();
}
});
}
final local = channel.port1 final local = channel.port1
.channel(explicitClose: message.protocolVersion >= ProtocolVersion.v1); .channel(explicitClose: message.protocolVersion >= ProtocolVersion.v1);
@ -350,7 +355,13 @@ final class _ProbeResult implements WasmProbeResult {
} }
Stream<WasmInitializationMessage> _readMessages( 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); final mappedMessages = messages.map(WasmInitializationMessage.read);
return Stream.multi((listener) { return Stream.multi((listener) {

View File

@ -1,10 +1,12 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:async'; 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:sqlite3/wasm.dart';
import 'package:web/web.dart'
show DedicatedWorkerGlobalScope, EventStreamProviders;
import '../../utils/synchronized.dart'; import '../../utils/synchronized.dart';
import 'protocol.dart'; import 'protocol.dart';
@ -22,7 +24,7 @@ class DedicatedDriftWorker {
: _servers = DriftServerController(setup); : _servers = DriftServerController(setup);
void start() { void start() {
self.onMessage.listen((event) { EventStreamProviders.messageEvent.forTarget(self).listen((event) {
final message = WasmInitializationMessage.read(event); final message = WasmInitializationMessage.read(event);
_handleMessage(message); _handleMessage(message);
}); });
@ -69,11 +71,10 @@ class DedicatedDriftWorker {
} }
DedicatedWorkerCompatibilityResult( DedicatedWorkerCompatibilityResult(
supportsNestedWorkers: hasProperty(globalThis, 'Worker'), supportsNestedWorkers: globalContext.has('Worker'),
canAccessOpfs: supportsOpfs, canAccessOpfs: supportsOpfs,
supportsIndexedDb: supportsIndexedDb, supportsIndexedDb: supportsIndexedDb,
supportsSharedArrayBuffers: supportsSharedArrayBuffers: globalContext.has('SharedArrayBuffer'),
hasProperty(globalThis, 'SharedArrayBuffer'),
opfsExists: opfsExists, opfsExists: opfsExists,
indexedDbExists: indexedDbExists, indexedDbExists: indexedDbExists,
existingDatabases: existingDatabases, existingDatabases: existingDatabases,
@ -83,7 +84,7 @@ class DedicatedDriftWorker {
_servers.serve(message); _servers.serve(message);
case StartFileSystemServer(sqlite3Options: final options): case StartFileSystemServer(sqlite3Options: final options):
final worker = await VfsWorker.create(options); final worker = await VfsWorker.create(options);
self.postMessage(true); self.postMessage(true.toJS);
await worker.start(); await worker.start();
case DeleteDatabase(database: (final storage, final name)): case DeleteDatabase(database: (final storage, final name)):
try { try {

View File

@ -1,9 +1,9 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:html'; import 'dart:js_interop';
import 'dart:js'; import 'dart:js_interop_unsafe';
import 'package:js/js_util.dart'; import 'package:web/web.dart' hide WorkerOptions;
import 'package:sqlite3/wasm.dart'; import 'package:sqlite3/wasm.dart';
import 'types.dart'; import 'types.dart';
@ -18,8 +18,8 @@ class ProtocolVersion {
const ProtocolVersion._(this.versionCode); const ProtocolVersion._(this.versionCode);
void writeToJs(Object object) { void writeToJs(JSObject object) {
setProperty(object, 'v', versionCode); object['v'] = versionCode.toJS;
} }
bool operator >=(ProtocolVersion other) { bool operator >=(ProtocolVersion other) {
@ -36,9 +36,9 @@ class ProtocolVersion {
}; };
} }
static ProtocolVersion fromJsObject(Object object) { static ProtocolVersion fromJsObject(JSObject object) {
if (hasProperty(object, 'v')) { if (object.has('v')) {
return negotiate(getProperty<int>(object, 'v')); return negotiate((object['v'] as JSNumber).toDartInt);
} else { } else {
return legacy; return legacy;
} }
@ -58,52 +58,56 @@ class ProtocolVersion {
static const current = v1; 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 /// Sealed superclass for JavaScript objects exchanged between the UI tab and
/// workers spawned by drift to find a suitable database implementation. /// workers spawned by drift to find a suitable database implementation.
sealed class WasmInitializationMessage { sealed class WasmInitializationMessage {
WasmInitializationMessage(); WasmInitializationMessage();
factory WasmInitializationMessage.fromJs(Object jsObject) { factory WasmInitializationMessage.fromJs(JSObject jsObject) {
final type = getProperty<String>(jsObject, 'type'); final type = (jsObject['type'] as JSString).toDart;
final payload = getProperty<Object?>(jsObject, 'payload'); final payload = jsObject['payload'];
return switch (type) { return switch (type) {
WorkerError.type => WorkerError.fromJsPayload(payload!), WorkerError.type => WorkerError.fromJsPayload(payload as JSObject),
ServeDriftDatabase.type => ServeDriftDatabase.fromJsPayload(payload!), ServeDriftDatabase.type =>
ServeDriftDatabase.fromJsPayload(payload as JSObject),
StartFileSystemServer.type => StartFileSystemServer.type =>
StartFileSystemServer.fromJsPayload(payload!), StartFileSystemServer.fromJsPayload(payload as JSObject),
RequestCompatibilityCheck.type => RequestCompatibilityCheck.type =>
RequestCompatibilityCheck.fromJsPayload(payload), RequestCompatibilityCheck.fromJsPayload(payload),
DedicatedWorkerCompatibilityResult.type => DedicatedWorkerCompatibilityResult.type =>
DedicatedWorkerCompatibilityResult.fromJsPayload(payload!), DedicatedWorkerCompatibilityResult.fromJsPayload(payload as JSObject),
SharedWorkerCompatibilityResult.type => SharedWorkerCompatibilityResult.type =>
SharedWorkerCompatibilityResult.fromJsPayload(payload!), SharedWorkerCompatibilityResult.fromJsPayload(payload as JSArray),
DeleteDatabase.type => DeleteDatabase.fromJsPayload(payload!), DeleteDatabase.type => DeleteDatabase.fromJsPayload(payload as JSAny),
_ => throw ArgumentError('Unknown type $type'), _ => throw ArgumentError('Unknown type $type'),
}; };
} }
factory WasmInitializationMessage.read(MessageEvent event) { factory WasmInitializationMessage.read(MessageEvent event) {
// Not using event.data because we don't want the SDK to dartify the raw JS return WasmInitializationMessage.fromJs(event.data as JSObject);
// object we're passing around.
final rawData = getProperty<Object>(event, 'data');
return WasmInitializationMessage.fromJs(rawData);
} }
void sendTo(PostMessage sender); void sendTo(PostMessage sender);
void sendToWorker(Worker worker) { void sendToWorker(Worker worker) {
sendTo(worker.postMessage); sendTo((msg, transfer) {
worker.postMessage(msg, (transfer ?? const []).toJS);
});
} }
void sendToPort(MessagePort port) { void sendToPort(MessagePort port) {
sendTo(port.postMessage); sendTo((msg, transfer) {
port.postMessage(msg, (transfer ?? const []).toJS);
});
} }
void sendToClient(DedicatedWorkerGlobalScope worker) { 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, required super.version,
}); });
factory SharedWorkerCompatibilityResult.fromJsPayload(Object payload) { factory SharedWorkerCompatibilityResult.fromJsPayload(JSArray payload) {
final asList = payload as List; final asList = payload.toDart;
final asBooleans = asList.cast<bool>(); final asBooleans = asList.cast<bool>();
final List<ExistingDatabase> existingDatabases; final List<ExistingDatabase> existingDatabases;
var version = ProtocolVersion.legacy; var version = ProtocolVersion.legacy;
if (asList.length > 5) { if (asList.length > 5) {
existingDatabases = existingDatabases = EncodeLocations.readFromJs(asList[5] as JSArray);
EncodeLocations.readFromJs(asList[5] as List<dynamic>);
if (asList.length > 6) { if (asList.length > 6) {
version = ProtocolVersion.negotiate(asList[6] as int); version = ProtocolVersion.negotiate(asList[6] as int);
@ -187,15 +190,17 @@ final class SharedWorkerCompatibilityResult extends CompatibilityResult {
@override @override
void sendTo(PostMessage sender) { void sendTo(PostMessage sender) {
sender.sendTyped(type, [ sender.sendTyped(
canSpawnDedicatedWorkers, type,
dedicatedWorkersCanUseOpfs, [
canUseIndexedDb, canSpawnDedicatedWorkers.toJS,
indexedDbExists, dedicatedWorkersCanUseOpfs.toJS,
opfsExists, canUseIndexedDb.toJS,
existingDatabases.encodeToJs(), indexedDbExists.toJS,
version.versionCode, opfsExists.toJS,
]); existingDatabases.encodeToJs(),
version.versionCode.toJS,
].toJS);
} }
@override @override
@ -216,13 +221,13 @@ final class WorkerError extends WasmInitializationMessage implements Exception {
WorkerError(this.error); WorkerError(this.error);
factory WorkerError.fromJsPayload(Object payload) { factory WorkerError.fromJsPayload(JSObject payload) {
return WorkerError(payload as String); return WorkerError((payload as JSString).toDart);
} }
@override @override
void sendTo(PostMessage sender) { void sendTo(PostMessage sender) {
sender.sendTyped(type, error); sender.sendTyped(type, error.toJS);
} }
@override @override
@ -252,32 +257,32 @@ final class ServeDriftDatabase extends WasmInitializationMessage {
required this.protocolVersion, required this.protocolVersion,
}); });
factory ServeDriftDatabase.fromJsPayload(Object payload) { factory ServeDriftDatabase.fromJsPayload(JSObject payload) {
return ServeDriftDatabase( return ServeDriftDatabase(
sqlite3WasmUri: Uri.parse(getProperty(payload, 'sqlite')), sqlite3WasmUri: Uri.parse((payload['sqlite'] as JSString).toDart),
port: getProperty(payload, 'port'), port: payload['port'] as MessagePort,
storage: WasmStorageImplementation.values storage: WasmStorageImplementation.values
.byName(getProperty(payload, 'storage')), .byName((payload['storage'] as JSString).toDart),
databaseName: getProperty(payload, 'database'), databaseName: (payload['database'] as JSString).toDart,
initializationPort: getProperty(payload, 'initPort'), initializationPort: payload['initPort'] as MessagePort?,
protocolVersion: ProtocolVersion.fromJsObject(payload), protocolVersion: ProtocolVersion.fromJsObject(payload),
); );
} }
@override @override
void sendTo(PostMessage sender) { void sendTo(PostMessage sender) {
final object = newObject<Object>(); final object = JSObject()
setProperty(object, 'sqlite', sqlite3WasmUri.toString()); ..['sqlite'] = sqlite3WasmUri.toString().toJS
setProperty(object, 'port', port); ..['port'] = port
setProperty(object, 'storage', storage.name); ..['storage'] = storage.name.toJS
setProperty(object, 'database', databaseName); ..['database'] = databaseName.toJS
final initPort = initializationPort; ..['initPort'] = initializationPort;
setProperty(object, 'initPort', initPort);
protocolVersion.writeToJs(object); protocolVersion.writeToJs(object);
sender.sendTyped(type, object, [ sender.sendTyped(type, object, [
port, port,
if (initPort != null) initPort, if (initializationPort != null) initializationPort!,
]); ]);
} }
} }
@ -293,13 +298,13 @@ final class RequestCompatibilityCheck extends WasmInitializationMessage {
RequestCompatibilityCheck(this.databaseName); RequestCompatibilityCheck(this.databaseName);
factory RequestCompatibilityCheck.fromJsPayload(Object? payload) { factory RequestCompatibilityCheck.fromJsPayload(JSAny? payload) {
return RequestCompatibilityCheck(payload as String); return RequestCompatibilityCheck((payload as JSString).toDart);
} }
@override @override
void sendTo(PostMessage sender) { 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, required super.version,
}); });
factory DedicatedWorkerCompatibilityResult.fromJsPayload(Object payload) { factory DedicatedWorkerCompatibilityResult.fromJsPayload(JSObject payload) {
final existingDatabases = <ExistingDatabase>[]; final existingDatabases = <ExistingDatabase>[];
if (hasProperty(payload, 'existing')) { if (payload.has('existing')) {
existingDatabases existingDatabases
.addAll(EncodeLocations.readFromJs(getProperty(payload, 'existing'))); .addAll(EncodeLocations.readFromJs(payload['existing'] as JSArray));
} }
return DedicatedWorkerCompatibilityResult( return DedicatedWorkerCompatibilityResult(
supportsNestedWorkers: getProperty(payload, 'supportsNestedWorkers'), supportsNestedWorkers:
canAccessOpfs: getProperty(payload, 'canAccessOpfs'), (payload['supportsNestedWorkers'] as JSBoolean).toDart,
canAccessOpfs: (payload['canAccessOpfs'] as JSBoolean).toDart,
supportsSharedArrayBuffers: supportsSharedArrayBuffers:
getProperty(payload, 'supportsSharedArrayBuffers'), (payload['supportsSharedArrayBuffers'] as JSBoolean).toDart,
supportsIndexedDb: getProperty(payload, 'supportsIndexedDb'), supportsIndexedDb: (payload['supportsIndexedDb'] as JSBoolean).toDart,
indexedDbExists: getProperty(payload, 'indexedDbExists'), indexedDbExists: (payload['indexedDbExists'] as JSBoolean).toDart,
opfsExists: getProperty(payload, 'opfsExists'), opfsExists: (payload['opfsExists'] as JSBoolean).toDart,
existingDatabases: existingDatabases, existingDatabases: existingDatabases,
version: ProtocolVersion.fromJsObject(payload), version: ProtocolVersion.fromJsObject(payload),
); );
@ -345,16 +351,14 @@ final class DedicatedWorkerCompatibilityResult extends CompatibilityResult {
@override @override
void sendTo(PostMessage sender) { void sendTo(PostMessage sender) {
final object = newObject<Object>(); final object = JSObject()
..['supportsNestedWorkers'] = supportsNestedWorkers.toJS
setProperty(object, 'supportsNestedWorkers', supportsNestedWorkers); ..['canAccessOpfs'] = canAccessOpfs.toJS
setProperty(object, 'canAccessOpfs', canAccessOpfs); ..['supportsIndexedDb'] = supportsIndexedDb.toJS
setProperty(object, 'supportsIndexedDb', supportsIndexedDb); ..['supportsSharedArrayBuffers'] = supportsSharedArrayBuffers.toJS
setProperty( ..['indexedDbExists'] = indexedDbExists.toJS
object, 'supportsSharedArrayBuffers', supportsSharedArrayBuffers); ..['opfsExists'] = opfsExists.toJS
setProperty(object, 'indexedDbExists', indexedDbExists); ..['existing'] = existingDatabases.encodeToJs();
setProperty(object, 'opfsExists', opfsExists);
setProperty(object, 'existing', existingDatabases.encodeToJs());
version.writeToJs(object); version.writeToJs(object);
sender.sendTyped(type, object); sender.sendTyped(type, object);
@ -381,13 +385,13 @@ final class StartFileSystemServer extends WasmInitializationMessage {
StartFileSystemServer(this.sqlite3Options); StartFileSystemServer(this.sqlite3Options);
factory StartFileSystemServer.fromJsPayload(Object payload) { factory StartFileSystemServer.fromJsPayload(JSObject payload) {
return StartFileSystemServer(payload as WorkerOptions); return StartFileSystemServer(payload as WorkerOptions);
} }
@override @override
void sendTo(PostMessage sender) { 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); DeleteDatabase(this.database);
factory DeleteDatabase.fromJsPayload(Object payload) { factory DeleteDatabase.fromJsPayload(JSAny payload) {
final asList = payload as List<Object?>; final asList = (payload as JSArray).toDart;
return DeleteDatabase(( return DeleteDatabase((
WebStorageApi.byName[asList[0] as String]!, WebStorageApi.byName[(asList[0] as JSString).toDart]!,
asList[1] as String, (asList[1] as JSString).toDart,
)); ));
} }
@override @override
void sendTo(PostMessage sender) { 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> { extension EncodeLocations on List<ExistingDatabase> {
static List<ExistingDatabase> readFromJs(List<Object?> object) { static List<ExistingDatabase> readFromJs(JSArray object) {
final existing = <ExistingDatabase>[]; final existing = <ExistingDatabase>[];
for (final entry in object) { for (final entry in object.toDart.cast<JSObject>()) {
existing.add(( existing.add((
WebStorageApi.byName[getProperty(entry as Object, 'l')]!, WebStorageApi.byName[(entry['l'] as JSString).toDart]!,
getProperty(entry, 'n'), (entry['n'] as JSString).toDart,
)); ));
} }
return existing; return existing;
} }
Object encodeToJs() { JSObject encodeToJs() {
final existing = JsArray<Object>(); final existing = <JSObject>[];
for (final entry in this) { for (final entry in this) {
final object = newObject<Object>(); existing.add(JSObject()
setProperty(object, 'l', entry.$1.name); ..['l'] = entry.$1.name.toJS
setProperty(object, 'n', entry.$2); ..['n'] = entry.$2.toJS);
existing.add(object);
} }
return existing; return existing.toJS;
} }
} }
extension on PostMessage { extension on PostMessage {
void sendTyped(String type, Object? payload, [List<Object>? transfer]) { void sendTyped(String type, JSAny? payload, [List<JSObject>? transfer]) {
final object = newObject<Object>(); final object = JSObject()
setProperty(object, 'type', type); ..['type'] = type.toJS
setProperty(object, 'payload', payload); ..['payload'] = payload;
call(object, transfer); call(object, transfer);
} }

View File

@ -1,17 +1,25 @@
import 'dart:async'; import 'dart:async';
import 'dart:html'; import 'dart:js_interop';
import 'dart:indexed_db'; import 'dart:js_interop_unsafe';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift/remote.dart'; import 'package:drift/remote.dart';
import 'package:drift/wasm.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 // ignore: implementation_imports
import 'package:sqlite3/src/wasm/js_interop/file_system_access.dart'; import 'package:sqlite3/src/wasm/js_interop/file_system_access.dart';
import 'package:sqlite3/wasm.dart'; import 'package:sqlite3/wasm.dart';
import 'package:stream_channel/stream_channel.dart'; import 'package:stream_channel/stream_channel.dart';
import '../channel.dart'; import '../new_channel.dart';
import 'protocol.dart'; import 'protocol.dart';
/// Checks whether the OPFS API is likely to be correctly implemented in the /// 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()` // In earlier versions of the OPFS standard, some methods like `getSize()`
// on a sync file handle have actually been asynchronous. We don't support // on a sync file handle have actually been asynchronous. We don't support
// Browsers that implement the outdated spec. // Browsers that implement the outdated spec.
final getSizeResult = callMethod<Object?>(openedFile, 'getSize', []); final getSizeResult = (openedFile as JSObject).callMethod('getSize'.toJS);
if (typeofEquals<Object?>(getSizeResult, 'object')) { if (getSizeResult.typeofEquals('object')) {
// Returned a promise, that's no good. // Returned a promise, that's no good.
await promiseToFuture<Object?>(getSizeResult!); await (getSizeResult as JSPromise).toDart;
return false; return false;
} }
@ -61,18 +69,18 @@ Future<bool> checkOpfsSupport() async {
/// Checks whether IndexedDB is working in the current browser. /// Checks whether IndexedDB is working in the current browser.
Future<bool> checkIndexedDbSupport() async { Future<bool> checkIndexedDbSupport() async {
if (!hasProperty(globalThis, 'indexedDB') || if (!globalContext.has('indexedDB') ||
// FileReader needed to read and write blobs efficiently // FileReader needed to read and write blobs efficiently
!hasProperty(globalThis, 'FileReader')) { !globalContext.has('FileReader')) {
return false; return false;
} }
final idb = getProperty<IdbFactory>(globalThis, 'indexedDB'); final idb = globalContext['indexedDB'] as IDBFactory;
try { try {
const name = 'drift_mock_db'; const name = 'drift_mock_db';
final mockDb = await idb.open(name); final mockDb = await idb.open(name).complete<IDBDatabase>();
mockDb.close(); mockDb.close();
idb.deleteDatabase(name); idb.deleteDatabase(name);
} catch (error) { } catch (error) {
@ -87,19 +95,16 @@ Future<bool> checkIndexedDbExists(String databaseName) async {
bool? indexedDbExists; bool? indexedDbExists;
try { try {
final idb = getProperty<IdbFactory>(globalThis, 'indexedDB'); final idb = globalContext['indexedDB'] as IDBFactory;
final database = await idb.open( final openRequest = idb.open(databaseName, 1);
databaseName, openRequest.onupgradeneeded = (IDBVersionChangeEvent event) {
// Current schema version used by the [IndexedDbFileSystem] // If there's an upgrade, we're going from 0 to 1 - the database doesn't
version: 1, // exist! Abort the transaction so that we don't create it here.
onUpgradeNeeded: (event) { openRequest.transaction!.abort();
// If there's an upgrade, we're going from 0 to 1 - the database doesn't indexedDbExists = false;
// exist! Abort the transaction so that we don't create it here. }.toJS;
event.target.transaction!.abort(); final database = await openRequest.complete<IDBDatabase>();
indexedDbExists = false;
},
);
indexedDbExists ??= true; indexedDbExists ??= true;
database.close(); database.close();
@ -112,9 +117,9 @@ Future<bool> checkIndexedDbExists(String databaseName) async {
/// Deletes a database from IndexedDb if supported. /// Deletes a database from IndexedDb if supported.
Future<void> deleteDatabaseInIndexedDb(String databaseName) async { Future<void> deleteDatabaseInIndexedDb(String databaseName) async {
final idb = window.indexedDB; if (globalContext.has('indexedDB')) {
if (idb != null) { final idb = globalContext['indexedDB'] as IDBFactory;
await idb.deleteDatabase(databaseName); await idb.deleteDatabase(databaseName).complete<JSAny?>();
} }
} }
@ -181,12 +186,16 @@ class DriftServerController {
final initPort = message.initializationPort; final initPort = message.initializationPort;
final initializer = initPort != null final initializer = initPort != null
? () async { ? () {
initPort.postMessage(true); final completer = Completer<Uint8List?>();
initPort.postMessage(true.toJS);
return await initPort.onMessage initPort.onmessage = (MessageEvent e) {
.map((e) => e.data as Uint8List?) final data = (e.data as JSUint8Array?);
.first; completer.complete(data?.toDart);
}.toJS;
return completer.future;
} }
: null; : null;
@ -269,7 +278,7 @@ class DriftServerController {
StartFileSystemServer(options).sendToWorker(worker); StartFileSystemServer(options).sendToWorker(worker);
// Wait for the server worker to report that it's ready // 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); return WasmVfs(workerOptions: options);
} }
@ -349,3 +358,21 @@ extension StorageClassification on WasmStorageImplementation {
this == WasmStorageImplementation.sharedIndexedDb || this == WasmStorageImplementation.sharedIndexedDb ||
this == WasmStorageImplementation.unsafeIndexedDb; 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;
}
}

View File

@ -1,8 +1,8 @@
// ignore_for_file: public_member_api_docs // ignore_for_file: public_member_api_docs
import 'dart:async'; 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 '../wasm_setup.dart';
import 'protocol.dart'; import 'protocol.dart';
@ -22,13 +22,15 @@ class SharedDriftWorker {
: _servers = DriftServerController(setup); : _servers = DriftServerController(setup);
void start() { void start() {
const event = EventStreamProvider<MessageEvent>('connect'); const event = EventStreamProviders.connectEvent;
event.forTarget(self).listen(_newConnection); event.forTarget(self).listen((e) => _newConnection(e as MessageEvent));
} }
void _newConnection(MessageEvent event) async { void _newConnection(MessageEvent event) async {
final clientPort = event.ports[0]; final clientPort = event.ports.toDart[0];
clientPort.onMessage clientPort.start();
EventStreamProviders.messageEvent
.forTarget(clientPort)
.listen((event) => _messageFromClient(clientPort, event)); .listen((event) => _messageFromClient(clientPort, event));
} }
@ -111,9 +113,9 @@ class SharedDriftWorker {
} }
} }
messageSubscription = worker.onMessage.listen((event) { messageSubscription =
final data = EventStreamProviders.messageEvent.forTarget(worker).listen((event) {
WasmInitializationMessage.fromJs(getProperty(event, 'data')); final data = WasmInitializationMessage.read(event);
final compatibilityResult = data as DedicatedWorkerCompatibilityResult; final compatibilityResult = data as DedicatedWorkerCompatibilityResult;
result( result(
@ -124,7 +126,8 @@ class SharedDriftWorker {
); );
}); });
errorSubscription = worker.onError.listen((event) { errorSubscription =
EventStreamProviders.errorEvent.forTarget(worker).listen((event) {
result(false, false, false, const []); result(false, false, false, const []);
worker.terminate(); worker.terminate();
_dedicatedWorker = null; _dedicatedWorker = null;

View File

@ -7,15 +7,17 @@
library drift.wasm; library drift.wasm;
import 'dart:async'; import 'dart:async';
import 'dart:html'; import 'dart:js_interop';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:collection/collection.dart'; 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 'package:sqlite3/wasm.dart';
import 'backends.dart'; import 'backends.dart';
import 'src/sqlite3/database.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/dedicated_worker.dart';
import 'src/web/wasm_setup/shared_worker.dart'; import 'src/web/wasm_setup/shared_worker.dart';
import 'src/web/wasm_setup/types.dart'; import 'src/web/wasm_setup/types.dart';
@ -205,12 +207,15 @@ class WasmDatabase extends DelegatedDatabase {
static void workerMainForOpen({ static void workerMainForOpen({
WasmDatabaseSetup? setupAllDatabases, WasmDatabaseSetup? setupAllDatabases,
}) { }) {
final self = WorkerGlobalScope.instance; final self = globalContext;
if (self is DedicatedWorkerGlobalScope) { if (self.instanceOfString('DedicatedWorkerGlobalScope')) {
DedicatedDriftWorker(self, setupAllDatabases).start(); DedicatedDriftWorker(
} else if (self is SharedWorkerGlobalScope) { self as DedicatedWorkerGlobalScope, setupAllDatabases)
SharedDriftWorker(self, setupAllDatabases).start(); .start();
} else if (self.instanceOfString('SharedWorkerGlobalScope')) {
SharedDriftWorker(self as SharedWorkerGlobalScope, setupAllDatabases)
.start();
} }
} }
} }

View File

@ -10,4 +10,4 @@ import 'package:meta/meta.dart';
export 'src/web/sql_js.dart'; export 'src/web/sql_js.dart';
export 'src/web/storage.dart' hide CustomSchemaVersionSave; export 'src/web/storage.dart' hide CustomSchemaVersionSave;
export 'src/web/web_db.dart'; export 'src/web/web_db.dart';
export 'src/web/channel.dart'; export 'src/web/channel.dart' show PortToChannel;

View File

@ -1,26 +1,28 @@
name: drift name: drift
description: Drift is a reactive library to store relational data in Dart and Flutter applications. 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 repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/ homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues issue_tracker: https://github.com/simolus3/drift/issues
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
dependencies: dependencies:
async: ^2.5.0 async: ^2.5.0
convert: ^3.0.0 convert: ^3.0.0
collection: ^1.15.0 collection: ^1.15.0
js: ^0.6.3 js: '>=0.6.3 <0.8.0'
meta: ^1.3.0 meta: ^1.3.0
stream_channel: ^2.1.0 stream_channel: ^2.1.0
sqlite3: ^2.4.0 sqlite3: ^2.4.0
path: ^1.8.0 path: ^1.8.0
stack_trace: ^1.11.1 stack_trace: ^1.11.1
web: ^0.5.0
dev_dependencies: dev_dependencies:
archive: ^3.3.1 archive: ^3.3.1
analyzer: ^6.4.1
build_test: ^2.0.0 build_test: ^2.0.0
build_runner_core: ^7.0.0 build_runner_core: ^7.0.0
build_verify: ^3.0.0 build_verify: ^3.0.0

View File

@ -35,14 +35,14 @@ void main() {
); );
b.replaceAll(db.categories, const [ b.replaceAll(db.categories, const [
CategoriesCompanion(id: Value(1), description: Value('new1')), CategoriesCompanion(id: Value(RowId(1)), description: Value('new1')),
CategoriesCompanion(id: Value(2), description: Value('new2')), CategoriesCompanion(id: Value(RowId(2)), description: Value('new2')),
]); ]);
b.deleteWhere<$CategoriesTable, Category>( b.deleteWhere<$CategoriesTable, Category>(
db.categories, (tbl) => tbl.id.equals(1)); db.categories, (tbl) => tbl.id.equals(1));
b.deleteAll(db.categories); 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'))); b.update(db.users, const UsersCompanion(name: Value('new name 2')));
@ -97,7 +97,7 @@ void main() {
db.categories, db.categories,
CategoriesCompanion.insert(description: 'description'), CategoriesCompanion.insert(description: 'description'),
onConflict: DoUpdate((old) { 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 { test('updates stream queries', () async {
await db.batch((b) { 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.update(db.users, const UsersCompanion(name: Value('new user name')));
b.replace( b.replace(
db.todosTable, 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.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( verify(

View File

@ -6,7 +6,7 @@ import '../generated/todos.dart';
void main() { void main() {
test('data classes can be serialized', () { test('data classes can be serialized', () {
final entry = TodoEntry( final entry = TodoEntry(
id: 13, id: RowId(13),
title: 'Title', title: 'Title',
content: 'Content', content: 'Content',
targetDate: DateTime.now(), targetDate: DateTime.now(),
@ -36,7 +36,7 @@ void main() {
driftRuntimeOptions.defaultSerializer = _MySerializer(); driftRuntimeOptions.defaultSerializer = _MySerializer();
final entry = TodoEntry( final entry = TodoEntry(
id: 13, id: RowId(13),
title: 'Title', title: 'Title',
content: 'Content', content: 'Content',
category: 3, category: 3,
@ -59,7 +59,7 @@ void main() {
test('can serialize and deserialize blob columns', () { test('can serialize and deserialize blob columns', () {
final user = User( final user = User(
id: 3, id: RowId(3),
name: 'Username', name: 'Username',
isAwesome: true, isAwesome: true,
profilePicture: Uint8List.fromList(const [1, 2, 3, 4]), profilePicture: Uint8List.fromList(const [1, 2, 3, 4]),
@ -79,7 +79,7 @@ void main() {
test('generated data classes can be converted to companions', () { test('generated data classes can be converted to companions', () {
const entry = Category( const entry = Category(
id: 3, id: RowId(3),
description: 'description', description: 'description',
priority: CategoryPriority.low, priority: CategoryPriority.low,
descriptionInUpperCase: 'ignored', descriptionInUpperCase: 'ignored',
@ -91,7 +91,7 @@ void main() {
companion, companion,
equals(CategoriesCompanion.insert( equals(CategoriesCompanion.insert(
description: 'description', description: 'description',
id: const Value(3), id: const Value(RowId(3)),
priority: const Value(CategoryPriority.low), priority: const Value(CategoryPriority.low),
)), )),
); );
@ -105,15 +105,16 @@ void main() {
expect(entry.toCompanion(true), const PureDefaultsCompanion()); expect(entry.toCompanion(true), const PureDefaultsCompanion());
}); });
test('nullable values cannot be used with nullOrAbsent', () { test('utilities to wrap nullable values', () {
expect( expect(
// ignore: prefer_const_constructors // ignore: prefer_const_constructors, deprecated_member_use_from_same_package
() => Value<int?>.ofNullable(null), () => Value<int?>.ofNullable(null),
throwsA(isA<AssertionError>())); throwsA(isA<AssertionError>()));
expect(const Value<int>.ofNullable(null).present, isFalse); expect(const Value<int?>.absentIfNull(null).present, isFalse);
expect(const Value<int?>.ofNullable(12).present, isTrue); expect(const Value<int>.absentIfNull(null).present, isFalse);
expect(const Value<int>.ofNullable(23).present, isTrue); expect(const Value<int?>.absentIfNull(12).present, isTrue);
expect(const Value<int>.absentIfNull(23).present, isTrue);
}); });
test('companions support hash and equals', () { test('companions support hash and equals', () {

View File

@ -72,7 +72,7 @@ void main() {
final executor = MockExecutor(); final executor = MockExecutor();
final db = TodoDb(executor); 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])); verify(executor.runSelect(argThat(contains('SELECT t.* FROM todos')), [1]));
}); });

View File

@ -44,6 +44,23 @@ void main() {
expect(exp, generates('?', [10])); expect(exp, generates('?', [10]));
expect(exp.driftSqlType, isA<_NegatedIntType>()); 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> { class _NegatedIntType implements CustomSqlType<int> {

View File

@ -125,7 +125,11 @@ void main() {
}); });
test('substring', () { 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'));
}); });
}); });

View File

@ -52,5 +52,8 @@ void main() {
test('substr', () { test('substr', () {
expect(expression.substr(10), generates('SUBSTR(col, 10)')); expect(expression.substr(10), generates('SUBSTR(col, 10)'));
expect(expression.substr(10, 2), generates('SUBSTR(col, 10, 2)')); 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]));
}); });
} }

View File

@ -23,7 +23,7 @@ void main() {
group('compiled custom queries', () { group('compiled custom queries', () {
// defined query: SELECT * FROM todos WHERE title = ?2 OR id IN ? OR title = ?1 // defined query: SELECT * FROM todos WHERE title = ?2 OR id IN ? OR title = ?1
test('work with arrays', () async { 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( verify(
executor.runSelect( executor.runSelect(

View File

@ -58,13 +58,14 @@ void main() {
final returnedValue = await db final returnedValue = await db
.delete(db.todosTable) .delete(db.todosTable)
.deleteReturning(const TodosTableCompanion(id: Value(10))); .deleteReturning(const TodosTableCompanion(id: Value(RowId(10))));
verify(executor.runSelect( verify(executor.runSelect(
'DELETE FROM "todos" WHERE "id" = ? RETURNING *;', [10])); 'DELETE FROM "todos" WHERE "id" = ? RETURNING *;', [10]));
verify(streamQueries.handleTableUpdates( verify(streamQueries.handleTableUpdates(
{TableUpdate.onTable(db.todosTable, kind: UpdateKind.delete)})); {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 { test('for multiple rows', () async {
@ -112,7 +113,7 @@ void main() {
}); });
test('deleteOne()', () async { test('deleteOne()', () async {
await db.users.deleteOne(const UsersCompanion(id: Value(3))); await db.users.deleteOne(const UsersCompanion(id: Value(RowId(3))));
verify( verify(
executor.runDelete('DELETE FROM "users" WHERE "id" = ?;', const [3])); executor.runDelete('DELETE FROM "users" WHERE "id" = ?;', const [3]));

View File

@ -56,7 +56,7 @@ void main() {
test('generates insert or replace statements', () async { test('generates insert or replace statements', () async {
await db.into(db.todosTable).insert( await db.into(db.todosTable).insert(
const TodoEntry( const TodoEntry(
id: 113, id: RowId(113),
content: 'Done', content: 'Done',
), ),
mode: InsertMode.insertOrReplace); 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( test(
'can use multiple upsert targets', 'can use multiple upsert targets',
() async { () async {
@ -389,7 +405,8 @@ void main() {
when(executor.runInsert(any, any)).thenAnswer((_) => Future.value(3)); when(executor.runInsert(any, any)).thenAnswer((_) => Future.value(3));
final id = await db.into(db.todosTable).insertOnConflictUpdate( 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( verify(executor.runInsert(
'INSERT INTO "todos" ("id", "content") VALUES (?, ?) ' 'INSERT INTO "todos" ("id", "content") VALUES (?, ?) '
@ -599,7 +616,7 @@ void main() {
expect( expect(
row, row,
const Category( const Category(
id: 1, id: RowId(1),
description: 'description', description: 'description',
descriptionInUpperCase: 'DESCRIPTION', descriptionInUpperCase: 'DESCRIPTION',
priority: CategoryPriority.medium, priority: CategoryPriority.medium,

View File

@ -81,7 +81,7 @@ void main() {
expect( expect(
row.readTable(todos), row.readTable(todos),
TodoEntry( TodoEntry(
id: 5, id: RowId(5),
title: 'title', title: 'title',
content: 'content', content: 'content',
targetDate: date, targetDate: date,
@ -92,7 +92,7 @@ void main() {
expect( expect(
row.readTable(categories), row.readTable(categories),
const Category( const Category(
id: 3, id: RowId(3),
description: 'description', description: 'description',
priority: CategoryPriority.high, priority: CategoryPriority.high,
descriptionInUpperCase: 'DESCRIPTION', descriptionInUpperCase: 'DESCRIPTION',
@ -134,7 +134,7 @@ void main() {
expect( expect(
row.readTable(db.todosTable), row.readTable(db.todosTable),
const TodoEntry( const TodoEntry(
id: 5, id: RowId(5),
title: 'title', title: 'title',
content: 'content', content: 'content',
)); ));
@ -256,7 +256,7 @@ void main() {
result.readTable(categories), result.readTable(categories),
equals( equals(
const Category( const Category(
id: 3, id: RowId(3),
description: 'Description', description: 'Description',
descriptionInUpperCase: 'DESCRIPTION', descriptionInUpperCase: 'DESCRIPTION',
priority: CategoryPriority.medium, priority: CategoryPriority.medium,
@ -306,7 +306,7 @@ void main() {
result.readTable(categories), result.readTable(categories),
equals( equals(
const Category( const Category(
id: 3, id: RowId(3),
description: 'Description', description: 'Description',
descriptionInUpperCase: 'DESCRIPTION', descriptionInUpperCase: 'DESCRIPTION',
priority: CategoryPriority.medium, priority: CategoryPriority.medium,
@ -362,7 +362,7 @@ void main() {
expect( expect(
result.readTable(categories), result.readTable(categories),
const Category( const Category(
id: 3, id: RowId(3),
description: 'desc', description: 'desc',
descriptionInUpperCase: 'DESC', descriptionInUpperCase: 'DESC',
priority: CategoryPriority.low, priority: CategoryPriority.low,

View File

@ -16,7 +16,7 @@ final _dataOfTodoEntry = {
}; };
const _todoEntry = TodoEntry( const _todoEntry = TodoEntry(
id: 10, id: RowId(10),
title: 'A todo title', title: 'A todo title',
content: 'Content', content: 'Content',
category: 3, category: 3,
@ -126,7 +126,7 @@ void main() {
} }
]; ];
const resolved = TodoEntry( const resolved = TodoEntry(
id: 10, id: RowId(10),
title: null, title: null,
content: 'Content', content: 'Content',
category: null, category: null,
@ -198,7 +198,7 @@ void main() {
expect( expect(
category, category,
const Category( const Category(
id: 1, id: RowId(1),
description: 'description', description: 'description',
descriptionInUpperCase: 'DESCRIPTION', descriptionInUpperCase: 'DESCRIPTION',
priority: CategoryPriority.high, priority: CategoryPriority.high,
@ -232,7 +232,7 @@ void main() {
expect(rows, [ expect(rows, [
TodoEntry( TodoEntry(
id: 10, id: RowId(10),
title: null, title: null,
content: 'Content', content: 'Content',
category: null, category: null,

View File

@ -55,7 +55,7 @@ void main() {
group('generates replace statements', () { group('generates replace statements', () {
test('regular', () async { test('regular', () async {
await db.update(db.todosTable).replace(const TodoEntry( await db.update(db.todosTable).replace(const TodoEntry(
id: 3, id: RowId(3),
title: 'Title', title: 'Title',
content: 'Updated content', content: 'Updated content',
status: TodoStatus.workInProgress, status: TodoStatus.workInProgress,
@ -71,7 +71,7 @@ void main() {
test('applies default values', () async { test('applies default values', () async {
await db.update(db.users).replace( await db.update(db.users).replace(
UsersCompanion( UsersCompanion(
id: const Value(3), id: const Value(RowId(3)),
name: const Value('Hummingbird'), name: const Value('Hummingbird'),
profilePicture: Value(Uint8List(0)), profilePicture: Value(Uint8List(0)),
), ),
@ -167,14 +167,14 @@ void main() {
group('update on table instances', () { group('update on table instances', () {
test('update()', () async { 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])); verify(executor.runUpdate('UPDATE "users" SET "id" = ?;', [3]));
}); });
test('replace', () async { test('replace', () async {
await db.categories.replaceOne(const CategoriesCompanion( await db.categories.replaceOne(const CategoriesCompanion(
id: Value(3), description: Value('new name'))); id: Value(RowId(3)), description: Value('new name')));
verify(executor.runUpdate( verify(executor.runUpdate(
'UPDATE "categories" SET "desc" = ?, "priority" = 0 WHERE "id" = ?;', 'UPDATE "categories" SET "desc" = ?, "priority" = 0 WHERE "id" = ?;',
@ -205,7 +205,7 @@ void main() {
expect(rows, const [ expect(rows, const [
Category( Category(
id: 3, id: RowId(3),
description: 'test', description: 'test',
priority: CategoryPriority.low, priority: CategoryPriority.low,
descriptionInUpperCase: 'TEST', descriptionInUpperCase: 'TEST',

View File

@ -59,7 +59,7 @@ void main() {
expect( expect(
todo, todo,
const TodoEntry( const TodoEntry(
id: 1, id: RowId(1),
title: 'some title', title: 'some title',
content: 'do this', content: 'do this',
targetDate: null, targetDate: null,

View File

@ -4,6 +4,7 @@ import 'package:drift/drift.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../../generated/converter.dart'; import '../../generated/converter.dart';
import '../../generated/todos.dart';
enum _MyEnum { one, two, three } 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', () { group('enum name', () {
const converter = EnumNameConverter(_MyEnum.values); const converter = EnumNameConverter(_MyEnum.values);
const values = { const values = {

View File

@ -4,8 +4,14 @@ import 'package:uuid/uuid.dart';
part 'todos.g.dart'; part 'todos.g.dart';
extension type RowId._(int id) {
const RowId(this.id);
}
mixin AutoIncrement on Table { mixin AutoIncrement on Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer()
.autoIncrement()
.map(TypeConverter.extensionType<RowId, int>())();
} }
@DataClassName('TodoEntry') @DataClassName('TodoEntry')

View File

@ -11,13 +11,14 @@ class $CategoriesTable extends Categories
$CategoriesTable(this.attachedDatabase, [this._alias]); $CategoriesTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id'); static const VerificationMeta _idMeta = const VerificationMeta('id');
@override @override
late final GeneratedColumn<int> id = GeneratedColumn<int>( late final GeneratedColumnWithTypeConverter<RowId, int> id = GeneratedColumn<
'id', aliasedName, false, int>('id', aliasedName, false,
hasAutoIncrement: true, hasAutoIncrement: true,
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'))
.withConverter<RowId>($CategoriesTable.$converterid);
static const VerificationMeta _descriptionMeta = static const VerificationMeta _descriptionMeta =
const VerificationMeta('description'); const VerificationMeta('description');
@override @override
@ -56,9 +57,7 @@ class $CategoriesTable extends Categories
{bool isInserting = false}) { {bool isInserting = false}) {
final context = VerificationContext(); final context = VerificationContext();
final data = instance.toColumns(true); final data = instance.toColumns(true);
if (data.containsKey('id')) { context.handle(_idMeta, const VerificationResult.success());
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('desc')) { if (data.containsKey('desc')) {
context.handle(_descriptionMeta, context.handle(_descriptionMeta,
description.isAcceptableOrUnknown(data['desc']!, _descriptionMeta)); description.isAcceptableOrUnknown(data['desc']!, _descriptionMeta));
@ -81,8 +80,8 @@ class $CategoriesTable extends Categories
Category map(Map<String, dynamic> data, {String? tablePrefix}) { Category map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return Category( return Category(
id: attachedDatabase.typeMapping id: $CategoriesTable.$converterid.fromSql(attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!, .read(DriftSqlType.int, data['${effectivePrefix}id'])!),
description: attachedDatabase.typeMapping description: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}desc'])!, .read(DriftSqlType.string, data['${effectivePrefix}desc'])!,
priority: $CategoriesTable.$converterpriority.fromSql(attachedDatabase priority: $CategoriesTable.$converterpriority.fromSql(attachedDatabase
@ -99,12 +98,14 @@ class $CategoriesTable extends Categories
return $CategoriesTable(attachedDatabase, alias); return $CategoriesTable(attachedDatabase, alias);
} }
static JsonTypeConverter2<RowId, int, int> $converterid =
TypeConverter.extensionType<RowId, int>();
static JsonTypeConverter2<CategoryPriority, int, int> $converterpriority = static JsonTypeConverter2<CategoryPriority, int, int> $converterpriority =
const EnumIndexConverter<CategoryPriority>(CategoryPriority.values); const EnumIndexConverter<CategoryPriority>(CategoryPriority.values);
} }
class Category extends DataClass implements Insertable<Category> { class Category extends DataClass implements Insertable<Category> {
final int id; final RowId id;
final String description; final String description;
final CategoryPriority priority; final CategoryPriority priority;
final String descriptionInUpperCase; final String descriptionInUpperCase;
@ -116,7 +117,9 @@ class Category extends DataClass implements Insertable<Category> {
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
map['id'] = Variable<int>(id); {
map['id'] = Variable<int>($CategoriesTable.$converterid.toSql(id));
}
map['desc'] = Variable<String>(description); map['desc'] = Variable<String>(description);
{ {
map['priority'] = map['priority'] =
@ -137,7 +140,8 @@ class Category extends DataClass implements Insertable<Category> {
{ValueSerializer? serializer}) { {ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return Category( return Category(
id: serializer.fromJson<int>(json['id']), id: $CategoriesTable.$converterid
.fromJson(serializer.fromJson<int>(json['id'])),
description: serializer.fromJson<String>(json['description']), description: serializer.fromJson<String>(json['description']),
priority: $CategoriesTable.$converterpriority priority: $CategoriesTable.$converterpriority
.fromJson(serializer.fromJson<int>(json['priority'])), .fromJson(serializer.fromJson<int>(json['priority'])),
@ -154,7 +158,7 @@ class Category extends DataClass implements Insertable<Category> {
Map<String, dynamic> toJson({ValueSerializer? serializer}) { Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'id': serializer.toJson<int>(id), 'id': serializer.toJson<int>($CategoriesTable.$converterid.toJson(id)),
'description': serializer.toJson<String>(description), 'description': serializer.toJson<String>(description),
'priority': serializer 'priority': serializer
.toJson<int>($CategoriesTable.$converterpriority.toJson(priority)), .toJson<int>($CategoriesTable.$converterpriority.toJson(priority)),
@ -164,7 +168,7 @@ class Category extends DataClass implements Insertable<Category> {
} }
Category copyWith( Category copyWith(
{int? id, {RowId? id,
String? description, String? description,
CategoryPriority? priority, CategoryPriority? priority,
String? descriptionInUpperCase}) => String? descriptionInUpperCase}) =>
@ -200,7 +204,7 @@ class Category extends DataClass implements Insertable<Category> {
} }
class CategoriesCompanion extends UpdateCompanion<Category> { class CategoriesCompanion extends UpdateCompanion<Category> {
final Value<int> id; final Value<RowId> id;
final Value<String> description; final Value<String> description;
final Value<CategoryPriority> priority; final Value<CategoryPriority> priority;
const CategoriesCompanion({ const CategoriesCompanion({
@ -226,7 +230,7 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
} }
CategoriesCompanion copyWith( CategoriesCompanion copyWith(
{Value<int>? id, {Value<RowId>? id,
Value<String>? description, Value<String>? description,
Value<CategoryPriority>? priority}) { Value<CategoryPriority>? priority}) {
return CategoriesCompanion( return CategoriesCompanion(
@ -240,7 +244,7 @@ class CategoriesCompanion extends UpdateCompanion<Category> {
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
if (id.present) { if (id.present) {
map['id'] = Variable<int>(id.value); map['id'] = Variable<int>($CategoriesTable.$converterid.toSql(id.value));
} }
if (description.present) { if (description.present) {
map['desc'] = Variable<String>(description.value); map['desc'] = Variable<String>(description.value);
@ -271,13 +275,14 @@ class $TodosTableTable extends TodosTable
$TodosTableTable(this.attachedDatabase, [this._alias]); $TodosTableTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id'); static const VerificationMeta _idMeta = const VerificationMeta('id');
@override @override
late final GeneratedColumn<int> id = GeneratedColumn<int>( late final GeneratedColumnWithTypeConverter<RowId, int> id = GeneratedColumn<
'id', aliasedName, false, int>('id', aliasedName, false,
hasAutoIncrement: true, hasAutoIncrement: true,
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'))
.withConverter<RowId>($TodosTableTable.$converterid);
static const VerificationMeta _titleMeta = const VerificationMeta('title'); static const VerificationMeta _titleMeta = const VerificationMeta('title');
@override @override
late final GeneratedColumn<String> title = GeneratedColumn<String>( late final GeneratedColumn<String> title = GeneratedColumn<String>(
@ -328,9 +333,7 @@ class $TodosTableTable extends TodosTable
{bool isInserting = false}) { {bool isInserting = false}) {
final context = VerificationContext(); final context = VerificationContext();
final data = instance.toColumns(true); final data = instance.toColumns(true);
if (data.containsKey('id')) { context.handle(_idMeta, const VerificationResult.success());
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('title')) { if (data.containsKey('title')) {
context.handle( context.handle(
_titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta)); _titleMeta, title.isAcceptableOrUnknown(data['title']!, _titleMeta));
@ -366,8 +369,8 @@ class $TodosTableTable extends TodosTable
TodoEntry map(Map<String, dynamic> data, {String? tablePrefix}) { TodoEntry map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return TodoEntry( return TodoEntry(
id: attachedDatabase.typeMapping id: $TodosTableTable.$converterid.fromSql(attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!, .read(DriftSqlType.int, data['${effectivePrefix}id'])!),
title: attachedDatabase.typeMapping title: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}title']), .read(DriftSqlType.string, data['${effectivePrefix}title']),
content: attachedDatabase.typeMapping content: attachedDatabase.typeMapping
@ -387,6 +390,8 @@ class $TodosTableTable extends TodosTable
return $TodosTableTable(attachedDatabase, alias); return $TodosTableTable(attachedDatabase, alias);
} }
static JsonTypeConverter2<RowId, int, int> $converterid =
TypeConverter.extensionType<RowId, int>();
static JsonTypeConverter2<TodoStatus, String, String> $converterstatus = static JsonTypeConverter2<TodoStatus, String, String> $converterstatus =
const EnumNameConverter<TodoStatus>(TodoStatus.values); const EnumNameConverter<TodoStatus>(TodoStatus.values);
static JsonTypeConverter2<TodoStatus?, String?, String?> $converterstatusn = static JsonTypeConverter2<TodoStatus?, String?, String?> $converterstatusn =
@ -394,7 +399,7 @@ class $TodosTableTable extends TodosTable
} }
class TodoEntry extends DataClass implements Insertable<TodoEntry> { class TodoEntry extends DataClass implements Insertable<TodoEntry> {
final int id; final RowId id;
final String? title; final String? title;
final String content; final String content;
final DateTime? targetDate; final DateTime? targetDate;
@ -410,7 +415,9 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
map['id'] = Variable<int>(id); {
map['id'] = Variable<int>($TodosTableTable.$converterid.toSql(id));
}
if (!nullToAbsent || title != null) { if (!nullToAbsent || title != null) {
map['title'] = Variable<String>(title); map['title'] = Variable<String>(title);
} }
@ -449,7 +456,8 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
{ValueSerializer? serializer}) { {ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return TodoEntry( return TodoEntry(
id: serializer.fromJson<int>(json['id']), id: $TodosTableTable.$converterid
.fromJson(serializer.fromJson<int>(json['id'])),
title: serializer.fromJson<String?>(json['title']), title: serializer.fromJson<String?>(json['title']),
content: serializer.fromJson<String>(json['content']), content: serializer.fromJson<String>(json['content']),
targetDate: serializer.fromJson<DateTime?>(json['target_date']), targetDate: serializer.fromJson<DateTime?>(json['target_date']),
@ -467,7 +475,7 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
Map<String, dynamic> toJson({ValueSerializer? serializer}) { Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'id': serializer.toJson<int>(id), 'id': serializer.toJson<int>($TodosTableTable.$converterid.toJson(id)),
'title': serializer.toJson<String?>(title), 'title': serializer.toJson<String?>(title),
'content': serializer.toJson<String>(content), 'content': serializer.toJson<String>(content),
'target_date': serializer.toJson<DateTime?>(targetDate), 'target_date': serializer.toJson<DateTime?>(targetDate),
@ -478,7 +486,7 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
} }
TodoEntry copyWith( TodoEntry copyWith(
{int? id, {RowId? id,
Value<String?> title = const Value.absent(), Value<String?> title = const Value.absent(),
String? content, String? content,
Value<DateTime?> targetDate = const Value.absent(), Value<DateTime?> targetDate = const Value.absent(),
@ -521,7 +529,7 @@ class TodoEntry extends DataClass implements Insertable<TodoEntry> {
} }
class TodosTableCompanion extends UpdateCompanion<TodoEntry> { class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
final Value<int> id; final Value<RowId> id;
final Value<String?> title; final Value<String?> title;
final Value<String> content; final Value<String> content;
final Value<DateTime?> targetDate; final Value<DateTime?> targetDate;
@ -562,7 +570,7 @@ class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
} }
TodosTableCompanion copyWith( TodosTableCompanion copyWith(
{Value<int>? id, {Value<RowId>? id,
Value<String?>? title, Value<String?>? title,
Value<String>? content, Value<String>? content,
Value<DateTime?>? targetDate, Value<DateTime?>? targetDate,
@ -582,7 +590,7 @@ class TodosTableCompanion extends UpdateCompanion<TodoEntry> {
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
if (id.present) { if (id.present) {
map['id'] = Variable<int>(id.value); map['id'] = Variable<int>($TodosTableTable.$converterid.toSql(id.value));
} }
if (title.present) { if (title.present) {
map['title'] = Variable<String>(title.value); map['title'] = Variable<String>(title.value);
@ -624,13 +632,14 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
$UsersTable(this.attachedDatabase, [this._alias]); $UsersTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id'); static const VerificationMeta _idMeta = const VerificationMeta('id');
@override @override
late final GeneratedColumn<int> id = GeneratedColumn<int>( late final GeneratedColumnWithTypeConverter<RowId, int> id = GeneratedColumn<
'id', aliasedName, false, int>('id', aliasedName, false,
hasAutoIncrement: true, hasAutoIncrement: true,
type: DriftSqlType.int, type: DriftSqlType.int,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT')); GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'))
.withConverter<RowId>($UsersTable.$converterid);
static const VerificationMeta _nameMeta = const VerificationMeta('name'); static const VerificationMeta _nameMeta = const VerificationMeta('name');
@override @override
late final GeneratedColumn<String> name = GeneratedColumn<String>( late final GeneratedColumn<String> name = GeneratedColumn<String>(
@ -678,9 +687,7 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
{bool isInserting = false}) { {bool isInserting = false}) {
final context = VerificationContext(); final context = VerificationContext();
final data = instance.toColumns(true); final data = instance.toColumns(true);
if (data.containsKey('id')) { context.handle(_idMeta, const VerificationResult.success());
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('name')) { if (data.containsKey('name')) {
context.handle( context.handle(
_nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); _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}) { User map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return User( return User(
id: attachedDatabase.typeMapping id: $UsersTable.$converterid.fromSql(attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!, .read(DriftSqlType.int, data['${effectivePrefix}id'])!),
name: attachedDatabase.typeMapping name: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}name'])!, .read(DriftSqlType.string, data['${effectivePrefix}name'])!,
isAwesome: attachedDatabase.typeMapping isAwesome: attachedDatabase.typeMapping
@ -731,10 +738,13 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
$UsersTable createAlias(String alias) { $UsersTable createAlias(String alias) {
return $UsersTable(attachedDatabase, alias); return $UsersTable(attachedDatabase, alias);
} }
static JsonTypeConverter2<RowId, int, int> $converterid =
TypeConverter.extensionType<RowId, int>();
} }
class User extends DataClass implements Insertable<User> { class User extends DataClass implements Insertable<User> {
final int id; final RowId id;
final String name; final String name;
final bool isAwesome; final bool isAwesome;
final Uint8List profilePicture; final Uint8List profilePicture;
@ -748,7 +758,9 @@ class User extends DataClass implements Insertable<User> {
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
map['id'] = Variable<int>(id); {
map['id'] = Variable<int>($UsersTable.$converterid.toSql(id));
}
map['name'] = Variable<String>(name); map['name'] = Variable<String>(name);
map['is_awesome'] = Variable<bool>(isAwesome); map['is_awesome'] = Variable<bool>(isAwesome);
map['profile_picture'] = Variable<Uint8List>(profilePicture); map['profile_picture'] = Variable<Uint8List>(profilePicture);
@ -770,7 +782,8 @@ class User extends DataClass implements Insertable<User> {
{ValueSerializer? serializer}) { {ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return User( return User(
id: serializer.fromJson<int>(json['id']), id: $UsersTable.$converterid
.fromJson(serializer.fromJson<int>(json['id'])),
name: serializer.fromJson<String>(json['name']), name: serializer.fromJson<String>(json['name']),
isAwesome: serializer.fromJson<bool>(json['isAwesome']), isAwesome: serializer.fromJson<bool>(json['isAwesome']),
profilePicture: serializer.fromJson<Uint8List>(json['profilePicture']), profilePicture: serializer.fromJson<Uint8List>(json['profilePicture']),
@ -785,7 +798,7 @@ class User extends DataClass implements Insertable<User> {
Map<String, dynamic> toJson({ValueSerializer? serializer}) { Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'id': serializer.toJson<int>(id), 'id': serializer.toJson<int>($UsersTable.$converterid.toJson(id)),
'name': serializer.toJson<String>(name), 'name': serializer.toJson<String>(name),
'isAwesome': serializer.toJson<bool>(isAwesome), 'isAwesome': serializer.toJson<bool>(isAwesome),
'profilePicture': serializer.toJson<Uint8List>(profilePicture), 'profilePicture': serializer.toJson<Uint8List>(profilePicture),
@ -794,7 +807,7 @@ class User extends DataClass implements Insertable<User> {
} }
User copyWith( User copyWith(
{int? id, {RowId? id,
String? name, String? name,
bool? isAwesome, bool? isAwesome,
Uint8List? profilePicture, Uint8List? profilePicture,
@ -834,7 +847,7 @@ class User extends DataClass implements Insertable<User> {
} }
class UsersCompanion extends UpdateCompanion<User> { class UsersCompanion extends UpdateCompanion<User> {
final Value<int> id; final Value<RowId> id;
final Value<String> name; final Value<String> name;
final Value<bool> isAwesome; final Value<bool> isAwesome;
final Value<Uint8List> profilePicture; final Value<Uint8List> profilePicture;
@ -871,7 +884,7 @@ class UsersCompanion extends UpdateCompanion<User> {
} }
UsersCompanion copyWith( UsersCompanion copyWith(
{Value<int>? id, {Value<RowId>? id,
Value<String>? name, Value<String>? name,
Value<bool>? isAwesome, Value<bool>? isAwesome,
Value<Uint8List>? profilePicture, Value<Uint8List>? profilePicture,
@ -889,7 +902,7 @@ class UsersCompanion extends UpdateCompanion<User> {
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
if (id.present) { if (id.present) {
map['id'] = Variable<int>(id.value); map['id'] = Variable<int>($UsersTable.$converterid.toSql(id.value));
} }
if (name.present) { if (name.present) {
map['name'] = Variable<String>(name.value); map['name'] = Variable<String>(name.value);
@ -1872,7 +1885,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
todosTable, todosTable,
}).map((QueryRow row) => AllTodosWithCategoryResult( }).map((QueryRow row) => AllTodosWithCategoryResult(
row: row, row: row,
id: row.read<int>('id'), id: $TodosTableTable.$converterid.fromSql(row.read<int>('id')),
title: row.readNullable<String>('title'), title: row.readNullable<String>('title'),
content: row.read<String>('content'), content: row.read<String>('content'),
targetDate: row.readNullable<DateTime>('target_date'), targetDate: row.readNullable<DateTime>('target_date'),
@ -1880,21 +1893,21 @@ abstract class _$TodoDb extends GeneratedDatabase {
status: NullAwareTypeConverter.wrapFromSql( status: NullAwareTypeConverter.wrapFromSql(
$TodosTableTable.$converterstatus, $TodosTableTable.$converterstatus,
row.readNullable<String>('status')), row.readNullable<String>('status')),
catId: row.read<int>('catId'), catId: $CategoriesTable.$converterid.fromSql(row.read<int>('catId')),
catDesc: row.read<String>('catDesc'), catDesc: row.read<String>('catDesc'),
)); ));
} }
Future<int> deleteTodoById(int var1) { Future<int> deleteTodoById(RowId var1) {
return customUpdate( return customUpdate(
'DELETE FROM todos WHERE id = ?1', 'DELETE FROM todos WHERE id = ?1',
variables: [Variable<int>(var1)], variables: [Variable<int>($TodosTableTable.$converterid.toSql(var1))],
updates: {todosTable}, updates: {todosTable},
updateKind: UpdateKind.delete, 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; var $arrayStartIndex = 3;
final expandedvar3 = $expandVar($arrayStartIndex, var3.length); final expandedvar3 = $expandVar($arrayStartIndex, var3.length);
$arrayStartIndex += var3.length; $arrayStartIndex += var3.length;
@ -1903,18 +1916,19 @@ abstract class _$TodoDb extends GeneratedDatabase {
variables: [ variables: [
Variable<String>(var1), Variable<String>(var1),
Variable<String>(var2), Variable<String>(var2),
for (var $ in var3) Variable<int>($) for (var $ in var3)
Variable<int>($TodosTableTable.$converterid.toSql($))
], ],
readsFrom: { readsFrom: {
todosTable, todosTable,
}).asyncMap(todosTable.mapFromRow); }).asyncMap(todosTable.mapFromRow);
} }
Selectable<TodoEntry> search({required int id}) { Selectable<TodoEntry> search({required RowId id}) {
return customSelect( return customSelect(
'SELECT * FROM todos WHERE CASE WHEN -1 = ?1 THEN 1 ELSE id = ?1 END', 'SELECT * FROM todos WHERE CASE WHEN -1 = ?1 THEN 1 ELSE id = ?1 END',
variables: [ variables: [
Variable<int>(id) Variable<int>($TodosTableTable.$converterid.toSql(id))
], ],
readsFrom: { readsFrom: {
todosTable, todosTable,
@ -1949,13 +1963,13 @@ abstract class _$TodoDb extends GeneratedDatabase {
} }
class AllTodosWithCategoryResult extends CustomResultSet { class AllTodosWithCategoryResult extends CustomResultSet {
final int id; final RowId id;
final String? title; final String? title;
final String content; final String content;
final DateTime? targetDate; final DateTime? targetDate;
final int? category; final int? category;
final TodoStatus? status; final TodoStatus? status;
final int catId; final RowId catId;
final String catDesc; final String catDesc;
AllTodosWithCategoryResult({ AllTodosWithCategoryResult({
required QueryRow row, required QueryRow row,
@ -2006,11 +2020,11 @@ mixin _$SomeDaoMixin on DatabaseAccessor<TodoDb> {
$SharedTodosTable get sharedTodos => attachedDatabase.sharedTodos; $SharedTodosTable get sharedTodos => attachedDatabase.sharedTodos;
$TodoWithCategoryViewView get todoWithCategoryView => $TodoWithCategoryViewView get todoWithCategoryView =>
attachedDatabase.todoWithCategoryView; attachedDatabase.todoWithCategoryView;
Selectable<TodoEntry> todosForUser({required int user}) { Selectable<TodoEntry> todosForUser({required RowId user}) {
return customSelect( 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', '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: [ variables: [
Variable<int>(user) Variable<int>($UsersTable.$converterid.toSql(user))
], ],
readsFrom: { readsFrom: {
todosTable, todosTable,

View File

@ -109,7 +109,7 @@ void main() {
expect( expect(
entry, entry,
const Category( const Category(
id: 1, id: RowId(1),
description: 'Description', description: 'Description',
priority: CategoryPriority.low, priority: CategoryPriority.low,
descriptionInUpperCase: 'DESCRIPTION', descriptionInUpperCase: 'DESCRIPTION',

View File

@ -26,7 +26,7 @@ void main() {
final rows = await (db.select(db.users) final rows = await (db.select(db.users)
..orderBy([(_) => OrderingTerm.random()])) ..orderBy([(_) => OrderingTerm.random()]))
.get(); .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 { test('can select view', () async {
@ -35,7 +35,7 @@ void main() {
await db.todosTable.insertOne(TodosTableCompanion.insert( await db.todosTable.insertOne(TodosTableCompanion.insert(
content: 'some content', content: 'some content',
title: const Value('title'), title: const Value('title'),
category: Value(category.id))); category: Value(category.id.id)));
final result = await db.todoWithCategoryView.select().getSingle(); final result = await db.todoWithCategoryView.select().getSingle();
expect( expect(

View File

@ -190,7 +190,7 @@ void main() {
stream, stream,
emits([ emits([
Category( Category(
id: 1, id: RowId(1),
description: 'From remote isolate!', description: 'From remote isolate!',
priority: CategoryPriority.low, priority: CategoryPriority.low,
descriptionInUpperCase: 'FROM REMOTE ISOLATE!', descriptionInUpperCase: 'FROM REMOTE ISOLATE!',
@ -240,6 +240,34 @@ void main() {
await testWith(DatabaseConnection(NativeDatabase.memory())); 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, 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 database.into(database.todosTable).insert(initialCompanion);
await expectLater( await expectLater(
stream, stream,
emits(const TodoEntry(id: 1, content: 'my content')), emits(const TodoEntry(id: RowId(1), content: 'my content')),
); );
}); });

View File

@ -98,7 +98,7 @@ void main() {
ExecuteQuery(StatementMethod.select, 'SELECT ?', [BigInt.one]), ExecuteQuery(StatementMethod.select, 'SELECT ?', [BigInt.one]),
); );
final mapped = protocol.deserialize(protocol.serialize(request)!); final mapped = _checkSimpleRoundtrip(protocol, request);
expect( expect(
mapped, mapped,
isA<Request>().having((e) => e.id, 'id', 1).having( isA<Request>().having((e) => e.id, 'id', 1).having(
@ -109,6 +109,27 @@ void main() {
.having((e) => e.args, 'args', [isA<BigInt>()]), .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 { 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> { extension<T> on StreamChannel<T> {
StreamChannel<T> get expectedToClose { StreamChannel<T> get expectedToClose {
return transformStream(StreamTransformer.fromHandlers( return transformStream(StreamTransformer.fromHandlers(

View File

@ -6,7 +6,7 @@ import 'generated/todos.dart';
final DateTime _someDate = DateTime(2019, 06, 08); final DateTime _someDate = DateTime(2019, 06, 08);
final TodoEntry _someTodoEntry = TodoEntry( final TodoEntry _someTodoEntry = TodoEntry(
id: 3, id: RowId(3),
title: null, title: null,
content: 'content', content: 'content',
targetDate: _someDate, targetDate: _someDate,

View File

@ -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.');
});
}

View File

@ -109,7 +109,7 @@ class _GeneratesSqlMatcher extends Matcher {
final argsMatchState = <String, Object?>{}; final argsMatchState = <String, Object?>{};
if (_matchVariables != null && if (_matchVariables != null &&
!_matchVariables!.matches(ctx.boundVariables, argsMatchState)) { !_matchVariables.matches(ctx.boundVariables, argsMatchState)) {
matchState['vars'] = ctx.boundVariables; matchState['vars'] = ctx.boundVariables;
matchState['vars_match'] = argsMatchState; matchState['vars_match'] = argsMatchState;
matches = false; matches = false;

View File

@ -100,3 +100,14 @@ class CustomTable extends Table with TableInfo<CustomTable, void> {
return; return;
} }
} }
class PretendDialectInterceptor extends QueryInterceptor {
final SqlDialect _dialect;
PretendDialectInterceptor(this._dialect);
@override
SqlDialect dialect(QueryExecutor executor) {
return _dialect;
}
}

View File

@ -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. // in drift/test/test_utils/test_utils.dart.
// Do not manually edit this file. // Do not manually edit this file.

View 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 - Keep import alias when referencing existing elements in generated code
([#2845](https://github.com/simolus3/drift/issues/2845)). ([#2845](https://github.com/simolus3/drift/issues/2845)).

View File

@ -11,8 +11,18 @@ export 'package:drift_dev/src/services/schema/verifier_common.dart'
show SchemaMismatch; show SchemaMismatch;
abstract class SchemaVerifier { abstract class SchemaVerifier {
factory SchemaVerifier(SchemaInstantiationHelper helper) = /// Creates a schema verifier for the drift-generated [helper].
VerifierImplementation; ///
/// 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 /// Creates a [DatabaseConnection] that contains empty tables created for the
/// known schema [version]. /// known schema [version].

View File

@ -45,13 +45,19 @@ class DriftOptions {
defaultValue: true) defaultValue: true)
final bool useColumnNameAsJsonKeyWhenDefinedInMoorFile; 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. /// Generate a `connect` constructor in database superclasses.
/// ///
/// This makes drift generate a constructor for database classes that takes a /// This makes drift generate a constructor for database classes that takes a
/// `DatabaseConnection` instead of just a `QueryExecutor` - meaning that /// `DatabaseConnection` instead of just a `QueryExecutor` - meaning that
/// stream queries can also be shared across multiple database instances. /// stream queries can also be shared across multiple database instances.
/// Starting from drift 2.5, the database connection class implements the /// 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) @JsonKey(name: 'generate_connect_constructor', defaultValue: false)
final bool generateConnectConstructor; final bool generateConnectConstructor;
@ -120,6 +126,7 @@ class DriftOptions {
this.skipVerificationCode = false, this.skipVerificationCode = false,
this.useDataClassNameForCompanions = false, this.useDataClassNameForCompanions = false,
this.useColumnNameAsJsonKeyWhenDefinedInMoorFile = true, this.useColumnNameAsJsonKeyWhenDefinedInMoorFile = true,
this.useSqlColumnNameAsJsonKey = false,
this.generateConnectConstructor = false, this.generateConnectConstructor = false,
this.dataClassToCompanions = true, this.dataClassToCompanions = true,
this.generateMutableClasses = false, this.generateMutableClasses = false,
@ -147,6 +154,7 @@ class DriftOptions {
required this.skipVerificationCode, required this.skipVerificationCode,
required this.useDataClassNameForCompanions, required this.useDataClassNameForCompanions,
required this.useColumnNameAsJsonKeyWhenDefinedInMoorFile, required this.useColumnNameAsJsonKeyWhenDefinedInMoorFile,
required this.useSqlColumnNameAsJsonKey,
required this.generateConnectConstructor, required this.generateConnectConstructor,
required this.dataClassToCompanions, required this.dataClassToCompanions,
required this.generateMutableClasses, required this.generateMutableClasses,

View File

@ -197,12 +197,14 @@ extension TypeUtils on DartType {
} }
class DataClassInformation { class DataClassInformation {
final String enforcedName; final String? enforcedName;
final String? companionName;
final CustomParentClass? extending; final CustomParentClass? extending;
final ExistingRowClass? existingClass; final ExistingRowClass? existingClass;
DataClassInformation( DataClassInformation(
this.enforcedName, this.enforcedName,
this.companionName,
this.extending, this.extending,
this.existingClass, this.existingClass,
); );
@ -233,16 +235,15 @@ class DataClassInformation {
)); ));
} }
String name; var name = dataClassName?.getField('name')!.toStringValue();
final companionName =
dataClassName?.getField('companionName')?.toStringValue();
CustomParentClass? customParentClass; CustomParentClass? customParentClass;
ExistingRowClass? existingClass; ExistingRowClass? existingClass;
if (dataClassName != null) { if (dataClassName != null) {
name = dataClassName.getField('name')!.toStringValue()!;
customParentClass = customParentClass =
parseCustomParentClass(name, dataClassName, element, resolver); parseCustomParentClass(name, dataClassName, element, resolver);
} else {
name = dataClassNameForClassName(element.name);
} }
if (useRowClass != null) { if (useRowClass != null) {
@ -277,7 +278,12 @@ class DataClassInformation {
} }
} }
return DataClassInformation(name, customParentClass, existingClass); return DataClassInformation(
name,
companionName,
customParentClass,
existingClass,
);
} }
} }

View File

@ -2,6 +2,7 @@ import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/syntactic_entity.dart'; import 'package:analyzer/dart/ast/syntactic_entity.dart';
import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/element.dart';
import 'package:collection/collection.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 'package:sqlparser/sqlparser.dart' as sql;
import '../../driver/error.dart'; import '../../driver/error.dart';
@ -54,7 +55,9 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
DriftDeclaration.dartElement(element), DriftDeclaration.dartElement(element),
columns: columns, columns: columns,
references: references.toList(), references: references.toList(),
nameOfRowClass: dataClassInfo.enforcedName, nameOfRowClass:
dataClassInfo.enforcedName ?? dataClassNameForClassName(element.name),
nameOfCompanionClass: dataClassInfo.companionName,
existingRowClass: dataClassInfo.existingClass, existingRowClass: dataClassInfo.existingClass,
customParentClass: dataClassInfo.extending, customParentClass: dataClassInfo.extending,
baseDartName: element.name, baseDartName: element.name,

View File

@ -9,6 +9,7 @@ import 'package:recase/recase.dart';
import '../../results/results.dart'; import '../../results/results.dart';
import '../intermediate_state.dart'; import '../intermediate_state.dart';
import '../resolver.dart'; import '../resolver.dart';
import '../shared/data_class.dart';
import 'helper.dart'; import 'helper.dart';
class DartViewResolver extends LocalElementResolver<DiscoveredDartView> { class DartViewResolver extends LocalElementResolver<DiscoveredDartView> {
@ -26,7 +27,8 @@ class DartViewResolver extends LocalElementResolver<DiscoveredDartView> {
discovered.ownId, discovered.ownId,
DriftDeclaration.dartElement(discovered.dartElement), DriftDeclaration.dartElement(discovered.dartElement),
columns: columns, columns: columns,
nameOfRowClass: dataClassInfo.enforcedName, nameOfRowClass: dataClassInfo.enforcedName ??
dataClassNameForClassName(discovered.dartElement.name),
existingRowClass: dataClassInfo.existingClass, existingRowClass: dataClassInfo.existingClass,
customParentClass: dataClassInfo.extending, customParentClass: dataClassInfo.extending,
entityInfoName: '\$${discovered.dartElement.name}View', entityInfoName: '\$${discovered.dartElement.name}View',

View File

@ -31,7 +31,7 @@ String dataClassNameForClassName(String tableName) {
} }
CustomParentClass? parseCustomParentClass( CustomParentClass? parseCustomParentClass(
String dartTypeName, String? dartTypeName,
DartObject dataClassName, DartObject dataClassName,
ClassElement element, ClassElement element,
LocalElementResolver resolver, LocalElementResolver resolver,
@ -87,7 +87,10 @@ CustomParentClass? parseCustomParentClass(
code = AnnotatedDartCode([ code = AnnotatedDartCode([
DartTopLevelSymbol.topLevelElement(extendingType.element), DartTopLevelSymbol.topLevelElement(extendingType.element),
'<', '<',
DartTopLevelSymbol(dartTypeName, null), DartTopLevelSymbol(
dartTypeName ?? dataClassNameForClassName(element.name),
null,
),
'>', '>',
]); ]);
} else { } else {

View File

@ -108,12 +108,13 @@ class DriftColumn implements HasType {
/// The actual json key to use when serializing a data class of this table /// The actual json key to use when serializing a data class of this table
/// to json. /// 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()]) { String getJsonKey([DriftOptions options = const DriftOptions.defaults()]) {
if (overriddenJsonName != null) return overriddenJsonName!; if (overriddenJsonName != null) return overriddenJsonName!;
final useColumnName = options.useColumnNameAsJsonKeyWhenDefinedInMoorFile && final useColumnName = options.useSqlColumnNameAsJsonKey ||
declaredInDriftFile; (options.useColumnNameAsJsonKeyWhenDefinedInMoorFile &&
declaredInDriftFile);
return useColumnName ? nameInSql : nameInDart; return useColumnName ? nameInSql : nameInDart;
} }

View File

@ -559,4 +559,11 @@ class _AddFromAst extends GeneralizingAstVisitor<void> {
_visitCommaSeparated(node.elements); _visitCommaSeparated(node.elements);
_childEntities([node.rightBracket]); _childEntities([node.rightBracket]);
} }
@override
void visitTypeArgumentList(TypeArgumentList node) {
_builder.addText('<');
_visitCommaSeparated(node.arguments);
_builder.addText('>');
}
} }

View File

@ -40,6 +40,9 @@ abstract class DriftElementWithResultSet extends DriftSchemaElement {
/// The name for the data class associated with this table or view. /// The name for the data class associated with this table or view.
String get nameOfRowClass; 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. /// All [columns] of this table, indexed by their name in SQL.
late final Map<String, DriftColumn> columnBySqlName = CaseInsensitiveMap.of({ late final Map<String, DriftColumn> columnBySqlName = CaseInsensitiveMap.of({
for (final column in columns) column.nameInSql: column, for (final column in columns) column.nameInSql: column,

View File

@ -32,6 +32,9 @@ class DriftTable extends DriftElementWithResultSet {
@override @override
final String nameOfRowClass; final String nameOfRowClass;
@override
final String? nameOfCompanionClass;
final bool withoutRowId; final bool withoutRowId;
/// Information about the virtual table creating statement backing this table, /// Information about the virtual table creating statement backing this table,
@ -69,6 +72,7 @@ class DriftTable extends DriftElementWithResultSet {
required this.columns, required this.columns,
required this.baseDartName, required this.baseDartName,
required this.nameOfRowClass, required this.nameOfRowClass,
this.nameOfCompanionClass,
this.references = const [], this.references = const [],
this.existingRowClass, this.existingRowClass,
this.customParentClass, this.customParentClass,

View File

@ -25,6 +25,9 @@ class DriftView extends DriftElementWithResultSet {
@override @override
final String nameOfRowClass; final String nameOfRowClass;
@override
final String? nameOfCompanionClass;
@override @override
List<DriftElement> references; List<DriftElement> references;
@ -38,6 +41,7 @@ class DriftView extends DriftElementWithResultSet {
required this.existingRowClass, required this.existingRowClass,
required this.nameOfRowClass, required this.nameOfRowClass,
required this.references, required this.references,
this.nameOfCompanionClass,
}); });
@override @override

View File

@ -57,6 +57,7 @@ class ElementSerializer {
'fixed_entity_info_name': element.fixedEntityInfoName, 'fixed_entity_info_name': element.fixedEntityInfoName,
'base_dart_name': element.baseDartName, 'base_dart_name': element.baseDartName,
'row_class_name': element.nameOfRowClass, 'row_class_name': element.nameOfRowClass,
'companion_class_name': element.nameOfCompanionClass,
'without_rowid': element.withoutRowId, 'without_rowid': element.withoutRowId,
'strict': element.strict, 'strict': element.strict,
if (element.isVirtual) if (element.isVirtual)
@ -146,6 +147,7 @@ class ElementSerializer {
'custom_parent_class': 'custom_parent_class':
_serializeCustomParentClass(element.customParentClass), _serializeCustomParentClass(element.customParentClass),
'name_of_row_class': element.nameOfRowClass, 'name_of_row_class': element.nameOfRowClass,
'name_of_companion_class': element.nameOfCompanionClass,
'source': serializedSource, 'source': serializedSource,
}; };
} else if (element is BaseDriftAccessor) { } else if (element is BaseDriftAccessor) {
@ -539,6 +541,7 @@ class ElementDeserializer {
fixedEntityInfoName: json['fixed_entity_info_name'] as String?, fixedEntityInfoName: json['fixed_entity_info_name'] as String?,
baseDartName: json['base_dart_name'] as String, baseDartName: json['base_dart_name'] as String,
nameOfRowClass: json['row_class_name'] as String, nameOfRowClass: json['row_class_name'] as String,
nameOfCompanionClass: json['companion_class_name'] as String?,
withoutRowId: json['without_rowid'] as bool, withoutRowId: json['without_rowid'] as bool,
strict: json['strict'] as bool, strict: json['strict'] as bool,
virtualTableData: virtualTableData, virtualTableData: virtualTableData,
@ -678,6 +681,7 @@ class ElementDeserializer {
customParentClass: customParentClass:
_readCustomParentClass(json['custom_parent_class'] as Map?), _readCustomParentClass(json['custom_parent_class'] as Map?),
nameOfRowClass: json['name_of_row_class'] as String, nameOfRowClass: json['name_of_row_class'] as String,
nameOfCompanionClass: json['name_of_companion_class'] as String,
existingRowClass: json['existing_data_class'] != null existingRowClass: json['existing_data_class'] != null
? await _readExistingRowClass( ? await _readExistingRowClass(
id.libraryUri, json['existing_data_class'] as Map) id.libraryUri, json['existing_data_class'] as Map)

View File

@ -18,6 +18,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate(
'skip_verification_code', 'skip_verification_code',
'use_data_class_name_for_companions', 'use_data_class_name_for_companions',
'use_column_name_as_json_key_when_defined_in_moor_file', 'use_column_name_as_json_key_when_defined_in_moor_file',
'use_sql_column_name_as_json_key',
'generate_connect_constructor', 'generate_connect_constructor',
'sqlite_modules', 'sqlite_modules',
'sqlite', 'sqlite',
@ -52,6 +53,8 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate(
useColumnNameAsJsonKeyWhenDefinedInMoorFile: $checkedConvert( useColumnNameAsJsonKeyWhenDefinedInMoorFile: $checkedConvert(
'use_column_name_as_json_key_when_defined_in_moor_file', 'use_column_name_as_json_key_when_defined_in_moor_file',
(v) => v as bool? ?? true), (v) => v as bool? ?? true),
useSqlColumnNameAsJsonKey: $checkedConvert(
'use_sql_column_name_as_json_key', (v) => v as bool? ?? false),
generateConnectConstructor: $checkedConvert( generateConnectConstructor: $checkedConvert(
'generate_connect_constructor', (v) => v as bool? ?? false), 'generate_connect_constructor', (v) => v as bool? ?? false),
dataClassToCompanions: $checkedConvert( dataClassToCompanions: $checkedConvert(
@ -111,6 +114,7 @@ DriftOptions _$DriftOptionsFromJson(Map json) => $checkedCreate(
'useDataClassNameForCompanions': 'use_data_class_name_for_companions', 'useDataClassNameForCompanions': 'use_data_class_name_for_companions',
'useColumnNameAsJsonKeyWhenDefinedInMoorFile': 'useColumnNameAsJsonKeyWhenDefinedInMoorFile':
'use_column_name_as_json_key_when_defined_in_moor_file', 'use_column_name_as_json_key_when_defined_in_moor_file',
'useSqlColumnNameAsJsonKey': 'use_sql_column_name_as_json_key',
'generateConnectConstructor': 'generate_connect_constructor', 'generateConnectConstructor': 'generate_connect_constructor',
'dataClassToCompanions': 'data_class_to_companions', 'dataClassToCompanions': 'data_class_to_companions',
'generateMutableClasses': 'mutable_classes', 'generateMutableClasses': 'mutable_classes',
@ -143,6 +147,7 @@ Map<String, dynamic> _$DriftOptionsToJson(DriftOptions instance) =>
instance.useDataClassNameForCompanions, instance.useDataClassNameForCompanions,
'use_column_name_as_json_key_when_defined_in_moor_file': 'use_column_name_as_json_key_when_defined_in_moor_file':
instance.useColumnNameAsJsonKeyWhenDefinedInMoorFile, instance.useColumnNameAsJsonKeyWhenDefinedInMoorFile,
'use_sql_column_name_as_json_key': instance.useSqlColumnNameAsJsonKey,
'generate_connect_constructor': instance.generateConnectConstructor, 'generate_connect_constructor': instance.generateConnectConstructor,
'sqlite_modules': 'sqlite_modules':
instance.modules.map((e) => _$SqlModuleEnumMap[e]!).toList(), instance.modules.map((e) => _$SqlModuleEnumMap[e]!).toList(),

View File

@ -13,8 +13,9 @@ Expando<List<Input>> expectedSchema = Expando();
class VerifierImplementation implements SchemaVerifier { class VerifierImplementation implements SchemaVerifier {
final SchemaInstantiationHelper helper; final SchemaInstantiationHelper helper;
final Random _random = Random(); final Random _random = Random();
final void Function(Database)? setup;
VerifierImplementation(this.helper); VerifierImplementation(this.helper, {this.setup});
@override @override
Future<void> migrateAndValidate(GeneratedDatabase db, int expectedVersion, Future<void> migrateAndValidate(GeneratedDatabase db, int expectedVersion,
@ -57,14 +58,20 @@ class VerifierImplementation implements SchemaVerifier {
return buffer.toString(); return buffer.toString();
} }
Database _setupDatabase(String uri) {
final database = sqlite3.open(uri, uri: true);
setup?.call(database);
return database;
}
@override @override
Future<InitializedSchema> schemaAt(int version) async { Future<InitializedSchema> schemaAt(int version) async {
// Use distinct executors for setup and use, allowing us to close the helper // Use distinct executors for setup and use, allowing us to close the helper
// db here and avoid creating it twice. // db here and avoid creating it twice.
// https://www.sqlite.org/inmemorydb.html#sharedmemdb // https://www.sqlite.org/inmemorydb.html#sharedmemdb
final uri = 'file:mem${_randomString()}?mode=memory&cache=shared'; final uri = 'file:mem${_randomString()}?mode=memory&cache=shared';
final dbForSetup = sqlite3.open(uri, uri: true); final dbForSetup = _setupDatabase(uri);
final dbForUse = sqlite3.open(uri, uri: true); final dbForUse = _setupDatabase(uri);
final executor = NativeDatabase.opened(dbForSetup); final executor = NativeDatabase.opened(dbForSetup);
final db = helper.databaseForVersion(executor, version); final db = helper.databaseForVersion(executor, version);
@ -74,7 +81,7 @@ class VerifierImplementation implements SchemaVerifier {
await db.close(); await db.close();
return InitializedSchema(dbForUse, () { return InitializedSchema(dbForUse, () {
final db = sqlite3.open(uri, uri: true); final db = _setupDatabase(uri);
return DatabaseConnection(NativeDatabase.opened(db)); return DatabaseConnection(NativeDatabase.opened(db));
}); });
} }

View File

@ -31,13 +31,23 @@ class ImportManagerForPartFiles extends ImportManager {
// Part files can't add their own imports, so try to find the element in an // Part files can't add their own imports, so try to find the element in an
// existing import. // existing import.
for (final MapEntry(:key, :value) in _namedImports.entries) { 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 key;
} }
} }
return null; 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 { class NullImportManager extends ImportManager {

View File

@ -74,9 +74,10 @@ abstract class _NodeOrWriter {
} }
AnnotatedDartCode companionType(DriftTable table) { AnnotatedDartCode companionType(DriftTable table) {
final baseName = writer.options.useDataClassNameForCompanions final baseName = table.nameOfCompanionClass ??
? table.nameOfRowClass (writer.options.useDataClassNameForCompanions
: table.baseDartName; ? table.nameOfRowClass
: table.baseDartName);
return generatedElement(table, '${baseName}Companion'); return generatedElement(table, '${baseName}Companion');
} }

View File

@ -1,6 +1,6 @@
name: drift_dev name: drift_dev
description: Dev-dependency for users of drift. Contains the generator and development tools. 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 repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/ homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues issue_tracker: https://github.com/simolus3/drift/issues
@ -30,7 +30,7 @@ dependencies:
io: ^1.0.3 io: ^1.0.3
# Drift-specific analysis and apis # 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' sqlite3: '>=0.1.6 <3.0.0'
sqlparser: '^0.34.0' sqlparser: '^0.34.0'

View File

@ -38,16 +38,17 @@ CREATE INDEX b_idx /* comment should be stripped */ ON b (foo, upper(foo));
final result = await emulateDriftBuild( final result = await emulateDriftBuild(
inputs: { inputs: {
'a|lib/main.dart': r''' 'a|lib/main.dart': r'''
import 'dart:io' as io;
import 'package:drift/drift.dart' as drift; import 'package:drift/drift.dart' as drift;
import 'tables.dart' as tables; import 'tables.dart' as tables;
@drift.DriftDatabase(tables: [tables.Texts]) @drift.DriftDatabase(tables: [tables.Files])
class MyDatabase extends _$MyDatabase {} class MyDatabase extends _$MyDatabase {}
''', ''',
'a|lib/tables.dart': ''' 'a|lib/tables.dart': '''
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
class Texts extends Table { class Files extends Table {
TextColumn get content => text()(); TextColumn get content => text()();
} }
''', ''',
@ -59,12 +60,12 @@ class Texts extends Table {
'a|lib/main.drift.dart': decodedMatches( 'a|lib/main.drift.dart': decodedMatches(
allOf( allOf(
contains( contains(
r'class $TextsTable extends tables.Texts with ' r'class $FilesTable extends tables.Files with '
r'drift.TableInfo<$TextsTable, Text>', r'drift.TableInfo<$FilesTable, File>',
), ),
contains( contains(
'class Text extends drift.DataClass implements ' 'class File extends drift.DataClass implements '
'drift.Insertable<Text>', 'drift.Insertable<File>',
), ),
), ),
), ),

View File

@ -5,7 +5,11 @@ import 'package:drift_dev/src/services/schema/verifier_impl.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
final verifier = SchemaVerifier(_TestHelper()); final verifier = SchemaVerifier(
_TestHelper(),
setup: (rawDb) => rawDb.createFunction(
functionName: 'test_function', function: (args) => 1),
);
group('startAt', () { group('startAt', () {
test('starts at the requested version', () async { test('starts at the requested version', () async {
@ -15,6 +19,12 @@ void main() {
expect(details.hadUpgrade, isFalse, reason: 'no upgrade expected'); 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', () { group('migrateAndValidate', () {

View File

@ -267,6 +267,120 @@ extension ItemToInsertable on i1.Item {
result.writer, 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 { class _GeneratesConstDataClasses extends Matcher {

View File

@ -3,7 +3,7 @@ version: 1.0.0
publish_to: none publish_to: none
environment: environment:
sdk: '>=2.18.0 <3.0.0' sdk: '>=3.3.0 <4.0.0'
dependencies: dependencies:
drift: drift:

View File

@ -3,7 +3,7 @@ publish_to: none
version: 1.0.0 version: 1.0.0
environment: environment:
sdk: '>=2.14.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
dependencies: dependencies:
drift: ^2.11.0 drift: ^2.11.0

View File

@ -120,7 +120,8 @@ class RemoteDatabase {
isAlive: isAlive, isAlive: isAlive,
scope: {'db': database.database.id!}, 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!)); final description = DatabaseDescription.fromJson(json.decode(value!));

View File

@ -12,11 +12,11 @@ dependencies:
sdk: flutter sdk: flutter
devtools_extensions: ^0.0.8 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 db_viewer: ^1.0.3
rxdart: ^0.27.7 rxdart: ^0.27.7
flutter_riverpod: ^3.0.0-dev.0 flutter_riverpod: ^3.0.0-dev.0
vm_service: ^11.10.0 vm_service: ^13.0.0
path: ^1.8.3 path: ^1.8.3
drift: ^2.12.1 drift: ^2.12.1
logging: ^1.2.0 logging: ^1.2.0
@ -29,5 +29,8 @@ dev_dependencies:
sdk: flutter sdk: flutter
flutter_lints: ^3.0.0 flutter_lints: ^3.0.0
dependency_overrides:
web: ^0.5.0
flutter: flutter:
uses-material-design: true uses-material-design: true

View File

@ -4,8 +4,10 @@ talking to PostgreSQL databases by using the `postgres` package.
## Using this ## Using this
For general notes on using drift, see [this guide](https://drift.simonbinder.eu/getting-started/). 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` To use drift_postgres, add this to your `pubspec.yaml`
```yaml ```yaml
dependencies: dependencies:
drift: "$latest version" 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 ## Running tests
To test this package, first run To test this package, first run

View File

@ -0,0 +1,9 @@
targets:
$default:
builders:
drift_dev:
options:
sql:
dialects:
- sqlite # remove this line if you only need postgres
- postgres

View File

@ -54,7 +54,8 @@ class _PgDelegate extends DatabaseDelegate {
late DbVersionDelegate versionDelegate; late DbVersionDelegate versionDelegate;
@override @override
Future<bool> get isOpen => Future.value(_openedSession != null); Future<bool> get isOpen =>
Future.value(_openedSession != null && _openedSession!.isOpen);
@override @override
Future<void> open(QueryExecutorUser user) async { Future<void> open(QueryExecutorUser user) async {

View File

@ -1,6 +1,6 @@
name: drift_postgres name: drift_postgres
description: Postgres implementation and APIs for the drift database package. 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 repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/docs/platforms/postgres/ homepage: https://drift.simonbinder.eu/docs/platforms/postgres/
issue_tracker: https://github.com/simolus3/drift/issues issue_tracker: https://github.com/simolus3/drift/issues

View File

@ -26,19 +26,19 @@ void main() {
return row.read(expression)!; 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', () { 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('uuid', () => testWith(PgTypes.uuid, Uuid().v4obj()));
group( group(
'interval', 'interval',
@ -60,6 +60,8 @@ void main() {
); );
}); });
group('bytea', () => testWith(null, Uint8List.fromList([1, 2, 3, 4, 5])));
test('compare datetimes', () async { test('compare datetimes', () async {
final time = DateTime.now(); final time = DateTime.now();
final before = Variable( final before = Variable(

View File

@ -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 { Future<void> deleteDatabase(WebStorageApi storageApi, String name) async {
await driver.executeAsync('delete_database(arguments[0], arguments[1])', [ await driver.executeAsync('delete_database(arguments[0], arguments[1])', [
json.encode([storageApi.name, name]), json.encode([storageApi.name, name]),

View File

@ -12,5 +12,13 @@ class TestDatabase extends _$TestDatabase {
TestDatabase(super.e); TestDatabase(super.e);
@override @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;
} }

View File

@ -13,7 +13,7 @@ dependencies:
shelf: ^1.4.1 shelf: ^1.4.1
shelf_proxy: ^1.0.4 shelf_proxy: ^1.0.4
path: ^1.8.3 path: ^1.8.3
js: ^0.6.7 js: ^0.7.0
package_config: ^2.1.0 package_config: ^2.1.0
async: ^2.11.0 async: ^2.11.0
http: ^1.0.0 http: ^1.0.0

View File

@ -40,7 +40,11 @@ enum Browser {
Future<Process> spawnDriver() async { Future<Process> spawnDriver() async {
return switch (this) { 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 => chrome =>
Process.start('chromedriver', ['--port=4444', '--url-base=/wd/hub']), Process.start('chromedriver', ['--port=4444', '--url-base=/wd/hub']),
}; };
@ -156,6 +160,20 @@ void main() {
final finalImpls = await driver.probeImplementations(); final finalImpls = await driver.probeImplementations();
expect(finalImpls.existing, isEmpty); 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( group(

View File

@ -18,6 +18,7 @@ TestDatabase? openedDatabase;
StreamQueue<void>? tableUpdates; StreamQueue<void>? tableUpdates;
InitializationMode initializationMode = InitializationMode.none; InitializationMode initializationMode = InitializationMode.none;
int schemaVersion = 1;
void main() { void main() {
_addCallbackForWebDriver('detectImplementations', _detectImplementations); _addCallbackForWebDriver('detectImplementations', _detectImplementations);
@ -32,6 +33,10 @@ void main() {
initializationMode = InitializationMode.values.byName(arg!); initializationMode = InitializationMode.values.byName(arg!);
return true; return true;
}); });
_addCallbackForWebDriver('set_schema_version', (arg) async {
schemaVersion = int.parse(arg!);
return true;
});
_addCallbackForWebDriver('delete_database', (arg) async { _addCallbackForWebDriver('delete_database', (arg) async {
final result = await WasmDatabase.probe( final result = await WasmDatabase.probe(
sqlite3Uri: sqlite3WasmUri, sqlite3Uri: sqlite3WasmUri,
@ -85,7 +90,7 @@ Future<Uint8List?> _initializeDatabase() async {
// Let's first open a custom WasmDatabase, the way it would have been // Let's first open a custom WasmDatabase, the way it would have been
// done before WasmDatabase.open. // 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); final fs = await IndexedDbFileSystem.open(dbName: dbName);
sqlite3.registerVirtualFileSystem(fs, makeDefault: true); sqlite3.registerVirtualFileSystem(fs, makeDefault: true);
@ -150,7 +155,7 @@ Future<void> _open(String? implementationName) async {
db.createFunction( db.createFunction(
functionName: 'database_host', functionName: 'database_host',
function: (args) => 'document', function: (args) => 'document',
argumentCount: const AllowedArgumentCount(1), argumentCount: const AllowedArgumentCount(0),
); );
}, },
); );
@ -158,7 +163,8 @@ Future<void> _open(String? implementationName) async {
connection = result.resolvedExecutor; connection = result.resolvedExecutor;
} }
final db = openedDatabase = TestDatabase(connection); final db =
openedDatabase = TestDatabase(connection)..schemaVersion = schemaVersion;
// Make sure it works! // Make sure it works!
await db.customSelect('SELECT database_host()').get(); await db.customSelect('SELECT database_host()').get();

View File

@ -6,7 +6,7 @@ void main() {
db.createFunction( db.createFunction(
functionName: 'database_host', functionName: 'database_host',
function: (args) => 'worker', function: (args) => 'worker',
argumentCount: const AllowedArgumentCount(1), argumentCount: const AllowedArgumentCount(0),
); );
}); });
} }

View File

@ -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 - Allow selecting from virtual tables using the table-valued function
syntax. syntax.

View File

@ -82,7 +82,7 @@ class ResolvedType {
} }
@override @override
bool operator ==(dynamic other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
other is ResolvedType && other is ResolvedType &&
other.type == type && other.type == type &&
@ -114,7 +114,7 @@ abstract class TypeHint {
int get hashCode => runtimeType.hashCode; int get hashCode => runtimeType.hashCode;
@override @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. /// Type hint to mark that this type will contain a boolean value.
@ -191,7 +191,7 @@ class ResolveResult {
} }
@override @override
bool operator ==(dynamic other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
other is ResolveResult && other is ResolveResult &&
other.type == type && other.type == type &&

View File

@ -64,7 +64,7 @@ class SimpleName extends DeclaredStatementIdentifier {
int get hashCode => name.hashCode; int get hashCode => name.hashCode;
@override @override
bool operator ==(dynamic other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other is SimpleName && other.name == name); (other is SimpleName && other.name == name);
} }
@ -87,7 +87,7 @@ class SpecialStatementIdentifier extends DeclaredStatementIdentifier {
String get name => specialName; String get name => specialName;
@override @override
bool operator ==(dynamic other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||
(other is SpecialStatementIdentifier && (other is SpecialStatementIdentifier &&
other.specialName == specialName); other.specialName == specialName);

View File

@ -241,7 +241,7 @@ class FrameBoundary {
} }
@override @override
bool operator ==(dynamic other) { bool operator ==(Object other) {
if (identical(this, other)) return true; if (identical(this, other)) return true;
if (other.runtimeType != runtimeType) return false; if (other.runtimeType != runtimeType) return false;

View File

@ -56,7 +56,7 @@ abstract class TriggerTarget extends AstNode {
int get hashCode => runtimeType.hashCode; int get hashCode => runtimeType.hashCode;
@override @override
bool operator ==(dynamic other) => other.runtimeType == runtimeType; bool operator ==(Object other) => other.runtimeType == runtimeType;
@override @override
Iterable<AstNode> get childNodes => const Iterable.empty(); Iterable<AstNode> get childNodes => const Iterable.empty();

View File

@ -2294,27 +2294,30 @@ class Parser {
supportAs ? const [TokenType.as, TokenType.$with] : [TokenType.$with]; supportAs ? const [TokenType.as, TokenType.$with] : [TokenType.$with];
if (enableDriftExtensions && (_match(types))) { if (enableDriftExtensions && (_match(types))) {
final first = _previous; return _startedDriftTableName(_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 null; 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 /// Parses a "CREATE TRIGGER" statement, assuming that the create token has
/// already been consumed. /// already been consumed.
CreateTriggerStatement? _createTrigger() { CreateTriggerStatement? _createTrigger() {
@ -2409,17 +2412,33 @@ class Parser {
final ifNotExists = _ifNotExists(); final ifNotExists = _ifNotExists();
final name = _consumeIdentifier('Expected a name for this view'); final name = _consumeIdentifier('Expected a name for this view');
// Don't allow the "AS ClassName" syntax for views since it causes an DriftTableName? driftTableName;
// ambiguity with the regular view syntax. var skippedToSelect = false;
final driftTableName = _driftTableName(supportAs: false);
List<String>? columnNames; if (enableDriftExtensions) {
if (_matchOne(TokenType.leftParen)) { if (_check(TokenType.$with)) {
columnNames = _columnNames(); driftTableName = _driftTableName();
_consume(TokenType.rightParen, 'Expected closing bracket'); } 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(); final query = _fullSelect();
if (query == null) { if (query == null) {

Some files were not shown because too many files have changed in this diff Show More