diff --git a/docs/lib/snippets/modular/drift/row_class.dart b/docs/lib/snippets/modular/drift/row_class.dart new file mode 100644 index 00000000..e9452cca --- /dev/null +++ b/docs/lib/snippets/modular/drift/row_class.dart @@ -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 friends; + + UserWithFriends(this.user, {this.friends = const []}); +} +// #enddocregion userwithfriends diff --git a/docs/lib/snippets/modular/drift/with_existing.drift b/docs/lib/snippets/modular/drift/with_existing.drift new file mode 100644 index 00000000..ed95d942 --- /dev/null +++ b/docs/lib/snippets/modular/drift/with_existing.drift @@ -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 diff --git a/docs/lib/snippets/modular/drift/with_existing.drift.dart b/docs/lib/snippets/modular/drift/with_existing.drift.dart new file mode 100644 index 00000000..a1c42054 --- /dev/null +++ b/docs/lib/snippets/modular/drift/with_existing.drift.dart @@ -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 { + @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 id = i0.GeneratedColumn( + '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 name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + @override + List 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 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 get $primaryKey => {id}; + @override + i1.User map(Map 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 { + final i0.Value id; + final i0.Value 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 custom({ + i0.Expression? id, + i0.Expression? name, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + }); + } + + i2.UsersCompanion copyWith({i0.Value? id, i0.Value? name}) { + return i2.UsersCompanion( + id: id ?? this.id, + name: name ?? this.name, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(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 { + @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 userA = i0.GeneratedColumn( + '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 userB = i0.GeneratedColumn( + 'user_b', aliasedName, false, + type: i0.DriftSqlType.int, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL REFERENCES users(id)'); + @override + List 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 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 get $primaryKey => {userA, userB}; + @override + i2.Friend map(Map 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 get customConstraints => const ['PRIMARY KEY(user_a, user_b)']; + @override + bool get dontWriteConstraints => true; +} + +class Friend extends i0.DataClass implements i0.Insertable { + final int userA; + final int userB; + const Friend({required this.userA, required this.userB}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_a'] = i0.Variable(userA); + map['user_b'] = i0.Variable(userB); + return map; + } + + i2.FriendsCompanion toCompanion(bool nullToAbsent) { + return i2.FriendsCompanion( + userA: i0.Value(userA), + userB: i0.Value(userB), + ); + } + + factory Friend.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return Friend( + userA: serializer.fromJson(json['user_a']), + userB: serializer.fromJson(json['user_b']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'user_a': serializer.toJson(userA), + 'user_b': serializer.toJson(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 { + final i0.Value userA; + final i0.Value userB; + final i0.Value 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 custom({ + i0.Expression? userA, + i0.Expression? userB, + i0.Expression? 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? userA, i0.Value? userB, i0.Value? rowid}) { + return i2.FriendsCompanion( + userA: userA ?? this.userA, + userB: userB ?? this.userB, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userA.present) { + map['user_a'] = i0.Variable(userA.value); + } + if (userB.present) { + map['user_b'] = i0.Variable(userB.value); + } + if (rowid.present) { + map['rowid'] = i0.Variable(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 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(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(row.read('\$n_0')), + i0.Variable(row.read('\$n_1')) + ], + readsFrom: { + users, + friends, + }) + .map((i0.QueryRow row) => i1.User( + row.read('id'), + row.read('name'), + )) + .get(), + )); + } + + i2.Users get users => this.resultSet('users'); + i2.Friends get friends => this.resultSet('friends'); +} diff --git a/docs/pages/docs/SQL API/drift_files.md b/docs/pages/docs/SQL API/drift_files.md index 2277d3cf..a2ff0a69 100644 --- a/docs/pages/docs/SQL API/drift_files.md +++ b/docs/pages/docs/SQL API/drift_files.md @@ -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 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 diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index aabbd0da..f05cb2f3 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.35.0-dev + +- Drift extensions: Allow custom class names for `CREATE VIEW` statements. + ## 0.34.1 - Allow selecting from virtual tables using the table-valued function diff --git a/sqlparser/lib/src/reader/parser.dart b/sqlparser/lib/src/reader/parser.dart index 63d85a75..758166c1 100644 --- a/sqlparser/lib/src/reader/parser.dart +++ b/sqlparser/lib/src/reader/parser.dart @@ -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? 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? 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) { diff --git a/sqlparser/lib/utils/node_to_text.dart b/sqlparser/lib/utils/node_to_text.dart index dd2fad7b..59554a36 100644 --- a/sqlparser/lib/utils/node_to_text.dart +++ b/sqlparser/lib/utils/node_to_text.dart @@ -428,6 +428,7 @@ class NodeSqlBuilder extends AstVisitor { _ifNotExists(e.ifNotExists); identifier(e.viewName); + e.driftTableName?.accept(this, arg); if (e.columns != null) { symbol('(', spaceBefore: true); diff --git a/sqlparser/test/parser/create_view_test.dart b/sqlparser/test/parser/create_view_test.dart index de357aae..8aaec48d 100644 --- a/sqlparser/test/parser/create_view_test.dart +++ b/sqlparser/test/parser/create_view_test.dart @@ -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', () { testStatement( 'CREATE VIEW IF NOT EXISTS my_complex_view (ids, name, count, type) AS ' diff --git a/sqlparser/test/utils/node_to_text_test.dart b/sqlparser/test/utils/node_to_text_test.dart index de0fb44b..9a2fd353 100644 --- a/sqlparser/test/utils/node_to_text_test.dart +++ b/sqlparser/test/utils/node_to_text_test.dart @@ -107,6 +107,14 @@ CREATE VIEW my_view (foo, bar) AS SELECT * FROM t1; testFormat(''' 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', () {