Support custom names for drift views

This commit is contained in:
Simon Binder 2024-03-08 18:13:50 +01:00
parent 78a05fdf0b
commit 29303100b8
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 470 additions and 64 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

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

View File

@ -1,3 +1,7 @@
## 3.35.0-dev
- Drift extensions: Allow custom class names for `CREATE VIEW` statements.
## 0.34.1 ## 0.34.1
- Allow selecting from virtual tables using the table-valued function - Allow selecting from virtual tables using the table-valued function

View File

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

View File

@ -428,6 +428,7 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
_ifNotExists(e.ifNotExists); _ifNotExists(e.ifNotExists);
identifier(e.viewName); identifier(e.viewName);
e.driftTableName?.accept(this, arg);
if (e.columns != null) { if (e.columns != null) {
symbol('(', spaceBefore: true); symbol('(', spaceBefore: true);

View File

@ -36,6 +36,25 @@ void main() {
); );
}); });
test('parses a CREATE VIEW statement with a custom Dart name', () {
testStatement(
'CREATE VIEW my_view AS DartClass AS SELECT 1',
CreateViewStatement(
viewName: 'my_view',
query: SelectStatement(
columns: [
ExpressionResultColumn(expression: NumericLiteral(1)),
],
),
driftTableName: DriftTableName(
overriddenDataClassName: 'DartClass',
useExistingDartClass: false,
),
),
driftMode: true,
);
});
test('parses a complex CREATE View statement', () { test('parses a complex CREATE View statement', () {
testStatement( testStatement(
'CREATE VIEW IF NOT EXISTS my_complex_view (ids, name, count, type) AS ' 'CREATE VIEW IF NOT EXISTS my_complex_view (ids, name, count, type) AS '

View File

@ -107,6 +107,14 @@ CREATE VIEW my_view (foo, bar) AS SELECT * FROM t1;
testFormat(''' testFormat('''
CREATE VIEW my_view AS SELECT * FROM t1; CREATE VIEW my_view AS SELECT * FROM t1;
'''); ''');
testFormat('''
CREATE VIEW my_view AS Foo (foo, bar) AS SELECT * FROM t1;
''');
testFormat('''
CREATE VIEW my_view WITH Foo.constr (foo, bar) AS SELECT * FROM t1;
''');
}); });
group('table', () { group('table', () {