Merge branch 'develop' into Banana-develop

This commit is contained in:
Simon Binder 2023-07-27 10:29:25 +02:00
commit 1c5432026c
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
74 changed files with 2000 additions and 782 deletions

View File

@ -107,4 +107,40 @@ extension GroupByQueries on MyDatabase {
}); });
} }
// #enddocregion createCategoryForUnassignedTodoEntries // #enddocregion createCategoryForUnassignedTodoEntries
// #docregion subquery
Future<List<(Category, int)>> amountOfLengthyTodoItemsPerCategory() async {
final longestTodos = Subquery(
select(todos)
..orderBy([(row) => OrderingTerm.desc(row.title.length)])
..limit(10),
's',
);
// In the main query, we want to count how many entries in longestTodos were
// found for each category. But we can't access todos.title directly since
// we're not selecting from `todos`. Instead, we'll use Subquery.ref to read
// from a column in a subquery.
final itemCount = longestTodos.ref(todos.title).count();
final query = select(categories).join(
[
innerJoin(
longestTodos,
// Again using .ref() here to access the category in the outer select
// statement.
longestTodos.ref(todos.category).equalsExp(categories.id),
useColumns: false,
)
],
)
..addColumns([itemCount])
..groupBy([categories.id]);
final rows = await query.get();
return [
for (final row in rows) (row.readTable(categories), row.read(itemCount)!),
];
}
// #enddocregion subquery
} }

View File

@ -110,6 +110,36 @@ in 3.34, so an error would be reported.
Currently, the generator can't provide compatibility checks for versions below 3.34, which is the Currently, the generator can't provide compatibility checks for versions below 3.34, which is the
minimum version needed in options. minimum version needed in options.
### Multi-dialect code generation
Thanks to community contributions, drift has in-progress support for Postgres and MariaDB.
You can change the `dialect` option to `postgres` or `mariadb` to generate code for those
database management systems.
In some cases, your generated code might have to support more than one DBMS. For instance,
you might want to share database code between your backend and a Flutter app. Or maybe
you're writing a server that should be able to talk to both MariaDB and Postgres, depending
on what the operator prefers.
Drift can generate code for multiple dialects - in that case, the right SQL will be chosen
at runtime when it makes a difference.
To enable this feature, remove the `dialect` option in the `sql` block and replace it with
a list of `dialects`:
```yaml
targets:
$default:
builders:
drift_dev:
options:
sql:
dialect:
- sqlite
- postgres
options:
version: "3.34"
```
### Available extensions ### Available extensions
__Note__: This enables extensions in the analyzer for custom queries only. For instance, when the `json1` extension is __Note__: This enables extensions in the analyzer for custom queries only. For instance, when the `json1` extension is

View File

@ -181,8 +181,9 @@ fields in existing types as well.
Depending on what kind of result set your query has, you can use different fields for the existing Dart class: Depending on what kind of result set your query has, you can use different fields for the existing Dart class:
1. For a nested table selected with `**`, your field needs to store an instance of the table's row class. 1. For a nested table selected with `**`, your field needs to store a structure compatible with the result set
This is true for both drift-generated row classes and tables with existing, user-defined row classes. the nested column points to. For `my_table.**`, that field could either be the generated row class for `MyTable`
or a custom class as described by rule 3.
2. For nested list results, you have to use a `List<T>`. The `T` has to be compatible with the inner result 2. For nested list results, you have to use a `List<T>`. The `T` has to be compatible with the inner result
set of the `LIST()` as described by these rules. set of the `LIST()` as described by these rules.
3. For a single-table result, you can use the table class, regardless of whether the table uses an existing table 3. For a single-table result, you can use the table class, regardless of whether the table uses an existing table
@ -221,8 +222,8 @@ class EmployeeWithStaff {
} }
``` ```
As `self` is a `**` column, rule 1 applies. Therefore, `T1` must be `Employee`, the row class for the As `self` is a `**` column, rule 1 applies. `self` references a table, `employees`.
`employees` table. By rule 3, this means that `T1` can be a `Employee`, the row class for the `employees` table.
On the other hand, `staff` is a `LIST()` column and rule 2 applies here. This means that `T3` must On the other hand, `staff` is a `LIST()` column and rule 2 applies here. This means that `T3` must
be a `List<Something>`. be a `List<Something>`.
The inner result set of the `LIST` references all columns of `employees` and nothing more, so rule The inner result set of the `LIST` references all columns of `employees` and nothing more, so rule
@ -235,6 +236,8 @@ class IdAndName {
final int id; final int id;
final String name; final String name;
// This class can be used since id and name column are available from the list query.
// We could have also used the `Employee` class or a record like `(int, String)`.
IdAndName(this.id, this.name); IdAndName(this.id, this.name);
} }

View File

@ -243,6 +243,11 @@ any rows. For instance, we could use this to find empty categories:
{% include "blocks/snippet" snippets = snippets name = 'emptyCategories' %} {% include "blocks/snippet" snippets = snippets name = 'emptyCategories' %}
### Full subqueries
Drift also supports subqueries that appear in `JOIN`s, which are described in the
[documentation for joins]({{ 'joins.md#subqueries' | pageUrl }}).
## Custom expressions ## Custom expressions
If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class. If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class.
It takes a `sql` parameter that lets you write custom expressions: It takes a `sql` parameter that lets you write custom expressions:

View File

@ -203,3 +203,17 @@ select statement.
In the example, the `newDescription` expression as added as a column to the query. In the example, the `newDescription` expression as added as a column to the query.
Then, the map entry `categories.description: newDescription` is used so that the `description` column Then, the map entry `categories.description: newDescription` is used so that the `description` column
for new category rows gets set to that expression. for new category rows gets set to that expression.
## Subqueries
Starting from drift 2.11, you can use `Subquery` to use an existing select statement as part of more
complex join.
This snippet uses `Subquery` to count how many of the top-10 todo items (by length of their title) are
in each category.
It does this by first creating a select statement for the top-10 items (but not executing it), and then
joining this select statement onto a larger one grouping by category:
{% include "blocks/snippet" snippets = snippets name = 'subquery' %}
Any statement can be used as a subquery. But be aware that, unlike [subquery expressions]({{ 'expressions.md#scalar-subqueries' | pageUrl }}), full subqueries can't use tables from the outer select statement.

View File

@ -75,7 +75,7 @@ class MyDatabase extends _$MyDatabase {
// ... // ...
``` ```
## Exporting a databasee ## Exporting a database
To export a sqlite3 database into a file, you can use the `VACUUM INTO` statement. To export a sqlite3 database into a file, you can use the `VACUUM INTO` statement.
Inside your database class, this could look like the following: Inside your database class, this could look like the following:

View File

@ -15,37 +15,11 @@ how to get started. You can watch it [here](https://youtu.be/zpWsedYMczM).
A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app). A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app).
## Adding the dependency ## Adding the dependency
First, lets add drift to your project's `pubspec.yaml`.
At the moment, the current version of `drift` is [![Drift version](https://img.shields.io/pub/v/drift.svg)](https://pub.dev/packages/drift)
and the latest version of `drift_dev` is [![Generator version](https://img.shields.io/pub/v/drift_dev.svg)](https://pub.dev/packages/drift_dev).
{% assign versions = 'package:drift_docs/versions.json' | readString | json_decode %} {% include "partials/dependencies" %}
{% assign snippets = 'package:drift_docs/snippets/tables/filename.dart.excerpt.json' | readString | json_decode %} {% assign snippets = 'package:drift_docs/snippets/tables/filename.dart.excerpt.json' | readString | json_decode %}
```yaml
dependencies:
drift: ^{{ versions.drift }}
sqlite3_flutter_libs: ^0.5.0
path_provider: ^2.0.0
path: ^{{ versions.path }}
dev_dependencies:
drift_dev: ^{{ versions.drift_dev }}
build_runner: ^{{ versions.build_runner }}
```
If you're wondering why so many packages are necessary, here's a quick overview over what each package does:
- `drift`: This is the core package defining most apis
- `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter,
but then you need to take care of including `sqlite3` yourself.
For an overview on other platforms, see [platforms]({{ '../platforms.md' | pageUrl }}).
- `path_provider` and `path`: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team
- `drift_dev`: This development-only dependency generates query code based on your tables. It will not be included in your final app.
- `build_runner`: Common tool for code-generation, maintained by the Dart team
{% include "partials/changed_to_ffi" %}
### Declaring tables ### Declaring tables
Using drift, you can model the structure of your tables with simple dart code. Using drift, you can model the structure of your tables with simple dart code.

View File

@ -12,35 +12,8 @@ declaring both tables and queries in Dart. This version will focus on how to use
A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app). A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app).
## Adding the dependency ## Adding the dependency
First, lets add drift to your project's `pubspec.yaml`.
At the moment, the current version of `drift` is [![Drift version](https://img.shields.io/pub/v/drift.svg)](https://pub.dev/packages/drift)
and the latest version of `drift_dev` is [![Generator version](https://img.shields.io/pub/v/drift_dev.svg)](https://pub.dev/packages/drift_dev).
{% assign versions = 'package:drift_docs/versions.json' | readString | json_decode %} {% include "partials/dependencies" %}
```yaml
dependencies:
drift: ^{{ versions.drift }}
sqlite3_flutter_libs: ^0.5.0
path_provider: ^2.0.0
path: ^{{ versions.path }}
dev_dependencies:
drift_dev: ^{{ versions.drift_dev }}
build_runner: ^{{ versions.build_runner }}
```
If you're wondering why so many packages are necessary, here's a quick overview over what each package does:
- `drift`: This is the core package defining most apis
- `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter,
but then you need to take care of including `sqlite3` yourself.
For an overview on other platforms, see [platforms]({{ '../platforms.md' | pageUrl }}).
- `path_provider` and `path`: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team
- `drift_dev`: This development-only dependency generates query code based on your tables. It will not be included in your final app.
- `build_runner`: Common tool for code-generation, maintained by the Dart team
{% include "partials/changed_to_ffi" %}
## Declaring tables and queries ## Declaring tables and queries

View File

@ -228,12 +228,10 @@ class RoutesWithNestedPointsResult {
Great! This class matches our intent much better than the flat result class Great! This class matches our intent much better than the flat result class
from before. from before.
At the moment, there are some limitations with this approach: These nested result columns (`**`) can appear in top-level select statements
only, they're not supported in compound select statements or subqueries yet.
- `**` is not yet supported in compound select statements However, they can refer to any result set in SQL that has been joined to the
- you can only use `table.**` if table is an actual table or a reference to it. select statement - including subqueries table-valued functions.
In particular, it doesn't work for result sets from `WITH` clauses or table-
valued functions.
You might be wondering how `**` works under the hood, since it's not valid sql. You might be wondering how `**` works under the hood, since it's not valid sql.
At build time, drift's generator will transform `**` into a list of all columns At build time, drift's generator will transform `**` into a list of all columns

View File

@ -80,7 +80,7 @@ Other database libraries can easily be integrated into drift as well.
{% block "blocks/feature.html" icon="fas fa-star" title="And much more!" %} {% block "blocks/feature.html" icon="fas fa-star" title="And much more!" %}
{% block "blocks/markdown.html" %} {% block "blocks/markdown.html" %}
Drift provides auto-updating streams for all your queries, makes dealing with transactions and migrations easy Drift provides auto-updating streams for all your queries, makes dealing with transactions and migrations easy
and lets your write modular database code with DAOs. We even have a [sql IDE]({{ 'docs/Using SQL/sql_ide.md' | pageUrl }}) builtin to the project and lets your write modular database code with DAOs. We even have a [sql IDE]({{ 'docs/Using SQL/sql_ide.md' | pageUrl }}) builtin to the project.
When using drift, working with databases in Dart is fun! When using drift, working with databases in Dart is fun!
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}

View File

@ -1,15 +0,0 @@
<div class="alert alert-primary" role="alert">
<h4 class="alert-heading">Changes to the recommended implementation</h4>
<p>
Previous versions of this article recommended to use <code>moor_flutter</code> or
the <code>moor_ffi</code> package.
For new users, we recommend to use <code>package:drift/native.dart</code> to open the database -
more on that below!
</p>
<p>
If you have an existing setup that works, there's no need to change anything.
</p>
</div>
Some versions of the Flutter tool create a broken `settings.gradle` on Android, which can cause problems with `drift/native.dart`.
If you get a "Failed to load dynamic library" exception, see [this comment](https://github.com/flutter/flutter/issues/55827#issuecomment-623779910).

View File

@ -0,0 +1,39 @@
{% block "blocks/markdown" %}
First, lets add drift to your project's `pubspec.yaml`.
At the moment, the current version of `drift` is [![Drift version](https://img.shields.io/pub/v/drift.svg)](https://pub.dev/packages/drift)
and the latest version of `drift_dev` is [![Generator version](https://img.shields.io/pub/v/drift_dev.svg)](https://pub.dev/packages/drift_dev).
{% assign versions = 'package:drift_docs/versions.json' | readString | json_decode %}
```yaml
dependencies:
drift: ^{{ versions.drift }}
sqlite3_flutter_libs: ^0.5.0
path_provider: ^2.0.0
path: ^{{ versions.path }}
dev_dependencies:
drift_dev: ^{{ versions.drift_dev }}
build_runner: ^{{ versions.build_runner }}
```
If you're wondering why so many packages are necessary, here's a quick overview over what each package does:
- `drift`: This is the core package defining the APIs you use to access drift databases.
- `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter,
but then you need to take care of including `sqlite3` yourself.
For an overview on other platforms, see [platforms]({{ '../platforms.md' | pageUrl }}).
Note that the `sqlite3_flutter_libs` package will include the native sqlite3 library for the following
architectures: `armv8`, `armv7`, `x86` and `x86_64`.
Most Flutter apps don't run on 32-bit x86 devices without further setup, so you should
[add a snippet](https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs#included-platforms)
to your `build.gradle` if you don't need `x86` builds.
Otherwise, the Play Store might allow users on `x86` devices to install your app even though it is not
supported.
In Flutter's current native build system, drift unfortunately can't do that for you.
- `path_provider` and `path`: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team.
- `drift_dev`: This development-only dependency generates query code based on your tables. It will not be included in your final app.
- `build_runner`: Common tool for code-generation, maintained by the Dart team.
{% endblock %}

View File

@ -1,3 +1,9 @@
## 2.11.0
- Add support for subqueries in the Dart query builder.
- Add `isInExp` and `isNotInExp` to construct `IS IN` expressions with arbitrary
expressions.
## 2.10.0 ## 2.10.0
- Adds the `schema steps` command to `drift_dev`. It generates an API making it - Adds the `schema steps` command to `drift_dev`. It generates an API making it

View File

@ -534,7 +534,7 @@ class $TodoCategoryItemCountView
@override @override
String get entityName => 'todo_category_item_count'; String get entityName => 'todo_category_item_count';
@override @override
String? get createViewStmt => null; Map<SqlDialect, String>? get createViewStatements => null;
@override @override
$TodoCategoryItemCountView get asDslTable => this; $TodoCategoryItemCountView get asDslTable => this;
@override @override
@ -639,7 +639,7 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo<
@override @override
String get entityName => 'customViewName'; String get entityName => 'customViewName';
@override @override
String? get createViewStmt => null; Map<SqlDialect, String>? get createViewStatements => null;
@override @override
$TodoItemWithCategoryNameViewView get asDslTable => this; $TodoItemWithCategoryNameViewView get asDslTable => this;
@override @override

View File

@ -157,6 +157,10 @@ class VersionedView implements ViewInfo<HasResultSet, QueryRow>, HasResultSet {
@override @override
final String createViewStmt; final String createViewStmt;
@override
Map<SqlDialect, String>? get createViewStatements =>
{SqlDialect.sqlite: createViewStmt};
@override @override
final List<GeneratedColumn> $columns; final List<GeneratedColumn> $columns;

View File

@ -58,8 +58,7 @@ class Join<T extends HasResultSet, D> extends Component {
context.buffer.write(' JOIN '); context.buffer.write(' JOIN ');
final resultSet = table as ResultSetImplementation<T, D>; final resultSet = table as ResultSetImplementation<T, D>;
context.buffer.write(resultSet.tableWithAlias); context.writeResultSet(resultSet);
context.watchedTables.add(resultSet);
if (_type != _JoinType.cross) { if (_type != _JoinType.cross) {
context.buffer.write(' ON '); context.buffer.write(' ON ');

View File

@ -0,0 +1,135 @@
part of '../query_builder.dart';
/// A subquery allows reading from another complex query in a join.
///
/// An existing query can be constructed via [DatabaseConnectionUser.select] or
/// [DatabaseConnectionUser.selectOnly] and then wrapped in [Subquery] to be
/// used in another query.
///
/// For instance, assuming database storing todo items with optional categories
/// (through a reference from todo items to categories), this query uses a
/// subquery to count how many of the top-10 todo items (by length) are in each
/// category:
///
/// ```dart
/// final longestTodos = Subquery(
/// select(todosTable)
/// ..orderBy([(row) => OrderingTerm.desc(row.title.length)])
/// ..limit(10),
/// 's',
/// );
///
/// final itemCount = subquery.ref(todosTable.id).count();
/// final query = select(categories).join([
/// innerJoin(
/// longestTodos,
/// subquery.ref(todosTable.category).equalsExp(categories.id),
/// useColumns: false,
/// )])
/// ..groupBy([categories.id])
/// ..addColumns([itemCount]);
/// ```
///
/// Note that the column from the subquery (here, the id of a todo entry) is not
/// directly available in the outer query, it needs to be accessed through
/// [Subquery.ref].
/// Columns added to the top-level query (via [ref]) can be accessed directly
/// through [TypedResult.read]. When columns from a subquery are added to the
/// top-level select as well, [TypedResult.readTable] can be used to read an
/// entire row from the subquery. It returns a nested [TypedResult] for the
/// subquery.
///
/// See also: [subqueryExpression], for subqueries which only return one row and
/// one column.
class Subquery<Row> extends ResultSetImplementation<Subquery, Row>
implements HasResultSet {
/// The inner [select] statement of this subquery.
final BaseSelectStatement<Row> select;
@override
final String entityName;
/// Creates a subqery from the inner [select] statement forming the base of
/// the subquery and a unique name of this subquery in the statement being
/// executed.
Subquery(this.select, this.entityName);
/// Makes a column from the subquery available to the outer select statement.
///
/// For instance, consider a complex column like `subqueryContentLength` being
/// added into a subquery:
///
/// ```dart
/// final subqueryContentLength = todoEntries.content.length.sum();
/// final subquery = Subquery(
/// db.selectOnly(todoEntries)
/// ..addColumns([todoEntries.category, subqueryContentLength])
/// ..groupBy([todoEntries.category]),
/// 's');
/// ```
///
/// When the `subqueryContentLength` column gets written, drift will write
/// the actual `SUM()` expression which is only valid in the subquery itself.
/// When an outer query joining the subqery wants to read the column, it needs
/// to refer to that expression by name. This is what [ref] is doing:
///
/// ```dart
/// final readableLength = subquery.ref(subqueryContentLength);
/// final query = selectOnly(categories)
/// ..addColumns([categories.id, readableLength])
/// ..join([
/// innerJoin(subquery,
/// subquery.ref(db.todosTable.category).equalsExp(db.categories.id))
/// ]);
/// ```
///
/// Here, [ref] is called two times: Once to obtain a column selected by the
/// outer query and once as a join condition.
///
/// [ref] needs to be used every time a column from a subquery is used in an
/// outer query, regardless of the context.
Expression<T> ref<T extends Object>(Expression<T> inner) {
final name = select._nameForColumn(inner);
if (name == null) {
throw ArgumentError(
'The source select statement does not contain that column');
}
return columnsByName[name]!.dartCast();
}
@override
late final List<GeneratedColumn<Object>> $columns = [
for (final (expr, name) in select._expandedColumns)
GeneratedColumn(
name,
entityName,
true,
type: expr.driftSqlType,
),
];
@override
late final Map<String, GeneratedColumn<Object>> columnsByName = {
for (final column in $columns) column.name: column,
};
@override
Subquery get asDslTable => this;
@override
DatabaseConnectionUser get attachedDatabase => (select as Query).database;
@override
FutureOr<Row> map(Map<String, dynamic> data, {String? tablePrefix}) {
if (tablePrefix == null) {
return select._mapRow(data);
} else {
final withoutPrefix = {
for (final MapEntry(:key, :value) in columnsByName.entries)
key: data['$tablePrefix.$value']
};
return select._mapRow(withoutPrefix);
}
}
}

View File

@ -11,6 +11,8 @@ class CustomExpression<D extends Object> extends Expression<D> {
/// The SQL of this expression /// The SQL of this expression
final String content; final String content;
final Map<SqlDialect, String>? _dialectSpecificContent;
/// Additional tables that this expression is watching. /// Additional tables that this expression is watching.
/// ///
/// When this expression is used in a stream query, the stream will update /// When this expression is used in a stream query, the stream will update
@ -24,11 +26,25 @@ class CustomExpression<D extends Object> extends Expression<D> {
/// Constructs a custom expression by providing the raw sql [content]. /// Constructs a custom expression by providing the raw sql [content].
const CustomExpression(this.content, const CustomExpression(this.content,
{this.watchedTables = const [], this.precedence = Precedence.unknown}); {this.watchedTables = const [], this.precedence = Precedence.unknown})
: _dialectSpecificContent = null;
/// Constructs a custom expression providing the raw SQL in [content] depending
/// on the SQL dialect when this expression is built.
const CustomExpression.dialectSpecific(Map<SqlDialect, String> content,
{this.watchedTables = const [], this.precedence = Precedence.unknown})
: _dialectSpecificContent = content,
content = '';
@override @override
void writeInto(GenerationContext context) { void writeInto(GenerationContext context) {
context.buffer.write(content); final dialectSpecific = _dialectSpecificContent;
if (dialectSpecific != null) {
} else {
context.buffer.write(content);
}
context.watchedTables.addAll(watchedTables); context.watchedTables.addAll(watchedTables);
} }

View File

@ -142,19 +142,37 @@ abstract class Expression<D extends Object> implements FunctionParameter {
/// An expression that is true if `this` resolves to any of the values in /// An expression that is true if `this` resolves to any of the values in
/// [values]. /// [values].
Expression<bool> isIn(Iterable<D> values) { Expression<bool> isIn(Iterable<D> values) {
if (values.isEmpty) { return isInExp([for (final value in values) Variable<D>(value)]);
return Constant(false);
}
return _InExpression(this, values.toList(), false);
} }
/// An expression that is true if `this` does not resolve to any of the values /// An expression that is true if `this` does not resolve to any of the values
/// in [values]. /// in [values].
Expression<bool> isNotIn(Iterable<D> values) { Expression<bool> isNotIn(Iterable<D> values) {
if (values.isEmpty) { return isNotInExp([for (final value in values) Variable<D>(value)]);
}
/// An expression that evaluates to `true` if this expression resolves to a
/// value that one of the [expressions] resolve to as well.
///
/// For an "is in" comparison with values, use [isIn].
Expression<bool> isInExp(List<Expression<D>> expressions) {
if (expressions.isEmpty) {
return Constant(true); return Constant(true);
} }
return _InExpression(this, values.toList(), true);
return _InExpression(this, expressions, false);
}
/// An expression that evaluates to `true` if this expression does not resolve
/// to any value that the [expressions] resolve to.
///
/// For an "is not in" comparison with values, use [isNotIn].
Expression<bool> isNotInExp(List<Expression<D>> expressions) {
if (expressions.isEmpty) {
return Constant(true);
}
return _InExpression(this, expressions, true);
} }
/// An expression checking whether `this` is included in any row of the /// An expression checking whether `this` is included in any row of the
@ -509,7 +527,7 @@ class FunctionCallExpression<R extends Object> extends Expression<R> {
} }
void _checkSubquery(BaseSelectStatement statement) { void _checkSubquery(BaseSelectStatement statement) {
final columns = statement._returnedColumnCount; final columns = statement._expandedColumns.length;
if (columns != 1) { if (columns != 1) {
throw ArgumentError.value(statement, 'statement', throw ArgumentError.value(statement, 'statement',
'Must return exactly one column (actually returns $columns)'); 'Must return exactly one column (actually returns $columns)');

View File

@ -1,6 +1,6 @@
part of '../query_builder.dart'; part of '../query_builder.dart';
abstract class _BaseInExpression extends Expression<bool> { sealed class _BaseInExpression extends Expression<bool> {
final Expression _expression; final Expression _expression;
final bool _not; final bool _not;
@ -25,8 +25,8 @@ abstract class _BaseInExpression extends Expression<bool> {
void _writeValues(GenerationContext context); void _writeValues(GenerationContext context);
} }
class _InExpression<T extends Object> extends _BaseInExpression { final class _InExpression<T extends Object> extends _BaseInExpression {
final List<T> _values; final List<Expression<T>> _values;
_InExpression(Expression expression, this._values, bool not) _InExpression(Expression expression, this._values, bool not)
: super(expression, not); : super(expression, not);
@ -35,15 +35,13 @@ class _InExpression<T extends Object> extends _BaseInExpression {
void _writeValues(GenerationContext context) { void _writeValues(GenerationContext context) {
var first = true; var first = true;
for (final value in _values) { for (final value in _values) {
final variable = Variable<T>(value);
if (first) { if (first) {
first = false; first = false;
} else { } else {
context.buffer.write(', '); context.buffer.write(', ');
} }
variable.writeInto(context); value.writeInto(context);
} }
} }
@ -59,7 +57,7 @@ class _InExpression<T extends Object> extends _BaseInExpression {
} }
} }
class _InSelectExpression extends _BaseInExpression { final class _InSelectExpression extends _BaseInExpression {
final BaseSelectStatement _select; final BaseSelectStatement _select;
_InSelectExpression(this._select, Expression expression, bool not) _InSelectExpression(this._select, Expression expression, bool not)

View File

@ -0,0 +1,41 @@
@internal
library;
import 'package:drift/drift.dart';
import 'package:meta/meta.dart';
/// Internal utilities for building queries that aren't exported.
extension WriteDefinition on GenerationContext {
/// Writes the result set to this context, suitable to implement `FROM`
/// clauses and joins.
void writeResultSet(ResultSetImplementation resultSet) {
if (resultSet is Subquery) {
buffer.write('(');
resultSet.select.writeInto(this);
buffer
..write(') ')
..write(resultSet.aliasedName);
} else {
buffer.write(resultSet.tableWithAlias);
watchedTables.add(resultSet);
}
}
/// Returns a suitable SQL string in [sql] based on the current dialect.
String pickForDialect(Map<SqlDialect, String> sql) {
assert(
sql.containsKey(dialect),
'Tried running SQL optimized for the following dialects: ${sql.keys.join}. '
'However, the database is running $dialect. Has that dialect been added '
'to the `dialects` drift builder option?',
);
final found = sql[dialect];
if (found != null) {
return found;
}
return sql.values.first; // Fallback
}
}

View File

@ -88,7 +88,7 @@ class Migrator {
} else if (entity is Index) { } else if (entity is Index) {
await createIndex(entity); await createIndex(entity);
} else if (entity is OnCreateQuery) { } else if (entity is OnCreateQuery) {
await _issueCustomQuery(entity.sql, const []); await _issueQueryByDialect(entity.sqlByDialect);
} else if (entity is ViewInfo) { } else if (entity is ViewInfo) {
await createView(entity); await createView(entity);
} else { } else {
@ -403,19 +403,19 @@ class Migrator {
/// Executes the `CREATE TRIGGER` statement that created the [trigger]. /// Executes the `CREATE TRIGGER` statement that created the [trigger].
Future<void> createTrigger(Trigger trigger) { Future<void> createTrigger(Trigger trigger) {
return _issueCustomQuery(trigger.createTriggerStmt, const []); return _issueQueryByDialect(trigger.createStatementsByDialect);
} }
/// Executes a `CREATE INDEX` statement to create the [index]. /// Executes a `CREATE INDEX` statement to create the [index].
Future<void> createIndex(Index index) { Future<void> createIndex(Index index) {
return _issueCustomQuery(index.createIndexStmt, const []); return _issueQueryByDialect(index.createStatementsByDialect);
} }
/// Executes a `CREATE VIEW` statement to create the [view]. /// Executes a `CREATE VIEW` statement to create the [view].
Future<void> createView(ViewInfo view) async { Future<void> createView(ViewInfo view) async {
final stmt = view.createViewStmt; final stmts = view.createViewStatements;
if (stmt != null) { if (stmts != null) {
await _issueCustomQuery(stmt, const []); await _issueQueryByDialect(stmts);
} else if (view.query != null) { } else if (view.query != null) {
final context = GenerationContext.fromDb(_db, supportsVariables: false); final context = GenerationContext.fromDb(_db, supportsVariables: false);
final columnNames = view.$columns.map((e) => e.escapedName).join(', '); final columnNames = view.$columns.map((e) => e.escapedName).join(', ');
@ -528,6 +528,11 @@ class Migrator {
return _issueCustomQuery(sql, args); return _issueCustomQuery(sql, args);
} }
Future<void> _issueQueryByDialect(Map<SqlDialect, String> sql) {
final context = _createContext();
return _issueCustomQuery(context.pickForDialect(sql), const []);
}
Future<void> _issueCustomQuery(String sql, [List<dynamic>? args]) { Future<void> _issueCustomQuery(String sql, [List<dynamic>? args]) {
return _db.customStatement(sql, args); return _db.customStatement(sql, args);
} }

View File

@ -23,8 +23,10 @@ import 'package:meta/meta.dart';
import '../../utils/async.dart'; import '../../utils/async.dart';
// New files should not be part of this mega library, which we're trying to // New files should not be part of this mega library, which we're trying to
// split up. // split up.
import 'expressions/case_when.dart'; import 'expressions/case_when.dart';
import 'expressions/internal.dart'; import 'expressions/internal.dart';
import 'helpers.dart';
export 'expressions/bitwise.dart'; export 'expressions/bitwise.dart';
export 'expressions/case_when.dart'; export 'expressions/case_when.dart';
@ -34,6 +36,7 @@ part 'components/group_by.dart';
part 'components/join.dart'; part 'components/join.dart';
part 'components/limit.dart'; part 'components/limit.dart';
part 'components/order_by.dart'; part 'components/order_by.dart';
part 'components/subquery.dart';
part 'components/where.dart'; part 'components/where.dart';
part 'expressions/aggregate.dart'; part 'expressions/aggregate.dart';
part 'expressions/algebra.dart'; part 'expressions/algebra.dart';

View File

@ -17,15 +17,26 @@ abstract class DatabaseSchemaEntity {
/// [sqlite-docs]: https://sqlite.org/lang_createtrigger.html /// [sqlite-docs]: https://sqlite.org/lang_createtrigger.html
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-trigger/ /// [sql-tut]: https://www.sqlitetutorial.net/sqlite-trigger/
class Trigger extends DatabaseSchemaEntity { class Trigger extends DatabaseSchemaEntity {
/// The `CREATE TRIGGER` sql statement that can be used to create this
/// trigger.
final String createTriggerStmt;
@override @override
final String entityName; final String entityName;
/// The `CREATE TRIGGER` sql statement that can be used to create this
/// trigger.
@Deprecated('Use createStatementsByDialect instead')
String get createTriggerStmt => createStatementsByDialect.values.first;
/// The `CREATE TRIGGER` SQL statements used to create this trigger, accessible
/// for each dialect enabled when generating code.
final Map<SqlDialect, String> createStatementsByDialect;
/// Creates a trigger representation by the [createTriggerStmt] and its /// Creates a trigger representation by the [createTriggerStmt] and its
/// [entityName]. Mainly used by generated code. /// [entityName]. Mainly used by generated code.
Trigger(this.createTriggerStmt, this.entityName); Trigger(String createTriggerStmt, String entityName)
: this.byDialect(entityName, {SqlDialect.sqlite: createTriggerStmt});
/// Creates the trigger model from its [entityName] in the schema and all
/// [createStatementsByDialect] for the supported dialects.
Trigger.byDialect(this.entityName, this.createStatementsByDialect);
} }
/// A sqlite index on columns or expressions. /// A sqlite index on columns or expressions.
@ -40,11 +51,21 @@ class Index extends DatabaseSchemaEntity {
final String entityName; final String entityName;
/// The `CREATE INDEX` sql statement that can be used to create this index. /// The `CREATE INDEX` sql statement that can be used to create this index.
final String createIndexStmt; @Deprecated('Use createStatementsByDialect instead')
String get createIndexStmt => createStatementsByDialect.values.first;
/// The `CREATE INDEX` SQL statements used to create this index, accessible
/// for each dialect enabled when generating code.
final Map<SqlDialect, String> createStatementsByDialect;
/// Creates an index model by the [createIndexStmt] and its [entityName]. /// Creates an index model by the [createIndexStmt] and its [entityName].
/// Mainly used by generated code. /// Mainly used by generated code.
Index(this.entityName, this.createIndexStmt); Index(this.entityName, String createIndexStmt)
: createStatementsByDialect = {SqlDialect.sqlite: createIndexStmt};
/// Creates an index model by its [entityName] used in the schema and the
/// `CREATE INDEX` statements for each supported dialect.
Index.byDialect(this.entityName, this.createStatementsByDialect);
} }
/// An internal schema entity to run an sql statement when the database is /// An internal schema entity to run an sql statement when the database is
@ -61,10 +82,19 @@ class Index extends DatabaseSchemaEntity {
/// drift file. /// drift file.
class OnCreateQuery extends DatabaseSchemaEntity { class OnCreateQuery extends DatabaseSchemaEntity {
/// The sql statement that should be run in the default `onCreate` clause. /// The sql statement that should be run in the default `onCreate` clause.
final String sql; @Deprecated('Use sqlByDialect instead')
String get sql => sqlByDialect.values.first;
/// The SQL statement to run, indexed by the dialect used in the database.
final Map<SqlDialect, String> sqlByDialect;
/// Create a query that will be run in the default `onCreate` migration. /// Create a query that will be run in the default `onCreate` migration.
OnCreateQuery(this.sql); OnCreateQuery(String sql) : this.byDialect({SqlDialect.sqlite: sql});
/// Creates the entity of a query to run in the default `onCreate` migration.
///
/// The migrator will lookup a suitable query from the [sqlByDialect] map.
OnCreateQuery.byDialect(this.sqlByDialect);
@override @override
String get entityName => r'$internal$'; String get entityName => r'$internal$';

View File

@ -151,7 +151,7 @@ extension TableInfoUtils<TableDsl, D> on ResultSetImplementation<TableDsl, D> {
/// Drift would generate code to call this method with `'c1': 'foo'` and /// Drift would generate code to call this method with `'c1': 'foo'` and
/// `'c2': 'bar'` in [alias]. /// `'c2': 'bar'` in [alias].
Future<D> mapFromRowWithAlias(QueryRow row, Map<String, String> alias) async { Future<D> mapFromRowWithAlias(QueryRow row, Map<String, String> alias) async {
return map({ return await map({
for (final entry in row.data.entries) alias[entry.key]!: entry.value, for (final entry in row.data.entries) alias[entry.key]!: entry.value,
}); });
} }

View File

@ -17,7 +17,14 @@ abstract class ViewInfo<Self extends HasResultSet, Row>
/// The `CREATE VIEW` sql statement that can be used to create this view. /// The `CREATE VIEW` sql statement that can be used to create this view.
/// ///
/// This will be null if the view was defined in Dart. /// This will be null if the view was defined in Dart.
String? get createViewStmt; @Deprecated('Use createViewStatements instead')
String? get createViewStmt => createViewStatements?.values.first;
/// The `CREATE VIEW` sql statement that can be used to create this view,
/// depending on the dialect used by the current database.
///
/// This will be null if the view was defined in Dart.
Map<SqlDialect, String>? get createViewStatements;
/// Predefined query from `View.as()` /// Predefined query from `View.as()`
/// ///

View File

@ -8,12 +8,14 @@ typedef OrderClauseGenerator<T> = OrderingTerm Function(T tbl);
/// ///
/// Users are not allowed to extend, implement or mix-in this class. /// Users are not allowed to extend, implement or mix-in this class.
@sealed @sealed
abstract class BaseSelectStatement extends Component { abstract class BaseSelectStatement<Row> extends Component {
int get _returnedColumnCount; Iterable<(Expression, String)> get _expandedColumns;
/// The name for the given [expression] in the result set, or `null` if /// The name for the given [expression] in the result set, or `null` if
/// [expression] was not added as a column to this select statement. /// [expression] was not added as a column to this select statement.
String? _nameForColumn(Expression expression); String? _nameForColumn(Expression expression);
FutureOr<Row> _mapRow(Map<String, Object?> fromDatabase);
} }
/// A select statement that doesn't use joins. /// A select statement that doesn't use joins.
@ -21,7 +23,7 @@ abstract class BaseSelectStatement extends Component {
/// For more information, see [DatabaseConnectionUser.select]. /// For more information, see [DatabaseConnectionUser.select].
class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D> class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
with SingleTableQueryMixin<T, D>, LimitContainerMixin<T, D>, Selectable<D> with SingleTableQueryMixin<T, D>, LimitContainerMixin<T, D>, Selectable<D>
implements BaseSelectStatement { implements BaseSelectStatement<D> {
/// Whether duplicate rows should be eliminated from the result (this is a /// Whether duplicate rows should be eliminated from the result (this is a
/// `SELECT DISTINCT` statement in sql). Defaults to false. /// `SELECT DISTINCT` statement in sql). Defaults to false.
final bool distinct; final bool distinct;
@ -39,7 +41,8 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
Set<ResultSetImplementation> get watchedTables => {table}; Set<ResultSetImplementation> get watchedTables => {table};
@override @override
int get _returnedColumnCount => table.$columns.length; Iterable<(Expression, String)> get _expandedColumns =>
table.$columns.map((e) => (e, e.name));
@override @override
String? _nameForColumn(Expression expression) { String? _nameForColumn(Expression expression) {
@ -54,8 +57,8 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
void writeStartPart(GenerationContext ctx) { void writeStartPart(GenerationContext ctx) {
ctx.buffer ctx.buffer
..write(_beginOfSelect(distinct)) ..write(_beginOfSelect(distinct))
..write(' * FROM ${table.tableWithAlias}'); ..write(' * FROM ');
ctx.watchedTables.add(table); ctx.writeResultSet(table);
} }
@override @override
@ -82,8 +85,13 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
}); });
} }
@override
FutureOr<D> _mapRow(Map<String, Object?> row) {
return table.map(row);
}
Future<List<D>> _mapResponse(List<Map<String, Object?>> rows) { Future<List<D>> _mapResponse(List<Map<String, Object?>> rows) {
return rows.mapAsyncAndAwait(table.map); return rows.mapAsyncAndAwait(_mapRow);
} }
/// Creates a select statement that operates on more than one table by /// Creates a select statement that operates on more than one table by

View File

@ -31,11 +31,11 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
/// ///
/// Each table column can be uniquely identified by its (potentially aliased) /// Each table column can be uniquely identified by its (potentially aliased)
/// table and its name. So a column named `id` in a table called `users` would /// table and its name. So a column named `id` in a table called `users` would
/// be written as `users.id AS "users.id"`. These columns will NOT be written /// be written as `users.id AS "users.id"`. These columns are also included in
/// into this map. /// the map when added through [addColumns], but they have a predicatable name.
/// ///
/// Other expressions used as columns will be included here. There just named /// More interestingly, other expressions used as columns will be included
/// in increasing order, so something like `AS c3`. /// here. They're just named in increasing order, so something like `AS c3`.
final Map<Expression, String> _columnAliases = {}; final Map<Expression, String> _columnAliases = {};
/// The tables this select statement reads from /// The tables this select statement reads from
@ -44,13 +44,16 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
Set<ResultSetImplementation> get watchedTables => _queriedTables().toSet(); Set<ResultSetImplementation> get watchedTables => _queriedTables().toSet();
@override @override
int get _returnedColumnCount { Iterable<(Expression<Object>, String)> get _expandedColumns sync* {
return _joins.fold(_selectedColumns.length, (prev, join) { for (final column in _selectedColumns) {
if (join.includeInResult ?? _includeJoinedTablesInResult) { yield (column, _columnAliases[column]!);
return prev + (join.table as ResultSetImplementation).$columns.length; }
for (final table in _queriedTables(true)) {
for (final column in table.$columns) {
yield (column, _nameForTableColumn(column));
} }
return prev; }
});
} }
@override @override
@ -122,9 +125,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
chosenAlias = _nameForTableColumn(column, chosenAlias = _nameForTableColumn(column,
generatingForView: ctx.generatingForView); generatingForView: ctx.generatingForView);
} else { } else {
chosenAlias = 'c$i'; chosenAlias = _columnAliases[column]!;
} }
_columnAliases[column] = chosenAlias;
column.writeInto(ctx); column.writeInto(ctx);
ctx.buffer ctx.buffer
@ -133,8 +135,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
..write('"'); ..write('"');
} }
ctx.buffer.write(' FROM ${table.tableWithAlias}'); ctx.buffer.write(' FROM ');
ctx.watchedTables.add(table); ctx.writeResultSet(table);
if (_joins.isNotEmpty) { if (_joins.isNotEmpty) {
ctx.writeWhitespace(); ctx.writeWhitespace();
@ -195,7 +197,21 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
/// - The docs on expressions: https://drift.simonbinder.eu/docs/getting-started/expressions/ /// - The docs on expressions: https://drift.simonbinder.eu/docs/getting-started/expressions/
/// {@endtemplate} /// {@endtemplate}
void addColumns(Iterable<Expression> expressions) { void addColumns(Iterable<Expression> expressions) {
_selectedColumns.addAll(expressions); for (final expression in expressions) {
// Otherwise, we generate an alias.
_columnAliases.putIfAbsent(expression, () {
// Only add the column if it hasn't been added yet - it's fine if the
// same column is added multiple times through the Dart API, they will
// read from the same SQL column internally.
_selectedColumns.add(expression);
if (expression is GeneratedColumn) {
return _nameForTableColumn(expression);
} else {
return 'c${_columnAliases.length}';
}
});
}
} }
/// Adds more joined tables to this [JoinedSelectStatement]. /// Adds more joined tables to this [JoinedSelectStatement].
@ -233,14 +249,14 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
return database return database
.createStream(fetcher) .createStream(fetcher)
.asyncMapPerSubscription((rows) => _mapResponse(ctx, rows)); .asyncMapPerSubscription((rows) => _mapResponse(rows));
} }
@override @override
Future<List<TypedResult>> get() async { Future<List<TypedResult>> get() async {
final ctx = constructQuery(); final ctx = constructQuery();
final raw = await _getRaw(ctx); final raw = await _getRaw(ctx);
return _mapResponse(ctx, raw); return _mapResponse(raw);
} }
Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) { Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) {
@ -260,24 +276,26 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
}); });
} }
Future<List<TypedResult>> _mapResponse( @override
GenerationContext ctx, List<Map<String, Object?>> rows) { Future<TypedResult> _mapRow(Map<String, Object?> row) async {
return Future.wait(rows.map((row) async { final readTables = <ResultSetImplementation, dynamic>{};
final readTables = <ResultSetImplementation, dynamic>{};
for (final table in _queriedTables(true)) { for (final table in _queriedTables(true)) {
final prefix = '${table.aliasedName}.'; final prefix = '${table.aliasedName}.';
// if all columns of this table are null, skip the table // if all columns of this table are null, skip the table
if (table.$columns.any((c) => row[prefix + c.$name] != null)) { if (table.$columns.any((c) => row[prefix + c.$name] != null)) {
readTables[table] = readTables[table] =
await table.map(row, tablePrefix: table.aliasedName); await table.map(row, tablePrefix: table.aliasedName);
}
} }
}
final driftRow = QueryRow(row, database); final driftRow = QueryRow(row, database);
return TypedResult( return TypedResult(
readTables, driftRow, _LazyExpressionMap(_columnAliases, driftRow)); readTables, driftRow, _LazyExpressionMap(_columnAliases, driftRow));
})); }
Future<List<TypedResult>> _mapResponse(List<Map<String, Object?>> rows) {
return Future.wait(rows.map(_mapRow));
} }
Never _warnAboutDuplicate( Never _warnAboutDuplicate(

View File

@ -1,6 +1,6 @@
name: drift name: drift
description: Drift is a reactive library to store relational data in Dart and Flutter applications. description: Drift is a reactive library to store relational data in Dart and Flutter applications.
version: 2.10.0 version: 2.11.0-dev
repository: https://github.com/simolus3/drift repository: https://github.com/simolus3/drift
homepage: https://drift.simonbinder.eu/ homepage: https://drift.simonbinder.eu/
issue_tracker: https://github.com/simolus3/drift/issues issue_tracker: https://github.com/simolus3/drift/issues

View File

@ -201,6 +201,27 @@ void main() {
.bitwiseAnd(Variable(BigInt.from(10)))), .bitwiseAnd(Variable(BigInt.from(10)))),
completion(BigInt.two)); completion(BigInt.two));
}); });
group('isIn and isNotIn', () {
test('non-empty', () async {
expect(await eval(Variable.withInt(3).isIn([2, 4])), isFalse);
expect(await eval(Variable.withInt(3).isIn([3, 5])), isTrue);
expect(await eval(Variable.withInt(3).isNotIn([2, 4])), isTrue);
expect(await eval(Variable.withInt(3).isNotIn([3, 5])), isFalse);
expect(await eval(const Constant<int>(null).isIn([2, 4])), isNull);
expect(await eval(const Constant<int>(null).isNotIn([2, 4])), isNull);
});
test('empty', () async {
expect(await eval(Variable.withInt(3).isIn([])), isFalse);
expect(await eval(Variable.withInt(3).isNotIn([])), isTrue);
expect(await eval(const Constant<int>(null).isIn([])), isFalse);
expect(await eval(const Constant<int>(null).isNotIn([])), isTrue);
});
});
}); });
} }

View File

@ -23,6 +23,26 @@ void main() {
}); });
}); });
group('expressions', () {
test('in', () {
final isInExpression = innerExpression.isInExp([
CustomExpression('a'),
CustomExpression('b'),
]);
expect(isInExpression, generates('name IN (a, b)'));
});
test('not in', () {
final isNotInExpression = innerExpression.isNotInExp([
CustomExpression('a'),
CustomExpression('b'),
]);
expect(isNotInExpression, generates('name NOT IN (a, b)'));
});
});
group('subquery', () { group('subquery', () {
test('in expressions are generated', () { test('in expressions are generated', () {
final isInExpression = innerExpression final isInExpression = innerExpression

View File

@ -224,7 +224,7 @@ void main() {
'c.desc': 'Description', 'c.desc': 'Description',
'c.description_in_upper_case': 'DESCRIPTION', 'c.description_in_upper_case': 'DESCRIPTION',
'c.priority': 1, 'c.priority': 1,
'c4': 11 'c0': 11
} }
]; ];
}); });
@ -234,7 +234,7 @@ void main() {
verify(executor.runSelect( verify(executor.runSelect(
'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", ' 'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", '
'"c"."priority" AS "c.priority", "c"."description_in_upper_case" AS ' '"c"."priority" AS "c.priority", "c"."description_in_upper_case" AS '
'"c.description_in_upper_case", LENGTH("c"."desc") AS "c4" ' '"c.description_in_upper_case", LENGTH("c"."desc") AS "c0" '
'FROM "categories" "c";', 'FROM "categories" "c";',
[], [],
)); ));
@ -273,7 +273,7 @@ void main() {
'c.desc': 'Description', 'c.desc': 'Description',
'c.description_in_upper_case': 'DESCRIPTION', 'c.description_in_upper_case': 'DESCRIPTION',
'c.priority': 1, 'c.priority': 1,
'c4': 11, 'c0': 11,
}, },
]; ];
}); });
@ -283,7 +283,7 @@ void main() {
verify(executor.runSelect( verify(executor.runSelect(
'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", "c"."priority" AS "c.priority"' 'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", "c"."priority" AS "c.priority"'
', "c"."description_in_upper_case" AS "c.description_in_upper_case", ' ', "c"."description_in_upper_case" AS "c.description_in_upper_case", '
'LENGTH("c"."desc") AS "c4" ' 'LENGTH("c"."desc") AS "c0" '
'FROM "categories" "c" ' 'FROM "categories" "c" '
'INNER JOIN "todos" "t" ON "c"."id" = "t"."category";', 'INNER JOIN "todos" "t" ON "c"."id" = "t"."category";',
[], [],
@ -328,7 +328,7 @@ void main() {
'c.id': 3, 'c.id': 3,
'c.desc': 'desc', 'c.desc': 'desc',
'c.priority': 0, 'c.priority': 0,
'c4': 10, 'c0': 10,
'c.description_in_upper_case': 'DESC', 'c.description_in_upper_case': 'DESC',
} }
]; ];
@ -340,7 +340,7 @@ void main() {
'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", ' 'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", '
'"c"."priority" AS "c.priority", ' '"c"."priority" AS "c.priority", '
'"c"."description_in_upper_case" AS "c.description_in_upper_case", ' '"c"."description_in_upper_case" AS "c.description_in_upper_case", '
'COUNT("t"."id") AS "c4" ' 'COUNT("t"."id") AS "c0" '
'FROM "categories" "c" INNER JOIN "todos" "t" ON "t"."category" = "c"."id" ' 'FROM "categories" "c" INNER JOIN "todos" "t" ON "t"."category" = "c"."id" '
'GROUP BY "c"."id" HAVING COUNT("t"."id") >= ?;', 'GROUP BY "c"."id" HAVING COUNT("t"."id") >= ?;',
[10])); [10]));
@ -474,4 +474,72 @@ void main() {
throwsA(isNot(isA<DriftWrappedException>())), throwsA(isNot(isA<DriftWrappedException>())),
); );
}); });
group('subquery', () {
test('can be joined', () async {
final subquery = Subquery(
db.select(db.todosTable)
..orderBy([(row) => OrderingTerm.desc(row.title.length)])
..limit(10),
's',
);
final query = db.selectOnly(db.categories)
..addColumns([db.categories.id])
..join([
innerJoin(subquery,
subquery.ref(db.todosTable.category).equalsExp(db.categories.id))
]);
await query.get();
verify(
executor.runSelect(
'SELECT "categories"."id" AS "categories.id" FROM "categories" '
'INNER JOIN (SELECT * FROM "todos" '
'ORDER BY LENGTH("todos"."title") DESC LIMIT 10) s '
'ON "s"."category" = "categories"."id";',
argThat(isEmpty),
),
);
});
test('use column from subquery', () async {
when(executor.runSelect(any, any)).thenAnswer((_) {
return Future.value([
{'c0': 42}
]);
});
final sumOfTitleLength = db.todosTable.title.length.sum();
final subquery = Subquery(
db.selectOnly(db.todosTable)
..addColumns([db.todosTable.category, sumOfTitleLength])
..groupBy([db.todosTable.category]),
's');
final readableLength = subquery.ref(sumOfTitleLength);
final query = db.selectOnly(db.categories)
..addColumns([readableLength])
..join([
innerJoin(subquery,
subquery.ref(db.todosTable.category).equalsExp(db.categories.id))
]);
final row = await query.getSingle();
verify(
executor.runSelect(
'SELECT "s"."c1" AS "c0" FROM "categories" '
'INNER JOIN ('
'SELECT "todos"."category" AS "todos.category", '
'SUM(LENGTH("todos"."title")) AS "c1" FROM "todos" '
'GROUP BY "todos"."category") s '
'ON "s"."todos.category" = "categories"."id";',
argThat(isEmpty),
),
);
expect(row.read(readableLength), 42);
});
});
} }

View File

@ -259,6 +259,32 @@ void main() {
)); ));
}); });
}); });
group('dialect-specific', () {
Map<SqlDialect, String> statements(String base) {
return {
for (final dialect in SqlDialect.values) dialect: '$base $dialect',
};
}
for (final dialect in [SqlDialect.sqlite, SqlDialect.postgres]) {
test('with dialect $dialect', () async {
final executor = MockExecutor();
when(executor.dialect).thenReturn(dialect);
final db = TodoDb(executor);
final migrator = db.createMigrator();
await migrator.create(Trigger.byDialect('a', statements('trigger')));
await migrator.create(Index.byDialect('a', statements('index')));
await migrator.create(OnCreateQuery.byDialect(statements('@')));
verify(executor.runCustom('trigger $dialect', []));
verify(executor.runCustom('index $dialect', []));
verify(executor.runCustom('@ $dialect', []));
});
}
});
} }
final class _FakeSchemaVersion extends VersionedSchema { final class _FakeSchemaVersion extends VersionedSchema {

View File

@ -214,4 +214,30 @@ void main() {
await pumpEventQueue(); await pumpEventQueue();
db.markTablesUpdated([db.categories]); db.markTablesUpdated([db.categories]);
}); });
test('select from subquery', () async {
final data = [
{
'id': 10,
'title': null,
'content': 'Content',
'category': null,
}
];
when(executor.runSelect(any, any)).thenAnswer((_) => Future.value(data));
final subquery = Subquery(db.todosTable.select(), 's');
final rows = await db.select(subquery).get();
expect(rows, [
TodoEntry(
id: 10,
title: null,
content: 'Content',
category: null,
)
]);
verify(executor.runSelect('SELECT * FROM (SELECT * FROM "todos") s;', []));
});
} }

View File

@ -1600,8 +1600,10 @@ class MyView extends ViewInfo<MyView, MyViewData> implements HasResultSet {
@override @override
String get entityName => 'my_view'; String get entityName => 'my_view';
@override @override
String get createViewStmt => Map<SqlDialect, String> get createViewStatements => {
'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2'; SqlDialect.sqlite:
'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2',
};
@override @override
MyView get asDslTable => this; MyView get asDslTable => this;
@override @override
@ -1678,12 +1680,13 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
], ],
readsFrom: { readsFrom: {
config, config,
}).asyncMap((QueryRow row) => config.mapFromRowWithAlias(row, const { }).asyncMap(
'ck': 'config_key', (QueryRow row) async => config.mapFromRowWithAlias(row, const {
'cf': 'config_value', 'ck': 'config_key',
'cs1': 'sync_state', 'cf': 'config_value',
'cs2': 'sync_state_implicit', 'cs1': 'sync_state',
})); 'cs2': 'sync_state_implicit',
}));
} }
Selectable<Config> readMultiple(List<String> var1, Selectable<Config> readMultiple(List<String> var1,
@ -1754,26 +1757,20 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
variables: [], variables: [],
readsFrom: { readsFrom: {
config, config,
}).map((QueryRow row) { }).map((QueryRow row) => JsonResult(
return JsonResult( row: row,
row: row, key: row.read<String>('key'),
key: row.read<String>('key'), value: row.readNullable<String>('value'),
value: row.readNullable<String>('value'), ));
);
});
} }
Selectable<JsonResult> another() { Selectable<JsonResult> another() {
return customSelect( return customSelect('SELECT \'one\' AS "key", NULLIF(\'two\', \'another\') AS value', variables: [], readsFrom: {})
'SELECT \'one\' AS "key", NULLIF(\'two\', \'another\') AS value', .map((QueryRow row) => JsonResult(
variables: [], row: row,
readsFrom: {}).map((QueryRow row) { key: row.read<String>('key'),
return JsonResult( value: row.readNullable<String>('value'),
row: row, ));
key: row.read<String>('key'),
value: row.readNullable<String>('value'),
);
});
} }
Selectable<MultipleResult> multiple({required Multiple$predicate predicate}) { Selectable<MultipleResult> multiple({required Multiple$predicate predicate}) {
@ -1793,14 +1790,13 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
withDefaults, withDefaults,
withConstraints, withConstraints,
...generatedpredicate.watchedTables, ...generatedpredicate.watchedTables,
}).asyncMap((QueryRow row) async { }).asyncMap((QueryRow row) async => MultipleResult(
return MultipleResult( row: row,
row: row, a: row.readNullable<String>('a'),
a: row.readNullable<String>('a'), b: row.readNullable<int>('b'),
b: row.readNullable<int>('b'), c: await withConstraints.mapFromRowOrNull(row,
c: await withConstraints.mapFromRowOrNull(row, tablePrefix: 'nested_0'), tablePrefix: 'nested_0'),
); ));
});
} }
Selectable<EMail> searchEmails({required String? term}) { Selectable<EMail> searchEmails({required String? term}) {
@ -1827,20 +1823,18 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
readsFrom: { readsFrom: {
config, config,
...generatedexpr.watchedTables, ...generatedexpr.watchedTables,
}).map((QueryRow row) { }).map((QueryRow row) => ReadRowIdResult(
return ReadRowIdResult( row: row,
row: row, rowid: row.read<int>('rowid'),
rowid: row.read<int>('rowid'), configKey: row.read<String>('config_key'),
configKey: row.read<String>('config_key'), configValue: row.readNullable<DriftAny>('config_value'),
configValue: row.readNullable<DriftAny>('config_value'), syncState: NullAwareTypeConverter.wrapFromSql(
syncState: NullAwareTypeConverter.wrapFromSql( ConfigTable.$convertersyncState,
ConfigTable.$convertersyncState, row.readNullable<int>('sync_state')),
row.readNullable<int>('sync_state')), syncStateImplicit: NullAwareTypeConverter.wrapFromSql(
syncStateImplicit: NullAwareTypeConverter.wrapFromSql( ConfigTable.$convertersyncStateImplicit,
ConfigTable.$convertersyncStateImplicit, row.readNullable<int>('sync_state_implicit')),
row.readNullable<int>('sync_state_implicit')), ));
);
});
} }
Selectable<MyViewData> readView({ReadView$where? where}) { Selectable<MyViewData> readView({ReadView$where? where}) {
@ -1895,21 +1889,19 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
readsFrom: { readsFrom: {
withConstraints, withConstraints,
withDefaults, withDefaults,
}).asyncMap((QueryRow row) async { }).asyncMap((QueryRow row) async => NestedResult(
return NestedResult( row: row,
row: row, defaults: await withDefaults.mapFromRow(row, tablePrefix: 'nested_0'),
defaults: await withDefaults.mapFromRow(row, tablePrefix: 'nested_0'), nestedQuery1: await customSelect(
nestedQuery0: await customSelect( 'SELECT * FROM with_constraints AS c WHERE c.b = ?1',
'SELECT * FROM with_constraints AS c WHERE c.b = ?1', variables: [
variables: [ Variable<int>(row.read('\$n_0'))
Variable<int>(row.read('\$n_0')) ],
], readsFrom: {
readsFrom: { withConstraints,
withConstraints, withDefaults,
withDefaults, }).asyncMap(withConstraints.mapFromRow).get(),
}).asyncMap(withConstraints.mapFromRow).get(), ));
);
});
} }
Selectable<MyCustomResultClass> customResult() { Selectable<MyCustomResultClass> customResult() {
@ -2081,25 +2073,25 @@ typedef ReadView$where = Expression<bool> Function(MyView my_view);
class NestedResult extends CustomResultSet { class NestedResult extends CustomResultSet {
final WithDefault defaults; final WithDefault defaults;
final List<WithConstraint> nestedQuery0; final List<WithConstraint> nestedQuery1;
NestedResult({ NestedResult({
required QueryRow row, required QueryRow row,
required this.defaults, required this.defaults,
required this.nestedQuery0, required this.nestedQuery1,
}) : super(row); }) : super(row);
@override @override
int get hashCode => Object.hash(defaults, nestedQuery0); int get hashCode => Object.hash(defaults, nestedQuery1);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
(other is NestedResult && (other is NestedResult &&
other.defaults == this.defaults && other.defaults == this.defaults &&
other.nestedQuery0 == this.nestedQuery0); other.nestedQuery1 == this.nestedQuery1);
@override @override
String toString() { String toString() {
return (StringBuffer('NestedResult(') return (StringBuffer('NestedResult(')
..write('defaults: $defaults, ') ..write('defaults: $defaults, ')
..write('nestedQuery0: $nestedQuery0') ..write('nestedQuery1: $nestedQuery1')
..write(')')) ..write(')'))
.toString(); .toString();
} }

View File

@ -641,16 +641,13 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
static const VerificationMeta _isAwesomeMeta = static const VerificationMeta _isAwesomeMeta =
const VerificationMeta('isAwesome'); const VerificationMeta('isAwesome');
@override @override
late final GeneratedColumn<bool> isAwesome = late final GeneratedColumn<bool> isAwesome = GeneratedColumn<bool>(
GeneratedColumn<bool>('is_awesome', aliasedName, false, 'is_awesome', aliasedName, false,
type: DriftSqlType.bool, type: DriftSqlType.bool,
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({ defaultConstraints:
SqlDialect.sqlite: 'CHECK ("is_awesome" IN (0, 1))', GeneratedColumn.constraintIsAlways('CHECK ("is_awesome" IN (0, 1))'),
SqlDialect.mysql: '', defaultValue: const Constant(true));
SqlDialect.postgres: '',
}),
defaultValue: const Constant(true));
static const VerificationMeta _profilePictureMeta = static const VerificationMeta _profilePictureMeta =
const VerificationMeta('profilePicture'); const VerificationMeta('profilePicture');
@override @override
@ -1549,7 +1546,7 @@ class $CategoryTodoCountViewView
@override @override
String get entityName => 'category_todo_count_view'; String get entityName => 'category_todo_count_view';
@override @override
String? get createViewStmt => null; Map<SqlDialect, String>? get createViewStatements => null;
@override @override
$CategoryTodoCountViewView get asDslTable => this; $CategoryTodoCountViewView get asDslTable => this;
@override @override
@ -1660,7 +1657,7 @@ class $TodoWithCategoryViewView
@override @override
String get entityName => 'todo_with_category_view'; String get entityName => 'todo_with_category_view';
@override @override
String? get createViewStmt => null; Map<SqlDialect, String>? get createViewStatements => null;
@override @override
$TodoWithCategoryViewView get asDslTable => this; $TodoWithCategoryViewView get asDslTable => this;
@override @override
@ -1714,21 +1711,19 @@ abstract class _$TodoDb extends GeneratedDatabase {
readsFrom: { readsFrom: {
categories, categories,
todosTable, todosTable,
}).map((QueryRow row) { }).map((QueryRow row) => AllTodosWithCategoryResult(
return AllTodosWithCategoryResult( row: row,
row: row, id: row.read<int>('id'),
id: row.read<int>('id'), title: row.readNullable<String>('title'),
title: row.readNullable<String>('title'), content: row.read<String>('content'),
content: row.read<String>('content'), targetDate: row.readNullable<DateTime>('target_date'),
targetDate: row.readNullable<DateTime>('target_date'), category: row.readNullable<int>('category'),
category: row.readNullable<int>('category'), status: NullAwareTypeConverter.wrapFromSql(
status: NullAwareTypeConverter.wrapFromSql( $TodosTableTable.$converterstatus,
$TodosTableTable.$converterstatus, row.readNullable<String>('status')),
row.readNullable<String>('status')), catId: row.read<int>('catId'),
catId: row.read<int>('catId'), catDesc: row.read<String>('catDesc'),
catDesc: row.read<String>('catDesc'), ));
);
});
} }
Future<int> deleteTodoById(int var1) { Future<int> deleteTodoById(int var1) {

View File

@ -136,7 +136,7 @@ void main() {
contains( contains(
isA<NestedResult>() isA<NestedResult>()
.having((e) => e.defaults, 'defaults', first) .having((e) => e.defaults, 'defaults', first)
.having((e) => e.nestedQuery0, 'nested', hasLength(2)), .having((e) => e.nestedQuery1, 'nested', hasLength(2)),
), ),
); );
@ -145,7 +145,7 @@ void main() {
contains( contains(
isA<NestedResult>() isA<NestedResult>()
.having((e) => e.defaults, 'defaults', second) .having((e) => e.defaults, 'defaults', second)
.having((e) => e.nestedQuery0, 'nested', hasLength(1)), .having((e) => e.nestedQuery1, 'nested', hasLength(1)),
), ),
); );
}); });

View File

@ -30,6 +30,6 @@ void main() {
final result = results.single; final result = results.single;
expect(result.defaults, defaults); expect(result.defaults, defaults);
expect(result.nestedQuery0, [constraints]); expect(result.nestedQuery1, [constraints]);
}); });
} }

View File

@ -50,4 +50,52 @@ void main() {
expect(await db.users.all().get(), [user]); expect(await db.users.all().get(), [user]);
}); });
test('subqueries', () async {
await db.batch((batch) {
batch.insertAll(db.categories, [
CategoriesCompanion.insert(description: 'a'),
CategoriesCompanion.insert(description: 'b'),
]);
batch.insertAll(
db.todosTable,
[
TodosTableCompanion.insert(content: 'aaaaa', category: Value(1)),
TodosTableCompanion.insert(content: 'aa', category: Value(1)),
TodosTableCompanion.insert(content: 'bbbbbb', category: Value(2)),
],
);
});
// Now write a query returning the amount of content chars in each
// category (written using subqueries).
final subqueryContentLength = db.todosTable.content.length.sum();
final subquery = Subquery(
db.selectOnly(db.todosTable)
..addColumns([db.todosTable.category, subqueryContentLength])
..groupBy([db.todosTable.category]),
's');
final readableLength = subquery.ref(subqueryContentLength);
final query = db.selectOnly(db.categories)
..addColumns([db.categories.id, readableLength])
..join([
innerJoin(subquery,
subquery.ref(db.todosTable.category).equalsExp(db.categories.id))
])
..orderBy([OrderingTerm.asc(db.categories.id)]);
final rows = await query.get();
expect(rows, hasLength(2));
final first = rows[0];
final second = rows[1];
expect(first.read(db.categories.id), 1);
expect(first.read(readableLength), 7);
expect(second.read(db.categories.id), 2);
expect(second.read(readableLength), 6);
});
} }

View File

@ -1,3 +1,10 @@
## 2.11.0
- [Nested result columns](https://drift.simonbinder.eu/docs/using-sql/drift_files/#nested-results)
in drift files can now refer to any result set (e.g. a table-valued function or a subquery).
They were restricted to direct table references before.
- Add the `dialects` builder option to generate code supporting multiple SQL dialects.
## 2.10.0 ## 2.10.0
- Add the `schema steps` command to generate help in writing step-by-step schema migrations. - Add the `schema steps` command to generate help in writing step-by-step schema migrations.

View File

@ -1,14 +1,14 @@
# Short description for each builder # Short description for each builder
# - preparing_builder: Infers the type of inline Dart expressions in moor files. # - preparing_builder: Infers the type of inline Dart expressions in drift files.
# We create a `input.temp.dart` file containing the expressions so that they # We create a `input.temp.dart` file containing the expressions so that they
# can be resolved. # can be resolved.
# - moor_generator: The regular SharedPartBuilder for @UseMoor and @UseDao # - drift_dev: The regular SharedPartBuilder for @DriftDatabase and @DriftAccessor
# annotations # annotations
# - moor_generator_not_shared: Like moor_generator, but as a PartBuilder instead of # - not_shared: Like drift_dev, but as a PartBuilder instead of
# a SharedPartBuilder. This builder is disabled by default, but users may choose # a SharedPartBuilder. This builder is disabled by default, but users may choose
# to use it so that generated classes can be used by other builders. # to use it so that generated classes can be used by other builders.
# - moor_cleanup: Deletes the `.temp.dart` files generated by the `preparing_builder`. # - cleanup: Deletes the `.temp.dart` files generated by the `preparing_builder`.
builders: builders:
preparing_builder: preparing_builder:

View File

@ -124,7 +124,7 @@ class DriftOptions {
this.modules = const [], this.modules = const [],
this.sqliteAnalysisOptions, this.sqliteAnalysisOptions,
this.storeDateTimeValuesAsText = false, this.storeDateTimeValuesAsText = false,
this.dialect = const DialectOptions(SqlDialect.sqlite, null), this.dialect = const DialectOptions(null, [SqlDialect.sqlite], null),
this.caseFromDartToSql = CaseFromDartToSql.snake, this.caseFromDartToSql = CaseFromDartToSql.snake,
this.writeToColumnsMixins = false, this.writeToColumnsMixins = false,
this.fatalWarnings = false, this.fatalWarnings = false,
@ -189,7 +189,18 @@ class DriftOptions {
/// Whether the [module] has been enabled in this configuration. /// Whether the [module] has been enabled in this configuration.
bool hasModule(SqlModule module) => effectiveModules.contains(module); bool hasModule(SqlModule module) => effectiveModules.contains(module);
SqlDialect get effectiveDialect => dialect?.dialect ?? SqlDialect.sqlite; List<SqlDialect> get supportedDialects {
final dialects = dialect?.dialects;
final singleDialect = dialect?.dialect;
if (dialects != null) {
return dialects;
} else if (singleDialect != null) {
return [singleDialect];
} else {
return const [SqlDialect.sqlite];
}
}
/// The assumed sqlite version used when analyzing queries. /// The assumed sqlite version used when analyzing queries.
SqliteVersion get sqliteVersion { SqliteVersion get sqliteVersion {
@ -201,10 +212,11 @@ class DriftOptions {
@JsonSerializable() @JsonSerializable()
class DialectOptions { class DialectOptions {
final SqlDialect dialect; final SqlDialect? dialect;
final List<SqlDialect>? dialects;
final SqliteAnalysisOptions? options; final SqliteAnalysisOptions? options;
const DialectOptions(this.dialect, this.options); const DialectOptions(this.dialect, this.dialects, this.options);
factory DialectOptions.fromJson(Map json) => _$DialectOptionsFromJson(json); factory DialectOptions.fromJson(Map json) => _$DialectOptionsFromJson(json);

View File

@ -198,18 +198,6 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
relevantNode: e, relevantNode: e,
)); ));
} }
// check that it actually refers to a table
final result = e.resultSet?.unalias();
if (result is! Table && result is! View) {
linter.sqlParserErrors.add(AnalysisError(
type: AnalysisErrorType.other,
message: 'Nested star columns must refer to a table directly. They '
"can't refer to a table-valued function or another select "
'statement.',
relevantNode: e,
));
}
} }
if (e is NestedQueryColumn) { if (e is NestedQueryColumn) {

View File

@ -1,3 +1,4 @@
import 'package:drift/drift.dart' show SqlDialect;
import 'package:recase/recase.dart'; import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart'; import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/sqlparser.dart' as sql; import 'package:sqlparser/sqlparser.dart' as sql;
@ -110,7 +111,8 @@ class DriftViewResolver extends DriftElementResolver<DiscoveredDriftView> {
query: stmt.query, query: stmt.query,
// Remove drift-specific syntax // Remove drift-specific syntax
driftTableName: null, driftTableName: null,
).toSqlWithoutDriftSpecificSyntax(resolver.driver.options); ).toSqlWithoutDriftSpecificSyntax(
resolver.driver.options, SqlDialect.sqlite);
return DriftView( return DriftView(
discovered.ownId, discovered.ownId,

View File

@ -35,7 +35,7 @@ class MatchExistingTypeForQuery {
} }
} }
ExistingQueryRowType? _findRowType( QueryRowType? _findRowType(
InferredResultSet resultSet, InferredResultSet resultSet,
dynamic /*DartType|RequestedQueryResultType*/ requestedType, dynamic /*DartType|RequestedQueryResultType*/ requestedType,
_ErrorReporter reportError, _ErrorReporter reportError,
@ -52,8 +52,8 @@ class MatchExistingTypeForQuery {
'Must be a DartType of a RequestedQueryResultType'); 'Must be a DartType of a RequestedQueryResultType');
} }
final positionalColumns = <ArgumentForExistingQueryRowType>[]; final positionalColumns = <ArgumentForQueryRowType>[];
final namedColumns = <String, ArgumentForExistingQueryRowType>{}; final namedColumns = <String, ArgumentForQueryRowType>{};
final unmatchedColumnsByName = { final unmatchedColumnsByName = {
for (final column in resultSet.columns) for (final column in resultSet.columns)
@ -94,10 +94,9 @@ class MatchExistingTypeForQuery {
addEntry(name, () => transformedTypeBuilder.addDartType(type)); addEntry(name, () => transformedTypeBuilder.addDartType(type));
} }
void addCheckedType( void addCheckedType(ArgumentForQueryRowType type, DartType originalType,
ArgumentForExistingQueryRowType type, DartType originalType,
{String? name}) { {String? name}) {
if (type is ExistingQueryRowType) { if (type is QueryRowType) {
addEntry(name, () => transformedTypeBuilder.addCode(type.rowType)); addEntry(name, () => transformedTypeBuilder.addCode(type.rowType));
} else if (type is MappedNestedListQuery) { } else if (type is MappedNestedListQuery) {
addEntry(name, () { addEntry(name, () {
@ -171,7 +170,7 @@ class MatchExistingTypeForQuery {
final verified = _verifyArgument(resultSet.scalarColumns.single, final verified = _verifyArgument(resultSet.scalarColumns.single,
desiredType, 'Single column', (ignore) {}); desiredType, 'Single column', (ignore) {});
if (verified != null) { if (verified != null) {
return ExistingQueryRowType( return QueryRowType(
rowType: AnnotatedDartCode.type(desiredType), rowType: AnnotatedDartCode.type(desiredType),
singleValue: verified, singleValue: verified,
positionalArguments: const [], positionalArguments: const [],
@ -184,7 +183,7 @@ class MatchExistingTypeForQuery {
final verified = final verified =
_verifyMatchingDriftTable(resultSet.matchingTable!, desiredType); _verifyMatchingDriftTable(resultSet.matchingTable!, desiredType);
if (verified != null) { if (verified != null) {
return ExistingQueryRowType( return QueryRowType(
rowType: AnnotatedDartCode.build((builder) => rowType: AnnotatedDartCode.build((builder) =>
builder.addElementRowType(resultSet.matchingTable!.table)), builder.addElementRowType(resultSet.matchingTable!.table)),
singleValue: verified, singleValue: verified,
@ -237,7 +236,7 @@ class MatchExistingTypeForQuery {
} }
} }
return ExistingQueryRowType( return QueryRowType(
rowType: annotatedTypeCode, rowType: annotatedTypeCode,
constructorName: constructorName ?? '', constructorName: constructorName ?? '',
isRecord: desiredType is RecordType, isRecord: desiredType is RecordType,
@ -249,13 +248,13 @@ class MatchExistingTypeForQuery {
/// Returns the default record type chosen by drift when a user declares the /// Returns the default record type chosen by drift when a user declares the
/// generic `Record` type as a desired result type. /// generic `Record` type as a desired result type.
ExistingQueryRowType _defaultRecord(InferredResultSet resultSet) { QueryRowType _defaultRecord(InferredResultSet resultSet) {
// If there's only a single scalar column, or if we're mapping this result // If there's only a single scalar column, or if we're mapping this result
// set to an existing table, then there's only a single value in the end. // set to an existing table, then there's only a single value in the end.
// Singleton records are forbidden, so we just return the inner type // Singleton records are forbidden, so we just return the inner type
// directly. // directly.
if (resultSet.singleColumn) { if (resultSet.singleColumn) {
return ExistingQueryRowType( return QueryRowType(
rowType: AnnotatedDartCode.build( rowType: AnnotatedDartCode.build(
(builder) => builder.addDriftType(resultSet.scalarColumns.single)), (builder) => builder.addDriftType(resultSet.scalarColumns.single)),
singleValue: resultSet.scalarColumns.single, singleValue: resultSet.scalarColumns.single,
@ -264,7 +263,7 @@ class MatchExistingTypeForQuery {
); );
} else if (resultSet.matchingTable != null) { } else if (resultSet.matchingTable != null) {
final table = resultSet.matchingTable!; final table = resultSet.matchingTable!;
return ExistingQueryRowType( return QueryRowType(
rowType: AnnotatedDartCode.build( rowType: AnnotatedDartCode.build(
(builder) => builder.addElementRowType(table.table)), (builder) => builder.addElementRowType(table.table)),
singleValue: table, singleValue: table,
@ -273,7 +272,7 @@ class MatchExistingTypeForQuery {
); );
} }
final namedArguments = <String, ArgumentForExistingQueryRowType>{}; final namedArguments = <String, ArgumentForQueryRowType>{};
final type = AnnotatedDartCode.build((builder) { final type = AnnotatedDartCode.build((builder) {
builder.addText('({'); builder.addText('({');
@ -288,8 +287,10 @@ class MatchExistingTypeForQuery {
builder.addDriftType(column); builder.addDriftType(column);
namedArguments[fieldName] = column; namedArguments[fieldName] = column;
} else if (column is NestedResultTable) { } else if (column is NestedResultTable) {
builder.addElementRowType(column.table); final innerRecord = _defaultRecord(column.innerResultSet);
namedArguments[fieldName] = column; builder.addCode(innerRecord.rowType);
namedArguments[fieldName] =
StructuredFromNestedColumn(column, innerRecord);
} else if (column is NestedResultQuery) { } else if (column is NestedResultQuery) {
final nestedResultSet = column.query.resultSet; final nestedResultSet = column.query.resultSet;
@ -310,7 +311,7 @@ class MatchExistingTypeForQuery {
builder.addText('})'); builder.addText('})');
}); });
return ExistingQueryRowType( return QueryRowType(
rowType: type, rowType: type,
singleValue: null, singleValue: null,
positionalArguments: const [], positionalArguments: const [],
@ -319,7 +320,14 @@ class MatchExistingTypeForQuery {
); );
} }
ArgumentForExistingQueryRowType? _verifyArgument( /// Finds a way to map the [column] into the desired [existingTypeForColumn],
/// which is represented as a [ArgumentForExistingQueryRowType].
///
/// If this doesn't succeed (mainly due to incompatible types), reports a
/// error through [reportError] and returns `null`.
/// [name] is used in error messages to inform the user about the field name
/// in their existing Dart class that is causing the problem.
ArgumentForQueryRowType? _verifyArgument(
ResultColumn column, ResultColumn column,
DartType existingTypeForColumn, DartType existingTypeForColumn,
String name, String name,
@ -339,25 +347,15 @@ class MatchExistingTypeForQuery {
if (matches) return column; if (matches) return column;
} else if (column is NestedResultTable) { } else if (column is NestedResultTable) {
final table = column.table; final foundInnerType = _findRowType(
column.innerResultSet,
existingTypeForColumn,
(msg) => reportError('For $name: $msg'),
);
// Usually, the table is about to be generated by drift - so we can't if (foundInnerType != null) {
// verify the existing type. If there's an existing row class though, we return StructuredFromNestedColumn(column, foundInnerType);
// can compare against that.
if (table.hasExistingRowClass) {
final existingType = table.existingRowClass!.targetType;
if (column.isNullable) {
existingTypeForColumn =
typeSystem.promoteToNonNull(existingTypeForColumn);
}
if (!typeSystem.isAssignableTo(existingType, existingTypeForColumn)) {
reportError('$name must accept '
'${existingType.getDisplayString(withNullability: true)}');
}
} }
return column;
} else if (column is NestedResultQuery) { } else if (column is NestedResultQuery) {
// A nested query has its own type, which we can recursively try to // A nested query has its own type, which we can recursively try to
// structure in the existing type. // structure in the existing type.
@ -378,13 +376,14 @@ class MatchExistingTypeForQuery {
return MappedNestedListQuery(column, innerExistingType); return MappedNestedListQuery(column, innerExistingType);
} }
} }
return null; return null;
} }
/// Allows using a matching drift table from a result set as an argument if /// Allows using a matching drift table from a result set as an argument if
/// the the [existingTypeForColumn] matches the table's type (either the /// the the [existingTypeForColumn] matches the table's type (either the
/// existing result type or `dynamic` if it's drift-generated). /// existing result type or `dynamic` if it's drift-generated).
ArgumentForExistingQueryRowType? _verifyMatchingDriftTable( ArgumentForQueryRowType? _verifyMatchingDriftTable(
MatchingDriftTable match, DartType existingTypeForColumn) { MatchingDriftTable match, DartType existingTypeForColumn) {
final table = match.table; final table = match.table;
if (table.hasExistingRowClass) { if (table.hasExistingRowClass) {

View File

@ -330,6 +330,7 @@ class QueryAnalyzer {
if (column is NestedStarResultColumn) { if (column is NestedStarResultColumn) {
final resolved = _resolveNestedResultTable(queryContext, column); final resolved = _resolveNestedResultTable(queryContext, column);
if (resolved != null) { if (resolved != null) {
// The single table optimization doesn't make sense when nested result // The single table optimization doesn't make sense when nested result
// sets are present. // sets are present.
@ -439,19 +440,37 @@ class QueryAnalyzer {
_QueryHandlerContext queryContext, NestedStarResultColumn column) { _QueryHandlerContext queryContext, NestedStarResultColumn column) {
final originalResult = column.resultSet; final originalResult = column.resultSet;
final result = originalResult?.unalias(); final result = originalResult?.unalias();
if (result is! Table && result is! View) { final rawColumns = result?.resolvedColumns;
return null;
} if (result == null || rawColumns == null) return null;
final driftResultSet = _inferResultSet(
_QueryHandlerContext(
foundElements: queryContext.foundElements,
root: queryContext.root,
queryName: queryContext.queryName,
nestedScope: queryContext.nestedScope,
sourceForFixedName: queryContext.sourceForFixedName,
// Remove desired result class, if any. It will be resolved by the
// parent _inferResultSet call.
),
rawColumns,
null,
);
final driftTable = _lookupReference<DriftElementWithResultSet>(
(result as NamedResultSet).name);
final analysis = JoinModel.of(column); final analysis = JoinModel.of(column);
final isNullable = final isNullable =
analysis == null || analysis.isNullableTable(originalResult!); analysis == null || analysis.isNullableTable(originalResult!);
final queryIndex = nestedQueryCounter++;
final resultClassName =
'${ReCase(queryContext.queryName).pascalCase}NestedColumn$queryIndex';
return NestedResultTable( return NestedResultTable(
column, from: column,
column.as ?? column.tableName, name: column.as ?? column.tableName,
driftTable, innerResultSet: driftResultSet,
nameForGeneratedRowClass: resultClassName,
isNullable: isNullable, isNullable: isNullable,
); );
} }

View File

@ -171,6 +171,11 @@ class AnnotatedDartCodeBuilder {
'This query (${query.name}) does not have a result set'); 'This query (${query.name}) does not have a result set');
} }
addResultSetRowType(resultSet, () => query.resultClassName);
}
void addResultSetRowType(
InferredResultSet resultSet, String Function() resultClassName) {
if (resultSet.existingRowType != null) { if (resultSet.existingRowType != null) {
return addCode(resultSet.existingRowType!.rowType); return addCode(resultSet.existingRowType!.rowType);
} }
@ -183,12 +188,13 @@ class AnnotatedDartCodeBuilder {
return addDriftType(resultSet.scalarColumns.single); return addDriftType(resultSet.scalarColumns.single);
} }
return addText(query.resultClassName); return addText(resultClassName());
} }
void addTypeOfNestedResult(NestedResult nested) { void addTypeOfNestedResult(NestedResult nested) {
if (nested is NestedResultTable) { if (nested is NestedResultTable) {
return addElementRowType(nested.table); return addResultSetRowType(
nested.innerResultSet, () => nested.nameForGeneratedRowClass);
} else if (nested is NestedResultQuery) { } else if (nested is NestedResultQuery) {
addSymbol('List', AnnotatedDartCode.dartCore); addSymbol('List', AnnotatedDartCode.dartCore);
addText('<'); addText('<');

View File

@ -1,7 +1,6 @@
import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show DriftSqlType, UpdateKind; import 'package:drift/drift.dart' show DriftSqlType, UpdateKind;
import 'package:meta/meta.dart';
import 'package:recase/recase.dart'; import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart'; import 'package:sqlparser/sqlparser.dart';
@ -25,7 +24,7 @@ abstract class DriftQueryDeclaration {
/// We deliberately only store very basic information here: The actual query /// We deliberately only store very basic information here: The actual query
/// model is very complex and hard to serialize. Further, lots of generation /// model is very complex and hard to serialize. Further, lots of generation
/// logic requires actual references to the AST which will be difficult to /// logic requires actual references to the AST which will be difficult to
/// translate across serialization run. /// translate across serialization runs.
/// Since SQL queries only need to be fully analyzed before generation, and /// Since SQL queries only need to be fully analyzed before generation, and
/// since they are local elements which can't be referenced by others, there's /// since they are local elements which can't be referenced by others, there's
/// no clear advantage wrt. incremental compilation if queries are fully /// no clear advantage wrt. incremental compilation if queries are fully
@ -162,15 +161,10 @@ abstract class SqlQuery {
placeholders = elements.whereType<FoundDartPlaceholder>().toList(); placeholders = elements.whereType<FoundDartPlaceholder>().toList();
} }
bool get needsAsyncMapping { bool get _useResultClassName {
final result = resultSet; final resultSet = this.resultSet!;
if (result != null) {
// Mapping to tables is asynchronous
if (result.matchingTable != null) return true;
if (result.nestedResults.any((e) => e is NestedResultTable)) return true;
}
return false; return resultSet.matchingTable == null && !resultSet.singleColumn;
} }
String get resultClassName { String get resultClassName {
@ -179,7 +173,7 @@ abstract class SqlQuery {
throw StateError('This query ($name) does not have a result set'); throw StateError('This query ($name) does not have a result set');
} }
if (resultSet.matchingTable != null || resultSet.singleColumn) { if (!_useResultClassName) {
throw UnsupportedError('This result set does not introduce a class, ' throw UnsupportedError('This result set does not introduce a class, '
'either because it has a matching table or because it only returns ' 'either because it has a matching table or because it only returns '
'one column.'); 'one column.');
@ -209,6 +203,16 @@ abstract class SqlQuery {
return elements; return elements;
} }
QueryRowType queryRowType(DriftOptions options) {
final resultSet = this.resultSet;
if (resultSet == null) {
throw StateError('This query ($name) does not have a result set');
}
return resultSet.mappingToRowClass(
_useResultClassName ? resultClassName : null, options);
}
} }
class SqlSelectQuery extends SqlQuery { class SqlSelectQuery extends SqlQuery {
@ -230,9 +234,6 @@ class SqlSelectQuery extends SqlQuery {
bool get hasNestedQuery => bool get hasNestedQuery =>
resultSet.nestedResults.any((e) => e is NestedResultQuery); resultSet.nestedResults.any((e) => e is NestedResultQuery);
@override
bool get needsAsyncMapping => hasNestedQuery || super.needsAsyncMapping;
SqlSelectQuery( SqlSelectQuery(
String name, String name,
this.fromContext, this.fromContext,
@ -416,7 +417,7 @@ class InferredResultSet {
/// If specified, an existing user-defined Dart type to use instead of /// If specified, an existing user-defined Dart type to use instead of
/// generating another class for the result of this query. /// generating another class for the result of this query.
final ExistingQueryRowType? existingRowType; final QueryRowType? existingRowType;
/// Explicitly controls that no result class should be generated for this /// Explicitly controls that no result class should be generated for this
/// result set. /// result set.
@ -494,21 +495,86 @@ class InferredResultSet {
const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality()); const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality());
return columnsEquality.equals(columns, other.columns); return columnsEquality.equals(columns, other.columns);
} }
/// Returns [existingRowType], or constructs an equivalent mapping to the
/// default row class generated by drift_dev.
///
/// The code to map raw result sets into structured data, be it into a class
/// written by a user or something generated by drift_dev, is really similar.
/// To share that logic in the query writer, we represent both mappings with
/// the same [QueryRowType] class.
QueryRowType mappingToRowClass(
String? resultClassName, DriftOptions options) {
final existingType = existingRowType;
final matchingTable = this.matchingTable;
if (existingType != null) {
return existingType;
} else if (singleColumn) {
final column = scalarColumns.single;
return QueryRowType(
rowType: AnnotatedDartCode.build((b) => b.addDriftType(column)),
singleValue: _columnAsArgument(column, options),
positionalArguments: const [],
namedArguments: const {},
);
} else if (matchingTable != null) {
return QueryRowType(
rowType: AnnotatedDartCode.build(
(b) => b.addElementRowType(matchingTable.table)),
singleValue: matchingTable,
positionalArguments: const [],
namedArguments: const {},
);
} else {
return QueryRowType(
rowType: AnnotatedDartCode.build((b) => b.addText(resultClassName!)),
singleValue: null,
positionalArguments: const [],
namedArguments: {
if (options.rawResultSetData) 'row': RawQueryRow(),
for (final column in columns)
dartNameFor(column): _columnAsArgument(column, options),
},
);
}
}
ArgumentForQueryRowType _columnAsArgument(
ResultColumn column,
DriftOptions options,
) {
return switch (column) {
ScalarResultColumn() => column,
NestedResultTable() => StructuredFromNestedColumn(
column,
column.innerResultSet
.mappingToRowClass(column.nameForGeneratedRowClass, options),
),
NestedResultQuery() => MappedNestedListQuery(
column,
column.query.queryRowType(options),
),
};
}
} }
class ExistingQueryRowType implements ArgumentForExistingQueryRowType { /// Describes a data type for a query, and how to map raw data into that
/// structured type.
class QueryRowType implements ArgumentForQueryRowType {
final AnnotatedDartCode rowType; final AnnotatedDartCode rowType;
final String constructorName; final String constructorName;
final bool isRecord; final bool isRecord;
/// When set, instead of constructing the [rowType] from the arguments, the /// When set, instead of constructing the [rowType] from the arguments, the
/// argument specified here can just be cast into the desired [rowType]. /// argument specified here can just be cast into the desired [rowType].
ArgumentForExistingQueryRowType? singleValue; ArgumentForQueryRowType? singleValue;
final List<ArgumentForExistingQueryRowType> positionalArguments; final List<ArgumentForQueryRowType> positionalArguments;
final Map<String, ArgumentForExistingQueryRowType> namedArguments; final Map<String, ArgumentForQueryRowType> namedArguments;
ExistingQueryRowType({ QueryRowType({
required this.rowType, required this.rowType,
required this.singleValue, required this.singleValue,
required this.positionalArguments, required this.positionalArguments,
@ -517,6 +583,19 @@ class ExistingQueryRowType implements ArgumentForExistingQueryRowType {
this.isRecord = false, this.isRecord = false,
}); });
Iterable<ArgumentForQueryRowType> get allArguments sync* {
if (singleValue != null) {
yield singleValue!;
} else {
yield* positionalArguments;
yield* namedArguments.values;
}
}
@override
bool get requiresAsynchronousContext =>
allArguments.any((arg) => arg.requiresAsynchronousContext);
@override @override
String toString() { String toString() {
return 'ExistingQueryRowType(type: $rowType, singleValue: $singleValue, ' return 'ExistingQueryRowType(type: $rowType, singleValue: $singleValue, '
@ -524,26 +603,60 @@ class ExistingQueryRowType implements ArgumentForExistingQueryRowType {
} }
} }
@sealed sealed class ArgumentForQueryRowType {
abstract class ArgumentForExistingQueryRowType {} /// Whether the code constructing this argument may need to be in an async
/// context.
bool get requiresAsynchronousContext;
}
class MappedNestedListQuery extends ArgumentForExistingQueryRowType { /// An argument that just maps the raw query row.
///
/// This is used for generated query classes which can optionally hold a
/// reference to the raw result set.
class RawQueryRow extends ArgumentForQueryRowType {
@override
bool get requiresAsynchronousContext => false;
}
class StructuredFromNestedColumn extends ArgumentForQueryRowType {
final NestedResultTable table;
final QueryRowType nestedType;
bool get nullable => table.isNullable;
StructuredFromNestedColumn(this.table, this.nestedType);
@override
bool get requiresAsynchronousContext =>
nestedType.requiresAsynchronousContext;
}
class MappedNestedListQuery extends ArgumentForQueryRowType {
final NestedResultQuery column; final NestedResultQuery column;
final ExistingQueryRowType nestedType; final QueryRowType nestedType;
MappedNestedListQuery(this.column, this.nestedType); MappedNestedListQuery(this.column, this.nestedType);
// List queries run another statement and always need an asynchronous mapping.
@override
bool get requiresAsynchronousContext => true;
} }
/// Information about a matching table. A table matches a query if a query /// Information about a matching table. A table matches a query if a query
/// selects all columns from that table, and nothing more. /// selects all columns from that table, and nothing more.
/// ///
/// We still need to handle column aliases. /// We still need to handle column aliases.
class MatchingDriftTable implements ArgumentForExistingQueryRowType { class MatchingDriftTable implements ArgumentForQueryRowType {
final DriftElementWithResultSet table; final DriftElementWithResultSet table;
final Map<String, DriftColumn> aliasToColumn; final Map<String, DriftColumn> aliasToColumn;
MatchingDriftTable(this.table, this.aliasToColumn); MatchingDriftTable(this.table, this.aliasToColumn);
@override
// Mapping from tables is currently asynchronous because the existing data
// class could be an asynchronous factory.
bool get requiresAsynchronousContext => true;
/// Whether the column alias can be ignored. /// Whether the column alias can be ignored.
/// ///
/// This is the case if each result column name maps to a drift column with /// This is the case if each result column name maps to a drift column with
@ -554,8 +667,7 @@ class MatchingDriftTable implements ArgumentForExistingQueryRowType {
} }
} }
@sealed sealed class ResultColumn {
abstract class ResultColumn {
/// A unique name for this column in Dart. /// A unique name for this column in Dart.
String dartGetterName(Iterable<String> existingNames); String dartGetterName(Iterable<String> existingNames);
@ -567,8 +679,8 @@ abstract class ResultColumn {
bool isCompatibleTo(ResultColumn other); bool isCompatibleTo(ResultColumn other);
} }
class ScalarResultColumn extends ResultColumn final class ScalarResultColumn extends ResultColumn
implements HasType, ArgumentForExistingQueryRowType { implements HasType, ArgumentForQueryRowType {
final String name; final String name;
@override @override
final DriftSqlType sqlType; final DriftSqlType sqlType;
@ -587,6 +699,9 @@ class ScalarResultColumn extends ResultColumn
@override @override
bool get isArray => false; bool get isArray => false;
@override
bool get requiresAsynchronousContext => false;
@override @override
String dartGetterName(Iterable<String> existingNames) { String dartGetterName(Iterable<String> existingNames) {
return dartNameForSqlColumn(name, existingNames: existingNames); return dartNameForSqlColumn(name, existingNames: existingNames);
@ -610,7 +725,7 @@ class ScalarResultColumn extends ResultColumn
/// A nested result, could either be a [NestedResultTable] or a /// A nested result, could either be a [NestedResultTable] or a
/// [NestedResultQuery]. /// [NestedResultQuery].
abstract class NestedResult extends ResultColumn {} sealed class NestedResult extends ResultColumn {}
/// A nested table extracted from a `**` column. /// A nested table extracted from a `**` column.
/// ///
@ -638,14 +753,24 @@ abstract class NestedResult extends ResultColumn {}
/// ///
/// Knowing that `User` should be extracted into a field is represented with a /// Knowing that `User` should be extracted into a field is represented with a
/// [NestedResultTable] information as part of the result set. /// [NestedResultTable] information as part of the result set.
class NestedResultTable extends NestedResult final class NestedResultTable extends NestedResult {
implements ArgumentForExistingQueryRowType {
final bool isNullable; final bool isNullable;
final NestedStarResultColumn from; final NestedStarResultColumn from;
final String name; final String name;
final DriftElementWithResultSet table;
NestedResultTable(this.from, this.name, this.table, {this.isNullable = true}); /// The inner result set, e.g. the table or subquery/table-valued function
/// that the [from] column resolves to.
final InferredResultSet innerResultSet;
final String nameForGeneratedRowClass;
NestedResultTable({
required this.from,
required this.name,
required this.innerResultSet,
required this.nameForGeneratedRowClass,
this.isNullable = true,
});
@override @override
String dartGetterName(Iterable<String> existingNames) { String dartGetterName(Iterable<String> existingNames) {
@ -655,7 +780,7 @@ class NestedResultTable extends NestedResult
/// [hashCode] that matches [isCompatibleTo] instead of `==`. /// [hashCode] that matches [isCompatibleTo] instead of `==`.
@override @override
int get compatibilityHashCode { int get compatibilityHashCode {
return Object.hash(name, table); return Object.hash(name, innerResultSet.compatibilityHashCode);
} }
/// Checks whether this is compatible to the [other] nested result, which is /// Checks whether this is compatible to the [other] nested result, which is
@ -665,12 +790,12 @@ class NestedResultTable extends NestedResult
if (other is! NestedResultTable) return false; if (other is! NestedResultTable) return false;
return other.name == name && return other.name == name &&
other.table == table && other.innerResultSet.isCompatibleTo(other.innerResultSet) &&
other.isNullable == isNullable; other.isNullable == isNullable;
} }
} }
class NestedResultQuery extends NestedResult { final class NestedResultQuery extends NestedResult {
final NestedQueryColumn from; final NestedQueryColumn from;
final SqlSelectQuery query; final SqlSelectQuery query;

View File

@ -179,11 +179,16 @@ DialectOptions _$DialectOptionsFromJson(Map json) => $checkedCreate(
($checkedConvert) { ($checkedConvert) {
$checkKeys( $checkKeys(
json, json,
allowedKeys: const ['dialect', 'options'], allowedKeys: const ['dialect', 'dialects', 'options'],
); );
final val = DialectOptions( final val = DialectOptions(
$checkedConvert( $checkedConvert(
'dialect', (v) => $enumDecode(_$SqlDialectEnumMap, v)), 'dialect', (v) => $enumDecodeNullable(_$SqlDialectEnumMap, v)),
$checkedConvert(
'dialects',
(v) => (v as List<dynamic>?)
?.map((e) => $enumDecode(_$SqlDialectEnumMap, e))
.toList()),
$checkedConvert( $checkedConvert(
'options', 'options',
(v) => (v) =>
@ -195,7 +200,9 @@ DialectOptions _$DialectOptionsFromJson(Map json) => $checkedCreate(
Map<String, dynamic> _$DialectOptionsToJson(DialectOptions instance) => Map<String, dynamic> _$DialectOptionsToJson(DialectOptions instance) =>
<String, dynamic>{ <String, dynamic>{
'dialect': _$SqlDialectEnumMap[instance.dialect]!, 'dialect': _$SqlDialectEnumMap[instance.dialect],
'dialects':
instance.dialects?.map((e) => _$SqlDialectEnumMap[e]!).toList(),
'options': instance.options?.toJson(), 'options': instance.options?.toJson(),
}; };

View File

@ -220,25 +220,37 @@ class DatabaseWriter {
} }
static String createTrigger(Scope scope, DriftTrigger entity) { static String createTrigger(Scope scope, DriftTrigger entity) {
final sql = scope.sqlCode(entity.parsedStatement!); final (sql, dialectSpecific) = scope.sqlByDialect(entity.parsedStatement!);
final trigger = scope.drift('Trigger'); final trigger = scope.drift('Trigger');
return '$trigger(${asDartLiteral(sql)}, ${asDartLiteral(entity.schemaName)})'; if (dialectSpecific) {
return '$trigger.byDialect(${asDartLiteral(entity.schemaName)}, $sql)';
} else {
return '$trigger($sql, ${asDartLiteral(entity.schemaName)})';
}
} }
static String createIndex(Scope scope, DriftIndex entity) { static String createIndex(Scope scope, DriftIndex entity) {
final sql = scope.sqlCode(entity.parsedStatement!); final (sql, dialectSpecific) = scope.sqlByDialect(entity.parsedStatement!);
final index = scope.drift('Index'); final index = scope.drift('Index');
return '$index(${asDartLiteral(entity.schemaName)}, ${asDartLiteral(sql)})'; if (dialectSpecific) {
return '$index.byDialect(${asDartLiteral(entity.schemaName)}, $sql)';
} else {
return '$index(${asDartLiteral(entity.schemaName)}, $sql)';
}
} }
static String createOnCreate( static String createOnCreate(
Scope scope, DefinedSqlQuery query, SqlQuery resolved) { Scope scope, DefinedSqlQuery query, SqlQuery resolved) {
final sql = scope.sqlCode(resolved.root!); final (sql, dialectSpecific) = scope.sqlByDialect(resolved.root!);
final onCreate = scope.drift('OnCreateQuery'); final onCreate = scope.drift('OnCreateQuery');
return '$onCreate(${asDartLiteral(sql)})'; if (dialectSpecific) {
return '$onCreate.byDialect($sql)';
} else {
return '$onCreate($sql)';
}
} }
} }

View File

@ -1,3 +1,4 @@
import 'package:drift/drift.dart';
import 'package:recase/recase.dart'; import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn; import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
@ -13,6 +14,16 @@ import 'utils.dart';
const highestAssignedIndexVar = '\$arrayStartIndex'; const highestAssignedIndexVar = '\$arrayStartIndex';
typedef _ArgumentContext = ({
// Indicates that the argument is available under a prefix in SQL, probably
// because it comes from a [NestedResultTable].
String? sqlPrefix,
// Indicates that, even if the argument appears to be non-nullable by itself,
// it comes from a [NestedResultTable] part of an outer join that could make
// the entire structure nullable.
bool isNullable,
});
/// Writes the handling code for a query. The code emitted will be a method that /// Writes the handling code for a query. The code emitted will be a method that
/// should be included in a generated database or dao class. /// should be included in a generated database or dao class.
class QueryWriter { class QueryWriter {
@ -74,132 +85,131 @@ class QueryWriter {
/// Writes the function literal that turns a "QueryRow" into the desired /// Writes the function literal that turns a "QueryRow" into the desired
/// custom return type of a query. /// custom return type of a query.
void _writeMappingLambda(SqlQuery query) { void _writeMappingLambda(InferredResultSet resultSet, QueryRowType rowClass) {
final resultSet = query.resultSet!;
final queryRow = _emitter.drift('QueryRow'); final queryRow = _emitter.drift('QueryRow');
final existingRowType = resultSet.existingRowType; final asyncModifier = rowClass.requiresAsynchronousContext ? 'async' : '';
final asyncModifier = query.needsAsyncMapping ? 'async' : '';
if (existingRowType != null) { // We can write every available mapping as a Dart expression via
_emitter.write('($queryRow row) $asyncModifier => '); // _writeArgumentExpression. This can be turned into a lambda by appending
_writeArgumentExpression(existingRowType, resultSet); // it with `(QueryRow row) => $expression`. That's also what we're doing,
} else if (resultSet.singleColumn) { // but if we'll just call mapFromRow in there, we can just tear that method
final column = resultSet.scalarColumns.single; // off instead. This is just an optimization.
_emitter.write('($queryRow row) => '); final singleValue = rowClass.singleValue;
_readScalar(column); if (singleValue is MatchingDriftTable && singleValue.effectivelyNoAlias) {
} else if (resultSet.matchingTable != null) { // Tear-off mapFromRow method on table
final match = resultSet.matchingTable!; _emitter.write('${singleValue.table.dbGetterName}.mapFromRow');
if (match.effectivelyNoAlias) {
// Tear-off mapFromRow method on table
_emitter.write('${match.table.dbGetterName}.mapFromRow');
} else {
_emitter.write('($queryRow row) => ');
_writeArgumentExpression(match, resultSet);
}
} else { } else {
_buffer // In all other cases, we're off to write the expression.
..writeln('($queryRow row) $asyncModifier {') _emitter.write('($queryRow row) $asyncModifier => ');
..write('return ${query.resultClassName}('); _writeArgumentExpression(
rowClass, resultSet, (sqlPrefix: null, isNullable: false));
if (options.rawResultSetData) {
_buffer.write('row: row,\n');
}
for (final column in resultSet.columns) {
final fieldName = resultSet.dartNameFor(column);
if (column is ScalarResultColumn) {
_buffer.write('$fieldName: ');
_readScalar(column);
_buffer.write(', ');
} else if (column is NestedResultTable) {
final prefix = resultSet.nestedPrefixFor(column);
if (prefix == null) continue;
_buffer.write('$fieldName: ');
_readNestedTable(column, prefix);
_buffer.write(',');
} else if (column is NestedResultQuery) {
_buffer.write('$fieldName: await ');
_writeCustomSelectStatement(column.query);
_buffer.write('.get(),');
}
}
_buffer.write(');\n}');
} }
} }
/// Writes code that will read the [argument] for an existing row type from /// Writes code that will read the [argument] for an existing row type from
/// the raw `QueryRow`. /// the raw `QueryRow`.
void _writeArgumentExpression( void _writeArgumentExpression(
ArgumentForExistingQueryRowType argument, InferredResultSet resultSet) { ArgumentForQueryRowType argument,
if (argument is MappedNestedListQuery) { InferredResultSet resultSet,
final queryRow = _emitter.drift('QueryRow'); _ArgumentContext context,
) {
_buffer.write('await '); switch (argument) {
_writeCustomSelectStatement(argument.column.query, case RawQueryRow():
includeMappingToDart: false); _buffer.write('row');
_buffer.write('.map('); case ScalarResultColumn():
_buffer.write('($queryRow row) => '); _readScalar(argument, context);
_writeArgumentExpression(argument.nestedType, resultSet); case MatchingDriftTable():
_buffer.write(').get()'); _readMatchingTable(argument, context);
} else if (argument is ExistingQueryRowType) { case StructuredFromNestedColumn():
final singleValue = argument.singleValue; final prefix = resultSet.nestedPrefixFor(argument.table);
if (singleValue != null) { _writeArgumentExpression(
return _writeArgumentExpression(singleValue, resultSet); argument.nestedType,
} resultSet,
(sqlPrefix: prefix, isNullable: argument.nullable),
if (!argument.isRecord) { );
// We're writing a constructor, so let's start with the class name. case MappedNestedListQuery():
_emitter.writeDart(argument.rowType); _buffer.write('await ');
final query = argument.column.query;
final constructorName = argument.constructorName; _writeCustomSelectStatement(query, argument.nestedType);
if (constructorName.isNotEmpty) { _buffer.write('.get()');
_emitter case QueryRowType():
..write('.') final singleValue = argument.singleValue;
..write(constructorName); if (singleValue != null) {
return _writeArgumentExpression(singleValue, resultSet, context);
} }
}
_buffer.write('('); if (context.isNullable) {
for (final positional in argument.positionalArguments) { // If this structed type is nullable, it's coming from an OUTER join
_writeArgumentExpression(positional, resultSet); // which means that, even if the individual components making up the
_buffer.write(', '); // structure are non-nullable, they might all be null in SQL. We
} // detect this case by looking for a non-nullable column and, if it's
argument.namedArguments.forEach((name, parameter) { // null, return null directly instead of creating the structured type.
_buffer.write('$name: '); for (final arg in argument.positionalArguments
_writeArgumentExpression(parameter, resultSet); .followedBy(argument.namedArguments.values)
_buffer.write(', '); .whereType<ScalarResultColumn>()) {
}); if (!arg.nullable) {
final keyInMap = context.applyPrefix(arg.name);
_buffer.write(
'row.data[${asDartLiteral(keyInMap)}] == null ? null : ');
}
}
}
_buffer.write(')'); final _ArgumentContext childContext = (
} else if (argument is NestedResultTable) { sqlPrefix: context.sqlPrefix,
final prefix = resultSet.nestedPrefixFor(argument); // Individual fields making up this query row type aren't covered by
_readNestedTable(argument, prefix!); // the outer nullability.
} else if (argument is ScalarResultColumn) { isNullable: false,
return _readScalar(argument); );
} else if (argument is MatchingDriftTable) {
_readMatchingTable(argument); if (!argument.isRecord) {
// We're writing a constructor, so let's start with the class name.
_emitter.writeDart(argument.rowType);
final constructorName = argument.constructorName;
if (constructorName.isNotEmpty) {
_emitter
..write('.')
..write(constructorName);
}
}
_buffer.write('(');
for (final positional in argument.positionalArguments) {
_writeArgumentExpression(positional, resultSet, childContext);
_buffer.write(', ');
}
argument.namedArguments.forEach((name, parameter) {
_buffer.write('$name: ');
_writeArgumentExpression(parameter, resultSet, childContext);
_buffer.write(', ');
});
_buffer.write(')');
} }
} }
/// Writes Dart code that, given a variable of type `QueryRow` named `row` /// Writes Dart code that, given a variable of type `QueryRow` named `row`
/// in the same scope, reads the [column] from that row and brings it into a /// in the same scope, reads the [column] from that row and brings it into a
/// suitable type. /// suitable type.
void _readScalar(ScalarResultColumn column) { void _readScalar(ScalarResultColumn column, _ArgumentContext context) {
final specialName = _transformer.newNameFor(column.sqlParserColumn!); final specialName = _transformer.newNameFor(column.sqlParserColumn!);
final isNullable = context.isNullable || column.nullable;
final dartLiteral = asDartLiteral(specialName ?? column.name); var name = specialName ?? column.name;
final method = column.nullable ? 'readNullable' : 'read'; if (context.sqlPrefix != null) {
name = '${context.sqlPrefix}.$name';
}
final dartLiteral = asDartLiteral(name);
final method = isNullable ? 'readNullable' : 'read';
final rawDartType = final rawDartType =
_emitter.dartCode(AnnotatedDartCode([dartTypeNames[column.sqlType]!])); _emitter.dartCode(AnnotatedDartCode([dartTypeNames[column.sqlType]!]));
var code = 'row.$method<$rawDartType>($dartLiteral)'; var code = 'row.$method<$rawDartType>($dartLiteral)';
final converter = column.typeConverter; final converter = column.typeConverter;
if (converter != null) { if (converter != null) {
if (converter.canBeSkippedForNulls && column.nullable) { if (converter.canBeSkippedForNulls && isNullable) {
// The type converter maps non-nullable types, but the column may be // The type converter maps non-nullable types, but the column may be
// nullable in SQL => just map null to null and only invoke the type // nullable in SQL => just map null to null and only invoke the type
// converter for non-null values. // converter for non-null values.
@ -214,36 +224,52 @@ class QueryWriter {
_emitter.write(code); _emitter.write(code);
} }
void _readMatchingTable(MatchingDriftTable match) { void _readMatchingTable(MatchingDriftTable match, _ArgumentContext context) {
// note that, even if the result set has a matching table, we can't just // note that, even if the result set has a matching table, we can't just
// use the mapFromRow() function of that table - the column names might // use the mapFromRow() function of that table - the column names might
// be different! // be different!
final table = match.table; final table = match.table;
if (match.effectivelyNoAlias) { if (match.effectivelyNoAlias) {
_emitter.write('${table.dbGetterName}.mapFromRow(row)'); final mappingMethod =
context.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
final sqlPrefix = context.sqlPrefix;
_emitter.write('await ${table.dbGetterName}.$mappingMethod(row');
if (sqlPrefix != null) {
_emitter.write(', tablePrefix: ${asDartLiteral(sqlPrefix)}');
}
_emitter.write(')');
} else { } else {
// If the entire table can be nullable, we can check whether a non-nullable
// column from the table is null. If it is, the entire table is null. This
// can happen when the table comes from an outer join.
if (context.isNullable) {
for (final MapEntry(:key, :value) in match.aliasToColumn.entries) {
if (!value.nullable) {
final mapKey = context.applyPrefix(key);
_emitter
.write('row.data[${asDartLiteral(mapKey)}] == null ? null : ');
}
}
}
_emitter.write('${table.dbGetterName}.mapFromRowWithAlias(row, const {'); _emitter.write('${table.dbGetterName}.mapFromRowWithAlias(row, const {');
for (final alias in match.aliasToColumn.entries) { for (final alias in match.aliasToColumn.entries) {
_emitter _emitter
..write(asDartLiteral(alias.key)) ..write(asDartLiteral(context.applyPrefix(alias.key)))
..write(': ') ..write(': ')
..write(asDartLiteral(alias.value.nameInSql)) ..write(asDartLiteral(alias.value.nameInSql))
..write(', '); ..write(', ');
} }
_emitter.write('})'); _emitter.write('})');
} }
} }
void _readNestedTable(NestedResultTable table, String prefix) {
final tableGetter = table.table.dbGetterName;
final mappingMethod = table.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
_emitter.write('await $tableGetter.$mappingMethod(row, '
'tablePrefix: ${asDartLiteral(prefix)})');
}
/// Writes a method returning a `Selectable<T>`, where `T` is the return type /// Writes a method returning a `Selectable<T>`, where `T` is the return type
/// of the custom query. /// of the custom query.
void _writeSelectStatementCreator(SqlSelectQuery select) { void _writeSelectStatementCreator(SqlSelectQuery select) {
@ -270,22 +296,22 @@ class QueryWriter {
} }
void _writeCustomSelectStatement(SqlSelectQuery select, void _writeCustomSelectStatement(SqlSelectQuery select,
{bool includeMappingToDart = true}) { [QueryRowType? resultType]) {
_buffer.write(' customSelect(${_queryCode(select)}, '); _buffer.write(' customSelect(${_queryCode(select)}, ');
_writeVariables(select); _writeVariables(select);
_buffer.write(', '); _buffer.write(', ');
_writeReadsFrom(select); _writeReadsFrom(select);
if (includeMappingToDart) { final resultSet = select.resultSet;
if (select.needsAsyncMapping) { resultType ??= select.queryRowType(options);
_buffer.write(').asyncMap(');
} else {
_buffer.write(').map(');
}
_writeMappingLambda(select); if (resultType.requiresAsynchronousContext) {
_buffer.write(').asyncMap(');
} else {
_buffer.write(').map(');
} }
_writeMappingLambda(resultSet, resultType);
_buffer.write(')'); _buffer.write(')');
} }
@ -311,13 +337,17 @@ class QueryWriter {
_writeCommonUpdateParameters(update); _writeCommonUpdateParameters(update);
_buffer.write(').then((rows) => '); _buffer.write(').then((rows) => ');
if (update.needsAsyncMapping) {
final resultSet = update.resultSet!;
final rowType = update.queryRowType(options);
if (rowType.requiresAsynchronousContext) {
_buffer.write('Future.wait(rows.map('); _buffer.write('Future.wait(rows.map(');
_writeMappingLambda(update); _writeMappingLambda(resultSet, rowType);
_buffer.write('))'); _buffer.write('))');
} else { } else {
_buffer.write('rows.map('); _buffer.write('rows.map(');
_writeMappingLambda(update); _writeMappingLambda(resultSet, rowType);
_buffer.write(').toList()'); _buffer.write(').toList()');
} }
_buffer.write(');\n}'); _buffer.write(');\n}');
@ -466,7 +496,45 @@ class QueryWriter {
/// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned /// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned
/// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'. /// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'.
String _queryCode(SqlQuery query) { String _queryCode(SqlQuery query) {
return SqlWriter(scope.options, query: query).write(); final dialectForCode = <String, List<SqlDialect>>{};
for (final dialect in scope.options.supportedDialects) {
final code =
SqlWriter(scope.options, dialect: dialect, query: query).write();
dialectForCode.putIfAbsent(code, () => []).add(dialect);
}
if (dialectForCode.length == 1) {
// All supported dialects use the same SQL syntax, so we can just use that
return dialectForCode.keys.single;
} else {
// Create a switch expression matching over the dialect of the database
// we're connected to.
final buffer = StringBuffer('switch (executor.dialect) {');
final dialectEnum = scope.drift('SqlDialect');
var index = 0;
for (final MapEntry(key: code, value: dialects)
in dialectForCode.entries) {
index++;
buffer
.write(dialects.map((e) => '$dialectEnum.${e.name}').join(' || '));
if (index == dialectForCode.length) {
// In the last branch, match all dialects as a fallback
buffer.write(' || _ ');
}
buffer
..write(' => ')
..write(code)
..write(', ');
}
buffer.writeln('}');
return buffer.toString();
}
} }
void _writeReadsFrom(SqlSelectQuery select) { void _writeReadsFrom(SqlSelectQuery select) {
@ -789,9 +857,14 @@ String? _defaultForDartPlaceholder(
if (kind is ExpressionDartPlaceholderType && kind.defaultValue != null) { if (kind is ExpressionDartPlaceholderType && kind.defaultValue != null) {
// Wrap the default expression in parentheses to avoid issues with // Wrap the default expression in parentheses to avoid issues with
// the surrounding precedence in SQL. // the surrounding precedence in SQL.
final sql = SqlWriter(scope.options) final (sql, dialectSpecific) =
.writeNodeIntoStringLiteral(Parentheses(kind.defaultValue!)); scope.sqlByDialect(Parentheses(kind.defaultValue!));
return 'const ${scope.drift('CustomExpression')}($sql)';
if (dialectSpecific) {
return 'const ${scope.drift('CustomExpression')}.dialectSpecific($sql)';
} else {
return 'const ${scope.drift('CustomExpression')}($sql)';
}
} else if (kind is SimpleDartPlaceholderType && } else if (kind is SimpleDartPlaceholderType &&
kind.kind == SimpleDartPlaceholderKind.orderBy) { kind.kind == SimpleDartPlaceholderKind.orderBy) {
return 'const ${scope.drift('OrderBy')}.nothing()'; return 'const ${scope.drift('OrderBy')}.nothing()';
@ -800,3 +873,12 @@ String? _defaultForDartPlaceholder(
return null; return null;
} }
} }
extension on _ArgumentContext {
String applyPrefix(String originalName) {
return switch (sqlPrefix) {
null => originalName,
var s => '$s.$originalName',
};
}
}

View File

@ -5,20 +5,23 @@ import '../writer.dart';
/// Writes a class holding the result of an sql query into Dart. /// Writes a class holding the result of an sql query into Dart.
class ResultSetWriter { class ResultSetWriter {
final SqlQuery query; final InferredResultSet resultSet;
final String resultClassName;
final Scope scope; final Scope scope;
ResultSetWriter(this.query, this.scope); ResultSetWriter(SqlQuery query, this.scope)
: resultSet = query.resultSet!,
resultClassName = query.resultClassName;
ResultSetWriter.fromResultSetAndClassName(
this.resultSet, this.resultClassName, this.scope);
void write() { void write() {
final className = query.resultClassName;
final fields = <EqualityField>[]; final fields = <EqualityField>[];
final nonNullableFields = <String>{}; final nonNullableFields = <String>{};
final into = scope.leaf(); final into = scope.leaf();
final resultSet = query.resultSet!; into.write('class $resultClassName ');
into.write('class $className ');
if (scope.options.rawResultSetData) { if (scope.options.rawResultSetData) {
into.write('extends CustomResultSet {\n'); into.write('extends CustomResultSet {\n');
} else { } else {
@ -39,6 +42,12 @@ class ResultSetWriter {
fields.add(EqualityField(fieldName, isList: column.isUint8ListInDart)); fields.add(EqualityField(fieldName, isList: column.isUint8ListInDart));
if (!column.nullable) nonNullableFields.add(fieldName); if (!column.nullable) nonNullableFields.add(fieldName);
} else if (column is NestedResultTable) { } else if (column is NestedResultTable) {
if (column.innerResultSet.needsOwnClass) {
ResultSetWriter.fromResultSetAndClassName(
column.innerResultSet, column.nameForGeneratedRowClass, scope)
.write();
}
into into
..write('$modifier ') ..write('$modifier ')
..writeDart( ..writeDart(
@ -66,9 +75,9 @@ class ResultSetWriter {
// write the constructor // write the constructor
if (scope.options.rawResultSetData) { if (scope.options.rawResultSetData) {
into.write('$className({required QueryRow row,'); into.write('$resultClassName({required QueryRow row,');
} else { } else {
into.write('$className({'); into.write('$resultClassName({');
} }
for (final column in fields) { for (final column in fields) {
@ -90,9 +99,9 @@ class ResultSetWriter {
writeHashCode(fields, into); writeHashCode(fields, into);
into.write(';\n'); into.write(';\n');
overrideEquals(fields, className, into); overrideEquals(fields, resultClassName, into);
overrideToString( overrideToString(
className, fields.map((f) => f.lexeme).toList(), into.buffer); resultClassName, fields.map((f) => f.lexeme).toList(), into.buffer);
} }
into.write('}\n'); into.write('}\n');

View File

@ -26,8 +26,9 @@ String placeholderContextName(FoundDartPlaceholder placeholder) {
} }
extension ToSqlText on AstNode { extension ToSqlText on AstNode {
String toSqlWithoutDriftSpecificSyntax(DriftOptions options) { String toSqlWithoutDriftSpecificSyntax(
final writer = SqlWriter(options, escapeForDart: false); DriftOptions options, SqlDialect dialect) {
final writer = SqlWriter(options, dialect: dialect, escapeForDart: false);
return writer.writeSql(this); return writer.writeSql(this);
} }
} }
@ -36,17 +37,19 @@ class SqlWriter extends NodeSqlBuilder {
final StringBuffer _out; final StringBuffer _out;
final SqlQuery? query; final SqlQuery? query;
final DriftOptions options; final DriftOptions options;
final SqlDialect dialect;
final Map<NestedStarResultColumn, NestedResultTable> _starColumnToResolved; final Map<NestedStarResultColumn, NestedResultTable> _starColumnToResolved;
bool get _isPostgres => options.effectiveDialect == SqlDialect.postgres; bool get _isPostgres => dialect == SqlDialect.postgres;
SqlWriter._(this.query, this.options, this._starColumnToResolved, SqlWriter._(this.query, this.options, this.dialect,
StringBuffer out, bool escapeForDart) this._starColumnToResolved, StringBuffer out, bool escapeForDart)
: _out = out, : _out = out,
super(escapeForDart ? _DartEscapingSink(out) : out); super(escapeForDart ? _DartEscapingSink(out) : out);
factory SqlWriter( factory SqlWriter(
DriftOptions options, { DriftOptions options, {
required SqlDialect dialect,
SqlQuery? query, SqlQuery? query,
bool escapeForDart = true, bool escapeForDart = true,
StringBuffer? buffer, StringBuffer? buffer,
@ -61,7 +64,7 @@ class SqlWriter extends NodeSqlBuilder {
if (nestedResult is NestedResultTable) nestedResult.from: nestedResult if (nestedResult is NestedResultTable) nestedResult.from: nestedResult
}; };
} }
return SqlWriter._(query, options, doubleStarColumnToResolvedTable, return SqlWriter._(query, options, dialect, doubleStarColumnToResolvedTable,
buffer ?? StringBuffer(), escapeForDart); buffer ?? StringBuffer(), escapeForDart);
} }
@ -84,7 +87,7 @@ class SqlWriter extends NodeSqlBuilder {
@override @override
bool isKeyword(String lexeme) { bool isKeyword(String lexeme) {
switch (options.effectiveDialect) { switch (dialect) {
case SqlDialect.postgres: case SqlDialect.postgres:
return isKeywordLexeme(lexeme) || isPostgresKeywordLexeme(lexeme); return isKeywordLexeme(lexeme) || isPostgresKeywordLexeme(lexeme);
default: default:
@ -194,14 +197,14 @@ class SqlWriter extends NodeSqlBuilder {
// Convert foo.** to "foo.a" AS "nested_0.a", ... for all columns in foo // Convert foo.** to "foo.a" AS "nested_0.a", ... for all columns in foo
var isFirst = true; var isFirst = true;
for (final column in result.table.columns) { for (final column in result.innerResultSet.scalarColumns) {
if (isFirst) { if (isFirst) {
isFirst = false; isFirst = false;
} else { } else {
_out.write(', '); _out.write(', ');
} }
final columnName = column.nameInSql; final columnName = column.name;
_out.write('"$table"."$columnName" AS "$prefix.$columnName"'); _out.write('"$table"."$columnName" AS "$prefix.$columnName"');
} }
} else if (e is DartPlaceholder) { } else if (e is DartPlaceholder) {

View File

@ -162,7 +162,7 @@ abstract class TableOrViewWriter {
} }
/// Returns the Dart type and the Dart expression creating a `GeneratedColumn` /// Returns the Dart type and the Dart expression creating a `GeneratedColumn`
/// instance in drift for the givne [column]. /// instance in drift for the given [column].
static (String, String) instantiateColumn( static (String, String) instantiateColumn(
DriftColumn column, DriftColumn column,
TextEmitter emitter, { TextEmitter emitter, {
@ -173,6 +173,10 @@ abstract class TableOrViewWriter {
final expressionBuffer = StringBuffer(); final expressionBuffer = StringBuffer();
final constraints = defaultConstraints(column); final constraints = defaultConstraints(column);
// Remove dialect-specific constraints for dialects we don't care about.
constraints.removeWhere(
(key, _) => !emitter.writer.options.supportedDialects.contains(key));
for (final constraint in column.constraints) { for (final constraint in column.constraints) {
if (constraint is LimitingTextLength) { if (constraint is LimitingTextLength) {
final buffer = final buffer =

View File

@ -75,18 +75,29 @@ class ViewWriter extends TableOrViewWriter {
..write('@override\n String get entityName=>' ..write('@override\n String get entityName=>'
' ${asDartLiteral(view.schemaName)};\n'); ' ${asDartLiteral(view.schemaName)};\n');
emitter
..writeln('@override')
..write('Map<${emitter.drift('SqlDialect')}, String>')
..write(source is! SqlViewSource ? '?' : '')
..write('get createViewStatements => ');
if (source is SqlViewSource) { if (source is SqlViewSource) {
final astNode = source.parsedStatement; final astNode = source.parsedStatement;
emitter.write('@override\nString get createViewStmt =>');
if (astNode != null) { if (astNode != null) {
emitter.writeSqlAsDartLiteral(astNode); emitter.writeSqlByDialectMap(astNode);
} else { } else {
emitter.write(asDartLiteral(source.sqlCreateViewStmt)); final firstDialect = scope.options.supportedDialects.first;
emitter
..write('{')
..writeDriftRef('SqlDialect')
..write('.${firstDialect.name}: ')
..write(asDartLiteral(source.sqlCreateViewStmt))
..write('}');
} }
buffer.writeln(';'); buffer.writeln(';');
} else { } else {
buffer.write('@override\n String? get createViewStmt => null;\n'); buffer.writeln('null;');
} }
writeAsDslTable(); writeAsDslTable();

View File

@ -1,3 +1,4 @@
import 'package:drift/drift.dart';
import 'package:recase/recase.dart'; import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart' as sql; import 'package:sqlparser/sqlparser.dart' as sql;
import 'package:path/path.dart' show url; import 'package:path/path.dart' show url;
@ -228,8 +229,50 @@ abstract class _NodeOrWriter {
return buffer.toString(); return buffer.toString();
} }
String sqlCode(sql.AstNode node) { String sqlCode(sql.AstNode node, SqlDialect dialect) {
return SqlWriter(writer.options, escapeForDart: false).writeSql(node); return SqlWriter(writer.options, dialect: dialect, escapeForDart: false)
.writeSql(node);
}
/// Builds a Dart expression writing the [node] into a Dart string.
///
/// If the code for [node] depends on the dialect, the code returned evaluates
/// to a `Map<SqlDialect, String>`. Otherwise, the code is a direct string
/// literal.
///
/// The boolean component in the record describes whether the code will be
/// dialect specific.
(String, bool) sqlByDialect(sql.AstNode node) {
final dialects = writer.options.supportedDialects;
if (dialects.length == 1) {
return (
SqlWriter(writer.options, dialect: dialects.single)
.writeNodeIntoStringLiteral(node),
false
);
}
final buffer = StringBuffer();
_writeSqlByDialectMap(node, buffer);
return (buffer.toString(), true);
}
void _writeSqlByDialectMap(sql.AstNode node, StringBuffer buffer) {
buffer.write('{');
for (final dialect in writer.options.supportedDialects) {
buffer
..write(drift('SqlDialect'))
..write(".${dialect.name}: '");
SqlWriter(writer.options, dialect: dialect, buffer: buffer)
.writeSql(node);
buffer.writeln("',");
}
buffer.write('}');
} }
} }
@ -302,16 +345,18 @@ class TextEmitter extends _Node {
void writeDart(AnnotatedDartCode code) => write(dartCode(code)); void writeDart(AnnotatedDartCode code) => write(dartCode(code));
void writeSql(sql.AstNode node, {bool escapeForDartString = true}) { void writeSql(sql.AstNode node,
SqlWriter(writer.options, {required SqlDialect dialect, bool escapeForDartString = true}) {
escapeForDart: escapeForDartString, buffer: buffer) SqlWriter(
.writeSql(node); writer.options,
dialect: dialect,
escapeForDart: escapeForDartString,
buffer: buffer,
).writeSql(node);
} }
void writeSqlAsDartLiteral(sql.AstNode node) { void writeSqlByDialectMap(sql.AstNode node) {
buffer.write("'"); _writeSqlByDialectMap(node, buffer);
writeSql(node);
buffer.write("'");
} }
} }

View File

@ -76,7 +76,8 @@ SELECT rowid, highlight(example_table_search, 0, '[match]', '[match]') name,
{'a|lib/a.drift': 'CREATE VIRTUAL TABLE demo USING spellfix1;'}, {'a|lib/a.drift': 'CREATE VIRTUAL TABLE demo USING spellfix1;'},
options: DriftOptions.defaults( options: DriftOptions.defaults(
dialect: DialectOptions( dialect: DialectOptions(
SqlDialect.sqlite, null,
[SqlDialect.sqlite],
SqliteAnalysisOptions( SqliteAnalysisOptions(
modules: [SqlModule.spellfix1], modules: [SqlModule.spellfix1],
), ),

View File

@ -1,7 +1,10 @@
import 'package:drift_dev/src/analysis/options.dart';
import 'package:drift_dev/src/analysis/results/results.dart'; import 'package:drift_dev/src/analysis/results/results.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../../test_utils.dart'; import '../../test_utils.dart';
import 'utils.dart';
void main() { void main() {
test('recognizes existing row classes', () async { test('recognizes existing row classes', () async {
@ -224,78 +227,124 @@ class MyQueryRow {
); );
}); });
test('nested - single column type', () async { group('nested column', () {
final state = TestBackend.inTest({ test('single column into field', () async {
'a|lib/a.drift': ''' final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart'; import 'a.dart';
foo WITH MyQueryRow: SELECT 1 a, LIST(SELECT 2 AS b) c; foo WITH MyQueryRow: SELECT 1 AS a, b.** FROM (SELECT 2 AS b) b;
''', ''',
'a|lib/a.dart': ''' 'a|lib/a.dart': '''
class MyQueryRow { class MyQueryRow {
MyQueryRow(int a, List<int> c); MyQueryRow(int a, int b);
} }
''', ''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyQueryRow',
positional: [
scalarColumn('a'),
structedFromNested(
isExistingRowType(
singleValue: scalarColumn('b'),
),
),
],
),
);
}); });
final file = await state.analyze('package:a/a.drift'); test('single column into single-element record', () async {
state.expectNoErrors(); final state = TestBackend.inTest({
'a|lib/a.drift': '''
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyQueryRow',
positional: [
scalarColumn('a'),
nestedListQuery(
'c',
isExistingRowType(
type: 'int',
singleValue: scalarColumn('b'),
),
),
],
),
);
});
test('nested - table', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart'; import 'a.dart';
CREATE TABLE tbl (foo TEXT, bar INT); foo WITH MyQueryRow: SELECT 1 AS a, b.** FROM (SELECT 2 AS b) b;
foo WITH MyRow: SELECT 1 AS a, b.** FROM tbl
INNER JOIN tbl b ON TRUE;
''', ''',
'a|lib/a.dart': ''' 'a|lib/a.dart': '''
class MyRow { class MyQueryRow {
MyRow(int a, TblData b); MyQueryRow(int a, (int) b);
} }
''', ''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyQueryRow',
positional: [
scalarColumn('a'),
structedFromNested(
isExistingRowType(
positional: [scalarColumn('b')],
isRecord: isTrue,
),
),
],
),
);
}); });
final file = await state.analyze('package:a/a.drift'); test('custom result set', () async {
state.expectNoErrors(); final state = TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
final query = file.fileAnalysis!.resolvedQueries.values.single; foo WITH MyQueryRow: SELECT 1 AS id, j.** FROM json_each('') AS j;
expect( ''',
query.resultSet?.existingRowType, 'a|lib/a.dart': '''
isExistingRowType( class MyQueryRow {
type: 'MyRow', MyQueryRow(int id, JsonStructure j);
positional: [ }
scalarColumn('a'),
nestedTableColumm('b'),
],
),
);
});
test('nested - table as alternative to row class', () async { class JsonStructure {
final state = TestBackend.inTest( JsonStructure(DriftAny key, DriftAny value, String type);
{ }
''',
},
options: const DriftOptions.defaults(
sqliteAnalysisOptions: SqliteAnalysisOptions(
// Make sure json_each is supported
version: SqliteVersion.v3(38),
),
),
);
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyQueryRow',
positional: [
scalarColumn('id'),
structedFromNested(
isExistingRowType(
type: 'JsonStructure',
),
),
],
),
);
});
test('table', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': ''' 'a|lib/a.drift': '''
import 'a.dart'; import 'a.dart';
@ -305,43 +354,122 @@ foo WITH MyRow: SELECT 1 AS a, b.** FROM tbl
INNER JOIN tbl b ON TRUE; INNER JOIN tbl b ON TRUE;
''', ''',
'a|lib/a.dart': ''' 'a|lib/a.dart': '''
class MyRow {
MyRow(int a, TblData b);
}
''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyRow',
positional: [
scalarColumn('a'),
structedFromNested(
isExistingRowType(
type: 'TblData',
singleValue: isA<MatchingDriftTable>(),
),
),
],
),
);
});
test('table as alternative to row class', () async {
final state = TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart';
CREATE TABLE tbl (foo TEXT, bar INT);
foo WITH MyRow: SELECT 1 AS a, b.** FROM tbl
INNER JOIN tbl b ON TRUE;
''',
'a|lib/a.dart': '''
class MyRow { class MyRow {
MyRow(int a, (String, int) b); MyRow(int a, (String, int) b);
} }
''', ''',
}, },
analyzerExperiments: ['records'], analyzerExperiments: ['records'],
); );
final file = await state.analyze('package:a/a.drift'); final file = await state.analyze('package:a/a.drift');
state.expectNoErrors(); state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single; final query = file.fileAnalysis!.resolvedQueries.values.single;
expect( expect(
query.resultSet?.existingRowType, query.resultSet?.existingRowType,
isExistingRowType( isExistingRowType(
type: 'MyRow', type: 'MyRow',
positional: [ positional: [
scalarColumn('a'), scalarColumn('a'),
isExistingRowType( structedFromNested(
type: '(String, int)', isExistingRowType(
positional: [scalarColumn('foo'), scalarColumn('bar')], type: '(String, int)',
), positional: [scalarColumn('foo'), scalarColumn('bar')],
], ),
), ),
); ],
}, skip: 'Blocked by https://github.com/simolus3/drift/issues/2233'); ),
);
});
});
test('nested - custom result set with class', () async { group('nested LIST query', () {
final state = TestBackend.inTest({ test('single column type', () async {
'a|lib/a.drift': ''' final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart';
foo WITH MyQueryRow: SELECT 1 a, LIST(SELECT 2 AS b) c;
''',
'a|lib/a.dart': '''
class MyQueryRow {
MyQueryRow(int a, List<int> c);
}
''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyQueryRow',
positional: [
scalarColumn('a'),
nestedListQuery(
'c',
isExistingRowType(
type: 'int',
singleValue: scalarColumn('b'),
),
),
],
),
);
});
test('custom result set with class', () async {
final state = TestBackend.inTest({
'a|lib/a.drift': '''
import 'a.dart'; import 'a.dart';
CREATE TABLE tbl (foo TEXT, bar INT); CREATE TABLE tbl (foo TEXT, bar INT);
foo WITH MyRow: SELECT 1 AS a, LIST(SELECT * FROM tbl) AS b FROM tbl; foo WITH MyRow: SELECT 1 AS a, LIST(SELECT * FROM tbl) AS b FROM tbl;
''', ''',
'a|lib/a.dart': ''' 'a|lib/a.dart': '''
class MyRow { class MyRow {
MyRow(int a, List<MyNestedTable> b); MyRow(int a, List<MyNestedTable> b);
} }
@ -350,69 +478,70 @@ class MyNestedTable {
MyNestedTable(String foo, int bar) MyNestedTable(String foo, int bar)
} }
''', ''',
});
final file = await state.analyze('package:a/a.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyRow',
positional: [
scalarColumn('a'),
nestedListQuery(
'b',
isExistingRowType(
type: 'MyNestedTable',
positional: [scalarColumn('foo'), scalarColumn('bar')],
),
),
],
),
);
}); });
final file = await state.analyze('package:a/a.drift'); test('custom result set with record', () async {
state.expectNoErrors(); final state = TestBackend.inTest(
{
final query = file.fileAnalysis!.resolvedQueries.values.single; 'a|lib/a.drift': '''
expect(
query.resultSet?.existingRowType,
isExistingRowType(
type: 'MyRow',
positional: [
scalarColumn('a'),
nestedListQuery(
'b',
isExistingRowType(
type: 'MyNestedTable',
positional: [scalarColumn('foo'), scalarColumn('bar')],
),
),
],
),
);
});
test('nested - custom result set with record', () async {
final state = TestBackend.inTest(
{
'a|lib/a.drift': '''
import 'a.dart'; import 'a.dart';
CREATE TABLE tbl (foo TEXT, bar INT); CREATE TABLE tbl (foo TEXT, bar INT);
foo WITH MyRow: SELECT 1 AS a, LIST(SELECT * FROM tbl) AS b FROM tbl; foo WITH MyRow: SELECT 1 AS a, LIST(SELECT * FROM tbl) AS b FROM tbl;
''', ''',
'a|lib/a.dart': ''' 'a|lib/a.dart': '''
class MyRow { class MyRow {
MyRow(int a, List<(String, int)> b); MyRow(int a, List<(String, int)> b);
} }
''', ''',
}, },
analyzerExperiments: ['records'], analyzerExperiments: ['records'],
); );
final file = await state.analyze('package:a/a.drift'); final file = await state.analyze('package:a/a.drift');
state.expectNoErrors(); state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single; final query = file.fileAnalysis!.resolvedQueries.values.single;
expect( expect(
query.resultSet?.existingRowType, query.resultSet?.existingRowType,
isExistingRowType( isExistingRowType(
type: 'MyRow', type: 'MyRow',
positional: [ positional: [
scalarColumn('a'), scalarColumn('a'),
nestedListQuery( nestedListQuery(
'b', 'b',
isExistingRowType( isExistingRowType(
type: '(String, int)', type: '(String, int)',
positional: [scalarColumn('foo'), scalarColumn('bar')], positional: [scalarColumn('foo'), scalarColumn('bar')],
),
), ),
), ],
], ),
), );
); });
}); });
test('into record', () async { test('into record', () async {
@ -536,7 +665,12 @@ class MyRow {
isExistingRowType(type: 'MyRow', positional: [ isExistingRowType(type: 'MyRow', positional: [
scalarColumn('name'), scalarColumn('name'),
], named: { ], named: {
'otherUser': nestedTableColumm('otherUser'), 'otherUser': structedFromNested(
isExistingRowType(
type: 'MyUser',
singleValue: isA<MatchingDriftTable>(),
),
),
'nested': nestedListQuery( 'nested': nestedListQuery(
'nested', 'nested',
isExistingRowType( isExistingRowType(
@ -723,46 +857,3 @@ class MyRow {
); );
}); });
} }
TypeMatcher<ScalarResultColumn> scalarColumn(String name) =>
isA<ScalarResultColumn>().having((e) => e.name, 'name', name);
TypeMatcher nestedTableColumm(String name) =>
isA<NestedResultTable>().having((e) => e.name, 'name', name);
TypeMatcher<MappedNestedListQuery> nestedListQuery(
String columnName, TypeMatcher<ExistingQueryRowType> nestedType) {
return isA<MappedNestedListQuery>()
.having((e) => e.column.filedName(), 'column', columnName)
.having((e) => e.nestedType, 'nestedType', nestedType);
}
TypeMatcher<ExistingQueryRowType> isExistingRowType({
String? type,
String? constructorName,
Object? singleValue,
Object? positional,
Object? named,
}) {
var matcher = isA<ExistingQueryRowType>();
if (type != null) {
matcher = matcher.having((e) => e.rowType.toString(), 'rowType', type);
}
if (constructorName != null) {
matcher = matcher.having(
(e) => e.constructorName, 'constructorName', constructorName);
}
if (singleValue != null) {
matcher = matcher.having((e) => e.singleValue, 'singleValue', singleValue);
}
if (positional != null) {
matcher = matcher.having(
(e) => e.positionalArguments, 'positionalArguments', positional);
}
if (named != null) {
matcher = matcher.having((e) => e.namedArguments, 'namedArguments', named);
}
return matcher;
}

View File

@ -75,22 +75,6 @@ q: SELECT * FROM t WHERE i IN ?1;
expect(result.allErrors, isEmpty); expect(result.allErrors, isEmpty);
}); });
test('warns when nested results refer to table-valued functions', () async {
final result = await TestBackend.analyzeSingle(
"a: SELECT json_each.** FROM json_each('');",
options: DriftOptions.defaults(modules: [SqlModule.json1]),
);
expect(
result.allErrors,
[
isDriftError(
contains('Nested star columns must refer to a table directly.'))
.withSpan('json_each.**')
],
);
});
test('warns about default values outside of expressions', () async { test('warns about default values outside of expressions', () async {
final state = TestBackend.inTest({ final state = TestBackend.inTest({
'foo|lib/a.drift': r''' 'foo|lib/a.drift': r'''

View File

@ -5,7 +5,7 @@ import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../../test_utils.dart'; import '../../test_utils.dart';
import 'existing_row_classes_test.dart'; import 'utils.dart';
void main() { void main() {
test('respects explicit type arguments', () async { test('respects explicit type arguments', () async {
@ -90,10 +90,52 @@ query: SELECT foo.**, bar.** FROM my_view foo, my_view bar;
final query = file.fileAnalysis!.resolvedQueries.values.single; final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(query.resultSet!.nestedResults, hasLength(2)); expect(query.resultSet!.nestedResults, hasLength(2));
final isFromView = isExistingRowType(
type: 'MyViewData',
singleValue: isA<MatchingDriftTable>()
.having((e) => e.table.schemaName, 'table.schemaName', 'my_view'),
);
expect( expect(
query.resultSet!.nestedResults, query.resultSet!.mappingToRowClass('', const DriftOptions.defaults()),
everyElement(isA<NestedResultTable>() isExistingRowType(
.having((e) => e.table.schemaName, 'table.schemName', 'my_view'))); named: {
'foo': structedFromNested(isFromView),
'bar': structedFromNested(isFromView),
},
),
);
});
test('infers nested result sets for custom result sets', () async {
final state = TestBackend.inTest({
'foo|lib/main.drift': r'''
query: SELECT 1 AS a, b.** FROM (SELECT 2 AS b, 3 AS c) AS b;
''',
});
final file = await state.analyze('package:foo/main.drift');
state.expectNoErrors();
final query = file.fileAnalysis!.resolvedQueries.values.single;
expect(
query.resultSet!.mappingToRowClass('Row', const DriftOptions.defaults()),
isExistingRowType(
type: 'Row',
named: {
'a': scalarColumn('a'),
'b': structedFromNested(isExistingRowType(
type: 'QueryNestedColumn0',
named: {
'b': scalarColumn('b'),
'c': scalarColumn('c'),
},
)),
},
),
);
}); });
for (final dateTimeAsText in [false, true]) { for (final dateTimeAsText in [false, true]) {
@ -178,7 +220,7 @@ FROM routes
expect( expect(
resultSet.nestedResults resultSet.nestedResults
.cast<NestedResultTable>() .cast<NestedResultTable>()
.map((e) => e.table.schemaName), .map((e) => e.innerResultSet.matchingTable!.table.schemaName),
['points', 'points'], ['points', 'points'],
); );
}); });

View File

@ -1,4 +1,5 @@
import 'package:drift_dev/src/analysis/results/results.dart'; import 'package:drift_dev/src/analysis/results/results.dart';
import 'package:test/expect.dart';
import '../../test_utils.dart'; import '../../test_utils.dart';
@ -10,3 +11,52 @@ Future<SqlQuery> analyzeSingleQueryInDriftFile(String driftFile) async {
Future<SqlQuery> analyzeQuery(String sql) async { Future<SqlQuery> analyzeQuery(String sql) async {
return analyzeSingleQueryInDriftFile('a: $sql'); return analyzeSingleQueryInDriftFile('a: $sql');
} }
TypeMatcher<ScalarResultColumn> scalarColumn(String name) =>
isA<ScalarResultColumn>().having((e) => e.name, 'name', name);
TypeMatcher<StructuredFromNestedColumn> structedFromNested(
TypeMatcher<QueryRowType> nestedType) =>
isA<StructuredFromNestedColumn>()
.having((e) => e.nestedType, 'nestedType', nestedType);
TypeMatcher<MappedNestedListQuery> nestedListQuery(
String columnName, TypeMatcher<QueryRowType> nestedType) {
return isA<MappedNestedListQuery>()
.having((e) => e.column.filedName(), 'column', columnName)
.having((e) => e.nestedType, 'nestedType', nestedType);
}
TypeMatcher<QueryRowType> isExistingRowType({
String? type,
String? constructorName,
Object? singleValue,
Object? positional,
Object? named,
Object? isRecord,
}) {
var matcher = isA<QueryRowType>();
if (type != null) {
matcher = matcher.having((e) => e.rowType.toString(), 'rowType', type);
}
if (constructorName != null) {
matcher = matcher.having(
(e) => e.constructorName, 'constructorName', constructorName);
}
if (singleValue != null) {
matcher = matcher.having((e) => e.singleValue, 'singleValue', singleValue);
}
if (positional != null) {
matcher = matcher.having(
(e) => e.positionalArguments, 'positionalArguments', positional);
}
if (named != null) {
matcher = matcher.having((e) => e.namedArguments, 'namedArguments', named);
}
if (isRecord != null) {
matcher = matcher.having((e) => e.isRecord, 'isRecord', isRecord);
}
return matcher;
}

View File

@ -307,7 +307,7 @@ TypeConverter<Object, int> myConverter() => throw UnimplementedError();
'a|lib/a.drift.dart': decodedMatches( 'a|lib/a.drift.dart': decodedMatches(
allOf( allOf(
contains( contains(
''''CREATE VIEW my_view AS SELECT CAST(1 AS INT) AS c1, CAST(\\'bar\\' AS TEXT) AS c2, 1 AS c3, NULLIF(1, 2) AS c4';'''), ''''CREATE VIEW my_view AS SELECT CAST(1 AS INT) AS c1, CAST(\\'bar\\' AS TEXT) AS c2, 1 AS c3, NULLIF(1, 2) AS c4','''),
contains(r'$converterc1 ='), contains(r'$converterc1 ='),
contains(r'$converterc2 ='), contains(r'$converterc2 ='),
contains(r'$converterc3 ='), contains(r'$converterc3 ='),

View File

@ -1,8 +1,10 @@
import 'package:build_test/build_test.dart'; import 'package:build_test/build_test.dart';
import 'package:drift/drift.dart';
import 'package:drift_dev/src/analysis/options.dart'; import 'package:drift_dev/src/analysis/options.dart';
import 'package:drift_dev/src/writer/import_manager.dart'; import 'package:drift_dev/src/writer/import_manager.dart';
import 'package:drift_dev/src/writer/queries/query_writer.dart'; import 'package:drift_dev/src/writer/queries/query_writer.dart';
import 'package:drift_dev/src/writer/writer.dart'; import 'package:drift_dev/src/writer/writer.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../../analysis/test_utils.dart'; import '../../analysis/test_utils.dart';
@ -10,13 +12,16 @@ import '../../utils.dart';
void main() { void main() {
Future<String> generateForQueryInDriftFile(String driftFile, Future<String> generateForQueryInDriftFile(String driftFile,
{DriftOptions options = const DriftOptions.defaults()}) async { {DriftOptions options = const DriftOptions.defaults(
generateNamedParameters: true,
)}) async {
final state = final state =
TestBackend.inTest({'a|lib/main.drift': driftFile}, options: options); TestBackend.inTest({'a|lib/main.drift': driftFile}, options: options);
final file = await state.analyze('package:a/main.drift'); final file = await state.analyze('package:a/main.drift');
state.expectNoErrors();
final writer = Writer( final writer = Writer(
const DriftOptions.defaults(generateNamedParameters: true), options,
generationOptions: GenerationOptions( generationOptions: GenerationOptions(
imports: ImportManagerForPartFiles(), imports: ImportManagerForPartFiles(),
), ),
@ -55,32 +60,112 @@ void main() {
); );
}); });
test('generates correct name for renamed nested star columns', () async { group('nested star column', () {
final generated = await generateForQueryInDriftFile(''' test('get renamed in SQL', () async {
final generated = await generateForQueryInDriftFile('''
CREATE TABLE tbl ( CREATE TABLE tbl (
id INTEGER NULL id INTEGER NULL
); );
query: SELECT t.** AS tableName FROM tbl AS t; query: SELECT t.** AS tableName FROM tbl AS t;
'''); ''');
expect( expect(
generated, generated,
allOf( allOf(
contains('SELECT"t"."id" AS "nested_0.id"'), contains('SELECT"t"."id" AS "nested_0.id"'),
contains('final TblData tableName;'), contains('final TblData tableName;'),
), ),
); );
});
test('makes single columns nullable if from outer join', () async {
final generated = await generateForQueryInDriftFile('''
query: SELECT 1 AS r, joined.** FROM (SELECT 1)
LEFT OUTER JOIN (SELECT 2 AS b) joined;
''');
expect(
generated,
allOf(
contains("joined: row.readNullable<int>('nested_0.b')"),
contains('final int? joined;'),
),
);
});
test('checks for nullable column in nested table', () async {
final generated = await generateForQueryInDriftFile('''
CREATE TABLE tbl (
id INTEGER NULL
);
query: SELECT 1 AS a, tbl.** FROM (SELECT 1) LEFT OUTER JOIN tbl;
''');
expect(
generated,
allOf(
contains(
"tbl: await tbl.mapFromRowOrNull(row, tablePrefix: 'nested_0')"),
contains('final TblData? tbl;'),
),
);
});
test('checks for nullable column in nested table with alias', () async {
final generated = await generateForQueryInDriftFile('''
CREATE TABLE tbl (
id INTEGER NULL,
col TEXT NOT NULL
);
query: SELECT 1 AS a, tbl.** FROM (SELECT 1) LEFT OUTER JOIN (SELECT id AS a, col AS b from tbl) tbl;
''');
expect(
generated,
allOf(
contains("tbl: row.data['nested_0.b'] == null ? null : "
'tbl.mapFromRowWithAlias(row'),
contains('final TblData? tbl;'),
),
);
});
test('checks for nullable column in nested result set', () async {
final generated = await generateForQueryInDriftFile('''
query: SELECT 1 AS r, joined.** FROM (SELECT 1)
LEFT OUTER JOIN (SELECT NULL AS b, 3 AS c) joined;
''');
expect(
generated,
allOf(
contains("joined: row.data['nested_0.c'] == null ? null : "
"QueryNestedColumn0(b: row.readNullable<String>('nested_0.b'), "
"c: row.read<int>('nested_0.c'), )"),
contains('final QueryNestedColumn0? joined;'),
),
);
});
}); });
test('generates correct returning mapping', () async { test('generates correct returning mapping', () async {
final generated = await generateForQueryInDriftFile(''' final generated = await generateForQueryInDriftFile(
'''
CREATE TABLE tbl ( CREATE TABLE tbl (
id INTEGER, id INTEGER,
text TEXT text TEXT
); );
query: INSERT INTO tbl (id, text) VALUES(10, 'test') RETURNING id; query: INSERT INTO tbl (id, text) VALUES(10, 'test') RETURNING id;
'''); ''',
options: const DriftOptions.defaults(
sqliteAnalysisOptions:
// Assuming 3.35 because dso that returning works.
SqliteAnalysisOptions(version: SqliteVersion.v3(35)),
),
);
expect(generated, contains('.toList()')); expect(generated, contains('.toList()'));
}); });
@ -346,20 +431,19 @@ failQuery:
], ],
readsFrom: { readsFrom: {
t, t,
}).asyncMap((i0.QueryRow row) async { }).asyncMap((i0.QueryRow row) async => FailQueryResult(
return FailQueryResult( a: row.readNullable<double>('a'),
a: row.readNullable<double>('a'), b: row.readNullable<int>('b'),
b: row.readNullable<int>('b'), nestedQuery0: await customSelect(
nestedQuery0: await customSelect( 'SELECT * FROM t AS x WHERE x.b = b OR x.b = ?1',
'SELECT * FROM t AS x WHERE x.b = b OR x.b = ?1', variables: [
variables: [ i0.Variable<int>(inB)
i0.Variable<int>(inB) ],
], readsFrom: {
readsFrom: { t,
t, }).asyncMap(t.mapFromRow).get(),
}).asyncMap(t.mapFromRow).get(), ));
); }
});
''')) '''))
}, outputs.dartOutputs, outputs); }, outputs.dartOutputs, outputs);
}); });
@ -447,4 +531,26 @@ class ADrift extends i1.ModularAccessor {
}''')) }'''))
}, outputs.dartOutputs, outputs); }, outputs.dartOutputs, outputs);
}); });
test('creates dialect-specific query code', () async {
final result = await generateForQueryInDriftFile(
r'''
query (:foo AS TEXT): SELECT :foo;
''',
options: const DriftOptions.defaults(
dialect: DialectOptions(
null, [SqlDialect.sqlite, SqlDialect.postgres], null),
),
);
expect(
result,
contains(
'switch (executor.dialect) {'
"SqlDialect.sqlite => 'SELECT ?1 AS _c0', "
"SqlDialect.postgres || _ => 'SELECT \\\$1 AS _c0', "
'}',
),
);
});
} }

View File

@ -6,14 +6,18 @@ import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
void check(String sql, String expectedDart, void check(
{DriftOptions options = const DriftOptions.defaults()}) { String sql,
String expectedDart, {
DriftOptions options = const DriftOptions.defaults(),
SqlDialect dialect = SqlDialect.sqlite,
}) {
final engine = SqlEngine(); final engine = SqlEngine();
final context = engine.analyze(sql); final context = engine.analyze(sql);
final query = SqlSelectQuery('name', context, context.root, [], [], final query = SqlSelectQuery('name', context, context.root, [], [],
InferredResultSet(null, []), null, null); InferredResultSet(null, []), null, null);
final result = SqlWriter(options, query: query).write(); final result = SqlWriter(options, dialect: dialect, query: query).write();
expect(result, expectedDart); expect(result, expectedDart);
} }
@ -33,7 +37,6 @@ void main() {
test('escapes postgres keywords', () { test('escapes postgres keywords', () {
check('SELECT * FROM user', "'SELECT * FROM user'"); check('SELECT * FROM user', "'SELECT * FROM user'");
check('SELECT * FROM user', "'SELECT * FROM \"user\"'", check('SELECT * FROM user', "'SELECT * FROM \"user\"'",
options: DriftOptions.defaults( dialect: SqlDialect.postgres);
dialect: DialectOptions(SqlDialect.postgres, null)));
}); });
} }

View File

@ -36,8 +36,7 @@ class MyApp extends StatelessWidget {
primarySwatch: Colors.amber, primarySwatch: Colors.amber,
typography: Typography.material2018(), typography: Typography.material2018(),
), ),
routeInformationParser: _router.routeInformationParser, routerConfig: _router,
routerDelegate: _router.routerDelegate,
), ),
); );
} }

View File

@ -15,7 +15,7 @@ dependencies:
file_picker: ^5.2.5 file_picker: ^5.2.5
flutter_colorpicker: ^1.0.3 flutter_colorpicker: ^1.0.3
flutter_riverpod: ^2.3.0 flutter_riverpod: ^2.3.0
go_router: ^9.0.0 go_router: ^10.0.0
intl: ^0.18.0 intl: ^0.18.0
sqlite3_flutter_libs: ^0.5.5 sqlite3_flutter_libs: ^0.5.5
sqlite3: ^2.0.0 sqlite3: ^2.0.0

View File

@ -602,8 +602,10 @@ class PopularUsers extends i0.ViewInfo<i1.PopularUsers, i1.PopularUser>
@override @override
String get entityName => 'popular_users'; String get entityName => 'popular_users';
@override @override
String get createViewStmt => Map<i0.SqlDialect, String> get createViewStatements => {
'CREATE VIEW popular_users AS SELECT * FROM users ORDER BY (SELECT count(*) FROM follows WHERE followed = users.id)'; i0.SqlDialect.sqlite:
'CREATE VIEW popular_users AS SELECT * FROM users ORDER BY (SELECT count(*) FROM follows WHERE followed = users.id)',
};
@override @override
PopularUsers get asDslTable => this; PopularUsers get asDslTable => this;
@override @override

View File

@ -9,9 +9,7 @@ targets:
raw_result_set_data: false raw_result_set_data: false
named_parameters: false named_parameters: false
sql: sql:
# As sqlite3 is compatible with the postgres dialect (but not vice-versa), we're dialects: [sqlite, postgres]
# using this dialect so that we can run the tests on postgres as well.
dialect: postgres
options: options:
version: "3.37" version: "3.37"
modules: modules:

View File

@ -336,7 +336,6 @@ class $FriendshipsTable extends Friendships
requiredDuringInsert: false, requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({ defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
SqlDialect.sqlite: 'CHECK ("really_good_friends" IN (0, 1))', SqlDialect.sqlite: 'CHECK ("really_good_friends" IN (0, 1))',
SqlDialect.mysql: '',
SqlDialect.postgres: '', SqlDialect.postgres: '',
}), }),
defaultValue: const Constant(false)); defaultValue: const Constant(false));
@ -554,7 +553,13 @@ abstract class _$Database extends GeneratedDatabase {
late final $FriendshipsTable friendships = $FriendshipsTable(this); late final $FriendshipsTable friendships = $FriendshipsTable(this);
Selectable<User> mostPopularUsers(int amount) { Selectable<User> mostPopularUsers(int amount) {
return customSelect( return customSelect(
'SELECT * FROM users AS u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT \$1', switch (executor.dialect) {
SqlDialect.sqlite =>
'SELECT * FROM users AS u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT ?1',
SqlDialect.postgres ||
_ =>
'SELECT * FROM users AS u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT \$1',
},
variables: [ variables: [
Variable<int>(amount) Variable<int>(amount)
], ],
@ -566,7 +571,13 @@ abstract class _$Database extends GeneratedDatabase {
Selectable<int> amountOfGoodFriends(int user) { Selectable<int> amountOfGoodFriends(int user) {
return customSelect( return customSelect(
'SELECT COUNT(*) AS _c0 FROM friendships AS f WHERE f.really_good_friends = TRUE AND(f.first_user = \$1 OR f.second_user = \$1)', switch (executor.dialect) {
SqlDialect.sqlite =>
'SELECT COUNT(*) AS _c0 FROM friendships AS f WHERE f.really_good_friends = TRUE AND(f.first_user = ?1 OR f.second_user = ?1)',
SqlDialect.postgres ||
_ =>
'SELECT COUNT(*) AS _c0 FROM friendships AS f WHERE f.really_good_friends = TRUE AND(f.first_user = \$1 OR f.second_user = \$1)',
},
variables: [ variables: [
Variable<int>(user) Variable<int>(user)
], ],
@ -577,19 +588,23 @@ abstract class _$Database extends GeneratedDatabase {
Selectable<FriendshipsOfResult> friendshipsOf(int user) { Selectable<FriendshipsOfResult> friendshipsOf(int user) {
return customSelect( return customSelect(
'SELECT f.really_good_friends,"user"."id" AS "nested_0.id", "user"."name" AS "nested_0.name", "user"."birth_date" AS "nested_0.birth_date", "user"."profile_picture" AS "nested_0.profile_picture", "user"."preferences" AS "nested_0.preferences" FROM friendships AS f INNER JOIN users AS "user" ON "user".id IN (f.first_user, f.second_user) AND "user".id != \$1 WHERE(f.first_user = \$1 OR f.second_user = \$1)', switch (executor.dialect) {
SqlDialect.sqlite =>
'SELECT f.really_good_friends,"user"."id" AS "nested_0.id", "user"."name" AS "nested_0.name", "user"."birth_date" AS "nested_0.birth_date", "user"."profile_picture" AS "nested_0.profile_picture", "user"."preferences" AS "nested_0.preferences" FROM friendships AS f INNER JOIN users AS user ON user.id IN (f.first_user, f.second_user) AND user.id != ?1 WHERE(f.first_user = ?1 OR f.second_user = ?1)',
SqlDialect.postgres ||
_ =>
'SELECT f.really_good_friends,"user"."id" AS "nested_0.id", "user"."name" AS "nested_0.name", "user"."birth_date" AS "nested_0.birth_date", "user"."profile_picture" AS "nested_0.profile_picture", "user"."preferences" AS "nested_0.preferences" FROM friendships AS f INNER JOIN users AS "user" ON "user".id IN (f.first_user, f.second_user) AND "user".id != \$1 WHERE(f.first_user = \$1 OR f.second_user = \$1)',
},
variables: [ variables: [
Variable<int>(user) Variable<int>(user)
], ],
readsFrom: { readsFrom: {
friendships, friendships,
users, users,
}).asyncMap((QueryRow row) async { }).asyncMap((QueryRow row) async => FriendshipsOfResult(
return FriendshipsOfResult( reallyGoodFriends: row.read<bool>('really_good_friends'),
reallyGoodFriends: row.read<bool>('really_good_friends'), user: await users.mapFromRow(row, tablePrefix: 'nested_0'),
user: await users.mapFromRow(row, tablePrefix: 'nested_0'), ));
);
});
} }
Selectable<int> userCount() { Selectable<int> userCount() {
@ -601,7 +616,13 @@ abstract class _$Database extends GeneratedDatabase {
} }
Selectable<Preferences?> settingsFor(int user) { Selectable<Preferences?> settingsFor(int user) {
return customSelect('SELECT preferences FROM users WHERE id = \$1', return customSelect(
switch (executor.dialect) {
SqlDialect.sqlite => 'SELECT preferences FROM users WHERE id = ?1',
SqlDialect.postgres ||
_ =>
'SELECT preferences FROM users WHERE id = \$1',
},
variables: [ variables: [
Variable<int>(user) Variable<int>(user)
], ],
@ -626,7 +647,13 @@ abstract class _$Database extends GeneratedDatabase {
Future<List<Friendship>> returning(int var1, int var2, bool var3) { Future<List<Friendship>> returning(int var1, int var2, bool var3) {
return customWriteReturning( return customWriteReturning(
'INSERT INTO friendships VALUES (\$1, \$2, \$3) RETURNING *', switch (executor.dialect) {
SqlDialect.sqlite =>
'INSERT INTO friendships VALUES (?1, ?2, ?3) RETURNING *',
SqlDialect.postgres ||
_ =>
'INSERT INTO friendships VALUES (\$1, \$2, \$3) RETURNING *',
},
variables: [ variables: [
Variable<int>(var1), Variable<int>(var1),
Variable<int>(var2), Variable<int>(var2),

View File

@ -4,7 +4,7 @@ version: 1.0.0
# homepage: https://www.example.com # homepage: https://www.example.com
environment: environment:
sdk: '>=2.17.0 <3.0.0' sdk: '>=3.0.0 <4.0.0'
dependencies: dependencies:
drift: ^2.0.0-0 drift: ^2.0.0-0

View File

@ -50,6 +50,13 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
currentColumnIndex += child.scope.expansionOfStarColumn?.length ?? 1; currentColumnIndex += child.scope.expansionOfStarColumn?.length ?? 1;
} else if (child is NestedQueryColumn) { } else if (child is NestedQueryColumn) {
visit(child.select, arg); visit(child.select, arg);
} else if (child is NestedStarResultColumn) {
final columns = child.resultSet?.resolvedColumns;
if (columns != null) {
for (final column in columns) {
_handleColumn(column, child);
}
}
} }
} else { } else {
visit(child, arg); visit(child, arg);

View File

@ -145,7 +145,7 @@ class SqlEngine {
/// Parses multiple [sql] statements, separated by a semicolon. /// Parses multiple [sql] statements, separated by a semicolon.
/// ///
/// You can use the [AstNode.children] of the returned [ParseResult.rootNode] /// You can use the [AstNode.childNodes] of the returned [ParseResult.rootNode]
/// to inspect the returned statements. /// to inspect the returned statements.
ParseResult parseMultiple(String sql) { ParseResult parseMultiple(String sql) {
final tokens = tokenize(sql); final tokens = tokenize(sql);

View File

@ -189,9 +189,14 @@ class Parser {
final first = _peek; final first = _peek;
final statements = <Statement>[]; final statements = <Statement>[];
while (!_isAtEnd) { while (!_isAtEnd) {
final firstForStatement = _peek;
final statement = _parseAsStatement(_statementWithoutSemicolon); final statement = _parseAsStatement(_statementWithoutSemicolon);
if (statement != null) { if (statement != null) {
statements.add(statement); statements.add(statement);
} else {
statements
.add(InvalidStatement()..setSpan(firstForStatement, _previous));
} }
} }

View File

@ -143,4 +143,35 @@ void main() {
), ),
); );
}); });
test('parseMultiple reports spans for invalid statements', () {
const sql = '''
UPDATE users SET foo = bar;
ALTER TABLE this syntax is not yet supported;
SELECT * FROM users;
''';
final engine = SqlEngine();
final ast = engine.parseMultiple(sql).rootNode;
enforceHasSpan(ast);
final statements = ast.childNodes.toList();
expect(statements, hasLength(3));
expect(
statements[0],
isA<UpdateStatement>()
.having((e) => e.span?.text, 'span', 'UPDATE users SET foo = bar;'),
);
expect(
statements[1],
isA<InvalidStatement>().having((e) => e.span?.text, 'span',
'ALTER TABLE this syntax is not yet supported;'),
);
expect(
statements[2],
isA<SelectStatement>()
.having((e) => e.span?.text, 'span', 'SELECT * FROM users;'),
);
});
} }