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
// #docregion updateTitle
extension UpdateTitle on DatabaseConnectionUser {
Future<Row?> updateTitle<T extends TableInfo<Table, Row>, Row>(
T table, int id, String newTitle) async {
final columnsByName = table.columnsByName;
final stmt = update(table)
..where((tbl) {
final idColumn = columnsByName['id'];
if (idColumn == null) {
throw ArgumentError.value(
this, 'this', 'Must be a table with an id column');
}
if (idColumn.type != DriftSqlType.int) {
throw ArgumentError('Column `id` is not an integer');
}
return idColumn.equals(id);
});
final rows = await stmt.writeReturning(RawValuesInsertable({
'title': Variable<String>(newTitle),
}));
return rows.singleOrNull;
}
}
// #enddocregion updateTitle
extension FindTodoEntryById on GeneratedDatabase {
Todos get todos => Todos(this);
@ -33,4 +63,10 @@ extension FindTodoEntryById on GeneratedDatabase {
return select(todos)..where((row) => row.id.equals(id));
}
// #enddocregion findTodoEntryById
// #docregion updateTodo
Future<Todo?> updateTodoTitle(int id, String newTitle) {
return updateTitle(todos, id, newTitle);
}
// #enddocregion updateTodo
}

View File

@ -31,7 +31,12 @@ class TodoItems extends Table {
@DriftDatabase(tables: [TodoItems])
class AppDatabase extends _$AppDatabase {
// #enddocregion open
// After generating code, this class needs to define a `schemaVersion` getter
// and a constructor telling drift where the database should be stored.
// These are described in the getting started guide: https://drift.simonbinder.eu/getting-started/#open
// #enddocregion before_generation
// #docregion open
AppDatabase() : super(_openConnection());
@override

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`
returns a `Todo` instance, the generated data class for this table.
## Updates and inserts
The same approach also works to construct update, delete and insert statements (although those require a [TableInfo] instead of a [ResultSetImplementation]
as views are read-only).
Also, updates and inserts use an `Insertable` object which represents a partial row of updated or
inserted columns, respectively.
With a known table, one would use the generated typed `Companion` objects for that.
But this can also be done with schema introspection thanks to the `RawValuesInsertable`, which
can be used as a generic `Insertable` backed by a map of column names to values.
This example builds on the previous one to update the `title` column of a generic table based on a filter
of the `id` column:
{% include "blocks/snippet" snippets = snippets name = 'updateTitle' %}
In a database or database accessor class, the method can then be called like this:
{% include "blocks/snippet" snippets = snippets name = 'updateTodo' %}
Hopefully, this page gives you some pointers to start reflectively inspecting your drift databases.
The linked Dart documentation also expains the concepts in more detail.
@ -59,4 +75,4 @@ If you have questions about this, or have a suggestion for more examples to incl
[ResultSetImplementation]: https://drift.simonbinder.eu/api/drift/resultsetimplementation-class
[TableInfo]: https://drift.simonbinder.eu/api/drift/tableinfo-mixin
[ViewInfo]: https://drift.simonbinder.eu/api/drift/viewinfo-class
[GeneratedColumn]: https://drift.simonbinder.eu/api/drift/generatedcolumn-class
[GeneratedColumn]: https://drift.simonbinder.eu/api/drift/generatedcolumn-class

View File

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

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`).
You can always override the json key by using a `JSON KEY` column constraint
(e.g. `user_name VARCHAR NOT NULL JSON KEY userName`).
* `use_sql_column_name_as_json_key` (defaults to false): Uses the column name in SQL as the JSON key for serialization,
regardless of whether the table was defined in a drift file or not.
* `generate_connect_constructor` (deprecated): Generates a named `connect()` constructor on database classes
that takes a `DatabaseConnection` instead of a `QueryExecutor`.
This option was deprecated in drift 2.5 because `DatabaseConnection` now implements `QueryExecutor`.

View File

@ -167,6 +167,8 @@ statement before it runs it.
a defined query by appending `WITH YourDartClass` to a `CREATE TABLE` statement.
- Alternatively, you may use `AS DesiredRowClassName` to change the name of the
row class generated by drift.
- Both custom row classes and custom table names also work for views, e.g. with
`CREATE VIEW my_view AS DartName AS SELECT ...;`.
- In a column definition, `MAPPED BY` can be used to [apply a converter](#type-converters)
to that column.
- Similarly, a `JSON KEY` constraint can be used to define the key drift will
@ -356,28 +358,17 @@ With that option, the variable will be inferred to `Preferences` instead of `Str
### Existing row classes
{% assign existingDrift = "package:drift_docs/snippets/modular/drift/with_existing.drift.excerpt.json" | readString | json_decode %}
{% assign rowClassDart = "package:drift_docs/snippets/modular/drift/row_class.dart.excerpt.json" | readString | json_decode %}
You can use custom row classes instead of having drift generate one for you.
For instance, let's say you had a Dart class defined as
```dart
class User {
final int id;
final String name;
User(this.id, this.name);
}
```
{% include "blocks/snippet" snippets = rowClassDart name = "user" %}
Then, you can instruct drift to use that class as a row class as follows:
```sql
import 'row_class.dart'; --import for where the row class is defined
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
) WITH User; -- This tells drift to use the existing Dart class
```
{% include "blocks/snippet" snippets = existingDrift name = "users" %}
When using custom row classes defined in another Dart file, you also need to import that file into the file where you define
the database.
@ -388,32 +379,11 @@ can be added after the name of the query.
For instance, let's say we expand the existing Dart code in `row_class.dart` by adding another class:
```dart
class UserWithFriends {
final User user;
final List<User> friends;
UserWithFriends(this.user, {this.friends = const []});
}
```
{% include "blocks/snippet" snippets = rowClassDart name = "userwithfriends" %}
Now, we can add a corresponding query using the new class for its rows:
```sql
-- table to demonstrate a more complex select query below.
-- also, remember to add the import for `UserWithFriends` to your drift file.
CREATE TABLE friends (
user_a INTEGER NOT NULL REFERENCES users(id),
user_b INTEGER NOT NULL REFERENCES users(id),
PRIMARY KEY (user_a, user_b)
);
allFriendsOf WITH UserWithFriends: SELECT users.**, LIST(
SELECT * FROM users a INNER JOIN friends ON user_a = a.id WHERE user_b = users.id
UNION ALL
SELECT * FROM users b INNER JOIN friends ON user_b = b.id WHERE user_a = users.id
) AS friends FROM users WHERE id = :id;
```
{% include "blocks/snippet" snippets = existingDrift name = "friends" %}
The `WITH UserWithFriends` syntax will make drift consider the `UserWithFriends` class.
For every field in the constructor, drift will check the column from the query and

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

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
instead of potentially allowing them to operate on a database in an
@ -12,6 +20,9 @@
- Improve stack traces for errors happening on drift isolates (which includes
usages of `NativeDatabase.createInBackground`).
- Don't cache `EXPLAIN` statements, avoiding schema locks.
- Deprecate `Value.ofNullable` in favor of `Value.absentIfNull`, which is more
explicit about its behavior and allows nullable types too.
- Migrate `WasmDatabase` to `dart:js_interop` and `package:web`.
## 2.15.0

View File

@ -308,7 +308,10 @@ final class TableIndex {
class DataClassName {
/// The overridden name to use when generating the data class for a table.
/// {@macro drift_custom_data_class}
final String name;
final String? name;
/// The overridden name to use when generating the companion class for a table.
final String? companion;
/// The parent type of the data class generated by drift.
///
@ -345,7 +348,11 @@ class DataClassName {
/// Customize the data class name for a given table.
/// {@macro drift_custom_data_class}
const DataClassName(this.name, {this.extending});
const DataClassName(this.name, {this.extending, this.companion});
/// Customize the data class name for a given table.
/// {@macro drift_custom_data_class}
const DataClassName.custom({this.name, this.extending, this.companion});
}
/// An annotation specifying an existing class to be used as a data class.

View File

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

View File

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

View File

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

View File

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

View File

@ -137,10 +137,31 @@ extension StringExpressionOperators on Expression<String> {
/// and [length] can be negative to return a section of the string before
/// [start].
Expression<String> substr(int start, [int? length]) {
return substrExpr(
Constant(start), length != null ? Constant(length) : null);
}
/// Calls the [`substr`](https://sqlite.org/lang_corefunc.html#substr)
/// function with arbitrary expressions as arguments.
///
/// For instance, this call uses [substrExpr] to remove the last 5 characters
/// from a column. As this depends on its [StringExpressionOperators.length],
/// it needs to use expressions:
///
/// ```dart
/// update(table).write(TableCompanion.custom(
/// column: column.substrExpr(Variable(1), column.length - Variable(5))
/// ));
/// ```
///
/// When both [start] and [length] are Dart values (e.g. [Variable]s or
/// [Constant]s), consider using [substr] instead.
Expression<String> substrExpr(Expression<int> start,
[Expression<int>? length]) {
return FunctionCallExpression('SUBSTR', [
this,
Constant<int>(start),
if (length != null) Constant<int>(length),
start,
if (length != null) length,
]);
}
}

View File

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

View File

@ -132,7 +132,7 @@ class Migrator {
return _issueCustomQuery(context.sql, context.boundVariables);
}
/// Alter columns of an existing tabe.
/// Alter columns of an existing table.
///
/// Since sqlite does not provide a way to alter the type or constraint of an
/// individual column, one needs to write a fairly complex migration procedure

View File

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

View File

@ -276,8 +276,15 @@ class InsertStatement<T extends Table, D> {
if (ctx.dialect == SqlDialect.mariadb) {
ctx.buffer.write(' ON DUPLICATE');
} else {
ctx.buffer.write(' ON CONFLICT(');
ctx.buffer.write(' ON CONFLICT');
if (target != null && target.isEmpty) {
// An empty list indicates that no explicit target should be generated
// by drift, the default rules by the database will apply instead.
return;
}
ctx.buffer.write('(');
final conflictTarget = target ?? table.$primaryKey.toList();
if (conflictTarget.isEmpty) {
@ -348,7 +355,7 @@ class InsertStatement<T extends Table, D> {
if (onConflict._where != null) {
ctx.writeWhitespace();
final where = onConflict._where!(
final where = onConflict._where(
table.asDslTable, table.createAlias('excluded').asDslTable);
where.writeInto(ctx);
}
@ -473,6 +480,8 @@ class DoUpdate<T extends Table, D> extends UpsertClause<T, D> {
/// specifies the uniqueness constraint that will trigger the upsert.
///
/// By default, the primary key of the table will be used.
/// This can be set to an empty list, in which case no explicit conflict
/// target will be generated by drift.
final List<Column>? target;
/// Creates a `DO UPDATE` clause.

View File

@ -47,6 +47,35 @@ abstract class TypeConverter<D, S> {
json: json,
);
}
/// A type converter mapping [extension types] to their underlying
/// representation to store them in databases.
///
/// Here, [ExtType] is the extension type to use in Dart classes, and [Inner]
/// is the underlying type stored in the database. For instance, if you had
/// a type to represent ids in a database:
///
/// ```dart
/// extension type IdNumber(int id) {}
/// ```
///
/// You could use `TypeConverter.extensionType<IdNumber, int>()` in a column
/// definition:
///
/// ```dart
/// class Users extends Table {
/// IntColumn get id => integer()
/// .autoIncrement()
/// .map(TypeConverter.extensionType<IdNumber, int>())();
/// TextColumn get name => text()();
/// }
/// ```
///
/// [extension types]: https://dart.dev/language/extension-types
static JsonTypeConverter<ExtType, Inner>
extensionType<ExtType, Inner extends Object>() {
return _ExtensionTypeConverter();
}
}
/// A mixin for [TypeConverter]s that should also apply to drift's builtin
@ -264,3 +293,17 @@ class _NullWrappingTypeConverterWithJson<D, S extends Object, J extends Object>
return value == null ? null : requireToJson(value);
}
}
class _ExtensionTypeConverter<ExtType, Inner extends Object>
extends TypeConverter<ExtType, Inner>
with JsonTypeConverter<ExtType, Inner> {
const _ExtensionTypeConverter();
@override
ExtType fromSql(Inner fromDb) {
return fromDb as ExtType;
}
@override
Inner toSql(ExtType value) => value as Inner;
}

View File

@ -124,9 +124,17 @@ final class SqlTypes {
return (dart.millisecondsSinceEpoch ~/ 1000).toString();
}
} else if (dart is Uint8List) {
// BLOB literals are string literals containing hexadecimal data and
// preceded by a single "x" or "X" character. Example: X'53514C697465'
return "x'${hex.encode(dart)}'";
final String hexString = hex.encode(dart);
if (dialect == SqlDialect.postgres) {
// Postgres BYTEA hex format
// https://www.postgresql.org/docs/current/datatype-binary.html#DATATYPE-BINARY-BYTEA-HEX-FORMAT
return "'\\x$hexString'::bytea";
} else {
// BLOB literals are string literals containing hexadecimal data and
// preceded by a single "x" or "X" character. Example: X'53514C697465'
return "x'$hexString'";
}
} else if (dart is DriftAny) {
return mapToSqlLiteral(dart.rawSqlValue);
}

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,14 +35,14 @@ void main() {
);
b.replaceAll(db.categories, const [
CategoriesCompanion(id: Value(1), description: Value('new1')),
CategoriesCompanion(id: Value(2), description: Value('new2')),
CategoriesCompanion(id: Value(RowId(1)), description: Value('new1')),
CategoriesCompanion(id: Value(RowId(2)), description: Value('new2')),
]);
b.deleteWhere<$CategoriesTable, Category>(
db.categories, (tbl) => tbl.id.equals(1));
b.deleteAll(db.categories);
b.delete(db.todosTable, const TodosTableCompanion(id: Value(3)));
b.delete(db.todosTable, const TodosTableCompanion(id: Value(RowId(3))));
b.update(db.users, const UsersCompanion(name: Value('new name 2')));
@ -97,7 +97,7 @@ void main() {
db.categories,
CategoriesCompanion.insert(description: 'description'),
onConflict: DoUpdate((old) {
return const CategoriesCompanion(id: Value(42));
return const CategoriesCompanion(id: Value(RowId(42)));
}),
);
});
@ -203,16 +203,17 @@ void main() {
test('updates stream queries', () async {
await db.batch((b) {
b.insert(db.todosTable, const TodoEntry(id: 3, content: 'content'));
b.insert(
db.todosTable, const TodoEntry(id: RowId(3), content: 'content'));
b.update(db.users, const UsersCompanion(name: Value('new user name')));
b.replace(
db.todosTable,
const TodosTableCompanion(id: Value(3), content: Value('new')),
const TodosTableCompanion(id: Value(RowId(3)), content: Value('new')),
);
b.deleteWhere(db.todosTable, (TodosTable row) => row.id.equals(3));
b.delete(db.todosTable, const TodosTableCompanion(id: Value(3)));
b.delete(db.todosTable, const TodosTableCompanion(id: Value(RowId(3))));
});
verify(

View File

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

View File

@ -72,7 +72,7 @@ void main() {
final executor = MockExecutor();
final db = TodoDb(executor);
await db.someDao.todosForUser(user: 1).get();
await db.someDao.todosForUser(user: RowId(1)).get();
verify(executor.runSelect(argThat(contains('SELECT t.* FROM todos')), [1]));
});

View File

@ -44,6 +44,23 @@ void main() {
expect(exp, generates('?', [10]));
expect(exp.driftSqlType, isA<_NegatedIntType>());
});
test('also supports dialect-aware types', () {
const b = CustomExpression(
'b',
customType: DialectAwareSqlType<int>.via(
fallback: _NegatedIntType(),
overrides: {SqlDialect.postgres: DriftSqlType.int},
),
precedence: Precedence.primary,
);
expect(b.equals(3), generates('b = ?', [-3]));
expect(
b.equals(3),
generatesWithOptions('b = \$1',
variables: [3], dialect: SqlDialect.postgres));
});
}
class _NegatedIntType implements CustomSqlType<int> {

View File

@ -125,7 +125,11 @@ void main() {
});
test('substring', () {
expect(eval(Constant('hello world').substr(7)), completion('world'));
final input = Constant('hello world');
expect(eval(input.substr(7)), completion('world'));
expect(eval(input.substrExpr(Variable(1), input.length - Variable(6))),
completion('hello'));
});
});

View File

@ -52,5 +52,8 @@ void main() {
test('substr', () {
expect(expression.substr(10), generates('SUBSTR(col, 10)'));
expect(expression.substr(10, 2), generates('SUBSTR(col, 10, 2)'));
expect(expression.substrExpr(Variable(1), expression.length - Variable(5)),
generates('SUBSTR(col, ?, LENGTH(col) - ?)', [1, 5]));
});
}

View File

@ -23,7 +23,7 @@ void main() {
group('compiled custom queries', () {
// defined query: SELECT * FROM todos WHERE title = ?2 OR id IN ? OR title = ?1
test('work with arrays', () async {
await db.withIn('one', 'two', [1, 2, 3]).get();
await db.withIn('one', 'two', [RowId(1), RowId(2), RowId(3)]).get();
verify(
executor.runSelect(

View File

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

View File

@ -56,7 +56,7 @@ void main() {
test('generates insert or replace statements', () async {
await db.into(db.todosTable).insert(
const TodoEntry(
id: 113,
id: RowId(113),
content: 'Done',
),
mode: InsertMode.insertOrReplace);
@ -263,6 +263,22 @@ void main() {
));
});
test('can ignore conflict target', () async {
await db.into(db.todosTable).insert(
TodosTableCompanion.insert(content: 'my content'),
onConflict: DoUpdate((old) {
return TodosTableCompanion.custom(
content: const Variable('important: ') + old.content);
}, target: []),
);
verify(executor.runInsert(
'INSERT INTO "todos" ("content") VALUES (?) '
'ON CONFLICT DO UPDATE SET "content" = ? || "content"',
argThat(equals(['my content', 'important: '])),
));
});
test(
'can use multiple upsert targets',
() async {
@ -389,7 +405,8 @@ void main() {
when(executor.runInsert(any, any)).thenAnswer((_) => Future.value(3));
final id = await db.into(db.todosTable).insertOnConflictUpdate(
TodosTableCompanion.insert(content: 'content', id: const Value(3)));
TodosTableCompanion.insert(
content: 'content', id: const Value(RowId(3))));
verify(executor.runInsert(
'INSERT INTO "todos" ("id", "content") VALUES (?, ?) '
@ -599,7 +616,7 @@ void main() {
expect(
row,
const Category(
id: 1,
id: RowId(1),
description: 'description',
descriptionInUpperCase: 'DESCRIPTION',
priority: CategoryPriority.medium,

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import 'package:drift/drift.dart';
import 'package:test/test.dart';
import '../../generated/converter.dart';
import '../../generated/todos.dart';
enum _MyEnum { one, two, three }
@ -34,6 +35,16 @@ void main() {
});
});
test('TypeConverter.extensionType', () {
final converter = TypeConverter.extensionType<RowId, int>();
expect(converter.toSql(RowId(123)), 123);
expect(converter.fromSql(15), RowId(15));
expect(converter.fromSql(15), 15);
expect(converter.fromJson(16), RowId(16));
expect(converter.toJson(RowId(124)), 124);
});
group('enum name', () {
const converter = EnumNameConverter(_MyEnum.values);
const values = {

View File

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

View File

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

View File

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

View File

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

View File

@ -190,7 +190,7 @@ void main() {
stream,
emits([
Category(
id: 1,
id: RowId(1),
description: 'From remote isolate!',
priority: CategoryPriority.low,
descriptionInUpperCase: 'FROM REMOTE ISOLATE!',
@ -240,6 +240,34 @@ void main() {
await testWith(DatabaseConnection(NativeDatabase.memory()));
});
});
test('uses correct dialect', () async {
// Regression test for https://github.com/simolus3/drift/issues/2894
final isolate = await DriftIsolate.spawn(() {
return NativeDatabase.memory()
.interceptWith(PretendDialectInterceptor(SqlDialect.postgres));
});
final database = TodoDb(await isolate.connect(singleClientMode: true));
addTearDown(database.close);
await database.transaction(() async {
await expectLater(
database.into(database.users).insertReturning(UsersCompanion.insert(
name: 'test user', profilePicture: Uint8List(0))),
throwsA(
isA<DriftRemoteException>().having(
(e) => e.remoteCause,
'remoteCause',
isA<SqliteException>().having(
(e) => e.causingStatement,
'causingStatement',
contains(r'VALUES ($1, $2)'),
),
),
),
);
});
});
}
void _runTests(FutureOr<DriftIsolate> Function() spawner, bool terminateIsolate,
@ -290,7 +318,7 @@ void _runTests(FutureOr<DriftIsolate> Function() spawner, bool terminateIsolate,
await database.into(database.todosTable).insert(initialCompanion);
await expectLater(
stream,
emits(const TodoEntry(id: 1, content: 'my content')),
emits(const TodoEntry(id: RowId(1), content: 'my content')),
);
});

View File

@ -98,7 +98,7 @@ void main() {
ExecuteQuery(StatementMethod.select, 'SELECT ?', [BigInt.one]),
);
final mapped = protocol.deserialize(protocol.serialize(request)!);
final mapped = _checkSimpleRoundtrip(protocol, request);
expect(
mapped,
isA<Request>().having((e) => e.id, 'id', 1).having(
@ -109,6 +109,27 @@ void main() {
.having((e) => e.args, 'args', [isA<BigInt>()]),
),
);
final response = SuccessResponse(
1,
SelectResult([
{'col': BigInt.one}
]));
final mappedResponse = _checkSimpleRoundtrip(protocol, response);
expect(
mappedResponse,
isA<SuccessResponse>().having((e) => e.requestId, 'requestId', 1).having(
(e) => e.response,
'response',
isA<SelectResult>().having(
(e) => e.rows,
'rows',
([
{'col': BigInt.one}
]),
),
),
);
});
test('can run protocol without using complex types', () async {
@ -244,6 +265,12 @@ void _checkSimple(Object? object) {
}
}
Message _checkSimpleRoundtrip(DriftProtocol protocol, Message source) {
final serialized = protocol.serialize(source);
_checkSimple(serialized);
return protocol.deserialize(serialized!);
}
extension<T> on StreamChannel<T> {
StreamChannel<T> get expectedToClose {
return transformStream(StreamTransformer.fromHandlers(

View File

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

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?>{};
if (_matchVariables != null &&
!_matchVariables!.matches(ctx.boundVariables, argsMatchState)) {
!_matchVariables.matches(ctx.boundVariables, argsMatchState)) {
matchState['vars'] = ctx.boundVariables;
matchState['vars_match'] = argsMatchState;
matches = false;

View File

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

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.
// 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
([#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;
abstract class SchemaVerifier {
factory SchemaVerifier(SchemaInstantiationHelper helper) =
VerifierImplementation;
/// Creates a schema verifier for the drift-generated [helper].
///
/// See [tests] for more information.
/// The optional [setup] parameter is used internally by the verifier for
/// every database connection it opens. This can be used to, for instance,
/// register custom functions expected by your database.
///
/// [tests]: https://drift.simonbinder.eu/docs/migrations/tests/
factory SchemaVerifier(
SchemaInstantiationHelper helper, {
void Function(Database raw)? setup,
}) = VerifierImplementation;
/// Creates a [DatabaseConnection] that contains empty tables created for the
/// known schema [version].

View File

@ -45,13 +45,19 @@ class DriftOptions {
defaultValue: true)
final bool useColumnNameAsJsonKeyWhenDefinedInMoorFile;
/// Uses the sql column name as the json key instead of the name in dart.
///
/// Overrides [useColumnNameAsJsonKeyWhenDefinedInMoorFile] when set to `true`.
@JsonKey(name: 'use_sql_column_name_as_json_key', defaultValue: false)
final bool useSqlColumnNameAsJsonKey;
/// Generate a `connect` constructor in database superclasses.
///
/// This makes drift generate a constructor for database classes that takes a
/// `DatabaseConnection` instead of just a `QueryExecutor` - meaning that
/// stream queries can also be shared across multiple database instances.
/// Starting from drift 2.5, the database connection class implements the
/// `QueryExecutor` interface, making this option unecessary.
/// `QueryExecutor` interface, making this option unnecessary.
@JsonKey(name: 'generate_connect_constructor', defaultValue: false)
final bool generateConnectConstructor;
@ -120,6 +126,7 @@ class DriftOptions {
this.skipVerificationCode = false,
this.useDataClassNameForCompanions = false,
this.useColumnNameAsJsonKeyWhenDefinedInMoorFile = true,
this.useSqlColumnNameAsJsonKey = false,
this.generateConnectConstructor = false,
this.dataClassToCompanions = true,
this.generateMutableClasses = false,
@ -147,6 +154,7 @@ class DriftOptions {
required this.skipVerificationCode,
required this.useDataClassNameForCompanions,
required this.useColumnNameAsJsonKeyWhenDefinedInMoorFile,
required this.useSqlColumnNameAsJsonKey,
required this.generateConnectConstructor,
required this.dataClassToCompanions,
required this.generateMutableClasses,

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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
// existing import.
for (final MapEntry(:key, :value) in _namedImports.entries) {
if (value.containsKey(elementName)) {
final foundHere = value[elementName];
if (foundHere != null && _matchingUrl(definitionUri, foundHere)) {
return key;
}
}
return null;
}
static bool _matchingUrl(Uri wanted, Element target) {
final targetUri = target.librarySource?.uri;
if (targetUri == null || targetUri.scheme != wanted.scheme) {
return false;
}
return true;
}
}
class NullImportManager extends ImportManager {

View File

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

View File

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

View File

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

View File

@ -5,7 +5,11 @@ import 'package:drift_dev/src/services/schema/verifier_impl.dart';
import 'package:test/test.dart';
void main() {
final verifier = SchemaVerifier(_TestHelper());
final verifier = SchemaVerifier(
_TestHelper(),
setup: (rawDb) => rawDb.createFunction(
functionName: 'test_function', function: (args) => 1),
);
group('startAt', () {
test('starts at the requested version', () async {
@ -15,6 +19,12 @@ void main() {
expect(details.hadUpgrade, isFalse, reason: 'no upgrade expected');
}));
});
test('registers custom functions', () async {
final db = (await verifier.startAt(17)).executor;
await db.ensureOpen(_DelegatedUser(17, (_, details) async {}));
await db.runSelect('select test_function()', []);
});
});
group('migrateAndValidate', () {

View File

@ -267,6 +267,120 @@ extension ItemToInsertable on i1.Item {
result.writer,
);
});
test(
'generates fromJson and toJson with the sql column names as json keys',
() async {
final writer = await emulateDriftBuild(
options: const BuilderOptions({
'use_sql_column_name_as_json_key': true,
}),
inputs: const {
'a|lib/main.dart': r'''
import 'package:drift/drift.dart';
part 'main.drift.dart';
class MyTable extends Table {
TextColumn get myFirstColumn => text()();
IntColumn get mySecondColumn => integer()();
}
@DriftDatabase(
tables: [MyTable],
)
class Database extends _$Database {}
'''
},
);
checkOutputs({
'a|lib/main.drift.dart': decodedMatches(contains(r'''
factory MyTableData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return MyTableData(
myFirstColumn: serializer.fromJson<String>(json['my_first_column']),
mySecondColumn: serializer.fromJson<int>(json['my_second_column']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'my_first_column': serializer.toJson<String>(myFirstColumn),
'my_second_column': serializer.toJson<int>(mySecondColumn),
};
}
''')),
}, writer.dartOutputs, writer.writer);
},
tags: 'analyzer',
);
test(
'It should use the provided names for the data classes and the companion class',
() async {
final writer = await emulateDriftBuild(
inputs: const {
'a|lib/main.dart': r'''
import 'package:drift/drift.dart';
part 'main.drift.dart';
@DataClassName('FirstDataClass', companion: 'FirstCompanionClass')
class FirstTable extends Table {
TextColumn get foo => text()();
IntColumn get bar => integer()();
}
@DataClassName.custom(name: 'SecondDataClass', companion: 'SecondCompanionClass')
class SecondTable extends Table {
TextColumn get foo => text()();
IntColumn get bar => integer()();
}
@DataClassName.custom(companion: 'ThirdCompanionClass')
class ThirdTable extends Table {
TextColumn get foo => text()();
IntColumn get bar => integer()();
}
@DriftDatabase(
tables: [FirstTable, SecondTable, ThirdTable],
)
class Database extends _$Database {}
'''
},
);
checkOutputs({
'a|lib/main.drift.dart': decodedMatches(allOf([
contains(
'class FirstDataClass extends DataClass implements Insertable<FirstDataClass> {',
),
contains(
'class FirstTableCompanion extends UpdateCompanion<FirstDataClass> {',
),
contains(
'class SecondDataClass extends DataClass implements Insertable<SecondDataClass> {',
),
contains(
'class SecondTableCompanion extends UpdateCompanion<SecondDataClass> {',
),
contains(
'class ThirdTableData extends DataClass implements Insertable<ThirdTableData> {',
),
contains(
'class ThirdTableCompanion extends UpdateCompanion<ThirdTableData> {',
),
])),
}, writer.dartOutputs, writer.writer);
},
tags: 'analyzer',
);
}
class _GeneratesConstDataClasses extends Matcher {

View File

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

View File

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

View File

@ -120,7 +120,8 @@ class RemoteDatabase {
isAlive: isAlive,
scope: {'db': database.database.id!},
);
final value = await eval.retrieveFullValueAsString(stringVal);
final value = await eval.service
.retrieveFullStringValue(eval.isolateRef!.id!, stringVal);
final description = DatabaseDescription.fromJson(json.decode(value!));

View File

@ -12,11 +12,11 @@ dependencies:
sdk: flutter
devtools_extensions: ^0.0.8
devtools_app_shared: '>=0.0.5 <0.0.6' # 0.0.6 requires unstable Flutter
devtools_app_shared: ^0.0.9
db_viewer: ^1.0.3
rxdart: ^0.27.7
flutter_riverpod: ^3.0.0-dev.0
vm_service: ^11.10.0
vm_service: ^13.0.0
path: ^1.8.3
drift: ^2.12.1
logging: ^1.2.0
@ -29,5 +29,8 @@ dev_dependencies:
sdk: flutter
flutter_lints: ^3.0.0
dependency_overrides:
web: ^0.5.0
flutter:
uses-material-design: true

View File

@ -4,8 +4,10 @@ talking to PostgreSQL databases by using the `postgres` package.
## Using this
For general notes on using drift, see [this guide](https://drift.simonbinder.eu/getting-started/).
Detailed docs on getting started with `drift_postgres` are available [here](https://drift.simonbinder.eu/docs/platforms/postgres/#setup).
To use drift_postgres, add this to your `pubspec.yaml`
```yaml
dependencies:
drift: "$latest version"
@ -25,6 +27,21 @@ final database = AppDatabase(PgDatabase(
));
```
Also, consider adding builder options to make drift generate postgres-specific code:
```yaml
# build.yaml
targets:
$default:
builders:
drift_dev:
options:
sql:
dialects:
- sqlite # remove this line if you only need postgres
- postgres
```
## Running tests
To test this package, first run

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;
@override
Future<bool> get isOpen => Future.value(_openedSession != null);
Future<bool> get isOpen =>
Future.value(_openedSession != null && _openedSession!.isOpen);
@override
Future<void> open(QueryExecutorUser user) async {

View File

@ -1,6 +1,6 @@
name: drift_postgres
description: Postgres implementation and APIs for the drift database package.
version: 1.2.0-dev
version: 1.2.0
repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/docs/platforms/postgres/
issue_tracker: https://github.com/simolus3/drift/issues

View File

@ -26,19 +26,19 @@ void main() {
return row.read(expression)!;
}
void testWith<T extends Object>(CustomSqlType<T>? type, T value) {
test('with variable', () async {
final variable = Variable(value, type);
expect(await eval(variable), value);
});
test('with constant', () async {
final constant = Constant(value, type);
expect(await eval(constant), value);
});
}
group('custom types pass through', () {
void testWith<T extends Object>(CustomSqlType<T> type, T value) {
test('with variable', () async {
final variable = Variable(value, type);
expect(await eval(variable), value);
});
test('with constant', () async {
final constant = Constant(value, type);
expect(await eval(constant), value);
});
}
group('uuid', () => testWith(PgTypes.uuid, Uuid().v4obj()));
group(
'interval',
@ -60,6 +60,8 @@ void main() {
);
});
group('bytea', () => testWith(null, Uint8List.fromList([1, 2, 3, 4, 5])));
test('compare datetimes', () async {
final time = DateTime.now();
final before = Variable(

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 {
await driver.executeAsync('delete_database(arguments[0], arguments[1])', [
json.encode([storageApi.name, name]),

View File

@ -12,5 +12,13 @@ class TestDatabase extends _$TestDatabase {
TestDatabase(super.e);
@override
int get schemaVersion => 1;
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (m, from, to) async {
await into(testTable).insert(
TestTableCompanion.insert(content: 'from onUpgrade migration'));
},
);
@override
int schemaVersion = 1;
}

View File

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

View File

@ -40,7 +40,11 @@ enum Browser {
Future<Process> spawnDriver() async {
return switch (this) {
firefox => Process.start('geckodriver', []),
firefox => Process.start('geckodriver', []).then((result) async {
// geckodriver seems to take a while to initialize
await Future.delayed(const Duration(seconds: 1));
return result;
}),
chrome =>
Process.start('chromedriver', ['--port=4444', '--url-base=/wd/hub']),
};
@ -156,6 +160,20 @@ void main() {
final finalImpls = await driver.probeImplementations();
expect(finalImpls.existing, isEmpty);
});
test('migrations', () async {
await driver.openDatabase(entry);
await driver.insertIntoDatabase();
await driver.waitForTableUpdate();
await driver.closeDatabase();
await driver.driver.refresh();
await driver.setSchemaVersion(2);
await driver.openDatabase(entry);
// The migration adds a row
expect(await driver.amountOfRows, 2);
});
}
group(

View File

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

View File

@ -6,7 +6,7 @@ void main() {
db.createFunction(
functionName: 'database_host',
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
syntax.

View File

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

View File

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

View File

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

View File

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

View File

@ -2294,27 +2294,30 @@ class Parser {
supportAs ? const [TokenType.as, TokenType.$with] : [TokenType.$with];
if (enableDriftExtensions && (_match(types))) {
final first = _previous;
final useExisting = _previous.type == TokenType.$with;
final name =
_consumeIdentifier('Expected the name for the data class').identifier;
String? constructorName;
if (_matchOne(TokenType.dot)) {
constructorName = _consumeIdentifier(
'Expected name of the constructor to use after the dot')
.identifier;
}
return DriftTableName(
useExistingDartClass: useExisting,
overriddenDataClassName: name,
constructorName: constructorName,
)..setSpan(first, _previous);
return _startedDriftTableName(_previous);
}
return null;
}
DriftTableName _startedDriftTableName(Token first) {
final useExisting = _previous.type == TokenType.$with;
final name =
_consumeIdentifier('Expected the name for the data class').identifier;
String? constructorName;
if (_matchOne(TokenType.dot)) {
constructorName = _consumeIdentifier(
'Expected name of the constructor to use after the dot')
.identifier;
}
return DriftTableName(
useExistingDartClass: useExisting,
overriddenDataClassName: name,
constructorName: constructorName,
)..setSpan(first, _previous);
}
/// Parses a "CREATE TRIGGER" statement, assuming that the create token has
/// already been consumed.
CreateTriggerStatement? _createTrigger() {
@ -2409,17 +2412,33 @@ class Parser {
final ifNotExists = _ifNotExists();
final name = _consumeIdentifier('Expected a name for this view');
// Don't allow the "AS ClassName" syntax for views since it causes an
// ambiguity with the regular view syntax.
final driftTableName = _driftTableName(supportAs: false);
DriftTableName? driftTableName;
var skippedToSelect = false;
List<String>? columnNames;
if (_matchOne(TokenType.leftParen)) {
columnNames = _columnNames();
_consume(TokenType.rightParen, 'Expected closing bracket');
if (enableDriftExtensions) {
if (_check(TokenType.$with)) {
driftTableName = _driftTableName();
} else if (_matchOne(TokenType.as)) {
// This can either be a data class name or the beginning of the select
if (_check(TokenType.identifier)) {
// It's a data class name
driftTableName = _startedDriftTableName(_previous);
} else {
// No, we'll expect the SELECT next.
skippedToSelect = true;
}
}
}
_consume(TokenType.as, 'Expected AS SELECT');
List<String>? columnNames;
if (!skippedToSelect) {
if (_matchOne(TokenType.leftParen)) {
columnNames = _columnNames();
_consume(TokenType.rightParen, 'Expected closing bracket');
}
_consume(TokenType.as, 'Expected AS SELECT');
}
final query = _fullSelect();
if (query == null) {

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