mirror of https://github.com/AMT-Cheif/drift.git
Merge branch 'develop' into Banana-develop
This commit is contained in:
commit
1c5432026c
|
@ -107,4 +107,40 @@ extension GroupByQueries on MyDatabase {
|
|||
});
|
||||
}
|
||||
// #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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
||||
__Note__: This enables extensions in the analyzer for custom queries only. For instance, when the `json1` extension is
|
||||
|
|
|
@ -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:
|
||||
|
||||
1. For a nested table selected with `**`, your field needs to store an instance of the table's row class.
|
||||
This is true for both drift-generated row classes and tables with existing, user-defined row classes.
|
||||
1. For a nested table selected with `**`, your field needs to store a structure compatible with the result set
|
||||
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
|
||||
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
|
||||
|
@ -221,8 +222,8 @@ class EmployeeWithStaff {
|
|||
}
|
||||
```
|
||||
|
||||
As `self` is a `**` column, rule 1 applies. Therefore, `T1` must be `Employee`, the row class for the
|
||||
`employees` table.
|
||||
As `self` is a `**` column, rule 1 applies. `self` references a table, `employees`.
|
||||
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
|
||||
be a `List<Something>`.
|
||||
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 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);
|
||||
}
|
||||
|
||||
|
|
|
@ -243,6 +243,11 @@ any rows. For instance, we could use this to find empty categories:
|
|||
|
||||
{% 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
|
||||
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:
|
||||
|
|
|
@ -203,3 +203,17 @@ select statement.
|
|||
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
|
||||
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.
|
||||
|
|
|
@ -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.
|
||||
Inside your database class, this could look like the following:
|
||||
|
|
|
@ -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).
|
||||
|
||||
## Adding the dependency
|
||||
First, lets add drift to your project's `pubspec.yaml`.
|
||||
At the moment, the current version of `drift` is [](https://pub.dev/packages/drift)
|
||||
and the latest version of `drift_dev` is [](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 %}
|
||||
|
||||
```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
|
||||
|
||||
Using drift, you can model the structure of your tables with simple dart code.
|
||||
|
|
|
@ -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).
|
||||
|
||||
## Adding the dependency
|
||||
First, lets add drift to your project's `pubspec.yaml`.
|
||||
At the moment, the current version of `drift` is [](https://pub.dev/packages/drift)
|
||||
and the latest version of `drift_dev` is [](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 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" %}
|
||||
{% include "partials/dependencies" %}
|
||||
|
||||
## Declaring tables and queries
|
||||
|
||||
|
|
|
@ -228,12 +228,10 @@ class RoutesWithNestedPointsResult {
|
|||
Great! This class matches our intent much better than the flat result class
|
||||
from before.
|
||||
|
||||
At the moment, there are some limitations with this approach:
|
||||
|
||||
- `**` is not yet supported in compound select statements
|
||||
- you can only use `table.**` if table is an actual table or a reference to it.
|
||||
In particular, it doesn't work for result sets from `WITH` clauses or table-
|
||||
valued functions.
|
||||
These nested result columns (`**`) can appear in top-level select statements
|
||||
only, they're not supported in compound select statements or subqueries yet.
|
||||
However, they can refer to any result set in SQL that has been joined to the
|
||||
select statement - including subqueries table-valued functions.
|
||||
|
||||
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
|
||||
|
|
|
@ -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/markdown.html" %}
|
||||
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!
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -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).
|
|
@ -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 [](https://pub.dev/packages/drift)
|
||||
and the latest version of `drift_dev` is [](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 %}
|
|
@ -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
|
||||
|
||||
- Adds the `schema steps` command to `drift_dev`. It generates an API making it
|
||||
|
|
|
@ -534,7 +534,7 @@ class $TodoCategoryItemCountView
|
|||
@override
|
||||
String get entityName => 'todo_category_item_count';
|
||||
@override
|
||||
String? get createViewStmt => null;
|
||||
Map<SqlDialect, String>? get createViewStatements => null;
|
||||
@override
|
||||
$TodoCategoryItemCountView get asDslTable => this;
|
||||
@override
|
||||
|
@ -639,7 +639,7 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo<
|
|||
@override
|
||||
String get entityName => 'customViewName';
|
||||
@override
|
||||
String? get createViewStmt => null;
|
||||
Map<SqlDialect, String>? get createViewStatements => null;
|
||||
@override
|
||||
$TodoItemWithCategoryNameViewView get asDslTable => this;
|
||||
@override
|
||||
|
|
|
@ -157,6 +157,10 @@ class VersionedView implements ViewInfo<HasResultSet, QueryRow>, HasResultSet {
|
|||
@override
|
||||
final String createViewStmt;
|
||||
|
||||
@override
|
||||
Map<SqlDialect, String>? get createViewStatements =>
|
||||
{SqlDialect.sqlite: createViewStmt};
|
||||
|
||||
@override
|
||||
final List<GeneratedColumn> $columns;
|
||||
|
||||
|
|
|
@ -58,8 +58,7 @@ class Join<T extends HasResultSet, D> extends Component {
|
|||
context.buffer.write(' JOIN ');
|
||||
|
||||
final resultSet = table as ResultSetImplementation<T, D>;
|
||||
context.buffer.write(resultSet.tableWithAlias);
|
||||
context.watchedTables.add(resultSet);
|
||||
context.writeResultSet(resultSet);
|
||||
|
||||
if (_type != _JoinType.cross) {
|
||||
context.buffer.write(' ON ');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ class CustomExpression<D extends Object> extends Expression<D> {
|
|||
/// The SQL of this expression
|
||||
final String content;
|
||||
|
||||
final Map<SqlDialect, String>? _dialectSpecificContent;
|
||||
|
||||
/// Additional tables that this expression is watching.
|
||||
///
|
||||
/// 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].
|
||||
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
|
||||
void writeInto(GenerationContext context) {
|
||||
final dialectSpecific = _dialectSpecificContent;
|
||||
|
||||
if (dialectSpecific != null) {
|
||||
} else {
|
||||
context.buffer.write(content);
|
||||
}
|
||||
|
||||
context.watchedTables.addAll(watchedTables);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
/// [values].
|
||||
Expression<bool> isIn(Iterable<D> values) {
|
||||
if (values.isEmpty) {
|
||||
return Constant(false);
|
||||
}
|
||||
return _InExpression(this, values.toList(), false);
|
||||
return isInExp([for (final value in values) Variable<D>(value)]);
|
||||
}
|
||||
|
||||
/// An expression that is true if `this` does not resolve to any of the values
|
||||
/// in [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 _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
|
||||
|
@ -509,7 +527,7 @@ class FunctionCallExpression<R extends Object> extends Expression<R> {
|
|||
}
|
||||
|
||||
void _checkSubquery(BaseSelectStatement statement) {
|
||||
final columns = statement._returnedColumnCount;
|
||||
final columns = statement._expandedColumns.length;
|
||||
if (columns != 1) {
|
||||
throw ArgumentError.value(statement, 'statement',
|
||||
'Must return exactly one column (actually returns $columns)');
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
part of '../query_builder.dart';
|
||||
|
||||
abstract class _BaseInExpression extends Expression<bool> {
|
||||
sealed class _BaseInExpression extends Expression<bool> {
|
||||
final Expression _expression;
|
||||
final bool _not;
|
||||
|
||||
|
@ -25,8 +25,8 @@ abstract class _BaseInExpression extends Expression<bool> {
|
|||
void _writeValues(GenerationContext context);
|
||||
}
|
||||
|
||||
class _InExpression<T extends Object> extends _BaseInExpression {
|
||||
final List<T> _values;
|
||||
final class _InExpression<T extends Object> extends _BaseInExpression {
|
||||
final List<Expression<T>> _values;
|
||||
|
||||
_InExpression(Expression expression, this._values, bool not)
|
||||
: super(expression, not);
|
||||
|
@ -35,15 +35,13 @@ class _InExpression<T extends Object> extends _BaseInExpression {
|
|||
void _writeValues(GenerationContext context) {
|
||||
var first = true;
|
||||
for (final value in _values) {
|
||||
final variable = Variable<T>(value);
|
||||
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
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;
|
||||
|
||||
_InSelectExpression(this._select, Expression expression, bool not)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -88,7 +88,7 @@ class Migrator {
|
|||
} else if (entity is Index) {
|
||||
await createIndex(entity);
|
||||
} else if (entity is OnCreateQuery) {
|
||||
await _issueCustomQuery(entity.sql, const []);
|
||||
await _issueQueryByDialect(entity.sqlByDialect);
|
||||
} else if (entity is ViewInfo) {
|
||||
await createView(entity);
|
||||
} else {
|
||||
|
@ -403,19 +403,19 @@ class Migrator {
|
|||
|
||||
/// Executes the `CREATE TRIGGER` statement that created the [trigger].
|
||||
Future<void> createTrigger(Trigger trigger) {
|
||||
return _issueCustomQuery(trigger.createTriggerStmt, const []);
|
||||
return _issueQueryByDialect(trigger.createStatementsByDialect);
|
||||
}
|
||||
|
||||
/// Executes a `CREATE INDEX` statement to create the [index].
|
||||
Future<void> createIndex(Index index) {
|
||||
return _issueCustomQuery(index.createIndexStmt, const []);
|
||||
return _issueQueryByDialect(index.createStatementsByDialect);
|
||||
}
|
||||
|
||||
/// Executes a `CREATE VIEW` statement to create the [view].
|
||||
Future<void> createView(ViewInfo view) async {
|
||||
final stmt = view.createViewStmt;
|
||||
if (stmt != null) {
|
||||
await _issueCustomQuery(stmt, const []);
|
||||
final stmts = view.createViewStatements;
|
||||
if (stmts != null) {
|
||||
await _issueQueryByDialect(stmts);
|
||||
} else if (view.query != null) {
|
||||
final context = GenerationContext.fromDb(_db, supportsVariables: false);
|
||||
final columnNames = view.$columns.map((e) => e.escapedName).join(', ');
|
||||
|
@ -528,6 +528,11 @@ class Migrator {
|
|||
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]) {
|
||||
return _db.customStatement(sql, args);
|
||||
}
|
||||
|
|
|
@ -23,8 +23,10 @@ import 'package:meta/meta.dart';
|
|||
import '../../utils/async.dart';
|
||||
// New files should not be part of this mega library, which we're trying to
|
||||
// split up.
|
||||
|
||||
import 'expressions/case_when.dart';
|
||||
import 'expressions/internal.dart';
|
||||
import 'helpers.dart';
|
||||
|
||||
export 'expressions/bitwise.dart';
|
||||
export 'expressions/case_when.dart';
|
||||
|
@ -34,6 +36,7 @@ part 'components/group_by.dart';
|
|||
part 'components/join.dart';
|
||||
part 'components/limit.dart';
|
||||
part 'components/order_by.dart';
|
||||
part 'components/subquery.dart';
|
||||
part 'components/where.dart';
|
||||
part 'expressions/aggregate.dart';
|
||||
part 'expressions/algebra.dart';
|
||||
|
|
|
@ -17,15 +17,26 @@ abstract class DatabaseSchemaEntity {
|
|||
/// [sqlite-docs]: https://sqlite.org/lang_createtrigger.html
|
||||
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-trigger/
|
||||
class Trigger extends DatabaseSchemaEntity {
|
||||
/// The `CREATE TRIGGER` sql statement that can be used to create this
|
||||
/// trigger.
|
||||
final String createTriggerStmt;
|
||||
@override
|
||||
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
|
||||
/// [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.
|
||||
|
@ -40,11 +51,21 @@ class Index extends DatabaseSchemaEntity {
|
|||
final String entityName;
|
||||
|
||||
/// 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].
|
||||
/// 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
|
||||
|
@ -61,10 +82,19 @@ class Index extends DatabaseSchemaEntity {
|
|||
/// drift file.
|
||||
class OnCreateQuery extends DatabaseSchemaEntity {
|
||||
/// 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.
|
||||
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
|
||||
String get entityName => r'$internal$';
|
||||
|
|
|
@ -151,7 +151,7 @@ extension TableInfoUtils<TableDsl, D> on ResultSetImplementation<TableDsl, D> {
|
|||
/// Drift would generate code to call this method with `'c1': 'foo'` and
|
||||
/// `'c2': 'bar'` in [alias].
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -17,7 +17,14 @@ abstract class ViewInfo<Self extends HasResultSet, Row>
|
|||
/// The `CREATE VIEW` sql statement that can be used to create this view.
|
||||
///
|
||||
/// 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()`
|
||||
///
|
||||
|
|
|
@ -8,12 +8,14 @@ typedef OrderClauseGenerator<T> = OrderingTerm Function(T tbl);
|
|||
///
|
||||
/// Users are not allowed to extend, implement or mix-in this class.
|
||||
@sealed
|
||||
abstract class BaseSelectStatement extends Component {
|
||||
int get _returnedColumnCount;
|
||||
abstract class BaseSelectStatement<Row> extends Component {
|
||||
Iterable<(Expression, String)> get _expandedColumns;
|
||||
|
||||
/// The name for the given [expression] in the result set, or `null` if
|
||||
/// [expression] was not added as a column to this select statement.
|
||||
String? _nameForColumn(Expression expression);
|
||||
|
||||
FutureOr<Row> _mapRow(Map<String, Object?> fromDatabase);
|
||||
}
|
||||
|
||||
/// A select statement that doesn't use joins.
|
||||
|
@ -21,7 +23,7 @@ abstract class BaseSelectStatement extends Component {
|
|||
/// For more information, see [DatabaseConnectionUser.select].
|
||||
class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, 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
|
||||
/// `SELECT DISTINCT` statement in sql). Defaults to false.
|
||||
final bool distinct;
|
||||
|
@ -39,7 +41,8 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
|||
Set<ResultSetImplementation> get watchedTables => {table};
|
||||
|
||||
@override
|
||||
int get _returnedColumnCount => table.$columns.length;
|
||||
Iterable<(Expression, String)> get _expandedColumns =>
|
||||
table.$columns.map((e) => (e, e.name));
|
||||
|
||||
@override
|
||||
String? _nameForColumn(Expression expression) {
|
||||
|
@ -54,8 +57,8 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
|||
void writeStartPart(GenerationContext ctx) {
|
||||
ctx.buffer
|
||||
..write(_beginOfSelect(distinct))
|
||||
..write(' * FROM ${table.tableWithAlias}');
|
||||
ctx.watchedTables.add(table);
|
||||
..write(' * FROM ');
|
||||
ctx.writeResultSet(table);
|
||||
}
|
||||
|
||||
@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) {
|
||||
return rows.mapAsyncAndAwait(table.map);
|
||||
return rows.mapAsyncAndAwait(_mapRow);
|
||||
}
|
||||
|
||||
/// Creates a select statement that operates on more than one table by
|
||||
|
|
|
@ -31,11 +31,11 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
///
|
||||
/// 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
|
||||
/// be written as `users.id AS "users.id"`. These columns will NOT be written
|
||||
/// into this map.
|
||||
/// be written as `users.id AS "users.id"`. These columns are also included in
|
||||
/// the map when added through [addColumns], but they have a predicatable name.
|
||||
///
|
||||
/// Other expressions used as columns will be included here. There just named
|
||||
/// in increasing order, so something like `AS c3`.
|
||||
/// More interestingly, other expressions used as columns will be included
|
||||
/// here. They're just named in increasing order, so something like `AS c3`.
|
||||
final Map<Expression, String> _columnAliases = {};
|
||||
|
||||
/// The tables this select statement reads from
|
||||
|
@ -44,13 +44,16 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
Set<ResultSetImplementation> get watchedTables => _queriedTables().toSet();
|
||||
|
||||
@override
|
||||
int get _returnedColumnCount {
|
||||
return _joins.fold(_selectedColumns.length, (prev, join) {
|
||||
if (join.includeInResult ?? _includeJoinedTablesInResult) {
|
||||
return prev + (join.table as ResultSetImplementation).$columns.length;
|
||||
Iterable<(Expression<Object>, String)> get _expandedColumns sync* {
|
||||
for (final column in _selectedColumns) {
|
||||
yield (column, _columnAliases[column]!);
|
||||
}
|
||||
|
||||
for (final table in _queriedTables(true)) {
|
||||
for (final column in table.$columns) {
|
||||
yield (column, _nameForTableColumn(column));
|
||||
}
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -122,9 +125,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
chosenAlias = _nameForTableColumn(column,
|
||||
generatingForView: ctx.generatingForView);
|
||||
} else {
|
||||
chosenAlias = 'c$i';
|
||||
chosenAlias = _columnAliases[column]!;
|
||||
}
|
||||
_columnAliases[column] = chosenAlias;
|
||||
|
||||
column.writeInto(ctx);
|
||||
ctx.buffer
|
||||
|
@ -133,8 +135,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
..write('"');
|
||||
}
|
||||
|
||||
ctx.buffer.write(' FROM ${table.tableWithAlias}');
|
||||
ctx.watchedTables.add(table);
|
||||
ctx.buffer.write(' FROM ');
|
||||
ctx.writeResultSet(table);
|
||||
|
||||
if (_joins.isNotEmpty) {
|
||||
ctx.writeWhitespace();
|
||||
|
@ -195,7 +197,21 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
/// - The docs on expressions: https://drift.simonbinder.eu/docs/getting-started/expressions/
|
||||
/// {@endtemplate}
|
||||
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].
|
||||
|
@ -233,14 +249,14 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
|
||||
return database
|
||||
.createStream(fetcher)
|
||||
.asyncMapPerSubscription((rows) => _mapResponse(ctx, rows));
|
||||
.asyncMapPerSubscription((rows) => _mapResponse(rows));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<TypedResult>> get() async {
|
||||
final ctx = constructQuery();
|
||||
final raw = await _getRaw(ctx);
|
||||
return _mapResponse(ctx, raw);
|
||||
return _mapResponse(raw);
|
||||
}
|
||||
|
||||
Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) {
|
||||
|
@ -260,9 +276,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
});
|
||||
}
|
||||
|
||||
Future<List<TypedResult>> _mapResponse(
|
||||
GenerationContext ctx, List<Map<String, Object?>> rows) {
|
||||
return Future.wait(rows.map((row) async {
|
||||
@override
|
||||
Future<TypedResult> _mapRow(Map<String, Object?> row) async {
|
||||
final readTables = <ResultSetImplementation, dynamic>{};
|
||||
|
||||
for (final table in _queriedTables(true)) {
|
||||
|
@ -277,7 +292,10 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
|||
final driftRow = QueryRow(row, database);
|
||||
return TypedResult(
|
||||
readTables, driftRow, _LazyExpressionMap(_columnAliases, driftRow));
|
||||
}));
|
||||
}
|
||||
|
||||
Future<List<TypedResult>> _mapResponse(List<Map<String, Object?>> rows) {
|
||||
return Future.wait(rows.map(_mapRow));
|
||||
}
|
||||
|
||||
Never _warnAboutDuplicate(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
name: drift
|
||||
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
|
||||
homepage: https://drift.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/drift/issues
|
||||
|
|
|
@ -201,6 +201,27 @@ void main() {
|
|||
.bitwiseAnd(Variable(BigInt.from(10)))),
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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', () {
|
||||
test('in expressions are generated', () {
|
||||
final isInExpression = innerExpression
|
||||
|
|
|
@ -224,7 +224,7 @@ void main() {
|
|||
'c.desc': 'Description',
|
||||
'c.description_in_upper_case': 'DESCRIPTION',
|
||||
'c.priority': 1,
|
||||
'c4': 11
|
||||
'c0': 11
|
||||
}
|
||||
];
|
||||
});
|
||||
|
@ -234,7 +234,7 @@ void main() {
|
|||
verify(executor.runSelect(
|
||||
'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", LENGTH("c"."desc") AS "c4" '
|
||||
'"c.description_in_upper_case", LENGTH("c"."desc") AS "c0" '
|
||||
'FROM "categories" "c";',
|
||||
[],
|
||||
));
|
||||
|
@ -273,7 +273,7 @@ void main() {
|
|||
'c.desc': 'Description',
|
||||
'c.description_in_upper_case': 'DESCRIPTION',
|
||||
'c.priority': 1,
|
||||
'c4': 11,
|
||||
'c0': 11,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
@ -283,7 +283,7 @@ void main() {
|
|||
verify(executor.runSelect(
|
||||
'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", '
|
||||
'LENGTH("c"."desc") AS "c4" '
|
||||
'LENGTH("c"."desc") AS "c0" '
|
||||
'FROM "categories" "c" '
|
||||
'INNER JOIN "todos" "t" ON "c"."id" = "t"."category";',
|
||||
[],
|
||||
|
@ -328,7 +328,7 @@ void main() {
|
|||
'c.id': 3,
|
||||
'c.desc': 'desc',
|
||||
'c.priority': 0,
|
||||
'c4': 10,
|
||||
'c0': 10,
|
||||
'c.description_in_upper_case': 'DESC',
|
||||
}
|
||||
];
|
||||
|
@ -340,7 +340,7 @@ void main() {
|
|||
'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", '
|
||||
'COUNT("t"."id") AS "c4" '
|
||||
'COUNT("t"."id") AS "c0" '
|
||||
'FROM "categories" "c" INNER JOIN "todos" "t" ON "t"."category" = "c"."id" '
|
||||
'GROUP BY "c"."id" HAVING COUNT("t"."id") >= ?;',
|
||||
[10]));
|
||||
|
@ -474,4 +474,72 @@ void main() {
|
|||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -214,4 +214,30 @@ void main() {
|
|||
await pumpEventQueue();
|
||||
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;', []));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1600,8 +1600,10 @@ class MyView extends ViewInfo<MyView, MyViewData> implements HasResultSet {
|
|||
@override
|
||||
String get entityName => 'my_view';
|
||||
@override
|
||||
String get createViewStmt =>
|
||||
'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2';
|
||||
Map<SqlDialect, String> get createViewStatements => {
|
||||
SqlDialect.sqlite:
|
||||
'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2',
|
||||
};
|
||||
@override
|
||||
MyView get asDslTable => this;
|
||||
@override
|
||||
|
@ -1678,7 +1680,8 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
],
|
||||
readsFrom: {
|
||||
config,
|
||||
}).asyncMap((QueryRow row) => config.mapFromRowWithAlias(row, const {
|
||||
}).asyncMap(
|
||||
(QueryRow row) async => config.mapFromRowWithAlias(row, const {
|
||||
'ck': 'config_key',
|
||||
'cf': 'config_value',
|
||||
'cs1': 'sync_state',
|
||||
|
@ -1754,26 +1757,20 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
variables: [],
|
||||
readsFrom: {
|
||||
config,
|
||||
}).map((QueryRow row) {
|
||||
return JsonResult(
|
||||
}).map((QueryRow row) => JsonResult(
|
||||
row: row,
|
||||
key: row.read<String>('key'),
|
||||
value: row.readNullable<String>('value'),
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
|
||||
Selectable<JsonResult> another() {
|
||||
return customSelect(
|
||||
'SELECT \'one\' AS "key", NULLIF(\'two\', \'another\') AS value',
|
||||
variables: [],
|
||||
readsFrom: {}).map((QueryRow row) {
|
||||
return JsonResult(
|
||||
return customSelect('SELECT \'one\' AS "key", NULLIF(\'two\', \'another\') AS value', variables: [], readsFrom: {})
|
||||
.map((QueryRow row) => JsonResult(
|
||||
row: row,
|
||||
key: row.read<String>('key'),
|
||||
value: row.readNullable<String>('value'),
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
|
||||
Selectable<MultipleResult> multiple({required Multiple$predicate predicate}) {
|
||||
|
@ -1793,14 +1790,13 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
withDefaults,
|
||||
withConstraints,
|
||||
...generatedpredicate.watchedTables,
|
||||
}).asyncMap((QueryRow row) async {
|
||||
return MultipleResult(
|
||||
}).asyncMap((QueryRow row) async => MultipleResult(
|
||||
row: row,
|
||||
a: row.readNullable<String>('a'),
|
||||
b: row.readNullable<int>('b'),
|
||||
c: await withConstraints.mapFromRowOrNull(row, tablePrefix: 'nested_0'),
|
||||
);
|
||||
});
|
||||
c: await withConstraints.mapFromRowOrNull(row,
|
||||
tablePrefix: 'nested_0'),
|
||||
));
|
||||
}
|
||||
|
||||
Selectable<EMail> searchEmails({required String? term}) {
|
||||
|
@ -1827,8 +1823,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
readsFrom: {
|
||||
config,
|
||||
...generatedexpr.watchedTables,
|
||||
}).map((QueryRow row) {
|
||||
return ReadRowIdResult(
|
||||
}).map((QueryRow row) => ReadRowIdResult(
|
||||
row: row,
|
||||
rowid: row.read<int>('rowid'),
|
||||
configKey: row.read<String>('config_key'),
|
||||
|
@ -1839,8 +1834,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
syncStateImplicit: NullAwareTypeConverter.wrapFromSql(
|
||||
ConfigTable.$convertersyncStateImplicit,
|
||||
row.readNullable<int>('sync_state_implicit')),
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
|
||||
Selectable<MyViewData> readView({ReadView$where? where}) {
|
||||
|
@ -1895,11 +1889,10 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
readsFrom: {
|
||||
withConstraints,
|
||||
withDefaults,
|
||||
}).asyncMap((QueryRow row) async {
|
||||
return NestedResult(
|
||||
}).asyncMap((QueryRow row) async => NestedResult(
|
||||
row: row,
|
||||
defaults: await withDefaults.mapFromRow(row, tablePrefix: 'nested_0'),
|
||||
nestedQuery0: await customSelect(
|
||||
nestedQuery1: await customSelect(
|
||||
'SELECT * FROM with_constraints AS c WHERE c.b = ?1',
|
||||
variables: [
|
||||
Variable<int>(row.read('\$n_0'))
|
||||
|
@ -1908,8 +1901,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
withConstraints,
|
||||
withDefaults,
|
||||
}).asyncMap(withConstraints.mapFromRow).get(),
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
|
||||
Selectable<MyCustomResultClass> customResult() {
|
||||
|
@ -2081,25 +2073,25 @@ typedef ReadView$where = Expression<bool> Function(MyView my_view);
|
|||
|
||||
class NestedResult extends CustomResultSet {
|
||||
final WithDefault defaults;
|
||||
final List<WithConstraint> nestedQuery0;
|
||||
final List<WithConstraint> nestedQuery1;
|
||||
NestedResult({
|
||||
required QueryRow row,
|
||||
required this.defaults,
|
||||
required this.nestedQuery0,
|
||||
required this.nestedQuery1,
|
||||
}) : super(row);
|
||||
@override
|
||||
int get hashCode => Object.hash(defaults, nestedQuery0);
|
||||
int get hashCode => Object.hash(defaults, nestedQuery1);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is NestedResult &&
|
||||
other.defaults == this.defaults &&
|
||||
other.nestedQuery0 == this.nestedQuery0);
|
||||
other.nestedQuery1 == this.nestedQuery1);
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('NestedResult(')
|
||||
..write('defaults: $defaults, ')
|
||||
..write('nestedQuery0: $nestedQuery0')
|
||||
..write('nestedQuery1: $nestedQuery1')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
|
|
@ -641,15 +641,12 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
|||
static const VerificationMeta _isAwesomeMeta =
|
||||
const VerificationMeta('isAwesome');
|
||||
@override
|
||||
late final GeneratedColumn<bool> isAwesome =
|
||||
GeneratedColumn<bool>('is_awesome', aliasedName, false,
|
||||
late final GeneratedColumn<bool> isAwesome = GeneratedColumn<bool>(
|
||||
'is_awesome', aliasedName, false,
|
||||
type: DriftSqlType.bool,
|
||||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
|
||||
SqlDialect.sqlite: 'CHECK ("is_awesome" IN (0, 1))',
|
||||
SqlDialect.mysql: '',
|
||||
SqlDialect.postgres: '',
|
||||
}),
|
||||
defaultConstraints:
|
||||
GeneratedColumn.constraintIsAlways('CHECK ("is_awesome" IN (0, 1))'),
|
||||
defaultValue: const Constant(true));
|
||||
static const VerificationMeta _profilePictureMeta =
|
||||
const VerificationMeta('profilePicture');
|
||||
|
@ -1549,7 +1546,7 @@ class $CategoryTodoCountViewView
|
|||
@override
|
||||
String get entityName => 'category_todo_count_view';
|
||||
@override
|
||||
String? get createViewStmt => null;
|
||||
Map<SqlDialect, String>? get createViewStatements => null;
|
||||
@override
|
||||
$CategoryTodoCountViewView get asDslTable => this;
|
||||
@override
|
||||
|
@ -1660,7 +1657,7 @@ class $TodoWithCategoryViewView
|
|||
@override
|
||||
String get entityName => 'todo_with_category_view';
|
||||
@override
|
||||
String? get createViewStmt => null;
|
||||
Map<SqlDialect, String>? get createViewStatements => null;
|
||||
@override
|
||||
$TodoWithCategoryViewView get asDslTable => this;
|
||||
@override
|
||||
|
@ -1714,8 +1711,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
readsFrom: {
|
||||
categories,
|
||||
todosTable,
|
||||
}).map((QueryRow row) {
|
||||
return AllTodosWithCategoryResult(
|
||||
}).map((QueryRow row) => AllTodosWithCategoryResult(
|
||||
row: row,
|
||||
id: row.read<int>('id'),
|
||||
title: row.readNullable<String>('title'),
|
||||
|
@ -1727,8 +1723,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
row.readNullable<String>('status')),
|
||||
catId: row.read<int>('catId'),
|
||||
catDesc: row.read<String>('catDesc'),
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
|
||||
Future<int> deleteTodoById(int var1) {
|
||||
|
|
|
@ -136,7 +136,7 @@ void main() {
|
|||
contains(
|
||||
isA<NestedResult>()
|
||||
.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(
|
||||
isA<NestedResult>()
|
||||
.having((e) => e.defaults, 'defaults', second)
|
||||
.having((e) => e.nestedQuery0, 'nested', hasLength(1)),
|
||||
.having((e) => e.nestedQuery1, 'nested', hasLength(1)),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -30,6 +30,6 @@ void main() {
|
|||
|
||||
final result = results.single;
|
||||
expect(result.defaults, defaults);
|
||||
expect(result.nestedQuery0, [constraints]);
|
||||
expect(result.nestedQuery1, [constraints]);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -50,4 +50,52 @@ void main() {
|
|||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
- Add the `schema steps` command to generate help in writing step-by-step schema migrations.
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
# 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
|
||||
# can be resolved.
|
||||
# - moor_generator: The regular SharedPartBuilder for @UseMoor and @UseDao
|
||||
# - drift_dev: The regular SharedPartBuilder for @DriftDatabase and @DriftAccessor
|
||||
# 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
|
||||
# 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:
|
||||
preparing_builder:
|
||||
|
|
|
@ -124,7 +124,7 @@ class DriftOptions {
|
|||
this.modules = const [],
|
||||
this.sqliteAnalysisOptions,
|
||||
this.storeDateTimeValuesAsText = false,
|
||||
this.dialect = const DialectOptions(SqlDialect.sqlite, null),
|
||||
this.dialect = const DialectOptions(null, [SqlDialect.sqlite], null),
|
||||
this.caseFromDartToSql = CaseFromDartToSql.snake,
|
||||
this.writeToColumnsMixins = false,
|
||||
this.fatalWarnings = false,
|
||||
|
@ -189,7 +189,18 @@ class DriftOptions {
|
|||
/// Whether the [module] has been enabled in this configuration.
|
||||
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.
|
||||
SqliteVersion get sqliteVersion {
|
||||
|
@ -201,10 +212,11 @@ class DriftOptions {
|
|||
|
||||
@JsonSerializable()
|
||||
class DialectOptions {
|
||||
final SqlDialect dialect;
|
||||
final SqlDialect? dialect;
|
||||
final List<SqlDialect>? dialects;
|
||||
final SqliteAnalysisOptions? options;
|
||||
|
||||
const DialectOptions(this.dialect, this.options);
|
||||
const DialectOptions(this.dialect, this.dialects, this.options);
|
||||
|
||||
factory DialectOptions.fromJson(Map json) => _$DialectOptionsFromJson(json);
|
||||
|
||||
|
|
|
@ -198,18 +198,6 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
|
|||
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) {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:drift/drift.dart' show SqlDialect;
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:sqlparser/sqlparser.dart' as sql;
|
||||
|
@ -110,7 +111,8 @@ class DriftViewResolver extends DriftElementResolver<DiscoveredDriftView> {
|
|||
query: stmt.query,
|
||||
// Remove drift-specific syntax
|
||||
driftTableName: null,
|
||||
).toSqlWithoutDriftSpecificSyntax(resolver.driver.options);
|
||||
).toSqlWithoutDriftSpecificSyntax(
|
||||
resolver.driver.options, SqlDialect.sqlite);
|
||||
|
||||
return DriftView(
|
||||
discovered.ownId,
|
||||
|
|
|
@ -35,7 +35,7 @@ class MatchExistingTypeForQuery {
|
|||
}
|
||||
}
|
||||
|
||||
ExistingQueryRowType? _findRowType(
|
||||
QueryRowType? _findRowType(
|
||||
InferredResultSet resultSet,
|
||||
dynamic /*DartType|RequestedQueryResultType*/ requestedType,
|
||||
_ErrorReporter reportError,
|
||||
|
@ -52,8 +52,8 @@ class MatchExistingTypeForQuery {
|
|||
'Must be a DartType of a RequestedQueryResultType');
|
||||
}
|
||||
|
||||
final positionalColumns = <ArgumentForExistingQueryRowType>[];
|
||||
final namedColumns = <String, ArgumentForExistingQueryRowType>{};
|
||||
final positionalColumns = <ArgumentForQueryRowType>[];
|
||||
final namedColumns = <String, ArgumentForQueryRowType>{};
|
||||
|
||||
final unmatchedColumnsByName = {
|
||||
for (final column in resultSet.columns)
|
||||
|
@ -94,10 +94,9 @@ class MatchExistingTypeForQuery {
|
|||
addEntry(name, () => transformedTypeBuilder.addDartType(type));
|
||||
}
|
||||
|
||||
void addCheckedType(
|
||||
ArgumentForExistingQueryRowType type, DartType originalType,
|
||||
void addCheckedType(ArgumentForQueryRowType type, DartType originalType,
|
||||
{String? name}) {
|
||||
if (type is ExistingQueryRowType) {
|
||||
if (type is QueryRowType) {
|
||||
addEntry(name, () => transformedTypeBuilder.addCode(type.rowType));
|
||||
} else if (type is MappedNestedListQuery) {
|
||||
addEntry(name, () {
|
||||
|
@ -171,7 +170,7 @@ class MatchExistingTypeForQuery {
|
|||
final verified = _verifyArgument(resultSet.scalarColumns.single,
|
||||
desiredType, 'Single column', (ignore) {});
|
||||
if (verified != null) {
|
||||
return ExistingQueryRowType(
|
||||
return QueryRowType(
|
||||
rowType: AnnotatedDartCode.type(desiredType),
|
||||
singleValue: verified,
|
||||
positionalArguments: const [],
|
||||
|
@ -184,7 +183,7 @@ class MatchExistingTypeForQuery {
|
|||
final verified =
|
||||
_verifyMatchingDriftTable(resultSet.matchingTable!, desiredType);
|
||||
if (verified != null) {
|
||||
return ExistingQueryRowType(
|
||||
return QueryRowType(
|
||||
rowType: AnnotatedDartCode.build((builder) =>
|
||||
builder.addElementRowType(resultSet.matchingTable!.table)),
|
||||
singleValue: verified,
|
||||
|
@ -237,7 +236,7 @@ class MatchExistingTypeForQuery {
|
|||
}
|
||||
}
|
||||
|
||||
return ExistingQueryRowType(
|
||||
return QueryRowType(
|
||||
rowType: annotatedTypeCode,
|
||||
constructorName: constructorName ?? '',
|
||||
isRecord: desiredType is RecordType,
|
||||
|
@ -249,13 +248,13 @@ class MatchExistingTypeForQuery {
|
|||
|
||||
/// Returns the default record type chosen by drift when a user declares the
|
||||
/// 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
|
||||
// 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
|
||||
// directly.
|
||||
if (resultSet.singleColumn) {
|
||||
return ExistingQueryRowType(
|
||||
return QueryRowType(
|
||||
rowType: AnnotatedDartCode.build(
|
||||
(builder) => builder.addDriftType(resultSet.scalarColumns.single)),
|
||||
singleValue: resultSet.scalarColumns.single,
|
||||
|
@ -264,7 +263,7 @@ class MatchExistingTypeForQuery {
|
|||
);
|
||||
} else if (resultSet.matchingTable != null) {
|
||||
final table = resultSet.matchingTable!;
|
||||
return ExistingQueryRowType(
|
||||
return QueryRowType(
|
||||
rowType: AnnotatedDartCode.build(
|
||||
(builder) => builder.addElementRowType(table.table)),
|
||||
singleValue: table,
|
||||
|
@ -273,7 +272,7 @@ class MatchExistingTypeForQuery {
|
|||
);
|
||||
}
|
||||
|
||||
final namedArguments = <String, ArgumentForExistingQueryRowType>{};
|
||||
final namedArguments = <String, ArgumentForQueryRowType>{};
|
||||
|
||||
final type = AnnotatedDartCode.build((builder) {
|
||||
builder.addText('({');
|
||||
|
@ -288,8 +287,10 @@ class MatchExistingTypeForQuery {
|
|||
builder.addDriftType(column);
|
||||
namedArguments[fieldName] = column;
|
||||
} else if (column is NestedResultTable) {
|
||||
builder.addElementRowType(column.table);
|
||||
namedArguments[fieldName] = column;
|
||||
final innerRecord = _defaultRecord(column.innerResultSet);
|
||||
builder.addCode(innerRecord.rowType);
|
||||
namedArguments[fieldName] =
|
||||
StructuredFromNestedColumn(column, innerRecord);
|
||||
} else if (column is NestedResultQuery) {
|
||||
final nestedResultSet = column.query.resultSet;
|
||||
|
||||
|
@ -310,7 +311,7 @@ class MatchExistingTypeForQuery {
|
|||
builder.addText('})');
|
||||
});
|
||||
|
||||
return ExistingQueryRowType(
|
||||
return QueryRowType(
|
||||
rowType: type,
|
||||
singleValue: null,
|
||||
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,
|
||||
DartType existingTypeForColumn,
|
||||
String name,
|
||||
|
@ -339,25 +347,15 @@ class MatchExistingTypeForQuery {
|
|||
|
||||
if (matches) return column;
|
||||
} 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
|
||||
// verify the existing type. If there's an existing row class though, we
|
||||
// can compare against that.
|
||||
if (table.hasExistingRowClass) {
|
||||
final existingType = table.existingRowClass!.targetType;
|
||||
if (column.isNullable) {
|
||||
existingTypeForColumn =
|
||||
typeSystem.promoteToNonNull(existingTypeForColumn);
|
||||
if (foundInnerType != null) {
|
||||
return StructuredFromNestedColumn(column, foundInnerType);
|
||||
}
|
||||
|
||||
if (!typeSystem.isAssignableTo(existingType, existingTypeForColumn)) {
|
||||
reportError('$name must accept '
|
||||
'${existingType.getDisplayString(withNullability: true)}');
|
||||
}
|
||||
}
|
||||
|
||||
return column;
|
||||
} else if (column is NestedResultQuery) {
|
||||
// A nested query has its own type, which we can recursively try to
|
||||
// structure in the existing type.
|
||||
|
@ -378,13 +376,14 @@ class MatchExistingTypeForQuery {
|
|||
return MappedNestedListQuery(column, innerExistingType);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Allows using a matching drift table from a result set as an argument if
|
||||
/// the the [existingTypeForColumn] matches the table's type (either the
|
||||
/// existing result type or `dynamic` if it's drift-generated).
|
||||
ArgumentForExistingQueryRowType? _verifyMatchingDriftTable(
|
||||
ArgumentForQueryRowType? _verifyMatchingDriftTable(
|
||||
MatchingDriftTable match, DartType existingTypeForColumn) {
|
||||
final table = match.table;
|
||||
if (table.hasExistingRowClass) {
|
||||
|
|
|
@ -330,6 +330,7 @@ class QueryAnalyzer {
|
|||
|
||||
if (column is NestedStarResultColumn) {
|
||||
final resolved = _resolveNestedResultTable(queryContext, column);
|
||||
|
||||
if (resolved != null) {
|
||||
// The single table optimization doesn't make sense when nested result
|
||||
// sets are present.
|
||||
|
@ -439,19 +440,37 @@ class QueryAnalyzer {
|
|||
_QueryHandlerContext queryContext, NestedStarResultColumn column) {
|
||||
final originalResult = column.resultSet;
|
||||
final result = originalResult?.unalias();
|
||||
if (result is! Table && result is! View) {
|
||||
return null;
|
||||
}
|
||||
final rawColumns = result?.resolvedColumns;
|
||||
|
||||
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 isNullable =
|
||||
analysis == null || analysis.isNullableTable(originalResult!);
|
||||
|
||||
final queryIndex = nestedQueryCounter++;
|
||||
final resultClassName =
|
||||
'${ReCase(queryContext.queryName).pascalCase}NestedColumn$queryIndex';
|
||||
|
||||
return NestedResultTable(
|
||||
column,
|
||||
column.as ?? column.tableName,
|
||||
driftTable,
|
||||
from: column,
|
||||
name: column.as ?? column.tableName,
|
||||
innerResultSet: driftResultSet,
|
||||
nameForGeneratedRowClass: resultClassName,
|
||||
isNullable: isNullable,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -171,6 +171,11 @@ class AnnotatedDartCodeBuilder {
|
|||
'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) {
|
||||
return addCode(resultSet.existingRowType!.rowType);
|
||||
}
|
||||
|
@ -183,12 +188,13 @@ class AnnotatedDartCodeBuilder {
|
|||
return addDriftType(resultSet.scalarColumns.single);
|
||||
}
|
||||
|
||||
return addText(query.resultClassName);
|
||||
return addText(resultClassName());
|
||||
}
|
||||
|
||||
void addTypeOfNestedResult(NestedResult nested) {
|
||||
if (nested is NestedResultTable) {
|
||||
return addElementRowType(nested.table);
|
||||
return addResultSetRowType(
|
||||
nested.innerResultSet, () => nested.nameForGeneratedRowClass);
|
||||
} else if (nested is NestedResultQuery) {
|
||||
addSymbol('List', AnnotatedDartCode.dartCore);
|
||||
addText('<');
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart' show DriftSqlType, UpdateKind;
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
|
@ -25,7 +24,7 @@ abstract class DriftQueryDeclaration {
|
|||
/// We deliberately only store very basic information here: The actual query
|
||||
/// model is very complex and hard to serialize. Further, lots of generation
|
||||
/// 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 they are local elements which can't be referenced by others, there's
|
||||
/// no clear advantage wrt. incremental compilation if queries are fully
|
||||
|
@ -162,15 +161,10 @@ abstract class SqlQuery {
|
|||
placeholders = elements.whereType<FoundDartPlaceholder>().toList();
|
||||
}
|
||||
|
||||
bool get needsAsyncMapping {
|
||||
final result = 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;
|
||||
}
|
||||
bool get _useResultClassName {
|
||||
final resultSet = this.resultSet!;
|
||||
|
||||
return false;
|
||||
return resultSet.matchingTable == null && !resultSet.singleColumn;
|
||||
}
|
||||
|
||||
String get resultClassName {
|
||||
|
@ -179,7 +173,7 @@ abstract class SqlQuery {
|
|||
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, '
|
||||
'either because it has a matching table or because it only returns '
|
||||
'one column.');
|
||||
|
@ -209,6 +203,16 @@ abstract class SqlQuery {
|
|||
|
||||
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 {
|
||||
|
@ -230,9 +234,6 @@ class SqlSelectQuery extends SqlQuery {
|
|||
bool get hasNestedQuery =>
|
||||
resultSet.nestedResults.any((e) => e is NestedResultQuery);
|
||||
|
||||
@override
|
||||
bool get needsAsyncMapping => hasNestedQuery || super.needsAsyncMapping;
|
||||
|
||||
SqlSelectQuery(
|
||||
String name,
|
||||
this.fromContext,
|
||||
|
@ -416,7 +417,7 @@ class InferredResultSet {
|
|||
|
||||
/// If specified, an existing user-defined Dart type to use instead of
|
||||
/// 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
|
||||
/// result set.
|
||||
|
@ -494,21 +495,86 @@ class InferredResultSet {
|
|||
const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality());
|
||||
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),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExistingQueryRowType implements ArgumentForExistingQueryRowType {
|
||||
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),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 String constructorName;
|
||||
final bool isRecord;
|
||||
|
||||
/// When set, instead of constructing the [rowType] from the arguments, the
|
||||
/// argument specified here can just be cast into the desired [rowType].
|
||||
ArgumentForExistingQueryRowType? singleValue;
|
||||
ArgumentForQueryRowType? singleValue;
|
||||
|
||||
final List<ArgumentForExistingQueryRowType> positionalArguments;
|
||||
final Map<String, ArgumentForExistingQueryRowType> namedArguments;
|
||||
final List<ArgumentForQueryRowType> positionalArguments;
|
||||
final Map<String, ArgumentForQueryRowType> namedArguments;
|
||||
|
||||
ExistingQueryRowType({
|
||||
QueryRowType({
|
||||
required this.rowType,
|
||||
required this.singleValue,
|
||||
required this.positionalArguments,
|
||||
|
@ -517,6 +583,19 @@ class ExistingQueryRowType implements ArgumentForExistingQueryRowType {
|
|||
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
|
||||
String toString() {
|
||||
return 'ExistingQueryRowType(type: $rowType, singleValue: $singleValue, '
|
||||
|
@ -524,26 +603,60 @@ class ExistingQueryRowType implements ArgumentForExistingQueryRowType {
|
|||
}
|
||||
}
|
||||
|
||||
@sealed
|
||||
abstract class ArgumentForExistingQueryRowType {}
|
||||
sealed class ArgumentForQueryRowType {
|
||||
/// 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 ExistingQueryRowType nestedType;
|
||||
final QueryRowType 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
|
||||
/// selects all columns from that table, and nothing more.
|
||||
///
|
||||
/// We still need to handle column aliases.
|
||||
class MatchingDriftTable implements ArgumentForExistingQueryRowType {
|
||||
class MatchingDriftTable implements ArgumentForQueryRowType {
|
||||
final DriftElementWithResultSet table;
|
||||
final Map<String, DriftColumn> 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.
|
||||
///
|
||||
/// This is the case if each result column name maps to a drift column with
|
||||
|
@ -554,8 +667,7 @@ class MatchingDriftTable implements ArgumentForExistingQueryRowType {
|
|||
}
|
||||
}
|
||||
|
||||
@sealed
|
||||
abstract class ResultColumn {
|
||||
sealed class ResultColumn {
|
||||
/// A unique name for this column in Dart.
|
||||
String dartGetterName(Iterable<String> existingNames);
|
||||
|
||||
|
@ -567,8 +679,8 @@ abstract class ResultColumn {
|
|||
bool isCompatibleTo(ResultColumn other);
|
||||
}
|
||||
|
||||
class ScalarResultColumn extends ResultColumn
|
||||
implements HasType, ArgumentForExistingQueryRowType {
|
||||
final class ScalarResultColumn extends ResultColumn
|
||||
implements HasType, ArgumentForQueryRowType {
|
||||
final String name;
|
||||
@override
|
||||
final DriftSqlType sqlType;
|
||||
|
@ -587,6 +699,9 @@ class ScalarResultColumn extends ResultColumn
|
|||
@override
|
||||
bool get isArray => false;
|
||||
|
||||
@override
|
||||
bool get requiresAsynchronousContext => false;
|
||||
|
||||
@override
|
||||
String dartGetterName(Iterable<String> existingNames) {
|
||||
return dartNameForSqlColumn(name, existingNames: existingNames);
|
||||
|
@ -610,7 +725,7 @@ class ScalarResultColumn extends ResultColumn
|
|||
|
||||
/// A nested result, could either be a [NestedResultTable] or a
|
||||
/// [NestedResultQuery].
|
||||
abstract class NestedResult extends ResultColumn {}
|
||||
sealed class NestedResult extends ResultColumn {}
|
||||
|
||||
/// 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
|
||||
/// [NestedResultTable] information as part of the result set.
|
||||
class NestedResultTable extends NestedResult
|
||||
implements ArgumentForExistingQueryRowType {
|
||||
final class NestedResultTable extends NestedResult {
|
||||
final bool isNullable;
|
||||
final NestedStarResultColumn from;
|
||||
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
|
||||
String dartGetterName(Iterable<String> existingNames) {
|
||||
|
@ -655,7 +780,7 @@ class NestedResultTable extends NestedResult
|
|||
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
|
||||
@override
|
||||
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
|
||||
|
@ -665,12 +790,12 @@ class NestedResultTable extends NestedResult
|
|||
if (other is! NestedResultTable) return false;
|
||||
|
||||
return other.name == name &&
|
||||
other.table == table &&
|
||||
other.innerResultSet.isCompatibleTo(other.innerResultSet) &&
|
||||
other.isNullable == isNullable;
|
||||
}
|
||||
}
|
||||
|
||||
class NestedResultQuery extends NestedResult {
|
||||
final class NestedResultQuery extends NestedResult {
|
||||
final NestedQueryColumn from;
|
||||
|
||||
final SqlSelectQuery query;
|
||||
|
|
|
@ -179,11 +179,16 @@ DialectOptions _$DialectOptionsFromJson(Map json) => $checkedCreate(
|
|||
($checkedConvert) {
|
||||
$checkKeys(
|
||||
json,
|
||||
allowedKeys: const ['dialect', 'options'],
|
||||
allowedKeys: const ['dialect', 'dialects', 'options'],
|
||||
);
|
||||
final val = DialectOptions(
|
||||
$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(
|
||||
'options',
|
||||
(v) =>
|
||||
|
@ -195,7 +200,9 @@ DialectOptions _$DialectOptionsFromJson(Map json) => $checkedCreate(
|
|||
|
||||
Map<String, dynamic> _$DialectOptionsToJson(DialectOptions instance) =>
|
||||
<String, dynamic>{
|
||||
'dialect': _$SqlDialectEnumMap[instance.dialect]!,
|
||||
'dialect': _$SqlDialectEnumMap[instance.dialect],
|
||||
'dialects':
|
||||
instance.dialects?.map((e) => _$SqlDialectEnumMap[e]!).toList(),
|
||||
'options': instance.options?.toJson(),
|
||||
};
|
||||
|
||||
|
|
|
@ -220,25 +220,37 @@ class DatabaseWriter {
|
|||
}
|
||||
|
||||
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');
|
||||
|
||||
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) {
|
||||
final sql = scope.sqlCode(entity.parsedStatement!);
|
||||
final (sql, dialectSpecific) = scope.sqlByDialect(entity.parsedStatement!);
|
||||
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(
|
||||
Scope scope, DefinedSqlQuery query, SqlQuery resolved) {
|
||||
final sql = scope.sqlCode(resolved.root!);
|
||||
final (sql, dialectSpecific) = scope.sqlByDialect(resolved.root!);
|
||||
final onCreate = scope.drift('OnCreateQuery');
|
||||
|
||||
return '$onCreate(${asDartLiteral(sql)})';
|
||||
if (dialectSpecific) {
|
||||
return '$onCreate.byDialect($sql)';
|
||||
} else {
|
||||
return '$onCreate($sql)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
||||
|
||||
|
@ -13,6 +14,16 @@ import 'utils.dart';
|
|||
|
||||
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
|
||||
/// should be included in a generated database or dao class.
|
||||
class QueryWriter {
|
||||
|
@ -74,83 +85,83 @@ class QueryWriter {
|
|||
|
||||
/// Writes the function literal that turns a "QueryRow" into the desired
|
||||
/// custom return type of a query.
|
||||
void _writeMappingLambda(SqlQuery query) {
|
||||
final resultSet = query.resultSet!;
|
||||
void _writeMappingLambda(InferredResultSet resultSet, QueryRowType rowClass) {
|
||||
final queryRow = _emitter.drift('QueryRow');
|
||||
final existingRowType = resultSet.existingRowType;
|
||||
final asyncModifier = query.needsAsyncMapping ? 'async' : '';
|
||||
final asyncModifier = rowClass.requiresAsynchronousContext ? 'async' : '';
|
||||
|
||||
if (existingRowType != null) {
|
||||
_emitter.write('($queryRow row) $asyncModifier => ');
|
||||
_writeArgumentExpression(existingRowType, resultSet);
|
||||
} else if (resultSet.singleColumn) {
|
||||
final column = resultSet.scalarColumns.single;
|
||||
_emitter.write('($queryRow row) => ');
|
||||
_readScalar(column);
|
||||
} else if (resultSet.matchingTable != null) {
|
||||
final match = resultSet.matchingTable!;
|
||||
|
||||
if (match.effectivelyNoAlias) {
|
||||
// We can write every available mapping as a Dart expression via
|
||||
// _writeArgumentExpression. This can be turned into a lambda by appending
|
||||
// it with `(QueryRow row) => $expression`. That's also what we're doing,
|
||||
// but if we'll just call mapFromRow in there, we can just tear that method
|
||||
// off instead. This is just an optimization.
|
||||
final singleValue = rowClass.singleValue;
|
||||
if (singleValue is MatchingDriftTable && singleValue.effectivelyNoAlias) {
|
||||
// Tear-off mapFromRow method on table
|
||||
_emitter.write('${match.table.dbGetterName}.mapFromRow');
|
||||
_emitter.write('${singleValue.table.dbGetterName}.mapFromRow');
|
||||
} else {
|
||||
_emitter.write('($queryRow row) => ');
|
||||
_writeArgumentExpression(match, resultSet);
|
||||
}
|
||||
} else {
|
||||
_buffer
|
||||
..writeln('($queryRow row) $asyncModifier {')
|
||||
..write('return ${query.resultClassName}(');
|
||||
|
||||
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}');
|
||||
// In all other cases, we're off to write the expression.
|
||||
_emitter.write('($queryRow row) $asyncModifier => ');
|
||||
_writeArgumentExpression(
|
||||
rowClass, resultSet, (sqlPrefix: null, isNullable: false));
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes code that will read the [argument] for an existing row type from
|
||||
/// the raw `QueryRow`.
|
||||
void _writeArgumentExpression(
|
||||
ArgumentForExistingQueryRowType argument, InferredResultSet resultSet) {
|
||||
if (argument is MappedNestedListQuery) {
|
||||
final queryRow = _emitter.drift('QueryRow');
|
||||
|
||||
ArgumentForQueryRowType argument,
|
||||
InferredResultSet resultSet,
|
||||
_ArgumentContext context,
|
||||
) {
|
||||
switch (argument) {
|
||||
case RawQueryRow():
|
||||
_buffer.write('row');
|
||||
case ScalarResultColumn():
|
||||
_readScalar(argument, context);
|
||||
case MatchingDriftTable():
|
||||
_readMatchingTable(argument, context);
|
||||
case StructuredFromNestedColumn():
|
||||
final prefix = resultSet.nestedPrefixFor(argument.table);
|
||||
_writeArgumentExpression(
|
||||
argument.nestedType,
|
||||
resultSet,
|
||||
(sqlPrefix: prefix, isNullable: argument.nullable),
|
||||
);
|
||||
case MappedNestedListQuery():
|
||||
_buffer.write('await ');
|
||||
_writeCustomSelectStatement(argument.column.query,
|
||||
includeMappingToDart: false);
|
||||
_buffer.write('.map(');
|
||||
_buffer.write('($queryRow row) => ');
|
||||
_writeArgumentExpression(argument.nestedType, resultSet);
|
||||
_buffer.write(').get()');
|
||||
} else if (argument is ExistingQueryRowType) {
|
||||
final query = argument.column.query;
|
||||
_writeCustomSelectStatement(query, argument.nestedType);
|
||||
_buffer.write('.get()');
|
||||
case QueryRowType():
|
||||
final singleValue = argument.singleValue;
|
||||
if (singleValue != null) {
|
||||
return _writeArgumentExpression(singleValue, resultSet);
|
||||
return _writeArgumentExpression(singleValue, resultSet, context);
|
||||
}
|
||||
|
||||
if (context.isNullable) {
|
||||
// If this structed type is nullable, it's coming from an OUTER join
|
||||
// which means that, even if the individual components making up the
|
||||
// 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
|
||||
// null, return null directly instead of creating the structured type.
|
||||
for (final arg in argument.positionalArguments
|
||||
.followedBy(argument.namedArguments.values)
|
||||
.whereType<ScalarResultColumn>()) {
|
||||
if (!arg.nullable) {
|
||||
final keyInMap = context.applyPrefix(arg.name);
|
||||
_buffer.write(
|
||||
'row.data[${asDartLiteral(keyInMap)}] == null ? null : ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final _ArgumentContext childContext = (
|
||||
sqlPrefix: context.sqlPrefix,
|
||||
// Individual fields making up this query row type aren't covered by
|
||||
// the outer nullability.
|
||||
isNullable: false,
|
||||
);
|
||||
|
||||
if (!argument.isRecord) {
|
||||
// We're writing a constructor, so let's start with the class name.
|
||||
_emitter.writeDart(argument.rowType);
|
||||
|
@ -165,41 +176,40 @@ class QueryWriter {
|
|||
|
||||
_buffer.write('(');
|
||||
for (final positional in argument.positionalArguments) {
|
||||
_writeArgumentExpression(positional, resultSet);
|
||||
_writeArgumentExpression(positional, resultSet, childContext);
|
||||
_buffer.write(', ');
|
||||
}
|
||||
argument.namedArguments.forEach((name, parameter) {
|
||||
_buffer.write('$name: ');
|
||||
_writeArgumentExpression(parameter, resultSet);
|
||||
_writeArgumentExpression(parameter, resultSet, childContext);
|
||||
_buffer.write(', ');
|
||||
});
|
||||
|
||||
_buffer.write(')');
|
||||
} else if (argument is NestedResultTable) {
|
||||
final prefix = resultSet.nestedPrefixFor(argument);
|
||||
_readNestedTable(argument, prefix!);
|
||||
} else if (argument is ScalarResultColumn) {
|
||||
return _readScalar(argument);
|
||||
} else if (argument is MatchingDriftTable) {
|
||||
_readMatchingTable(argument);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// suitable type.
|
||||
void _readScalar(ScalarResultColumn column) {
|
||||
void _readScalar(ScalarResultColumn column, _ArgumentContext context) {
|
||||
final specialName = _transformer.newNameFor(column.sqlParserColumn!);
|
||||
final isNullable = context.isNullable || column.nullable;
|
||||
|
||||
final dartLiteral = asDartLiteral(specialName ?? column.name);
|
||||
final method = column.nullable ? 'readNullable' : 'read';
|
||||
var name = specialName ?? column.name;
|
||||
if (context.sqlPrefix != null) {
|
||||
name = '${context.sqlPrefix}.$name';
|
||||
}
|
||||
|
||||
final dartLiteral = asDartLiteral(name);
|
||||
final method = isNullable ? 'readNullable' : 'read';
|
||||
final rawDartType =
|
||||
_emitter.dartCode(AnnotatedDartCode([dartTypeNames[column.sqlType]!]));
|
||||
var code = 'row.$method<$rawDartType>($dartLiteral)';
|
||||
|
||||
final converter = column.typeConverter;
|
||||
if (converter != null) {
|
||||
if (converter.canBeSkippedForNulls && column.nullable) {
|
||||
if (converter.canBeSkippedForNulls && isNullable) {
|
||||
// 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
|
||||
// converter for non-null values.
|
||||
|
@ -214,36 +224,52 @@ class QueryWriter {
|
|||
_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
|
||||
// use the mapFromRow() function of that table - the column names might
|
||||
// be different!
|
||||
final table = match.table;
|
||||
|
||||
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 {
|
||||
// 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 {');
|
||||
|
||||
for (final alias in match.aliasToColumn.entries) {
|
||||
_emitter
|
||||
..write(asDartLiteral(alias.key))
|
||||
..write(asDartLiteral(context.applyPrefix(alias.key)))
|
||||
..write(': ')
|
||||
..write(asDartLiteral(alias.value.nameInSql))
|
||||
..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
|
||||
/// of the custom query.
|
||||
void _writeSelectStatementCreator(SqlSelectQuery select) {
|
||||
|
@ -270,22 +296,22 @@ class QueryWriter {
|
|||
}
|
||||
|
||||
void _writeCustomSelectStatement(SqlSelectQuery select,
|
||||
{bool includeMappingToDart = true}) {
|
||||
[QueryRowType? resultType]) {
|
||||
_buffer.write(' customSelect(${_queryCode(select)}, ');
|
||||
_writeVariables(select);
|
||||
_buffer.write(', ');
|
||||
_writeReadsFrom(select);
|
||||
|
||||
if (includeMappingToDart) {
|
||||
if (select.needsAsyncMapping) {
|
||||
final resultSet = select.resultSet;
|
||||
resultType ??= select.queryRowType(options);
|
||||
|
||||
if (resultType.requiresAsynchronousContext) {
|
||||
_buffer.write(').asyncMap(');
|
||||
} else {
|
||||
_buffer.write(').map(');
|
||||
}
|
||||
|
||||
_writeMappingLambda(select);
|
||||
}
|
||||
|
||||
_writeMappingLambda(resultSet, resultType);
|
||||
_buffer.write(')');
|
||||
}
|
||||
|
||||
|
@ -311,13 +337,17 @@ class QueryWriter {
|
|||
_writeCommonUpdateParameters(update);
|
||||
|
||||
_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(');
|
||||
_writeMappingLambda(update);
|
||||
_writeMappingLambda(resultSet, rowType);
|
||||
_buffer.write('))');
|
||||
} else {
|
||||
_buffer.write('rows.map(');
|
||||
_writeMappingLambda(update);
|
||||
_writeMappingLambda(resultSet, rowType);
|
||||
_buffer.write(').toList()');
|
||||
}
|
||||
_buffer.write(');\n}');
|
||||
|
@ -466,7 +496,45 @@ class QueryWriter {
|
|||
/// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned
|
||||
/// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'.
|
||||
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) {
|
||||
|
@ -789,9 +857,14 @@ String? _defaultForDartPlaceholder(
|
|||
if (kind is ExpressionDartPlaceholderType && kind.defaultValue != null) {
|
||||
// Wrap the default expression in parentheses to avoid issues with
|
||||
// the surrounding precedence in SQL.
|
||||
final sql = SqlWriter(scope.options)
|
||||
.writeNodeIntoStringLiteral(Parentheses(kind.defaultValue!));
|
||||
final (sql, dialectSpecific) =
|
||||
scope.sqlByDialect(Parentheses(kind.defaultValue!));
|
||||
|
||||
if (dialectSpecific) {
|
||||
return 'const ${scope.drift('CustomExpression')}.dialectSpecific($sql)';
|
||||
} else {
|
||||
return 'const ${scope.drift('CustomExpression')}($sql)';
|
||||
}
|
||||
} else if (kind is SimpleDartPlaceholderType &&
|
||||
kind.kind == SimpleDartPlaceholderKind.orderBy) {
|
||||
return 'const ${scope.drift('OrderBy')}.nothing()';
|
||||
|
@ -800,3 +873,12 @@ String? _defaultForDartPlaceholder(
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extension on _ArgumentContext {
|
||||
String applyPrefix(String originalName) {
|
||||
return switch (sqlPrefix) {
|
||||
null => originalName,
|
||||
var s => '$s.$originalName',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,20 +5,23 @@ import '../writer.dart';
|
|||
|
||||
/// Writes a class holding the result of an sql query into Dart.
|
||||
class ResultSetWriter {
|
||||
final SqlQuery query;
|
||||
final InferredResultSet resultSet;
|
||||
final String resultClassName;
|
||||
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() {
|
||||
final className = query.resultClassName;
|
||||
final fields = <EqualityField>[];
|
||||
final nonNullableFields = <String>{};
|
||||
final into = scope.leaf();
|
||||
|
||||
final resultSet = query.resultSet!;
|
||||
|
||||
into.write('class $className ');
|
||||
into.write('class $resultClassName ');
|
||||
if (scope.options.rawResultSetData) {
|
||||
into.write('extends CustomResultSet {\n');
|
||||
} else {
|
||||
|
@ -39,6 +42,12 @@ class ResultSetWriter {
|
|||
fields.add(EqualityField(fieldName, isList: column.isUint8ListInDart));
|
||||
if (!column.nullable) nonNullableFields.add(fieldName);
|
||||
} else if (column is NestedResultTable) {
|
||||
if (column.innerResultSet.needsOwnClass) {
|
||||
ResultSetWriter.fromResultSetAndClassName(
|
||||
column.innerResultSet, column.nameForGeneratedRowClass, scope)
|
||||
.write();
|
||||
}
|
||||
|
||||
into
|
||||
..write('$modifier ')
|
||||
..writeDart(
|
||||
|
@ -66,9 +75,9 @@ class ResultSetWriter {
|
|||
|
||||
// write the constructor
|
||||
if (scope.options.rawResultSetData) {
|
||||
into.write('$className({required QueryRow row,');
|
||||
into.write('$resultClassName({required QueryRow row,');
|
||||
} else {
|
||||
into.write('$className({');
|
||||
into.write('$resultClassName({');
|
||||
}
|
||||
|
||||
for (final column in fields) {
|
||||
|
@ -90,9 +99,9 @@ class ResultSetWriter {
|
|||
writeHashCode(fields, into);
|
||||
into.write(';\n');
|
||||
|
||||
overrideEquals(fields, className, into);
|
||||
overrideEquals(fields, resultClassName, into);
|
||||
overrideToString(
|
||||
className, fields.map((f) => f.lexeme).toList(), into.buffer);
|
||||
resultClassName, fields.map((f) => f.lexeme).toList(), into.buffer);
|
||||
}
|
||||
|
||||
into.write('}\n');
|
||||
|
|
|
@ -26,8 +26,9 @@ String placeholderContextName(FoundDartPlaceholder placeholder) {
|
|||
}
|
||||
|
||||
extension ToSqlText on AstNode {
|
||||
String toSqlWithoutDriftSpecificSyntax(DriftOptions options) {
|
||||
final writer = SqlWriter(options, escapeForDart: false);
|
||||
String toSqlWithoutDriftSpecificSyntax(
|
||||
DriftOptions options, SqlDialect dialect) {
|
||||
final writer = SqlWriter(options, dialect: dialect, escapeForDart: false);
|
||||
return writer.writeSql(this);
|
||||
}
|
||||
}
|
||||
|
@ -36,17 +37,19 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
final StringBuffer _out;
|
||||
final SqlQuery? query;
|
||||
final DriftOptions options;
|
||||
final SqlDialect dialect;
|
||||
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,
|
||||
StringBuffer out, bool escapeForDart)
|
||||
SqlWriter._(this.query, this.options, this.dialect,
|
||||
this._starColumnToResolved, StringBuffer out, bool escapeForDart)
|
||||
: _out = out,
|
||||
super(escapeForDart ? _DartEscapingSink(out) : out);
|
||||
|
||||
factory SqlWriter(
|
||||
DriftOptions options, {
|
||||
required SqlDialect dialect,
|
||||
SqlQuery? query,
|
||||
bool escapeForDart = true,
|
||||
StringBuffer? buffer,
|
||||
|
@ -61,7 +64,7 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
if (nestedResult is NestedResultTable) nestedResult.from: nestedResult
|
||||
};
|
||||
}
|
||||
return SqlWriter._(query, options, doubleStarColumnToResolvedTable,
|
||||
return SqlWriter._(query, options, dialect, doubleStarColumnToResolvedTable,
|
||||
buffer ?? StringBuffer(), escapeForDart);
|
||||
}
|
||||
|
||||
|
@ -84,7 +87,7 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
|
||||
@override
|
||||
bool isKeyword(String lexeme) {
|
||||
switch (options.effectiveDialect) {
|
||||
switch (dialect) {
|
||||
case SqlDialect.postgres:
|
||||
return isKeywordLexeme(lexeme) || isPostgresKeywordLexeme(lexeme);
|
||||
default:
|
||||
|
@ -194,14 +197,14 @@ class SqlWriter extends NodeSqlBuilder {
|
|||
// Convert foo.** to "foo.a" AS "nested_0.a", ... for all columns in foo
|
||||
var isFirst = true;
|
||||
|
||||
for (final column in result.table.columns) {
|
||||
for (final column in result.innerResultSet.scalarColumns) {
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
} else {
|
||||
_out.write(', ');
|
||||
}
|
||||
|
||||
final columnName = column.nameInSql;
|
||||
final columnName = column.name;
|
||||
_out.write('"$table"."$columnName" AS "$prefix.$columnName"');
|
||||
}
|
||||
} else if (e is DartPlaceholder) {
|
||||
|
|
|
@ -162,7 +162,7 @@ abstract class TableOrViewWriter {
|
|||
}
|
||||
|
||||
/// 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(
|
||||
DriftColumn column,
|
||||
TextEmitter emitter, {
|
||||
|
@ -173,6 +173,10 @@ abstract class TableOrViewWriter {
|
|||
final expressionBuffer = StringBuffer();
|
||||
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) {
|
||||
if (constraint is LimitingTextLength) {
|
||||
final buffer =
|
||||
|
|
|
@ -75,18 +75,29 @@ class ViewWriter extends TableOrViewWriter {
|
|||
..write('@override\n String get entityName=>'
|
||||
' ${asDartLiteral(view.schemaName)};\n');
|
||||
|
||||
emitter
|
||||
..writeln('@override')
|
||||
..write('Map<${emitter.drift('SqlDialect')}, String>')
|
||||
..write(source is! SqlViewSource ? '?' : '')
|
||||
..write('get createViewStatements => ');
|
||||
if (source is SqlViewSource) {
|
||||
final astNode = source.parsedStatement;
|
||||
|
||||
emitter.write('@override\nString get createViewStmt =>');
|
||||
if (astNode != null) {
|
||||
emitter.writeSqlAsDartLiteral(astNode);
|
||||
emitter.writeSqlByDialectMap(astNode);
|
||||
} 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(';');
|
||||
} else {
|
||||
buffer.write('@override\n String? get createViewStmt => null;\n');
|
||||
buffer.writeln('null;');
|
||||
}
|
||||
|
||||
writeAsDslTable();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:sqlparser/sqlparser.dart' as sql;
|
||||
import 'package:path/path.dart' show url;
|
||||
|
@ -228,8 +229,50 @@ abstract class _NodeOrWriter {
|
|||
return buffer.toString();
|
||||
}
|
||||
|
||||
String sqlCode(sql.AstNode node) {
|
||||
return SqlWriter(writer.options, escapeForDart: false).writeSql(node);
|
||||
String sqlCode(sql.AstNode node, SqlDialect dialect) {
|
||||
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 writeSql(sql.AstNode node, {bool escapeForDartString = true}) {
|
||||
SqlWriter(writer.options,
|
||||
escapeForDart: escapeForDartString, buffer: buffer)
|
||||
.writeSql(node);
|
||||
void writeSql(sql.AstNode node,
|
||||
{required SqlDialect dialect, bool escapeForDartString = true}) {
|
||||
SqlWriter(
|
||||
writer.options,
|
||||
dialect: dialect,
|
||||
escapeForDart: escapeForDartString,
|
||||
buffer: buffer,
|
||||
).writeSql(node);
|
||||
}
|
||||
|
||||
void writeSqlAsDartLiteral(sql.AstNode node) {
|
||||
buffer.write("'");
|
||||
writeSql(node);
|
||||
buffer.write("'");
|
||||
void writeSqlByDialectMap(sql.AstNode node) {
|
||||
_writeSqlByDialectMap(node, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,8 @@ SELECT rowid, highlight(example_table_search, 0, '[match]', '[match]') name,
|
|||
{'a|lib/a.drift': 'CREATE VIRTUAL TABLE demo USING spellfix1;'},
|
||||
options: DriftOptions.defaults(
|
||||
dialect: DialectOptions(
|
||||
SqlDialect.sqlite,
|
||||
null,
|
||||
[SqlDialect.sqlite],
|
||||
SqliteAnalysisOptions(
|
||||
modules: [SqlModule.spellfix1],
|
||||
),
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import 'package:drift_dev/src/analysis/options.dart';
|
||||
import 'package:drift_dev/src/analysis/results/results.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../test_utils.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
void main() {
|
||||
test('recognizes existing row classes', () async {
|
||||
|
@ -224,7 +227,204 @@ class MyQueryRow {
|
|||
);
|
||||
});
|
||||
|
||||
test('nested - single column type', () async {
|
||||
group('nested column', () {
|
||||
test('single column into field', () async {
|
||||
final state = TestBackend.inTest({
|
||||
'a|lib/a.drift': '''
|
||||
import 'a.dart';
|
||||
|
||||
foo WITH MyQueryRow: SELECT 1 AS a, b.** FROM (SELECT 2 AS b) b;
|
||||
''',
|
||||
'a|lib/a.dart': '''
|
||||
class MyQueryRow {
|
||||
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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('single column into single-element record', () async {
|
||||
final state = TestBackend.inTest({
|
||||
'a|lib/a.drift': '''
|
||||
import 'a.dart';
|
||||
|
||||
foo WITH MyQueryRow: SELECT 1 AS a, b.** FROM (SELECT 2 AS b) b;
|
||||
''',
|
||||
'a|lib/a.dart': '''
|
||||
class MyQueryRow {
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('custom result set', () async {
|
||||
final state = TestBackend.inTest(
|
||||
{
|
||||
'a|lib/a.drift': '''
|
||||
import 'a.dart';
|
||||
|
||||
foo WITH MyQueryRow: SELECT 1 AS id, j.** FROM json_each('') AS j;
|
||||
''',
|
||||
'a|lib/a.dart': '''
|
||||
class MyQueryRow {
|
||||
MyQueryRow(int id, JsonStructure j);
|
||||
}
|
||||
|
||||
class JsonStructure {
|
||||
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': '''
|
||||
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 {
|
||||
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 {
|
||||
MyRow(int a, (String, int) b);
|
||||
}
|
||||
''',
|
||||
},
|
||||
analyzerExperiments: ['records'],
|
||||
);
|
||||
|
||||
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: '(String, int)',
|
||||
positional: [scalarColumn('foo'), scalarColumn('bar')],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('nested LIST query', () {
|
||||
test('single column type', () async {
|
||||
final state = TestBackend.inTest({
|
||||
'a|lib/a.drift': '''
|
||||
import 'a.dart';
|
||||
|
@ -260,79 +460,7 @@ class MyQueryRow {
|
|||
);
|
||||
});
|
||||
|
||||
test('nested - table', () 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 {
|
||||
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'),
|
||||
nestedTableColumm('b'),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('nested - 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 {
|
||||
MyRow(int a, (String, int) b);
|
||||
}
|
||||
''',
|
||||
},
|
||||
analyzerExperiments: ['records'],
|
||||
);
|
||||
|
||||
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'),
|
||||
isExistingRowType(
|
||||
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 {
|
||||
test('custom result set with class', () async {
|
||||
final state = TestBackend.inTest({
|
||||
'a|lib/a.drift': '''
|
||||
import 'a.dart';
|
||||
|
@ -374,7 +502,7 @@ class MyNestedTable {
|
|||
);
|
||||
});
|
||||
|
||||
test('nested - custom result set with record', () async {
|
||||
test('custom result set with record', () async {
|
||||
final state = TestBackend.inTest(
|
||||
{
|
||||
'a|lib/a.drift': '''
|
||||
|
@ -414,6 +542,7 @@ class MyRow {
|
|||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('into record', () async {
|
||||
final state = TestBackend.inTest(
|
||||
|
@ -536,7 +665,12 @@ class MyRow {
|
|||
isExistingRowType(type: 'MyRow', positional: [
|
||||
scalarColumn('name'),
|
||||
], named: {
|
||||
'otherUser': nestedTableColumm('otherUser'),
|
||||
'otherUser': structedFromNested(
|
||||
isExistingRowType(
|
||||
type: 'MyUser',
|
||||
singleValue: isA<MatchingDriftTable>(),
|
||||
),
|
||||
),
|
||||
'nested': nestedListQuery(
|
||||
'nested',
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -75,22 +75,6 @@ q: SELECT * FROM t WHERE i IN ?1;
|
|||
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 {
|
||||
final state = TestBackend.inTest({
|
||||
'foo|lib/a.drift': r'''
|
||||
|
|
|
@ -5,7 +5,7 @@ import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import '../../test_utils.dart';
|
||||
import 'existing_row_classes_test.dart';
|
||||
import 'utils.dart';
|
||||
|
||||
void main() {
|
||||
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;
|
||||
|
||||
expect(query.resultSet!.nestedResults, hasLength(2));
|
||||
|
||||
final isFromView = isExistingRowType(
|
||||
type: 'MyViewData',
|
||||
singleValue: isA<MatchingDriftTable>()
|
||||
.having((e) => e.table.schemaName, 'table.schemaName', 'my_view'),
|
||||
);
|
||||
|
||||
expect(
|
||||
query.resultSet!.nestedResults,
|
||||
everyElement(isA<NestedResultTable>()
|
||||
.having((e) => e.table.schemaName, 'table.schemName', 'my_view')));
|
||||
query.resultSet!.mappingToRowClass('', const DriftOptions.defaults()),
|
||||
isExistingRowType(
|
||||
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]) {
|
||||
|
@ -178,7 +220,7 @@ FROM routes
|
|||
expect(
|
||||
resultSet.nestedResults
|
||||
.cast<NestedResultTable>()
|
||||
.map((e) => e.table.schemaName),
|
||||
.map((e) => e.innerResultSet.matchingTable!.table.schemaName),
|
||||
['points', 'points'],
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:drift_dev/src/analysis/results/results.dart';
|
||||
import 'package:test/expect.dart';
|
||||
|
||||
import '../../test_utils.dart';
|
||||
|
||||
|
@ -10,3 +11,52 @@ Future<SqlQuery> analyzeSingleQueryInDriftFile(String driftFile) async {
|
|||
Future<SqlQuery> analyzeQuery(String sql) async {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -307,7 +307,7 @@ TypeConverter<Object, int> myConverter() => throw UnimplementedError();
|
|||
'a|lib/a.drift.dart': decodedMatches(
|
||||
allOf(
|
||||
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'$converterc2 ='),
|
||||
contains(r'$converterc3 ='),
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
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/writer/import_manager.dart';
|
||||
import 'package:drift_dev/src/writer/queries/query_writer.dart';
|
||||
import 'package:drift_dev/src/writer/writer.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../analysis/test_utils.dart';
|
||||
|
@ -10,13 +12,16 @@ import '../../utils.dart';
|
|||
|
||||
void main() {
|
||||
Future<String> generateForQueryInDriftFile(String driftFile,
|
||||
{DriftOptions options = const DriftOptions.defaults()}) async {
|
||||
{DriftOptions options = const DriftOptions.defaults(
|
||||
generateNamedParameters: true,
|
||||
)}) async {
|
||||
final state =
|
||||
TestBackend.inTest({'a|lib/main.drift': driftFile}, options: options);
|
||||
final file = await state.analyze('package:a/main.drift');
|
||||
state.expectNoErrors();
|
||||
|
||||
final writer = Writer(
|
||||
const DriftOptions.defaults(generateNamedParameters: true),
|
||||
options,
|
||||
generationOptions: GenerationOptions(
|
||||
imports: ImportManagerForPartFiles(),
|
||||
),
|
||||
|
@ -55,7 +60,8 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('generates correct name for renamed nested star columns', () async {
|
||||
group('nested star column', () {
|
||||
test('get renamed in SQL', () async {
|
||||
final generated = await generateForQueryInDriftFile('''
|
||||
CREATE TABLE tbl (
|
||||
id INTEGER NULL
|
||||
|
@ -72,15 +78,94 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('generates correct returning mapping', () async {
|
||||
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 {
|
||||
final generated = await generateForQueryInDriftFile(
|
||||
'''
|
||||
CREATE TABLE tbl (
|
||||
id INTEGER,
|
||||
text TEXT
|
||||
);
|
||||
|
||||
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()'));
|
||||
});
|
||||
|
||||
|
@ -346,8 +431,7 @@ failQuery:
|
|||
],
|
||||
readsFrom: {
|
||||
t,
|
||||
}).asyncMap((i0.QueryRow row) async {
|
||||
return FailQueryResult(
|
||||
}).asyncMap((i0.QueryRow row) async => FailQueryResult(
|
||||
a: row.readNullable<double>('a'),
|
||||
b: row.readNullable<int>('b'),
|
||||
nestedQuery0: await customSelect(
|
||||
|
@ -358,8 +442,8 @@ failQuery:
|
|||
readsFrom: {
|
||||
t,
|
||||
}).asyncMap(t.mapFromRow).get(),
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
'''))
|
||||
}, outputs.dartOutputs, outputs);
|
||||
});
|
||||
|
@ -447,4 +531,26 @@ class ADrift extends i1.ModularAccessor {
|
|||
}'''))
|
||||
}, 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', "
|
||||
'}',
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,14 +6,18 @@ import 'package:sqlparser/sqlparser.dart';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
void check(String sql, String expectedDart,
|
||||
{DriftOptions options = const DriftOptions.defaults()}) {
|
||||
void check(
|
||||
String sql,
|
||||
String expectedDart, {
|
||||
DriftOptions options = const DriftOptions.defaults(),
|
||||
SqlDialect dialect = SqlDialect.sqlite,
|
||||
}) {
|
||||
final engine = SqlEngine();
|
||||
final context = engine.analyze(sql);
|
||||
final query = SqlSelectQuery('name', context, context.root, [], [],
|
||||
InferredResultSet(null, []), null, null);
|
||||
|
||||
final result = SqlWriter(options, query: query).write();
|
||||
final result = SqlWriter(options, dialect: dialect, query: query).write();
|
||||
|
||||
expect(result, expectedDart);
|
||||
}
|
||||
|
@ -33,7 +37,6 @@ void main() {
|
|||
test('escapes postgres keywords', () {
|
||||
check('SELECT * FROM user', "'SELECT * FROM user'");
|
||||
check('SELECT * FROM user', "'SELECT * FROM \"user\"'",
|
||||
options: DriftOptions.defaults(
|
||||
dialect: DialectOptions(SqlDialect.postgres, null)));
|
||||
dialect: SqlDialect.postgres);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -36,8 +36,7 @@ class MyApp extends StatelessWidget {
|
|||
primarySwatch: Colors.amber,
|
||||
typography: Typography.material2018(),
|
||||
),
|
||||
routeInformationParser: _router.routeInformationParser,
|
||||
routerDelegate: _router.routerDelegate,
|
||||
routerConfig: _router,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ dependencies:
|
|||
file_picker: ^5.2.5
|
||||
flutter_colorpicker: ^1.0.3
|
||||
flutter_riverpod: ^2.3.0
|
||||
go_router: ^9.0.0
|
||||
go_router: ^10.0.0
|
||||
intl: ^0.18.0
|
||||
sqlite3_flutter_libs: ^0.5.5
|
||||
sqlite3: ^2.0.0
|
||||
|
|
|
@ -602,8 +602,10 @@ class PopularUsers extends i0.ViewInfo<i1.PopularUsers, i1.PopularUser>
|
|||
@override
|
||||
String get entityName => 'popular_users';
|
||||
@override
|
||||
String get createViewStmt =>
|
||||
'CREATE VIEW popular_users AS SELECT * FROM users ORDER BY (SELECT count(*) FROM follows WHERE followed = users.id)';
|
||||
Map<i0.SqlDialect, String> get createViewStatements => {
|
||||
i0.SqlDialect.sqlite:
|
||||
'CREATE VIEW popular_users AS SELECT * FROM users ORDER BY (SELECT count(*) FROM follows WHERE followed = users.id)',
|
||||
};
|
||||
@override
|
||||
PopularUsers get asDslTable => this;
|
||||
@override
|
||||
|
|
|
@ -9,9 +9,7 @@ targets:
|
|||
raw_result_set_data: false
|
||||
named_parameters: false
|
||||
sql:
|
||||
# As sqlite3 is compatible with the postgres dialect (but not vice-versa), we're
|
||||
# using this dialect so that we can run the tests on postgres as well.
|
||||
dialect: postgres
|
||||
dialects: [sqlite, postgres]
|
||||
options:
|
||||
version: "3.37"
|
||||
modules:
|
||||
|
|
|
@ -336,7 +336,6 @@ class $FriendshipsTable extends Friendships
|
|||
requiredDuringInsert: false,
|
||||
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
|
||||
SqlDialect.sqlite: 'CHECK ("really_good_friends" IN (0, 1))',
|
||||
SqlDialect.mysql: '',
|
||||
SqlDialect.postgres: '',
|
||||
}),
|
||||
defaultValue: const Constant(false));
|
||||
|
@ -554,7 +553,13 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
late final $FriendshipsTable friendships = $FriendshipsTable(this);
|
||||
Selectable<User> mostPopularUsers(int amount) {
|
||||
return customSelect(
|
||||
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: [
|
||||
Variable<int>(amount)
|
||||
],
|
||||
|
@ -566,7 +571,13 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
|
||||
Selectable<int> amountOfGoodFriends(int user) {
|
||||
return customSelect(
|
||||
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: [
|
||||
Variable<int>(user)
|
||||
],
|
||||
|
@ -577,19 +588,23 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
|
||||
Selectable<FriendshipsOfResult> friendshipsOf(int user) {
|
||||
return customSelect(
|
||||
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: [
|
||||
Variable<int>(user)
|
||||
],
|
||||
readsFrom: {
|
||||
friendships,
|
||||
users,
|
||||
}).asyncMap((QueryRow row) async {
|
||||
return FriendshipsOfResult(
|
||||
}).asyncMap((QueryRow row) async => FriendshipsOfResult(
|
||||
reallyGoodFriends: row.read<bool>('really_good_friends'),
|
||||
user: await users.mapFromRow(row, tablePrefix: 'nested_0'),
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
|
||||
Selectable<int> userCount() {
|
||||
|
@ -601,7 +616,13 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
}
|
||||
|
||||
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: [
|
||||
Variable<int>(user)
|
||||
],
|
||||
|
@ -626,7 +647,13 @@ abstract class _$Database extends GeneratedDatabase {
|
|||
|
||||
Future<List<Friendship>> returning(int var1, int var2, bool var3) {
|
||||
return customWriteReturning(
|
||||
switch (executor.dialect) {
|
||||
SqlDialect.sqlite =>
|
||||
'INSERT INTO friendships VALUES (?1, ?2, ?3) RETURNING *',
|
||||
SqlDialect.postgres ||
|
||||
_ =>
|
||||
'INSERT INTO friendships VALUES (\$1, \$2, \$3) RETURNING *',
|
||||
},
|
||||
variables: [
|
||||
Variable<int>(var1),
|
||||
Variable<int>(var2),
|
||||
|
|
|
@ -4,7 +4,7 @@ version: 1.0.0
|
|||
# homepage: https://www.example.com
|
||||
|
||||
environment:
|
||||
sdk: '>=2.17.0 <3.0.0'
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
drift: ^2.0.0-0
|
||||
|
|
|
@ -50,6 +50,13 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
|
|||
currentColumnIndex += child.scope.expansionOfStarColumn?.length ?? 1;
|
||||
} else if (child is NestedQueryColumn) {
|
||||
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 {
|
||||
visit(child, arg);
|
||||
|
|
|
@ -145,7 +145,7 @@ class SqlEngine {
|
|||
|
||||
/// 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.
|
||||
ParseResult parseMultiple(String sql) {
|
||||
final tokens = tokenize(sql);
|
||||
|
|
|
@ -189,9 +189,14 @@ class Parser {
|
|||
final first = _peek;
|
||||
final statements = <Statement>[];
|
||||
while (!_isAtEnd) {
|
||||
final firstForStatement = _peek;
|
||||
final statement = _parseAsStatement(_statementWithoutSemicolon);
|
||||
|
||||
if (statement != null) {
|
||||
statements.add(statement);
|
||||
} else {
|
||||
statements
|
||||
.add(InvalidStatement()..setSpan(firstForStatement, _previous));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;'),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue