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
|
// #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
|
Currently, the generator can't provide compatibility checks for versions below 3.34, which is the
|
||||||
minimum version needed in options.
|
minimum version needed in options.
|
||||||
|
|
||||||
|
### Multi-dialect code generation
|
||||||
|
|
||||||
|
Thanks to community contributions, drift has in-progress support for Postgres and MariaDB.
|
||||||
|
You can change the `dialect` option to `postgres` or `mariadb` to generate code for those
|
||||||
|
database management systems.
|
||||||
|
|
||||||
|
In some cases, your generated code might have to support more than one DBMS. For instance,
|
||||||
|
you might want to share database code between your backend and a Flutter app. Or maybe
|
||||||
|
you're writing a server that should be able to talk to both MariaDB and Postgres, depending
|
||||||
|
on what the operator prefers.
|
||||||
|
Drift can generate code for multiple dialects - in that case, the right SQL will be chosen
|
||||||
|
at runtime when it makes a difference.
|
||||||
|
|
||||||
|
To enable this feature, remove the `dialect` option in the `sql` block and replace it with
|
||||||
|
a list of `dialects`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
targets:
|
||||||
|
$default:
|
||||||
|
builders:
|
||||||
|
drift_dev:
|
||||||
|
options:
|
||||||
|
sql:
|
||||||
|
dialect:
|
||||||
|
- sqlite
|
||||||
|
- postgres
|
||||||
|
options:
|
||||||
|
version: "3.34"
|
||||||
|
```
|
||||||
|
|
||||||
### Available extensions
|
### Available extensions
|
||||||
|
|
||||||
__Note__: This enables extensions in the analyzer for custom queries only. For instance, when the `json1` extension is
|
__Note__: This enables extensions in the analyzer for custom queries only. For instance, when the `json1` extension is
|
||||||
|
|
|
@ -181,8 +181,9 @@ fields in existing types as well.
|
||||||
|
|
||||||
Depending on what kind of result set your query has, you can use different fields for the existing Dart class:
|
Depending on what kind of result set your query has, you can use different fields for the existing Dart class:
|
||||||
|
|
||||||
1. For a nested table selected with `**`, your field needs to store an instance of the table's row class.
|
1. For a nested table selected with `**`, your field needs to store a structure compatible with the result set
|
||||||
This is true for both drift-generated row classes and tables with existing, user-defined row classes.
|
the nested column points to. For `my_table.**`, that field could either be the generated row class for `MyTable`
|
||||||
|
or a custom class as described by rule 3.
|
||||||
2. For nested list results, you have to use a `List<T>`. The `T` has to be compatible with the inner result
|
2. For nested list results, you have to use a `List<T>`. The `T` has to be compatible with the inner result
|
||||||
set of the `LIST()` as described by these rules.
|
set of the `LIST()` as described by these rules.
|
||||||
3. For a single-table result, you can use the table class, regardless of whether the table uses an existing table
|
3. For a single-table result, you can use the table class, regardless of whether the table uses an existing table
|
||||||
|
@ -221,8 +222,8 @@ class EmployeeWithStaff {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
As `self` is a `**` column, rule 1 applies. Therefore, `T1` must be `Employee`, the row class for the
|
As `self` is a `**` column, rule 1 applies. `self` references a table, `employees`.
|
||||||
`employees` table.
|
By rule 3, this means that `T1` can be a `Employee`, the row class for the `employees` table.
|
||||||
On the other hand, `staff` is a `LIST()` column and rule 2 applies here. This means that `T3` must
|
On the other hand, `staff` is a `LIST()` column and rule 2 applies here. This means that `T3` must
|
||||||
be a `List<Something>`.
|
be a `List<Something>`.
|
||||||
The inner result set of the `LIST` references all columns of `employees` and nothing more, so rule
|
The inner result set of the `LIST` references all columns of `employees` and nothing more, so rule
|
||||||
|
@ -235,6 +236,8 @@ class IdAndName {
|
||||||
final int id;
|
final int id;
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
|
// This class can be used since id and name column are available from the list query.
|
||||||
|
// We could have also used the `Employee` class or a record like `(int, String)`.
|
||||||
IdAndName(this.id, this.name);
|
IdAndName(this.id, this.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -243,6 +243,11 @@ any rows. For instance, we could use this to find empty categories:
|
||||||
|
|
||||||
{% include "blocks/snippet" snippets = snippets name = 'emptyCategories' %}
|
{% include "blocks/snippet" snippets = snippets name = 'emptyCategories' %}
|
||||||
|
|
||||||
|
### Full subqueries
|
||||||
|
|
||||||
|
Drift also supports subqueries that appear in `JOIN`s, which are described in the
|
||||||
|
[documentation for joins]({{ 'joins.md#subqueries' | pageUrl }}).
|
||||||
|
|
||||||
## Custom expressions
|
## Custom expressions
|
||||||
If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class.
|
If you want to inline custom sql into Dart queries, you can use a `CustomExpression` class.
|
||||||
It takes a `sql` parameter that lets you write custom expressions:
|
It takes a `sql` parameter that lets you write custom expressions:
|
||||||
|
|
|
@ -203,3 +203,17 @@ select statement.
|
||||||
In the example, the `newDescription` expression as added as a column to the query.
|
In the example, the `newDescription` expression as added as a column to the query.
|
||||||
Then, the map entry `categories.description: newDescription` is used so that the `description` column
|
Then, the map entry `categories.description: newDescription` is used so that the `description` column
|
||||||
for new category rows gets set to that expression.
|
for new category rows gets set to that expression.
|
||||||
|
|
||||||
|
## Subqueries
|
||||||
|
|
||||||
|
Starting from drift 2.11, you can use `Subquery` to use an existing select statement as part of more
|
||||||
|
complex join.
|
||||||
|
|
||||||
|
This snippet uses `Subquery` to count how many of the top-10 todo items (by length of their title) are
|
||||||
|
in each category.
|
||||||
|
It does this by first creating a select statement for the top-10 items (but not executing it), and then
|
||||||
|
joining this select statement onto a larger one grouping by category:
|
||||||
|
|
||||||
|
{% include "blocks/snippet" snippets = snippets name = 'subquery' %}
|
||||||
|
|
||||||
|
Any statement can be used as a subquery. But be aware that, unlike [subquery expressions]({{ 'expressions.md#scalar-subqueries' | pageUrl }}), full subqueries can't use tables from the outer select statement.
|
||||||
|
|
|
@ -75,7 +75,7 @@ class MyDatabase extends _$MyDatabase {
|
||||||
// ...
|
// ...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Exporting a databasee
|
## Exporting a database
|
||||||
|
|
||||||
To export a sqlite3 database into a file, you can use the `VACUUM INTO` statement.
|
To export a sqlite3 database into a file, you can use the `VACUUM INTO` statement.
|
||||||
Inside your database class, this could look like the following:
|
Inside your database class, this could look like the following:
|
||||||
|
|
|
@ -15,37 +15,11 @@ how to get started. You can watch it [here](https://youtu.be/zpWsedYMczM).
|
||||||
A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app).
|
A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app).
|
||||||
|
|
||||||
## Adding the dependency
|
## Adding the dependency
|
||||||
First, lets add drift to your project's `pubspec.yaml`.
|
|
||||||
At the moment, the current version of `drift` is [](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 %}
|
{% assign snippets = 'package:drift_docs/snippets/tables/filename.dart.excerpt.json' | readString | json_decode %}
|
||||||
|
|
||||||
```yaml
|
|
||||||
dependencies:
|
|
||||||
drift: ^{{ versions.drift }}
|
|
||||||
sqlite3_flutter_libs: ^0.5.0
|
|
||||||
path_provider: ^2.0.0
|
|
||||||
path: ^{{ versions.path }}
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
drift_dev: ^{{ versions.drift_dev }}
|
|
||||||
build_runner: ^{{ versions.build_runner }}
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're wondering why so many packages are necessary, here's a quick overview over what each package does:
|
|
||||||
|
|
||||||
- `drift`: This is the core package defining most apis
|
|
||||||
- `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter,
|
|
||||||
but then you need to take care of including `sqlite3` yourself.
|
|
||||||
For an overview on other platforms, see [platforms]({{ '../platforms.md' | pageUrl }}).
|
|
||||||
- `path_provider` and `path`: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team
|
|
||||||
- `drift_dev`: This development-only dependency generates query code based on your tables. It will not be included in your final app.
|
|
||||||
- `build_runner`: Common tool for code-generation, maintained by the Dart team
|
|
||||||
|
|
||||||
{% include "partials/changed_to_ffi" %}
|
|
||||||
|
|
||||||
### Declaring tables
|
### Declaring tables
|
||||||
|
|
||||||
Using drift, you can model the structure of your tables with simple dart code.
|
Using drift, you can model the structure of your tables with simple dart code.
|
||||||
|
|
|
@ -12,35 +12,8 @@ declaring both tables and queries in Dart. This version will focus on how to use
|
||||||
A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app).
|
A complete cross-platform Flutter app using drift is also available [here](https://github.com/simolus3/drift/tree/develop/examples/app).
|
||||||
|
|
||||||
## Adding the dependency
|
## Adding the dependency
|
||||||
First, lets add drift to your project's `pubspec.yaml`.
|
|
||||||
At the moment, the current version of `drift` is [](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" %}
|
||||||
|
|
||||||
```yaml
|
|
||||||
dependencies:
|
|
||||||
drift: ^{{ versions.drift }}
|
|
||||||
sqlite3_flutter_libs: ^0.5.0
|
|
||||||
path_provider: ^2.0.0
|
|
||||||
path: ^{{ versions.path }}
|
|
||||||
|
|
||||||
dev_dependencies:
|
|
||||||
drift_dev: ^{{ versions.drift_dev }}
|
|
||||||
build_runner: ^{{ versions.build_runner }}
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're wondering why so many packages are necessary, here's a quick overview over what each package does:
|
|
||||||
|
|
||||||
- `drift`: This is the core package defining most apis
|
|
||||||
- `sqlite3_flutter_libs`: Ships the latest `sqlite3` version with your Android or iOS app. This is not required when you're _not_ using Flutter,
|
|
||||||
but then you need to take care of including `sqlite3` yourself.
|
|
||||||
For an overview on other platforms, see [platforms]({{ '../platforms.md' | pageUrl }}).
|
|
||||||
- `path_provider` and `path`: Used to find a suitable location to store the database. Maintained by the Flutter and Dart team
|
|
||||||
- `drift_dev`: This development-only dependency generates query code based on your tables. It will not be included in your final app.
|
|
||||||
- `build_runner`: Common tool for code-generation, maintained by the Dart team
|
|
||||||
|
|
||||||
{% include "partials/changed_to_ffi" %}
|
|
||||||
|
|
||||||
## Declaring tables and queries
|
## Declaring tables and queries
|
||||||
|
|
||||||
|
|
|
@ -228,12 +228,10 @@ class RoutesWithNestedPointsResult {
|
||||||
Great! This class matches our intent much better than the flat result class
|
Great! This class matches our intent much better than the flat result class
|
||||||
from before.
|
from before.
|
||||||
|
|
||||||
At the moment, there are some limitations with this approach:
|
These nested result columns (`**`) can appear in top-level select statements
|
||||||
|
only, they're not supported in compound select statements or subqueries yet.
|
||||||
- `**` is not yet supported in compound select statements
|
However, they can refer to any result set in SQL that has been joined to the
|
||||||
- you can only use `table.**` if table is an actual table or a reference to it.
|
select statement - including subqueries table-valued functions.
|
||||||
In particular, it doesn't work for result sets from `WITH` clauses or table-
|
|
||||||
valued functions.
|
|
||||||
|
|
||||||
You might be wondering how `**` works under the hood, since it's not valid sql.
|
You might be wondering how `**` works under the hood, since it's not valid sql.
|
||||||
At build time, drift's generator will transform `**` into a list of all columns
|
At build time, drift's generator will transform `**` into a list of all columns
|
||||||
|
|
|
@ -80,7 +80,7 @@ Other database libraries can easily be integrated into drift as well.
|
||||||
{% block "blocks/feature.html" icon="fas fa-star" title="And much more!" %}
|
{% block "blocks/feature.html" icon="fas fa-star" title="And much more!" %}
|
||||||
{% block "blocks/markdown.html" %}
|
{% block "blocks/markdown.html" %}
|
||||||
Drift provides auto-updating streams for all your queries, makes dealing with transactions and migrations easy
|
Drift provides auto-updating streams for all your queries, makes dealing with transactions and migrations easy
|
||||||
and lets your write modular database code with DAOs. We even have a [sql IDE]({{ 'docs/Using SQL/sql_ide.md' | pageUrl }}) builtin to the project
|
and lets your write modular database code with DAOs. We even have a [sql IDE]({{ 'docs/Using SQL/sql_ide.md' | pageUrl }}) builtin to the project.
|
||||||
When using drift, working with databases in Dart is fun!
|
When using drift, working with databases in Dart is fun!
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -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
|
## 2.10.0
|
||||||
|
|
||||||
- Adds the `schema steps` command to `drift_dev`. It generates an API making it
|
- Adds the `schema steps` command to `drift_dev`. It generates an API making it
|
||||||
|
|
|
@ -534,7 +534,7 @@ class $TodoCategoryItemCountView
|
||||||
@override
|
@override
|
||||||
String get entityName => 'todo_category_item_count';
|
String get entityName => 'todo_category_item_count';
|
||||||
@override
|
@override
|
||||||
String? get createViewStmt => null;
|
Map<SqlDialect, String>? get createViewStatements => null;
|
||||||
@override
|
@override
|
||||||
$TodoCategoryItemCountView get asDslTable => this;
|
$TodoCategoryItemCountView get asDslTable => this;
|
||||||
@override
|
@override
|
||||||
|
@ -639,7 +639,7 @@ class $TodoItemWithCategoryNameViewView extends ViewInfo<
|
||||||
@override
|
@override
|
||||||
String get entityName => 'customViewName';
|
String get entityName => 'customViewName';
|
||||||
@override
|
@override
|
||||||
String? get createViewStmt => null;
|
Map<SqlDialect, String>? get createViewStatements => null;
|
||||||
@override
|
@override
|
||||||
$TodoItemWithCategoryNameViewView get asDslTable => this;
|
$TodoItemWithCategoryNameViewView get asDslTable => this;
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -157,6 +157,10 @@ class VersionedView implements ViewInfo<HasResultSet, QueryRow>, HasResultSet {
|
||||||
@override
|
@override
|
||||||
final String createViewStmt;
|
final String createViewStmt;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<SqlDialect, String>? get createViewStatements =>
|
||||||
|
{SqlDialect.sqlite: createViewStmt};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final List<GeneratedColumn> $columns;
|
final List<GeneratedColumn> $columns;
|
||||||
|
|
||||||
|
|
|
@ -58,8 +58,7 @@ class Join<T extends HasResultSet, D> extends Component {
|
||||||
context.buffer.write(' JOIN ');
|
context.buffer.write(' JOIN ');
|
||||||
|
|
||||||
final resultSet = table as ResultSetImplementation<T, D>;
|
final resultSet = table as ResultSetImplementation<T, D>;
|
||||||
context.buffer.write(resultSet.tableWithAlias);
|
context.writeResultSet(resultSet);
|
||||||
context.watchedTables.add(resultSet);
|
|
||||||
|
|
||||||
if (_type != _JoinType.cross) {
|
if (_type != _JoinType.cross) {
|
||||||
context.buffer.write(' ON ');
|
context.buffer.write(' ON ');
|
||||||
|
|
|
@ -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
|
/// The SQL of this expression
|
||||||
final String content;
|
final String content;
|
||||||
|
|
||||||
|
final Map<SqlDialect, String>? _dialectSpecificContent;
|
||||||
|
|
||||||
/// Additional tables that this expression is watching.
|
/// Additional tables that this expression is watching.
|
||||||
///
|
///
|
||||||
/// When this expression is used in a stream query, the stream will update
|
/// When this expression is used in a stream query, the stream will update
|
||||||
|
@ -24,11 +26,25 @@ class CustomExpression<D extends Object> extends Expression<D> {
|
||||||
|
|
||||||
/// Constructs a custom expression by providing the raw sql [content].
|
/// Constructs a custom expression by providing the raw sql [content].
|
||||||
const CustomExpression(this.content,
|
const CustomExpression(this.content,
|
||||||
{this.watchedTables = const [], this.precedence = Precedence.unknown});
|
{this.watchedTables = const [], this.precedence = Precedence.unknown})
|
||||||
|
: _dialectSpecificContent = null;
|
||||||
|
|
||||||
|
/// Constructs a custom expression providing the raw SQL in [content] depending
|
||||||
|
/// on the SQL dialect when this expression is built.
|
||||||
|
const CustomExpression.dialectSpecific(Map<SqlDialect, String> content,
|
||||||
|
{this.watchedTables = const [], this.precedence = Precedence.unknown})
|
||||||
|
: _dialectSpecificContent = content,
|
||||||
|
content = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void writeInto(GenerationContext context) {
|
void writeInto(GenerationContext context) {
|
||||||
|
final dialectSpecific = _dialectSpecificContent;
|
||||||
|
|
||||||
|
if (dialectSpecific != null) {
|
||||||
|
} else {
|
||||||
context.buffer.write(content);
|
context.buffer.write(content);
|
||||||
|
}
|
||||||
|
|
||||||
context.watchedTables.addAll(watchedTables);
|
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
|
/// An expression that is true if `this` resolves to any of the values in
|
||||||
/// [values].
|
/// [values].
|
||||||
Expression<bool> isIn(Iterable<D> values) {
|
Expression<bool> isIn(Iterable<D> values) {
|
||||||
if (values.isEmpty) {
|
return isInExp([for (final value in values) Variable<D>(value)]);
|
||||||
return Constant(false);
|
|
||||||
}
|
|
||||||
return _InExpression(this, values.toList(), false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An expression that is true if `this` does not resolve to any of the values
|
/// An expression that is true if `this` does not resolve to any of the values
|
||||||
/// in [values].
|
/// in [values].
|
||||||
Expression<bool> isNotIn(Iterable<D> values) {
|
Expression<bool> isNotIn(Iterable<D> values) {
|
||||||
if (values.isEmpty) {
|
return isNotInExp([for (final value in values) Variable<D>(value)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An expression that evaluates to `true` if this expression resolves to a
|
||||||
|
/// value that one of the [expressions] resolve to as well.
|
||||||
|
///
|
||||||
|
/// For an "is in" comparison with values, use [isIn].
|
||||||
|
Expression<bool> isInExp(List<Expression<D>> expressions) {
|
||||||
|
if (expressions.isEmpty) {
|
||||||
return Constant(true);
|
return Constant(true);
|
||||||
}
|
}
|
||||||
return _InExpression(this, values.toList(), true);
|
|
||||||
|
return _InExpression(this, expressions, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An expression that evaluates to `true` if this expression does not resolve
|
||||||
|
/// to any value that the [expressions] resolve to.
|
||||||
|
///
|
||||||
|
/// For an "is not in" comparison with values, use [isNotIn].
|
||||||
|
Expression<bool> isNotInExp(List<Expression<D>> expressions) {
|
||||||
|
if (expressions.isEmpty) {
|
||||||
|
return Constant(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _InExpression(this, expressions, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An expression checking whether `this` is included in any row of the
|
/// An expression checking whether `this` is included in any row of the
|
||||||
|
@ -509,7 +527,7 @@ class FunctionCallExpression<R extends Object> extends Expression<R> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _checkSubquery(BaseSelectStatement statement) {
|
void _checkSubquery(BaseSelectStatement statement) {
|
||||||
final columns = statement._returnedColumnCount;
|
final columns = statement._expandedColumns.length;
|
||||||
if (columns != 1) {
|
if (columns != 1) {
|
||||||
throw ArgumentError.value(statement, 'statement',
|
throw ArgumentError.value(statement, 'statement',
|
||||||
'Must return exactly one column (actually returns $columns)');
|
'Must return exactly one column (actually returns $columns)');
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
part of '../query_builder.dart';
|
part of '../query_builder.dart';
|
||||||
|
|
||||||
abstract class _BaseInExpression extends Expression<bool> {
|
sealed class _BaseInExpression extends Expression<bool> {
|
||||||
final Expression _expression;
|
final Expression _expression;
|
||||||
final bool _not;
|
final bool _not;
|
||||||
|
|
||||||
|
@ -25,8 +25,8 @@ abstract class _BaseInExpression extends Expression<bool> {
|
||||||
void _writeValues(GenerationContext context);
|
void _writeValues(GenerationContext context);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InExpression<T extends Object> extends _BaseInExpression {
|
final class _InExpression<T extends Object> extends _BaseInExpression {
|
||||||
final List<T> _values;
|
final List<Expression<T>> _values;
|
||||||
|
|
||||||
_InExpression(Expression expression, this._values, bool not)
|
_InExpression(Expression expression, this._values, bool not)
|
||||||
: super(expression, not);
|
: super(expression, not);
|
||||||
|
@ -35,15 +35,13 @@ class _InExpression<T extends Object> extends _BaseInExpression {
|
||||||
void _writeValues(GenerationContext context) {
|
void _writeValues(GenerationContext context) {
|
||||||
var first = true;
|
var first = true;
|
||||||
for (final value in _values) {
|
for (final value in _values) {
|
||||||
final variable = Variable<T>(value);
|
|
||||||
|
|
||||||
if (first) {
|
if (first) {
|
||||||
first = false;
|
first = false;
|
||||||
} else {
|
} else {
|
||||||
context.buffer.write(', ');
|
context.buffer.write(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
variable.writeInto(context);
|
value.writeInto(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +57,7 @@ class _InExpression<T extends Object> extends _BaseInExpression {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InSelectExpression extends _BaseInExpression {
|
final class _InSelectExpression extends _BaseInExpression {
|
||||||
final BaseSelectStatement _select;
|
final BaseSelectStatement _select;
|
||||||
|
|
||||||
_InSelectExpression(this._select, Expression expression, bool not)
|
_InSelectExpression(this._select, Expression expression, bool not)
|
||||||
|
|
|
@ -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) {
|
} else if (entity is Index) {
|
||||||
await createIndex(entity);
|
await createIndex(entity);
|
||||||
} else if (entity is OnCreateQuery) {
|
} else if (entity is OnCreateQuery) {
|
||||||
await _issueCustomQuery(entity.sql, const []);
|
await _issueQueryByDialect(entity.sqlByDialect);
|
||||||
} else if (entity is ViewInfo) {
|
} else if (entity is ViewInfo) {
|
||||||
await createView(entity);
|
await createView(entity);
|
||||||
} else {
|
} else {
|
||||||
|
@ -403,19 +403,19 @@ class Migrator {
|
||||||
|
|
||||||
/// Executes the `CREATE TRIGGER` statement that created the [trigger].
|
/// Executes the `CREATE TRIGGER` statement that created the [trigger].
|
||||||
Future<void> createTrigger(Trigger trigger) {
|
Future<void> createTrigger(Trigger trigger) {
|
||||||
return _issueCustomQuery(trigger.createTriggerStmt, const []);
|
return _issueQueryByDialect(trigger.createStatementsByDialect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes a `CREATE INDEX` statement to create the [index].
|
/// Executes a `CREATE INDEX` statement to create the [index].
|
||||||
Future<void> createIndex(Index index) {
|
Future<void> createIndex(Index index) {
|
||||||
return _issueCustomQuery(index.createIndexStmt, const []);
|
return _issueQueryByDialect(index.createStatementsByDialect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Executes a `CREATE VIEW` statement to create the [view].
|
/// Executes a `CREATE VIEW` statement to create the [view].
|
||||||
Future<void> createView(ViewInfo view) async {
|
Future<void> createView(ViewInfo view) async {
|
||||||
final stmt = view.createViewStmt;
|
final stmts = view.createViewStatements;
|
||||||
if (stmt != null) {
|
if (stmts != null) {
|
||||||
await _issueCustomQuery(stmt, const []);
|
await _issueQueryByDialect(stmts);
|
||||||
} else if (view.query != null) {
|
} else if (view.query != null) {
|
||||||
final context = GenerationContext.fromDb(_db, supportsVariables: false);
|
final context = GenerationContext.fromDb(_db, supportsVariables: false);
|
||||||
final columnNames = view.$columns.map((e) => e.escapedName).join(', ');
|
final columnNames = view.$columns.map((e) => e.escapedName).join(', ');
|
||||||
|
@ -528,6 +528,11 @@ class Migrator {
|
||||||
return _issueCustomQuery(sql, args);
|
return _issueCustomQuery(sql, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _issueQueryByDialect(Map<SqlDialect, String> sql) {
|
||||||
|
final context = _createContext();
|
||||||
|
return _issueCustomQuery(context.pickForDialect(sql), const []);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _issueCustomQuery(String sql, [List<dynamic>? args]) {
|
Future<void> _issueCustomQuery(String sql, [List<dynamic>? args]) {
|
||||||
return _db.customStatement(sql, args);
|
return _db.customStatement(sql, args);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,10 @@ import 'package:meta/meta.dart';
|
||||||
import '../../utils/async.dart';
|
import '../../utils/async.dart';
|
||||||
// New files should not be part of this mega library, which we're trying to
|
// New files should not be part of this mega library, which we're trying to
|
||||||
// split up.
|
// split up.
|
||||||
|
|
||||||
import 'expressions/case_when.dart';
|
import 'expressions/case_when.dart';
|
||||||
import 'expressions/internal.dart';
|
import 'expressions/internal.dart';
|
||||||
|
import 'helpers.dart';
|
||||||
|
|
||||||
export 'expressions/bitwise.dart';
|
export 'expressions/bitwise.dart';
|
||||||
export 'expressions/case_when.dart';
|
export 'expressions/case_when.dart';
|
||||||
|
@ -34,6 +36,7 @@ part 'components/group_by.dart';
|
||||||
part 'components/join.dart';
|
part 'components/join.dart';
|
||||||
part 'components/limit.dart';
|
part 'components/limit.dart';
|
||||||
part 'components/order_by.dart';
|
part 'components/order_by.dart';
|
||||||
|
part 'components/subquery.dart';
|
||||||
part 'components/where.dart';
|
part 'components/where.dart';
|
||||||
part 'expressions/aggregate.dart';
|
part 'expressions/aggregate.dart';
|
||||||
part 'expressions/algebra.dart';
|
part 'expressions/algebra.dart';
|
||||||
|
|
|
@ -17,15 +17,26 @@ abstract class DatabaseSchemaEntity {
|
||||||
/// [sqlite-docs]: https://sqlite.org/lang_createtrigger.html
|
/// [sqlite-docs]: https://sqlite.org/lang_createtrigger.html
|
||||||
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-trigger/
|
/// [sql-tut]: https://www.sqlitetutorial.net/sqlite-trigger/
|
||||||
class Trigger extends DatabaseSchemaEntity {
|
class Trigger extends DatabaseSchemaEntity {
|
||||||
/// The `CREATE TRIGGER` sql statement that can be used to create this
|
|
||||||
/// trigger.
|
|
||||||
final String createTriggerStmt;
|
|
||||||
@override
|
@override
|
||||||
final String entityName;
|
final String entityName;
|
||||||
|
|
||||||
|
/// The `CREATE TRIGGER` sql statement that can be used to create this
|
||||||
|
/// trigger.
|
||||||
|
@Deprecated('Use createStatementsByDialect instead')
|
||||||
|
String get createTriggerStmt => createStatementsByDialect.values.first;
|
||||||
|
|
||||||
|
/// The `CREATE TRIGGER` SQL statements used to create this trigger, accessible
|
||||||
|
/// for each dialect enabled when generating code.
|
||||||
|
final Map<SqlDialect, String> createStatementsByDialect;
|
||||||
|
|
||||||
/// Creates a trigger representation by the [createTriggerStmt] and its
|
/// Creates a trigger representation by the [createTriggerStmt] and its
|
||||||
/// [entityName]. Mainly used by generated code.
|
/// [entityName]. Mainly used by generated code.
|
||||||
Trigger(this.createTriggerStmt, this.entityName);
|
Trigger(String createTriggerStmt, String entityName)
|
||||||
|
: this.byDialect(entityName, {SqlDialect.sqlite: createTriggerStmt});
|
||||||
|
|
||||||
|
/// Creates the trigger model from its [entityName] in the schema and all
|
||||||
|
/// [createStatementsByDialect] for the supported dialects.
|
||||||
|
Trigger.byDialect(this.entityName, this.createStatementsByDialect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A sqlite index on columns or expressions.
|
/// A sqlite index on columns or expressions.
|
||||||
|
@ -40,11 +51,21 @@ class Index extends DatabaseSchemaEntity {
|
||||||
final String entityName;
|
final String entityName;
|
||||||
|
|
||||||
/// The `CREATE INDEX` sql statement that can be used to create this index.
|
/// The `CREATE INDEX` sql statement that can be used to create this index.
|
||||||
final String createIndexStmt;
|
@Deprecated('Use createStatementsByDialect instead')
|
||||||
|
String get createIndexStmt => createStatementsByDialect.values.first;
|
||||||
|
|
||||||
|
/// The `CREATE INDEX` SQL statements used to create this index, accessible
|
||||||
|
/// for each dialect enabled when generating code.
|
||||||
|
final Map<SqlDialect, String> createStatementsByDialect;
|
||||||
|
|
||||||
/// Creates an index model by the [createIndexStmt] and its [entityName].
|
/// Creates an index model by the [createIndexStmt] and its [entityName].
|
||||||
/// Mainly used by generated code.
|
/// Mainly used by generated code.
|
||||||
Index(this.entityName, this.createIndexStmt);
|
Index(this.entityName, String createIndexStmt)
|
||||||
|
: createStatementsByDialect = {SqlDialect.sqlite: createIndexStmt};
|
||||||
|
|
||||||
|
/// Creates an index model by its [entityName] used in the schema and the
|
||||||
|
/// `CREATE INDEX` statements for each supported dialect.
|
||||||
|
Index.byDialect(this.entityName, this.createStatementsByDialect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An internal schema entity to run an sql statement when the database is
|
/// An internal schema entity to run an sql statement when the database is
|
||||||
|
@ -61,10 +82,19 @@ class Index extends DatabaseSchemaEntity {
|
||||||
/// drift file.
|
/// drift file.
|
||||||
class OnCreateQuery extends DatabaseSchemaEntity {
|
class OnCreateQuery extends DatabaseSchemaEntity {
|
||||||
/// The sql statement that should be run in the default `onCreate` clause.
|
/// The sql statement that should be run in the default `onCreate` clause.
|
||||||
final String sql;
|
@Deprecated('Use sqlByDialect instead')
|
||||||
|
String get sql => sqlByDialect.values.first;
|
||||||
|
|
||||||
|
/// The SQL statement to run, indexed by the dialect used in the database.
|
||||||
|
final Map<SqlDialect, String> sqlByDialect;
|
||||||
|
|
||||||
/// Create a query that will be run in the default `onCreate` migration.
|
/// Create a query that will be run in the default `onCreate` migration.
|
||||||
OnCreateQuery(this.sql);
|
OnCreateQuery(String sql) : this.byDialect({SqlDialect.sqlite: sql});
|
||||||
|
|
||||||
|
/// Creates the entity of a query to run in the default `onCreate` migration.
|
||||||
|
///
|
||||||
|
/// The migrator will lookup a suitable query from the [sqlByDialect] map.
|
||||||
|
OnCreateQuery.byDialect(this.sqlByDialect);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get entityName => r'$internal$';
|
String get entityName => r'$internal$';
|
||||||
|
|
|
@ -151,7 +151,7 @@ extension TableInfoUtils<TableDsl, D> on ResultSetImplementation<TableDsl, D> {
|
||||||
/// Drift would generate code to call this method with `'c1': 'foo'` and
|
/// Drift would generate code to call this method with `'c1': 'foo'` and
|
||||||
/// `'c2': 'bar'` in [alias].
|
/// `'c2': 'bar'` in [alias].
|
||||||
Future<D> mapFromRowWithAlias(QueryRow row, Map<String, String> alias) async {
|
Future<D> mapFromRowWithAlias(QueryRow row, Map<String, String> alias) async {
|
||||||
return map({
|
return await map({
|
||||||
for (final entry in row.data.entries) alias[entry.key]!: entry.value,
|
for (final entry in row.data.entries) alias[entry.key]!: entry.value,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,14 @@ abstract class ViewInfo<Self extends HasResultSet, Row>
|
||||||
/// The `CREATE VIEW` sql statement that can be used to create this view.
|
/// The `CREATE VIEW` sql statement that can be used to create this view.
|
||||||
///
|
///
|
||||||
/// This will be null if the view was defined in Dart.
|
/// This will be null if the view was defined in Dart.
|
||||||
String? get createViewStmt;
|
@Deprecated('Use createViewStatements instead')
|
||||||
|
String? get createViewStmt => createViewStatements?.values.first;
|
||||||
|
|
||||||
|
/// The `CREATE VIEW` sql statement that can be used to create this view,
|
||||||
|
/// depending on the dialect used by the current database.
|
||||||
|
///
|
||||||
|
/// This will be null if the view was defined in Dart.
|
||||||
|
Map<SqlDialect, String>? get createViewStatements;
|
||||||
|
|
||||||
/// Predefined query from `View.as()`
|
/// Predefined query from `View.as()`
|
||||||
///
|
///
|
||||||
|
|
|
@ -8,12 +8,14 @@ typedef OrderClauseGenerator<T> = OrderingTerm Function(T tbl);
|
||||||
///
|
///
|
||||||
/// Users are not allowed to extend, implement or mix-in this class.
|
/// Users are not allowed to extend, implement or mix-in this class.
|
||||||
@sealed
|
@sealed
|
||||||
abstract class BaseSelectStatement extends Component {
|
abstract class BaseSelectStatement<Row> extends Component {
|
||||||
int get _returnedColumnCount;
|
Iterable<(Expression, String)> get _expandedColumns;
|
||||||
|
|
||||||
/// The name for the given [expression] in the result set, or `null` if
|
/// The name for the given [expression] in the result set, or `null` if
|
||||||
/// [expression] was not added as a column to this select statement.
|
/// [expression] was not added as a column to this select statement.
|
||||||
String? _nameForColumn(Expression expression);
|
String? _nameForColumn(Expression expression);
|
||||||
|
|
||||||
|
FutureOr<Row> _mapRow(Map<String, Object?> fromDatabase);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A select statement that doesn't use joins.
|
/// A select statement that doesn't use joins.
|
||||||
|
@ -21,7 +23,7 @@ abstract class BaseSelectStatement extends Component {
|
||||||
/// For more information, see [DatabaseConnectionUser.select].
|
/// For more information, see [DatabaseConnectionUser.select].
|
||||||
class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
||||||
with SingleTableQueryMixin<T, D>, LimitContainerMixin<T, D>, Selectable<D>
|
with SingleTableQueryMixin<T, D>, LimitContainerMixin<T, D>, Selectable<D>
|
||||||
implements BaseSelectStatement {
|
implements BaseSelectStatement<D> {
|
||||||
/// Whether duplicate rows should be eliminated from the result (this is a
|
/// Whether duplicate rows should be eliminated from the result (this is a
|
||||||
/// `SELECT DISTINCT` statement in sql). Defaults to false.
|
/// `SELECT DISTINCT` statement in sql). Defaults to false.
|
||||||
final bool distinct;
|
final bool distinct;
|
||||||
|
@ -39,7 +41,8 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
||||||
Set<ResultSetImplementation> get watchedTables => {table};
|
Set<ResultSetImplementation> get watchedTables => {table};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get _returnedColumnCount => table.$columns.length;
|
Iterable<(Expression, String)> get _expandedColumns =>
|
||||||
|
table.$columns.map((e) => (e, e.name));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String? _nameForColumn(Expression expression) {
|
String? _nameForColumn(Expression expression) {
|
||||||
|
@ -54,8 +57,8 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
||||||
void writeStartPart(GenerationContext ctx) {
|
void writeStartPart(GenerationContext ctx) {
|
||||||
ctx.buffer
|
ctx.buffer
|
||||||
..write(_beginOfSelect(distinct))
|
..write(_beginOfSelect(distinct))
|
||||||
..write(' * FROM ${table.tableWithAlias}');
|
..write(' * FROM ');
|
||||||
ctx.watchedTables.add(table);
|
ctx.writeResultSet(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -82,8 +85,13 @@ class SimpleSelectStatement<T extends HasResultSet, D> extends Query<T, D>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
FutureOr<D> _mapRow(Map<String, Object?> row) {
|
||||||
|
return table.map(row);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<D>> _mapResponse(List<Map<String, Object?>> rows) {
|
Future<List<D>> _mapResponse(List<Map<String, Object?>> rows) {
|
||||||
return rows.mapAsyncAndAwait(table.map);
|
return rows.mapAsyncAndAwait(_mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a select statement that operates on more than one table by
|
/// Creates a select statement that operates on more than one table by
|
||||||
|
|
|
@ -31,11 +31,11 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||||
///
|
///
|
||||||
/// Each table column can be uniquely identified by its (potentially aliased)
|
/// Each table column can be uniquely identified by its (potentially aliased)
|
||||||
/// table and its name. So a column named `id` in a table called `users` would
|
/// table and its name. So a column named `id` in a table called `users` would
|
||||||
/// be written as `users.id AS "users.id"`. These columns will NOT be written
|
/// be written as `users.id AS "users.id"`. These columns are also included in
|
||||||
/// into this map.
|
/// the map when added through [addColumns], but they have a predicatable name.
|
||||||
///
|
///
|
||||||
/// Other expressions used as columns will be included here. There just named
|
/// More interestingly, other expressions used as columns will be included
|
||||||
/// in increasing order, so something like `AS c3`.
|
/// here. They're just named in increasing order, so something like `AS c3`.
|
||||||
final Map<Expression, String> _columnAliases = {};
|
final Map<Expression, String> _columnAliases = {};
|
||||||
|
|
||||||
/// The tables this select statement reads from
|
/// The tables this select statement reads from
|
||||||
|
@ -44,13 +44,16 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||||
Set<ResultSetImplementation> get watchedTables => _queriedTables().toSet();
|
Set<ResultSetImplementation> get watchedTables => _queriedTables().toSet();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get _returnedColumnCount {
|
Iterable<(Expression<Object>, String)> get _expandedColumns sync* {
|
||||||
return _joins.fold(_selectedColumns.length, (prev, join) {
|
for (final column in _selectedColumns) {
|
||||||
if (join.includeInResult ?? _includeJoinedTablesInResult) {
|
yield (column, _columnAliases[column]!);
|
||||||
return prev + (join.table as ResultSetImplementation).$columns.length;
|
}
|
||||||
|
|
||||||
|
for (final table in _queriedTables(true)) {
|
||||||
|
for (final column in table.$columns) {
|
||||||
|
yield (column, _nameForTableColumn(column));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return prev;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -122,9 +125,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||||
chosenAlias = _nameForTableColumn(column,
|
chosenAlias = _nameForTableColumn(column,
|
||||||
generatingForView: ctx.generatingForView);
|
generatingForView: ctx.generatingForView);
|
||||||
} else {
|
} else {
|
||||||
chosenAlias = 'c$i';
|
chosenAlias = _columnAliases[column]!;
|
||||||
}
|
}
|
||||||
_columnAliases[column] = chosenAlias;
|
|
||||||
|
|
||||||
column.writeInto(ctx);
|
column.writeInto(ctx);
|
||||||
ctx.buffer
|
ctx.buffer
|
||||||
|
@ -133,8 +135,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||||
..write('"');
|
..write('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.buffer.write(' FROM ${table.tableWithAlias}');
|
ctx.buffer.write(' FROM ');
|
||||||
ctx.watchedTables.add(table);
|
ctx.writeResultSet(table);
|
||||||
|
|
||||||
if (_joins.isNotEmpty) {
|
if (_joins.isNotEmpty) {
|
||||||
ctx.writeWhitespace();
|
ctx.writeWhitespace();
|
||||||
|
@ -195,7 +197,21 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||||
/// - The docs on expressions: https://drift.simonbinder.eu/docs/getting-started/expressions/
|
/// - The docs on expressions: https://drift.simonbinder.eu/docs/getting-started/expressions/
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
void addColumns(Iterable<Expression> expressions) {
|
void addColumns(Iterable<Expression> expressions) {
|
||||||
_selectedColumns.addAll(expressions);
|
for (final expression in expressions) {
|
||||||
|
// Otherwise, we generate an alias.
|
||||||
|
_columnAliases.putIfAbsent(expression, () {
|
||||||
|
// Only add the column if it hasn't been added yet - it's fine if the
|
||||||
|
// same column is added multiple times through the Dart API, they will
|
||||||
|
// read from the same SQL column internally.
|
||||||
|
_selectedColumns.add(expression);
|
||||||
|
|
||||||
|
if (expression is GeneratedColumn) {
|
||||||
|
return _nameForTableColumn(expression);
|
||||||
|
} else {
|
||||||
|
return 'c${_columnAliases.length}';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds more joined tables to this [JoinedSelectStatement].
|
/// Adds more joined tables to this [JoinedSelectStatement].
|
||||||
|
@ -233,14 +249,14 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||||
|
|
||||||
return database
|
return database
|
||||||
.createStream(fetcher)
|
.createStream(fetcher)
|
||||||
.asyncMapPerSubscription((rows) => _mapResponse(ctx, rows));
|
.asyncMapPerSubscription((rows) => _mapResponse(rows));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<TypedResult>> get() async {
|
Future<List<TypedResult>> get() async {
|
||||||
final ctx = constructQuery();
|
final ctx = constructQuery();
|
||||||
final raw = await _getRaw(ctx);
|
final raw = await _getRaw(ctx);
|
||||||
return _mapResponse(ctx, raw);
|
return _mapResponse(raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) {
|
Future<List<Map<String, Object?>>> _getRaw(GenerationContext ctx) {
|
||||||
|
@ -260,9 +276,8 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<TypedResult>> _mapResponse(
|
@override
|
||||||
GenerationContext ctx, List<Map<String, Object?>> rows) {
|
Future<TypedResult> _mapRow(Map<String, Object?> row) async {
|
||||||
return Future.wait(rows.map((row) async {
|
|
||||||
final readTables = <ResultSetImplementation, dynamic>{};
|
final readTables = <ResultSetImplementation, dynamic>{};
|
||||||
|
|
||||||
for (final table in _queriedTables(true)) {
|
for (final table in _queriedTables(true)) {
|
||||||
|
@ -277,7 +292,10 @@ class JoinedSelectStatement<FirstT extends HasResultSet, FirstD>
|
||||||
final driftRow = QueryRow(row, database);
|
final driftRow = QueryRow(row, database);
|
||||||
return TypedResult(
|
return TypedResult(
|
||||||
readTables, driftRow, _LazyExpressionMap(_columnAliases, driftRow));
|
readTables, driftRow, _LazyExpressionMap(_columnAliases, driftRow));
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
Future<List<TypedResult>> _mapResponse(List<Map<String, Object?>> rows) {
|
||||||
|
return Future.wait(rows.map(_mapRow));
|
||||||
}
|
}
|
||||||
|
|
||||||
Never _warnAboutDuplicate(
|
Never _warnAboutDuplicate(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
name: drift
|
name: drift
|
||||||
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
|
description: Drift is a reactive library to store relational data in Dart and Flutter applications.
|
||||||
version: 2.10.0
|
version: 2.11.0-dev
|
||||||
repository: https://github.com/simolus3/drift
|
repository: https://github.com/simolus3/drift
|
||||||
homepage: https://drift.simonbinder.eu/
|
homepage: https://drift.simonbinder.eu/
|
||||||
issue_tracker: https://github.com/simolus3/drift/issues
|
issue_tracker: https://github.com/simolus3/drift/issues
|
||||||
|
|
|
@ -201,6 +201,27 @@ void main() {
|
||||||
.bitwiseAnd(Variable(BigInt.from(10)))),
|
.bitwiseAnd(Variable(BigInt.from(10)))),
|
||||||
completion(BigInt.two));
|
completion(BigInt.two));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('isIn and isNotIn', () {
|
||||||
|
test('non-empty', () async {
|
||||||
|
expect(await eval(Variable.withInt(3).isIn([2, 4])), isFalse);
|
||||||
|
expect(await eval(Variable.withInt(3).isIn([3, 5])), isTrue);
|
||||||
|
|
||||||
|
expect(await eval(Variable.withInt(3).isNotIn([2, 4])), isTrue);
|
||||||
|
expect(await eval(Variable.withInt(3).isNotIn([3, 5])), isFalse);
|
||||||
|
|
||||||
|
expect(await eval(const Constant<int>(null).isIn([2, 4])), isNull);
|
||||||
|
expect(await eval(const Constant<int>(null).isNotIn([2, 4])), isNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('empty', () async {
|
||||||
|
expect(await eval(Variable.withInt(3).isIn([])), isFalse);
|
||||||
|
expect(await eval(Variable.withInt(3).isNotIn([])), isTrue);
|
||||||
|
|
||||||
|
expect(await eval(const Constant<int>(null).isIn([])), isFalse);
|
||||||
|
expect(await eval(const Constant<int>(null).isNotIn([])), isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,26 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('expressions', () {
|
||||||
|
test('in', () {
|
||||||
|
final isInExpression = innerExpression.isInExp([
|
||||||
|
CustomExpression('a'),
|
||||||
|
CustomExpression('b'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(isInExpression, generates('name IN (a, b)'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('not in', () {
|
||||||
|
final isNotInExpression = innerExpression.isNotInExp([
|
||||||
|
CustomExpression('a'),
|
||||||
|
CustomExpression('b'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(isNotInExpression, generates('name NOT IN (a, b)'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('subquery', () {
|
group('subquery', () {
|
||||||
test('in expressions are generated', () {
|
test('in expressions are generated', () {
|
||||||
final isInExpression = innerExpression
|
final isInExpression = innerExpression
|
||||||
|
|
|
@ -224,7 +224,7 @@ void main() {
|
||||||
'c.desc': 'Description',
|
'c.desc': 'Description',
|
||||||
'c.description_in_upper_case': 'DESCRIPTION',
|
'c.description_in_upper_case': 'DESCRIPTION',
|
||||||
'c.priority': 1,
|
'c.priority': 1,
|
||||||
'c4': 11
|
'c0': 11
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
@ -234,7 +234,7 @@ void main() {
|
||||||
verify(executor.runSelect(
|
verify(executor.runSelect(
|
||||||
'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", '
|
'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", '
|
||||||
'"c"."priority" AS "c.priority", "c"."description_in_upper_case" AS '
|
'"c"."priority" AS "c.priority", "c"."description_in_upper_case" AS '
|
||||||
'"c.description_in_upper_case", LENGTH("c"."desc") AS "c4" '
|
'"c.description_in_upper_case", LENGTH("c"."desc") AS "c0" '
|
||||||
'FROM "categories" "c";',
|
'FROM "categories" "c";',
|
||||||
[],
|
[],
|
||||||
));
|
));
|
||||||
|
@ -273,7 +273,7 @@ void main() {
|
||||||
'c.desc': 'Description',
|
'c.desc': 'Description',
|
||||||
'c.description_in_upper_case': 'DESCRIPTION',
|
'c.description_in_upper_case': 'DESCRIPTION',
|
||||||
'c.priority': 1,
|
'c.priority': 1,
|
||||||
'c4': 11,
|
'c0': 11,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
@ -283,7 +283,7 @@ void main() {
|
||||||
verify(executor.runSelect(
|
verify(executor.runSelect(
|
||||||
'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", "c"."priority" AS "c.priority"'
|
'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", "c"."priority" AS "c.priority"'
|
||||||
', "c"."description_in_upper_case" AS "c.description_in_upper_case", '
|
', "c"."description_in_upper_case" AS "c.description_in_upper_case", '
|
||||||
'LENGTH("c"."desc") AS "c4" '
|
'LENGTH("c"."desc") AS "c0" '
|
||||||
'FROM "categories" "c" '
|
'FROM "categories" "c" '
|
||||||
'INNER JOIN "todos" "t" ON "c"."id" = "t"."category";',
|
'INNER JOIN "todos" "t" ON "c"."id" = "t"."category";',
|
||||||
[],
|
[],
|
||||||
|
@ -328,7 +328,7 @@ void main() {
|
||||||
'c.id': 3,
|
'c.id': 3,
|
||||||
'c.desc': 'desc',
|
'c.desc': 'desc',
|
||||||
'c.priority': 0,
|
'c.priority': 0,
|
||||||
'c4': 10,
|
'c0': 10,
|
||||||
'c.description_in_upper_case': 'DESC',
|
'c.description_in_upper_case': 'DESC',
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -340,7 +340,7 @@ void main() {
|
||||||
'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", '
|
'SELECT "c"."id" AS "c.id", "c"."desc" AS "c.desc", '
|
||||||
'"c"."priority" AS "c.priority", '
|
'"c"."priority" AS "c.priority", '
|
||||||
'"c"."description_in_upper_case" AS "c.description_in_upper_case", '
|
'"c"."description_in_upper_case" AS "c.description_in_upper_case", '
|
||||||
'COUNT("t"."id") AS "c4" '
|
'COUNT("t"."id") AS "c0" '
|
||||||
'FROM "categories" "c" INNER JOIN "todos" "t" ON "t"."category" = "c"."id" '
|
'FROM "categories" "c" INNER JOIN "todos" "t" ON "t"."category" = "c"."id" '
|
||||||
'GROUP BY "c"."id" HAVING COUNT("t"."id") >= ?;',
|
'GROUP BY "c"."id" HAVING COUNT("t"."id") >= ?;',
|
||||||
[10]));
|
[10]));
|
||||||
|
@ -474,4 +474,72 @@ void main() {
|
||||||
throwsA(isNot(isA<DriftWrappedException>())),
|
throwsA(isNot(isA<DriftWrappedException>())),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('subquery', () {
|
||||||
|
test('can be joined', () async {
|
||||||
|
final subquery = Subquery(
|
||||||
|
db.select(db.todosTable)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.title.length)])
|
||||||
|
..limit(10),
|
||||||
|
's',
|
||||||
|
);
|
||||||
|
|
||||||
|
final query = db.selectOnly(db.categories)
|
||||||
|
..addColumns([db.categories.id])
|
||||||
|
..join([
|
||||||
|
innerJoin(subquery,
|
||||||
|
subquery.ref(db.todosTable.category).equalsExp(db.categories.id))
|
||||||
|
]);
|
||||||
|
await query.get();
|
||||||
|
|
||||||
|
verify(
|
||||||
|
executor.runSelect(
|
||||||
|
'SELECT "categories"."id" AS "categories.id" FROM "categories" '
|
||||||
|
'INNER JOIN (SELECT * FROM "todos" '
|
||||||
|
'ORDER BY LENGTH("todos"."title") DESC LIMIT 10) s '
|
||||||
|
'ON "s"."category" = "categories"."id";',
|
||||||
|
argThat(isEmpty),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('use column from subquery', () async {
|
||||||
|
when(executor.runSelect(any, any)).thenAnswer((_) {
|
||||||
|
return Future.value([
|
||||||
|
{'c0': 42}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
final sumOfTitleLength = db.todosTable.title.length.sum();
|
||||||
|
final subquery = Subquery(
|
||||||
|
db.selectOnly(db.todosTable)
|
||||||
|
..addColumns([db.todosTable.category, sumOfTitleLength])
|
||||||
|
..groupBy([db.todosTable.category]),
|
||||||
|
's');
|
||||||
|
|
||||||
|
final readableLength = subquery.ref(sumOfTitleLength);
|
||||||
|
final query = db.selectOnly(db.categories)
|
||||||
|
..addColumns([readableLength])
|
||||||
|
..join([
|
||||||
|
innerJoin(subquery,
|
||||||
|
subquery.ref(db.todosTable.category).equalsExp(db.categories.id))
|
||||||
|
]);
|
||||||
|
|
||||||
|
final row = await query.getSingle();
|
||||||
|
|
||||||
|
verify(
|
||||||
|
executor.runSelect(
|
||||||
|
'SELECT "s"."c1" AS "c0" FROM "categories" '
|
||||||
|
'INNER JOIN ('
|
||||||
|
'SELECT "todos"."category" AS "todos.category", '
|
||||||
|
'SUM(LENGTH("todos"."title")) AS "c1" FROM "todos" '
|
||||||
|
'GROUP BY "todos"."category") s '
|
||||||
|
'ON "s"."todos.category" = "categories"."id";',
|
||||||
|
argThat(isEmpty),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(row.read(readableLength), 42);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -259,6 +259,32 @@ void main() {
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('dialect-specific', () {
|
||||||
|
Map<SqlDialect, String> statements(String base) {
|
||||||
|
return {
|
||||||
|
for (final dialect in SqlDialect.values) dialect: '$base $dialect',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final dialect in [SqlDialect.sqlite, SqlDialect.postgres]) {
|
||||||
|
test('with dialect $dialect', () async {
|
||||||
|
final executor = MockExecutor();
|
||||||
|
when(executor.dialect).thenReturn(dialect);
|
||||||
|
|
||||||
|
final db = TodoDb(executor);
|
||||||
|
final migrator = db.createMigrator();
|
||||||
|
|
||||||
|
await migrator.create(Trigger.byDialect('a', statements('trigger')));
|
||||||
|
await migrator.create(Index.byDialect('a', statements('index')));
|
||||||
|
await migrator.create(OnCreateQuery.byDialect(statements('@')));
|
||||||
|
|
||||||
|
verify(executor.runCustom('trigger $dialect', []));
|
||||||
|
verify(executor.runCustom('index $dialect', []));
|
||||||
|
verify(executor.runCustom('@ $dialect', []));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final class _FakeSchemaVersion extends VersionedSchema {
|
final class _FakeSchemaVersion extends VersionedSchema {
|
||||||
|
|
|
@ -214,4 +214,30 @@ void main() {
|
||||||
await pumpEventQueue();
|
await pumpEventQueue();
|
||||||
db.markTablesUpdated([db.categories]);
|
db.markTablesUpdated([db.categories]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('select from subquery', () async {
|
||||||
|
final data = [
|
||||||
|
{
|
||||||
|
'id': 10,
|
||||||
|
'title': null,
|
||||||
|
'content': 'Content',
|
||||||
|
'category': null,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
when(executor.runSelect(any, any)).thenAnswer((_) => Future.value(data));
|
||||||
|
|
||||||
|
final subquery = Subquery(db.todosTable.select(), 's');
|
||||||
|
final rows = await db.select(subquery).get();
|
||||||
|
|
||||||
|
expect(rows, [
|
||||||
|
TodoEntry(
|
||||||
|
id: 10,
|
||||||
|
title: null,
|
||||||
|
content: 'Content',
|
||||||
|
category: null,
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
verify(executor.runSelect('SELECT * FROM (SELECT * FROM "todos") s;', []));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1600,8 +1600,10 @@ class MyView extends ViewInfo<MyView, MyViewData> implements HasResultSet {
|
||||||
@override
|
@override
|
||||||
String get entityName => 'my_view';
|
String get entityName => 'my_view';
|
||||||
@override
|
@override
|
||||||
String get createViewStmt =>
|
Map<SqlDialect, String> get createViewStatements => {
|
||||||
'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2';
|
SqlDialect.sqlite:
|
||||||
|
'CREATE VIEW my_view AS SELECT * FROM config WHERE sync_state = 2',
|
||||||
|
};
|
||||||
@override
|
@override
|
||||||
MyView get asDslTable => this;
|
MyView get asDslTable => this;
|
||||||
@override
|
@override
|
||||||
|
@ -1678,7 +1680,8 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
],
|
],
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
config,
|
config,
|
||||||
}).asyncMap((QueryRow row) => config.mapFromRowWithAlias(row, const {
|
}).asyncMap(
|
||||||
|
(QueryRow row) async => config.mapFromRowWithAlias(row, const {
|
||||||
'ck': 'config_key',
|
'ck': 'config_key',
|
||||||
'cf': 'config_value',
|
'cf': 'config_value',
|
||||||
'cs1': 'sync_state',
|
'cs1': 'sync_state',
|
||||||
|
@ -1754,26 +1757,20 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
variables: [],
|
variables: [],
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
config,
|
config,
|
||||||
}).map((QueryRow row) {
|
}).map((QueryRow row) => JsonResult(
|
||||||
return JsonResult(
|
|
||||||
row: row,
|
row: row,
|
||||||
key: row.read<String>('key'),
|
key: row.read<String>('key'),
|
||||||
value: row.readNullable<String>('value'),
|
value: row.readNullable<String>('value'),
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Selectable<JsonResult> another() {
|
Selectable<JsonResult> another() {
|
||||||
return customSelect(
|
return customSelect('SELECT \'one\' AS "key", NULLIF(\'two\', \'another\') AS value', variables: [], readsFrom: {})
|
||||||
'SELECT \'one\' AS "key", NULLIF(\'two\', \'another\') AS value',
|
.map((QueryRow row) => JsonResult(
|
||||||
variables: [],
|
|
||||||
readsFrom: {}).map((QueryRow row) {
|
|
||||||
return JsonResult(
|
|
||||||
row: row,
|
row: row,
|
||||||
key: row.read<String>('key'),
|
key: row.read<String>('key'),
|
||||||
value: row.readNullable<String>('value'),
|
value: row.readNullable<String>('value'),
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Selectable<MultipleResult> multiple({required Multiple$predicate predicate}) {
|
Selectable<MultipleResult> multiple({required Multiple$predicate predicate}) {
|
||||||
|
@ -1793,14 +1790,13 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
withDefaults,
|
withDefaults,
|
||||||
withConstraints,
|
withConstraints,
|
||||||
...generatedpredicate.watchedTables,
|
...generatedpredicate.watchedTables,
|
||||||
}).asyncMap((QueryRow row) async {
|
}).asyncMap((QueryRow row) async => MultipleResult(
|
||||||
return MultipleResult(
|
|
||||||
row: row,
|
row: row,
|
||||||
a: row.readNullable<String>('a'),
|
a: row.readNullable<String>('a'),
|
||||||
b: row.readNullable<int>('b'),
|
b: row.readNullable<int>('b'),
|
||||||
c: await withConstraints.mapFromRowOrNull(row, tablePrefix: 'nested_0'),
|
c: await withConstraints.mapFromRowOrNull(row,
|
||||||
);
|
tablePrefix: 'nested_0'),
|
||||||
});
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Selectable<EMail> searchEmails({required String? term}) {
|
Selectable<EMail> searchEmails({required String? term}) {
|
||||||
|
@ -1827,8 +1823,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
config,
|
config,
|
||||||
...generatedexpr.watchedTables,
|
...generatedexpr.watchedTables,
|
||||||
}).map((QueryRow row) {
|
}).map((QueryRow row) => ReadRowIdResult(
|
||||||
return ReadRowIdResult(
|
|
||||||
row: row,
|
row: row,
|
||||||
rowid: row.read<int>('rowid'),
|
rowid: row.read<int>('rowid'),
|
||||||
configKey: row.read<String>('config_key'),
|
configKey: row.read<String>('config_key'),
|
||||||
|
@ -1839,8 +1834,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
syncStateImplicit: NullAwareTypeConverter.wrapFromSql(
|
syncStateImplicit: NullAwareTypeConverter.wrapFromSql(
|
||||||
ConfigTable.$convertersyncStateImplicit,
|
ConfigTable.$convertersyncStateImplicit,
|
||||||
row.readNullable<int>('sync_state_implicit')),
|
row.readNullable<int>('sync_state_implicit')),
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Selectable<MyViewData> readView({ReadView$where? where}) {
|
Selectable<MyViewData> readView({ReadView$where? where}) {
|
||||||
|
@ -1895,11 +1889,10 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
withConstraints,
|
withConstraints,
|
||||||
withDefaults,
|
withDefaults,
|
||||||
}).asyncMap((QueryRow row) async {
|
}).asyncMap((QueryRow row) async => NestedResult(
|
||||||
return NestedResult(
|
|
||||||
row: row,
|
row: row,
|
||||||
defaults: await withDefaults.mapFromRow(row, tablePrefix: 'nested_0'),
|
defaults: await withDefaults.mapFromRow(row, tablePrefix: 'nested_0'),
|
||||||
nestedQuery0: await customSelect(
|
nestedQuery1: await customSelect(
|
||||||
'SELECT * FROM with_constraints AS c WHERE c.b = ?1',
|
'SELECT * FROM with_constraints AS c WHERE c.b = ?1',
|
||||||
variables: [
|
variables: [
|
||||||
Variable<int>(row.read('\$n_0'))
|
Variable<int>(row.read('\$n_0'))
|
||||||
|
@ -1908,8 +1901,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
||||||
withConstraints,
|
withConstraints,
|
||||||
withDefaults,
|
withDefaults,
|
||||||
}).asyncMap(withConstraints.mapFromRow).get(),
|
}).asyncMap(withConstraints.mapFromRow).get(),
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Selectable<MyCustomResultClass> customResult() {
|
Selectable<MyCustomResultClass> customResult() {
|
||||||
|
@ -2081,25 +2073,25 @@ typedef ReadView$where = Expression<bool> Function(MyView my_view);
|
||||||
|
|
||||||
class NestedResult extends CustomResultSet {
|
class NestedResult extends CustomResultSet {
|
||||||
final WithDefault defaults;
|
final WithDefault defaults;
|
||||||
final List<WithConstraint> nestedQuery0;
|
final List<WithConstraint> nestedQuery1;
|
||||||
NestedResult({
|
NestedResult({
|
||||||
required QueryRow row,
|
required QueryRow row,
|
||||||
required this.defaults,
|
required this.defaults,
|
||||||
required this.nestedQuery0,
|
required this.nestedQuery1,
|
||||||
}) : super(row);
|
}) : super(row);
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(defaults, nestedQuery0);
|
int get hashCode => Object.hash(defaults, nestedQuery1);
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
(other is NestedResult &&
|
(other is NestedResult &&
|
||||||
other.defaults == this.defaults &&
|
other.defaults == this.defaults &&
|
||||||
other.nestedQuery0 == this.nestedQuery0);
|
other.nestedQuery1 == this.nestedQuery1);
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return (StringBuffer('NestedResult(')
|
return (StringBuffer('NestedResult(')
|
||||||
..write('defaults: $defaults, ')
|
..write('defaults: $defaults, ')
|
||||||
..write('nestedQuery0: $nestedQuery0')
|
..write('nestedQuery1: $nestedQuery1')
|
||||||
..write(')'))
|
..write(')'))
|
||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -641,15 +641,12 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
|
||||||
static const VerificationMeta _isAwesomeMeta =
|
static const VerificationMeta _isAwesomeMeta =
|
||||||
const VerificationMeta('isAwesome');
|
const VerificationMeta('isAwesome');
|
||||||
@override
|
@override
|
||||||
late final GeneratedColumn<bool> isAwesome =
|
late final GeneratedColumn<bool> isAwesome = GeneratedColumn<bool>(
|
||||||
GeneratedColumn<bool>('is_awesome', aliasedName, false,
|
'is_awesome', aliasedName, false,
|
||||||
type: DriftSqlType.bool,
|
type: DriftSqlType.bool,
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
|
defaultConstraints:
|
||||||
SqlDialect.sqlite: 'CHECK ("is_awesome" IN (0, 1))',
|
GeneratedColumn.constraintIsAlways('CHECK ("is_awesome" IN (0, 1))'),
|
||||||
SqlDialect.mysql: '',
|
|
||||||
SqlDialect.postgres: '',
|
|
||||||
}),
|
|
||||||
defaultValue: const Constant(true));
|
defaultValue: const Constant(true));
|
||||||
static const VerificationMeta _profilePictureMeta =
|
static const VerificationMeta _profilePictureMeta =
|
||||||
const VerificationMeta('profilePicture');
|
const VerificationMeta('profilePicture');
|
||||||
|
@ -1549,7 +1546,7 @@ class $CategoryTodoCountViewView
|
||||||
@override
|
@override
|
||||||
String get entityName => 'category_todo_count_view';
|
String get entityName => 'category_todo_count_view';
|
||||||
@override
|
@override
|
||||||
String? get createViewStmt => null;
|
Map<SqlDialect, String>? get createViewStatements => null;
|
||||||
@override
|
@override
|
||||||
$CategoryTodoCountViewView get asDslTable => this;
|
$CategoryTodoCountViewView get asDslTable => this;
|
||||||
@override
|
@override
|
||||||
|
@ -1660,7 +1657,7 @@ class $TodoWithCategoryViewView
|
||||||
@override
|
@override
|
||||||
String get entityName => 'todo_with_category_view';
|
String get entityName => 'todo_with_category_view';
|
||||||
@override
|
@override
|
||||||
String? get createViewStmt => null;
|
Map<SqlDialect, String>? get createViewStatements => null;
|
||||||
@override
|
@override
|
||||||
$TodoWithCategoryViewView get asDslTable => this;
|
$TodoWithCategoryViewView get asDslTable => this;
|
||||||
@override
|
@override
|
||||||
|
@ -1714,8 +1711,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
categories,
|
categories,
|
||||||
todosTable,
|
todosTable,
|
||||||
}).map((QueryRow row) {
|
}).map((QueryRow row) => AllTodosWithCategoryResult(
|
||||||
return AllTodosWithCategoryResult(
|
|
||||||
row: row,
|
row: row,
|
||||||
id: row.read<int>('id'),
|
id: row.read<int>('id'),
|
||||||
title: row.readNullable<String>('title'),
|
title: row.readNullable<String>('title'),
|
||||||
|
@ -1727,8 +1723,7 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
||||||
row.readNullable<String>('status')),
|
row.readNullable<String>('status')),
|
||||||
catId: row.read<int>('catId'),
|
catId: row.read<int>('catId'),
|
||||||
catDesc: row.read<String>('catDesc'),
|
catDesc: row.read<String>('catDesc'),
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<int> deleteTodoById(int var1) {
|
Future<int> deleteTodoById(int var1) {
|
||||||
|
|
|
@ -136,7 +136,7 @@ void main() {
|
||||||
contains(
|
contains(
|
||||||
isA<NestedResult>()
|
isA<NestedResult>()
|
||||||
.having((e) => e.defaults, 'defaults', first)
|
.having((e) => e.defaults, 'defaults', first)
|
||||||
.having((e) => e.nestedQuery0, 'nested', hasLength(2)),
|
.having((e) => e.nestedQuery1, 'nested', hasLength(2)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ void main() {
|
||||||
contains(
|
contains(
|
||||||
isA<NestedResult>()
|
isA<NestedResult>()
|
||||||
.having((e) => e.defaults, 'defaults', second)
|
.having((e) => e.defaults, 'defaults', second)
|
||||||
.having((e) => e.nestedQuery0, 'nested', hasLength(1)),
|
.having((e) => e.nestedQuery1, 'nested', hasLength(1)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,6 +30,6 @@ void main() {
|
||||||
|
|
||||||
final result = results.single;
|
final result = results.single;
|
||||||
expect(result.defaults, defaults);
|
expect(result.defaults, defaults);
|
||||||
expect(result.nestedQuery0, [constraints]);
|
expect(result.nestedQuery1, [constraints]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,4 +50,52 @@ void main() {
|
||||||
|
|
||||||
expect(await db.users.all().get(), [user]);
|
expect(await db.users.all().get(), [user]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('subqueries', () async {
|
||||||
|
await db.batch((batch) {
|
||||||
|
batch.insertAll(db.categories, [
|
||||||
|
CategoriesCompanion.insert(description: 'a'),
|
||||||
|
CategoriesCompanion.insert(description: 'b'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
batch.insertAll(
|
||||||
|
db.todosTable,
|
||||||
|
[
|
||||||
|
TodosTableCompanion.insert(content: 'aaaaa', category: Value(1)),
|
||||||
|
TodosTableCompanion.insert(content: 'aa', category: Value(1)),
|
||||||
|
TodosTableCompanion.insert(content: 'bbbbbb', category: Value(2)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now write a query returning the amount of content chars in each
|
||||||
|
// category (written using subqueries).
|
||||||
|
final subqueryContentLength = db.todosTable.content.length.sum();
|
||||||
|
final subquery = Subquery(
|
||||||
|
db.selectOnly(db.todosTable)
|
||||||
|
..addColumns([db.todosTable.category, subqueryContentLength])
|
||||||
|
..groupBy([db.todosTable.category]),
|
||||||
|
's');
|
||||||
|
|
||||||
|
final readableLength = subquery.ref(subqueryContentLength);
|
||||||
|
final query = db.selectOnly(db.categories)
|
||||||
|
..addColumns([db.categories.id, readableLength])
|
||||||
|
..join([
|
||||||
|
innerJoin(subquery,
|
||||||
|
subquery.ref(db.todosTable.category).equalsExp(db.categories.id))
|
||||||
|
])
|
||||||
|
..orderBy([OrderingTerm.asc(db.categories.id)]);
|
||||||
|
|
||||||
|
final rows = await query.get();
|
||||||
|
expect(rows, hasLength(2));
|
||||||
|
|
||||||
|
final first = rows[0];
|
||||||
|
final second = rows[1];
|
||||||
|
|
||||||
|
expect(first.read(db.categories.id), 1);
|
||||||
|
expect(first.read(readableLength), 7);
|
||||||
|
|
||||||
|
expect(second.read(db.categories.id), 2);
|
||||||
|
expect(second.read(readableLength), 6);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,10 @@
|
||||||
|
## 2.11.0
|
||||||
|
|
||||||
|
- [Nested result columns](https://drift.simonbinder.eu/docs/using-sql/drift_files/#nested-results)
|
||||||
|
in drift files can now refer to any result set (e.g. a table-valued function or a subquery).
|
||||||
|
They were restricted to direct table references before.
|
||||||
|
- Add the `dialects` builder option to generate code supporting multiple SQL dialects.
|
||||||
|
|
||||||
## 2.10.0
|
## 2.10.0
|
||||||
|
|
||||||
- Add the `schema steps` command to generate help in writing step-by-step schema migrations.
|
- Add the `schema steps` command to generate help in writing step-by-step schema migrations.
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# Short description for each builder
|
# Short description for each builder
|
||||||
|
|
||||||
# - preparing_builder: Infers the type of inline Dart expressions in moor files.
|
# - preparing_builder: Infers the type of inline Dart expressions in drift files.
|
||||||
# We create a `input.temp.dart` file containing the expressions so that they
|
# We create a `input.temp.dart` file containing the expressions so that they
|
||||||
# can be resolved.
|
# can be resolved.
|
||||||
# - moor_generator: The regular SharedPartBuilder for @UseMoor and @UseDao
|
# - drift_dev: The regular SharedPartBuilder for @DriftDatabase and @DriftAccessor
|
||||||
# annotations
|
# annotations
|
||||||
# - moor_generator_not_shared: Like moor_generator, but as a PartBuilder instead of
|
# - not_shared: Like drift_dev, but as a PartBuilder instead of
|
||||||
# a SharedPartBuilder. This builder is disabled by default, but users may choose
|
# a SharedPartBuilder. This builder is disabled by default, but users may choose
|
||||||
# to use it so that generated classes can be used by other builders.
|
# to use it so that generated classes can be used by other builders.
|
||||||
# - moor_cleanup: Deletes the `.temp.dart` files generated by the `preparing_builder`.
|
# - cleanup: Deletes the `.temp.dart` files generated by the `preparing_builder`.
|
||||||
|
|
||||||
builders:
|
builders:
|
||||||
preparing_builder:
|
preparing_builder:
|
||||||
|
|
|
@ -124,7 +124,7 @@ class DriftOptions {
|
||||||
this.modules = const [],
|
this.modules = const [],
|
||||||
this.sqliteAnalysisOptions,
|
this.sqliteAnalysisOptions,
|
||||||
this.storeDateTimeValuesAsText = false,
|
this.storeDateTimeValuesAsText = false,
|
||||||
this.dialect = const DialectOptions(SqlDialect.sqlite, null),
|
this.dialect = const DialectOptions(null, [SqlDialect.sqlite], null),
|
||||||
this.caseFromDartToSql = CaseFromDartToSql.snake,
|
this.caseFromDartToSql = CaseFromDartToSql.snake,
|
||||||
this.writeToColumnsMixins = false,
|
this.writeToColumnsMixins = false,
|
||||||
this.fatalWarnings = false,
|
this.fatalWarnings = false,
|
||||||
|
@ -189,7 +189,18 @@ class DriftOptions {
|
||||||
/// Whether the [module] has been enabled in this configuration.
|
/// Whether the [module] has been enabled in this configuration.
|
||||||
bool hasModule(SqlModule module) => effectiveModules.contains(module);
|
bool hasModule(SqlModule module) => effectiveModules.contains(module);
|
||||||
|
|
||||||
SqlDialect get effectiveDialect => dialect?.dialect ?? SqlDialect.sqlite;
|
List<SqlDialect> get supportedDialects {
|
||||||
|
final dialects = dialect?.dialects;
|
||||||
|
final singleDialect = dialect?.dialect;
|
||||||
|
|
||||||
|
if (dialects != null) {
|
||||||
|
return dialects;
|
||||||
|
} else if (singleDialect != null) {
|
||||||
|
return [singleDialect];
|
||||||
|
} else {
|
||||||
|
return const [SqlDialect.sqlite];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The assumed sqlite version used when analyzing queries.
|
/// The assumed sqlite version used when analyzing queries.
|
||||||
SqliteVersion get sqliteVersion {
|
SqliteVersion get sqliteVersion {
|
||||||
|
@ -201,10 +212,11 @@ class DriftOptions {
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class DialectOptions {
|
class DialectOptions {
|
||||||
final SqlDialect dialect;
|
final SqlDialect? dialect;
|
||||||
|
final List<SqlDialect>? dialects;
|
||||||
final SqliteAnalysisOptions? options;
|
final SqliteAnalysisOptions? options;
|
||||||
|
|
||||||
const DialectOptions(this.dialect, this.options);
|
const DialectOptions(this.dialect, this.dialects, this.options);
|
||||||
|
|
||||||
factory DialectOptions.fromJson(Map json) => _$DialectOptionsFromJson(json);
|
factory DialectOptions.fromJson(Map json) => _$DialectOptionsFromJson(json);
|
||||||
|
|
||||||
|
|
|
@ -198,18 +198,6 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
|
||||||
relevantNode: e,
|
relevantNode: e,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that it actually refers to a table
|
|
||||||
final result = e.resultSet?.unalias();
|
|
||||||
if (result is! Table && result is! View) {
|
|
||||||
linter.sqlParserErrors.add(AnalysisError(
|
|
||||||
type: AnalysisErrorType.other,
|
|
||||||
message: 'Nested star columns must refer to a table directly. They '
|
|
||||||
"can't refer to a table-valued function or another select "
|
|
||||||
'statement.',
|
|
||||||
relevantNode: e,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e is NestedQueryColumn) {
|
if (e is NestedQueryColumn) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:drift/drift.dart' show SqlDialect;
|
||||||
import 'package:recase/recase.dart';
|
import 'package:recase/recase.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart' as sql;
|
import 'package:sqlparser/sqlparser.dart' as sql;
|
||||||
|
@ -110,7 +111,8 @@ class DriftViewResolver extends DriftElementResolver<DiscoveredDriftView> {
|
||||||
query: stmt.query,
|
query: stmt.query,
|
||||||
// Remove drift-specific syntax
|
// Remove drift-specific syntax
|
||||||
driftTableName: null,
|
driftTableName: null,
|
||||||
).toSqlWithoutDriftSpecificSyntax(resolver.driver.options);
|
).toSqlWithoutDriftSpecificSyntax(
|
||||||
|
resolver.driver.options, SqlDialect.sqlite);
|
||||||
|
|
||||||
return DriftView(
|
return DriftView(
|
||||||
discovered.ownId,
|
discovered.ownId,
|
||||||
|
|
|
@ -35,7 +35,7 @@ class MatchExistingTypeForQuery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ExistingQueryRowType? _findRowType(
|
QueryRowType? _findRowType(
|
||||||
InferredResultSet resultSet,
|
InferredResultSet resultSet,
|
||||||
dynamic /*DartType|RequestedQueryResultType*/ requestedType,
|
dynamic /*DartType|RequestedQueryResultType*/ requestedType,
|
||||||
_ErrorReporter reportError,
|
_ErrorReporter reportError,
|
||||||
|
@ -52,8 +52,8 @@ class MatchExistingTypeForQuery {
|
||||||
'Must be a DartType of a RequestedQueryResultType');
|
'Must be a DartType of a RequestedQueryResultType');
|
||||||
}
|
}
|
||||||
|
|
||||||
final positionalColumns = <ArgumentForExistingQueryRowType>[];
|
final positionalColumns = <ArgumentForQueryRowType>[];
|
||||||
final namedColumns = <String, ArgumentForExistingQueryRowType>{};
|
final namedColumns = <String, ArgumentForQueryRowType>{};
|
||||||
|
|
||||||
final unmatchedColumnsByName = {
|
final unmatchedColumnsByName = {
|
||||||
for (final column in resultSet.columns)
|
for (final column in resultSet.columns)
|
||||||
|
@ -94,10 +94,9 @@ class MatchExistingTypeForQuery {
|
||||||
addEntry(name, () => transformedTypeBuilder.addDartType(type));
|
addEntry(name, () => transformedTypeBuilder.addDartType(type));
|
||||||
}
|
}
|
||||||
|
|
||||||
void addCheckedType(
|
void addCheckedType(ArgumentForQueryRowType type, DartType originalType,
|
||||||
ArgumentForExistingQueryRowType type, DartType originalType,
|
|
||||||
{String? name}) {
|
{String? name}) {
|
||||||
if (type is ExistingQueryRowType) {
|
if (type is QueryRowType) {
|
||||||
addEntry(name, () => transformedTypeBuilder.addCode(type.rowType));
|
addEntry(name, () => transformedTypeBuilder.addCode(type.rowType));
|
||||||
} else if (type is MappedNestedListQuery) {
|
} else if (type is MappedNestedListQuery) {
|
||||||
addEntry(name, () {
|
addEntry(name, () {
|
||||||
|
@ -171,7 +170,7 @@ class MatchExistingTypeForQuery {
|
||||||
final verified = _verifyArgument(resultSet.scalarColumns.single,
|
final verified = _verifyArgument(resultSet.scalarColumns.single,
|
||||||
desiredType, 'Single column', (ignore) {});
|
desiredType, 'Single column', (ignore) {});
|
||||||
if (verified != null) {
|
if (verified != null) {
|
||||||
return ExistingQueryRowType(
|
return QueryRowType(
|
||||||
rowType: AnnotatedDartCode.type(desiredType),
|
rowType: AnnotatedDartCode.type(desiredType),
|
||||||
singleValue: verified,
|
singleValue: verified,
|
||||||
positionalArguments: const [],
|
positionalArguments: const [],
|
||||||
|
@ -184,7 +183,7 @@ class MatchExistingTypeForQuery {
|
||||||
final verified =
|
final verified =
|
||||||
_verifyMatchingDriftTable(resultSet.matchingTable!, desiredType);
|
_verifyMatchingDriftTable(resultSet.matchingTable!, desiredType);
|
||||||
if (verified != null) {
|
if (verified != null) {
|
||||||
return ExistingQueryRowType(
|
return QueryRowType(
|
||||||
rowType: AnnotatedDartCode.build((builder) =>
|
rowType: AnnotatedDartCode.build((builder) =>
|
||||||
builder.addElementRowType(resultSet.matchingTable!.table)),
|
builder.addElementRowType(resultSet.matchingTable!.table)),
|
||||||
singleValue: verified,
|
singleValue: verified,
|
||||||
|
@ -237,7 +236,7 @@ class MatchExistingTypeForQuery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ExistingQueryRowType(
|
return QueryRowType(
|
||||||
rowType: annotatedTypeCode,
|
rowType: annotatedTypeCode,
|
||||||
constructorName: constructorName ?? '',
|
constructorName: constructorName ?? '',
|
||||||
isRecord: desiredType is RecordType,
|
isRecord: desiredType is RecordType,
|
||||||
|
@ -249,13 +248,13 @@ class MatchExistingTypeForQuery {
|
||||||
|
|
||||||
/// Returns the default record type chosen by drift when a user declares the
|
/// Returns the default record type chosen by drift when a user declares the
|
||||||
/// generic `Record` type as a desired result type.
|
/// generic `Record` type as a desired result type.
|
||||||
ExistingQueryRowType _defaultRecord(InferredResultSet resultSet) {
|
QueryRowType _defaultRecord(InferredResultSet resultSet) {
|
||||||
// If there's only a single scalar column, or if we're mapping this result
|
// If there's only a single scalar column, or if we're mapping this result
|
||||||
// set to an existing table, then there's only a single value in the end.
|
// set to an existing table, then there's only a single value in the end.
|
||||||
// Singleton records are forbidden, so we just return the inner type
|
// Singleton records are forbidden, so we just return the inner type
|
||||||
// directly.
|
// directly.
|
||||||
if (resultSet.singleColumn) {
|
if (resultSet.singleColumn) {
|
||||||
return ExistingQueryRowType(
|
return QueryRowType(
|
||||||
rowType: AnnotatedDartCode.build(
|
rowType: AnnotatedDartCode.build(
|
||||||
(builder) => builder.addDriftType(resultSet.scalarColumns.single)),
|
(builder) => builder.addDriftType(resultSet.scalarColumns.single)),
|
||||||
singleValue: resultSet.scalarColumns.single,
|
singleValue: resultSet.scalarColumns.single,
|
||||||
|
@ -264,7 +263,7 @@ class MatchExistingTypeForQuery {
|
||||||
);
|
);
|
||||||
} else if (resultSet.matchingTable != null) {
|
} else if (resultSet.matchingTable != null) {
|
||||||
final table = resultSet.matchingTable!;
|
final table = resultSet.matchingTable!;
|
||||||
return ExistingQueryRowType(
|
return QueryRowType(
|
||||||
rowType: AnnotatedDartCode.build(
|
rowType: AnnotatedDartCode.build(
|
||||||
(builder) => builder.addElementRowType(table.table)),
|
(builder) => builder.addElementRowType(table.table)),
|
||||||
singleValue: table,
|
singleValue: table,
|
||||||
|
@ -273,7 +272,7 @@ class MatchExistingTypeForQuery {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final namedArguments = <String, ArgumentForExistingQueryRowType>{};
|
final namedArguments = <String, ArgumentForQueryRowType>{};
|
||||||
|
|
||||||
final type = AnnotatedDartCode.build((builder) {
|
final type = AnnotatedDartCode.build((builder) {
|
||||||
builder.addText('({');
|
builder.addText('({');
|
||||||
|
@ -288,8 +287,10 @@ class MatchExistingTypeForQuery {
|
||||||
builder.addDriftType(column);
|
builder.addDriftType(column);
|
||||||
namedArguments[fieldName] = column;
|
namedArguments[fieldName] = column;
|
||||||
} else if (column is NestedResultTable) {
|
} else if (column is NestedResultTable) {
|
||||||
builder.addElementRowType(column.table);
|
final innerRecord = _defaultRecord(column.innerResultSet);
|
||||||
namedArguments[fieldName] = column;
|
builder.addCode(innerRecord.rowType);
|
||||||
|
namedArguments[fieldName] =
|
||||||
|
StructuredFromNestedColumn(column, innerRecord);
|
||||||
} else if (column is NestedResultQuery) {
|
} else if (column is NestedResultQuery) {
|
||||||
final nestedResultSet = column.query.resultSet;
|
final nestedResultSet = column.query.resultSet;
|
||||||
|
|
||||||
|
@ -310,7 +311,7 @@ class MatchExistingTypeForQuery {
|
||||||
builder.addText('})');
|
builder.addText('})');
|
||||||
});
|
});
|
||||||
|
|
||||||
return ExistingQueryRowType(
|
return QueryRowType(
|
||||||
rowType: type,
|
rowType: type,
|
||||||
singleValue: null,
|
singleValue: null,
|
||||||
positionalArguments: const [],
|
positionalArguments: const [],
|
||||||
|
@ -319,7 +320,14 @@ class MatchExistingTypeForQuery {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ArgumentForExistingQueryRowType? _verifyArgument(
|
/// Finds a way to map the [column] into the desired [existingTypeForColumn],
|
||||||
|
/// which is represented as a [ArgumentForExistingQueryRowType].
|
||||||
|
///
|
||||||
|
/// If this doesn't succeed (mainly due to incompatible types), reports a
|
||||||
|
/// error through [reportError] and returns `null`.
|
||||||
|
/// [name] is used in error messages to inform the user about the field name
|
||||||
|
/// in their existing Dart class that is causing the problem.
|
||||||
|
ArgumentForQueryRowType? _verifyArgument(
|
||||||
ResultColumn column,
|
ResultColumn column,
|
||||||
DartType existingTypeForColumn,
|
DartType existingTypeForColumn,
|
||||||
String name,
|
String name,
|
||||||
|
@ -339,25 +347,15 @@ class MatchExistingTypeForQuery {
|
||||||
|
|
||||||
if (matches) return column;
|
if (matches) return column;
|
||||||
} else if (column is NestedResultTable) {
|
} else if (column is NestedResultTable) {
|
||||||
final table = column.table;
|
final foundInnerType = _findRowType(
|
||||||
|
column.innerResultSet,
|
||||||
|
existingTypeForColumn,
|
||||||
|
(msg) => reportError('For $name: $msg'),
|
||||||
|
);
|
||||||
|
|
||||||
// Usually, the table is about to be generated by drift - so we can't
|
if (foundInnerType != null) {
|
||||||
// verify the existing type. If there's an existing row class though, we
|
return StructuredFromNestedColumn(column, foundInnerType);
|
||||||
// can compare against that.
|
|
||||||
if (table.hasExistingRowClass) {
|
|
||||||
final existingType = table.existingRowClass!.targetType;
|
|
||||||
if (column.isNullable) {
|
|
||||||
existingTypeForColumn =
|
|
||||||
typeSystem.promoteToNonNull(existingTypeForColumn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!typeSystem.isAssignableTo(existingType, existingTypeForColumn)) {
|
|
||||||
reportError('$name must accept '
|
|
||||||
'${existingType.getDisplayString(withNullability: true)}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return column;
|
|
||||||
} else if (column is NestedResultQuery) {
|
} else if (column is NestedResultQuery) {
|
||||||
// A nested query has its own type, which we can recursively try to
|
// A nested query has its own type, which we can recursively try to
|
||||||
// structure in the existing type.
|
// structure in the existing type.
|
||||||
|
@ -378,13 +376,14 @@ class MatchExistingTypeForQuery {
|
||||||
return MappedNestedListQuery(column, innerExistingType);
|
return MappedNestedListQuery(column, innerExistingType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allows using a matching drift table from a result set as an argument if
|
/// Allows using a matching drift table from a result set as an argument if
|
||||||
/// the the [existingTypeForColumn] matches the table's type (either the
|
/// the the [existingTypeForColumn] matches the table's type (either the
|
||||||
/// existing result type or `dynamic` if it's drift-generated).
|
/// existing result type or `dynamic` if it's drift-generated).
|
||||||
ArgumentForExistingQueryRowType? _verifyMatchingDriftTable(
|
ArgumentForQueryRowType? _verifyMatchingDriftTable(
|
||||||
MatchingDriftTable match, DartType existingTypeForColumn) {
|
MatchingDriftTable match, DartType existingTypeForColumn) {
|
||||||
final table = match.table;
|
final table = match.table;
|
||||||
if (table.hasExistingRowClass) {
|
if (table.hasExistingRowClass) {
|
||||||
|
|
|
@ -330,6 +330,7 @@ class QueryAnalyzer {
|
||||||
|
|
||||||
if (column is NestedStarResultColumn) {
|
if (column is NestedStarResultColumn) {
|
||||||
final resolved = _resolveNestedResultTable(queryContext, column);
|
final resolved = _resolveNestedResultTable(queryContext, column);
|
||||||
|
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
// The single table optimization doesn't make sense when nested result
|
// The single table optimization doesn't make sense when nested result
|
||||||
// sets are present.
|
// sets are present.
|
||||||
|
@ -439,19 +440,37 @@ class QueryAnalyzer {
|
||||||
_QueryHandlerContext queryContext, NestedStarResultColumn column) {
|
_QueryHandlerContext queryContext, NestedStarResultColumn column) {
|
||||||
final originalResult = column.resultSet;
|
final originalResult = column.resultSet;
|
||||||
final result = originalResult?.unalias();
|
final result = originalResult?.unalias();
|
||||||
if (result is! Table && result is! View) {
|
final rawColumns = result?.resolvedColumns;
|
||||||
return null;
|
|
||||||
}
|
if (result == null || rawColumns == null) return null;
|
||||||
|
|
||||||
|
final driftResultSet = _inferResultSet(
|
||||||
|
_QueryHandlerContext(
|
||||||
|
foundElements: queryContext.foundElements,
|
||||||
|
root: queryContext.root,
|
||||||
|
queryName: queryContext.queryName,
|
||||||
|
nestedScope: queryContext.nestedScope,
|
||||||
|
sourceForFixedName: queryContext.sourceForFixedName,
|
||||||
|
// Remove desired result class, if any. It will be resolved by the
|
||||||
|
// parent _inferResultSet call.
|
||||||
|
),
|
||||||
|
rawColumns,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
final driftTable = _lookupReference<DriftElementWithResultSet>(
|
|
||||||
(result as NamedResultSet).name);
|
|
||||||
final analysis = JoinModel.of(column);
|
final analysis = JoinModel.of(column);
|
||||||
final isNullable =
|
final isNullable =
|
||||||
analysis == null || analysis.isNullableTable(originalResult!);
|
analysis == null || analysis.isNullableTable(originalResult!);
|
||||||
|
|
||||||
|
final queryIndex = nestedQueryCounter++;
|
||||||
|
final resultClassName =
|
||||||
|
'${ReCase(queryContext.queryName).pascalCase}NestedColumn$queryIndex';
|
||||||
|
|
||||||
return NestedResultTable(
|
return NestedResultTable(
|
||||||
column,
|
from: column,
|
||||||
column.as ?? column.tableName,
|
name: column.as ?? column.tableName,
|
||||||
driftTable,
|
innerResultSet: driftResultSet,
|
||||||
|
nameForGeneratedRowClass: resultClassName,
|
||||||
isNullable: isNullable,
|
isNullable: isNullable,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,6 +171,11 @@ class AnnotatedDartCodeBuilder {
|
||||||
'This query (${query.name}) does not have a result set');
|
'This query (${query.name}) does not have a result set');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addResultSetRowType(resultSet, () => query.resultClassName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addResultSetRowType(
|
||||||
|
InferredResultSet resultSet, String Function() resultClassName) {
|
||||||
if (resultSet.existingRowType != null) {
|
if (resultSet.existingRowType != null) {
|
||||||
return addCode(resultSet.existingRowType!.rowType);
|
return addCode(resultSet.existingRowType!.rowType);
|
||||||
}
|
}
|
||||||
|
@ -183,12 +188,13 @@ class AnnotatedDartCodeBuilder {
|
||||||
return addDriftType(resultSet.scalarColumns.single);
|
return addDriftType(resultSet.scalarColumns.single);
|
||||||
}
|
}
|
||||||
|
|
||||||
return addText(query.resultClassName);
|
return addText(resultClassName());
|
||||||
}
|
}
|
||||||
|
|
||||||
void addTypeOfNestedResult(NestedResult nested) {
|
void addTypeOfNestedResult(NestedResult nested) {
|
||||||
if (nested is NestedResultTable) {
|
if (nested is NestedResultTable) {
|
||||||
return addElementRowType(nested.table);
|
return addResultSetRowType(
|
||||||
|
nested.innerResultSet, () => nested.nameForGeneratedRowClass);
|
||||||
} else if (nested is NestedResultQuery) {
|
} else if (nested is NestedResultQuery) {
|
||||||
addSymbol('List', AnnotatedDartCode.dartCore);
|
addSymbol('List', AnnotatedDartCode.dartCore);
|
||||||
addText('<');
|
addText('<');
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:analyzer/dart/element/type.dart';
|
import 'package:analyzer/dart/element/type.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:drift/drift.dart' show DriftSqlType, UpdateKind;
|
import 'package:drift/drift.dart' show DriftSqlType, UpdateKind;
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
import 'package:recase/recase.dart';
|
import 'package:recase/recase.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart';
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
|
|
||||||
|
@ -25,7 +24,7 @@ abstract class DriftQueryDeclaration {
|
||||||
/// We deliberately only store very basic information here: The actual query
|
/// We deliberately only store very basic information here: The actual query
|
||||||
/// model is very complex and hard to serialize. Further, lots of generation
|
/// model is very complex and hard to serialize. Further, lots of generation
|
||||||
/// logic requires actual references to the AST which will be difficult to
|
/// logic requires actual references to the AST which will be difficult to
|
||||||
/// translate across serialization run.
|
/// translate across serialization runs.
|
||||||
/// Since SQL queries only need to be fully analyzed before generation, and
|
/// Since SQL queries only need to be fully analyzed before generation, and
|
||||||
/// since they are local elements which can't be referenced by others, there's
|
/// since they are local elements which can't be referenced by others, there's
|
||||||
/// no clear advantage wrt. incremental compilation if queries are fully
|
/// no clear advantage wrt. incremental compilation if queries are fully
|
||||||
|
@ -162,15 +161,10 @@ abstract class SqlQuery {
|
||||||
placeholders = elements.whereType<FoundDartPlaceholder>().toList();
|
placeholders = elements.whereType<FoundDartPlaceholder>().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get needsAsyncMapping {
|
bool get _useResultClassName {
|
||||||
final result = resultSet;
|
final resultSet = this.resultSet!;
|
||||||
if (result != null) {
|
|
||||||
// Mapping to tables is asynchronous
|
|
||||||
if (result.matchingTable != null) return true;
|
|
||||||
if (result.nestedResults.any((e) => e is NestedResultTable)) return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return resultSet.matchingTable == null && !resultSet.singleColumn;
|
||||||
}
|
}
|
||||||
|
|
||||||
String get resultClassName {
|
String get resultClassName {
|
||||||
|
@ -179,7 +173,7 @@ abstract class SqlQuery {
|
||||||
throw StateError('This query ($name) does not have a result set');
|
throw StateError('This query ($name) does not have a result set');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resultSet.matchingTable != null || resultSet.singleColumn) {
|
if (!_useResultClassName) {
|
||||||
throw UnsupportedError('This result set does not introduce a class, '
|
throw UnsupportedError('This result set does not introduce a class, '
|
||||||
'either because it has a matching table or because it only returns '
|
'either because it has a matching table or because it only returns '
|
||||||
'one column.');
|
'one column.');
|
||||||
|
@ -209,6 +203,16 @@ abstract class SqlQuery {
|
||||||
|
|
||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryRowType queryRowType(DriftOptions options) {
|
||||||
|
final resultSet = this.resultSet;
|
||||||
|
if (resultSet == null) {
|
||||||
|
throw StateError('This query ($name) does not have a result set');
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultSet.mappingToRowClass(
|
||||||
|
_useResultClassName ? resultClassName : null, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SqlSelectQuery extends SqlQuery {
|
class SqlSelectQuery extends SqlQuery {
|
||||||
|
@ -230,9 +234,6 @@ class SqlSelectQuery extends SqlQuery {
|
||||||
bool get hasNestedQuery =>
|
bool get hasNestedQuery =>
|
||||||
resultSet.nestedResults.any((e) => e is NestedResultQuery);
|
resultSet.nestedResults.any((e) => e is NestedResultQuery);
|
||||||
|
|
||||||
@override
|
|
||||||
bool get needsAsyncMapping => hasNestedQuery || super.needsAsyncMapping;
|
|
||||||
|
|
||||||
SqlSelectQuery(
|
SqlSelectQuery(
|
||||||
String name,
|
String name,
|
||||||
this.fromContext,
|
this.fromContext,
|
||||||
|
@ -416,7 +417,7 @@ class InferredResultSet {
|
||||||
|
|
||||||
/// If specified, an existing user-defined Dart type to use instead of
|
/// If specified, an existing user-defined Dart type to use instead of
|
||||||
/// generating another class for the result of this query.
|
/// generating another class for the result of this query.
|
||||||
final ExistingQueryRowType? existingRowType;
|
final QueryRowType? existingRowType;
|
||||||
|
|
||||||
/// Explicitly controls that no result class should be generated for this
|
/// Explicitly controls that no result class should be generated for this
|
||||||
/// result set.
|
/// result set.
|
||||||
|
@ -494,21 +495,86 @@ class InferredResultSet {
|
||||||
const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality());
|
const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality());
|
||||||
return columnsEquality.equals(columns, other.columns);
|
return columnsEquality.equals(columns, other.columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns [existingRowType], or constructs an equivalent mapping to the
|
||||||
|
/// default row class generated by drift_dev.
|
||||||
|
///
|
||||||
|
/// The code to map raw result sets into structured data, be it into a class
|
||||||
|
/// written by a user or something generated by drift_dev, is really similar.
|
||||||
|
/// To share that logic in the query writer, we represent both mappings with
|
||||||
|
/// the same [QueryRowType] class.
|
||||||
|
QueryRowType mappingToRowClass(
|
||||||
|
String? resultClassName, DriftOptions options) {
|
||||||
|
final existingType = existingRowType;
|
||||||
|
final matchingTable = this.matchingTable;
|
||||||
|
|
||||||
|
if (existingType != null) {
|
||||||
|
return existingType;
|
||||||
|
} else if (singleColumn) {
|
||||||
|
final column = scalarColumns.single;
|
||||||
|
|
||||||
|
return QueryRowType(
|
||||||
|
rowType: AnnotatedDartCode.build((b) => b.addDriftType(column)),
|
||||||
|
singleValue: _columnAsArgument(column, options),
|
||||||
|
positionalArguments: const [],
|
||||||
|
namedArguments: const {},
|
||||||
|
);
|
||||||
|
} else if (matchingTable != null) {
|
||||||
|
return QueryRowType(
|
||||||
|
rowType: AnnotatedDartCode.build(
|
||||||
|
(b) => b.addElementRowType(matchingTable.table)),
|
||||||
|
singleValue: matchingTable,
|
||||||
|
positionalArguments: const [],
|
||||||
|
namedArguments: const {},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return QueryRowType(
|
||||||
|
rowType: AnnotatedDartCode.build((b) => b.addText(resultClassName!)),
|
||||||
|
singleValue: null,
|
||||||
|
positionalArguments: const [],
|
||||||
|
namedArguments: {
|
||||||
|
if (options.rawResultSetData) 'row': RawQueryRow(),
|
||||||
|
for (final column in columns)
|
||||||
|
dartNameFor(column): _columnAsArgument(column, options),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArgumentForQueryRowType _columnAsArgument(
|
||||||
|
ResultColumn column,
|
||||||
|
DriftOptions options,
|
||||||
|
) {
|
||||||
|
return switch (column) {
|
||||||
|
ScalarResultColumn() => column,
|
||||||
|
NestedResultTable() => StructuredFromNestedColumn(
|
||||||
|
column,
|
||||||
|
column.innerResultSet
|
||||||
|
.mappingToRowClass(column.nameForGeneratedRowClass, options),
|
||||||
|
),
|
||||||
|
NestedResultQuery() => MappedNestedListQuery(
|
||||||
|
column,
|
||||||
|
column.query.queryRowType(options),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ExistingQueryRowType implements ArgumentForExistingQueryRowType {
|
/// Describes a data type for a query, and how to map raw data into that
|
||||||
|
/// structured type.
|
||||||
|
class QueryRowType implements ArgumentForQueryRowType {
|
||||||
final AnnotatedDartCode rowType;
|
final AnnotatedDartCode rowType;
|
||||||
final String constructorName;
|
final String constructorName;
|
||||||
final bool isRecord;
|
final bool isRecord;
|
||||||
|
|
||||||
/// When set, instead of constructing the [rowType] from the arguments, the
|
/// When set, instead of constructing the [rowType] from the arguments, the
|
||||||
/// argument specified here can just be cast into the desired [rowType].
|
/// argument specified here can just be cast into the desired [rowType].
|
||||||
ArgumentForExistingQueryRowType? singleValue;
|
ArgumentForQueryRowType? singleValue;
|
||||||
|
|
||||||
final List<ArgumentForExistingQueryRowType> positionalArguments;
|
final List<ArgumentForQueryRowType> positionalArguments;
|
||||||
final Map<String, ArgumentForExistingQueryRowType> namedArguments;
|
final Map<String, ArgumentForQueryRowType> namedArguments;
|
||||||
|
|
||||||
ExistingQueryRowType({
|
QueryRowType({
|
||||||
required this.rowType,
|
required this.rowType,
|
||||||
required this.singleValue,
|
required this.singleValue,
|
||||||
required this.positionalArguments,
|
required this.positionalArguments,
|
||||||
|
@ -517,6 +583,19 @@ class ExistingQueryRowType implements ArgumentForExistingQueryRowType {
|
||||||
this.isRecord = false,
|
this.isRecord = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Iterable<ArgumentForQueryRowType> get allArguments sync* {
|
||||||
|
if (singleValue != null) {
|
||||||
|
yield singleValue!;
|
||||||
|
} else {
|
||||||
|
yield* positionalArguments;
|
||||||
|
yield* namedArguments.values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get requiresAsynchronousContext =>
|
||||||
|
allArguments.any((arg) => arg.requiresAsynchronousContext);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'ExistingQueryRowType(type: $rowType, singleValue: $singleValue, '
|
return 'ExistingQueryRowType(type: $rowType, singleValue: $singleValue, '
|
||||||
|
@ -524,26 +603,60 @@ class ExistingQueryRowType implements ArgumentForExistingQueryRowType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@sealed
|
sealed class ArgumentForQueryRowType {
|
||||||
abstract class ArgumentForExistingQueryRowType {}
|
/// Whether the code constructing this argument may need to be in an async
|
||||||
|
/// context.
|
||||||
|
bool get requiresAsynchronousContext;
|
||||||
|
}
|
||||||
|
|
||||||
class MappedNestedListQuery extends ArgumentForExistingQueryRowType {
|
/// An argument that just maps the raw query row.
|
||||||
|
///
|
||||||
|
/// This is used for generated query classes which can optionally hold a
|
||||||
|
/// reference to the raw result set.
|
||||||
|
class RawQueryRow extends ArgumentForQueryRowType {
|
||||||
|
@override
|
||||||
|
bool get requiresAsynchronousContext => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StructuredFromNestedColumn extends ArgumentForQueryRowType {
|
||||||
|
final NestedResultTable table;
|
||||||
|
final QueryRowType nestedType;
|
||||||
|
|
||||||
|
bool get nullable => table.isNullable;
|
||||||
|
|
||||||
|
StructuredFromNestedColumn(this.table, this.nestedType);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get requiresAsynchronousContext =>
|
||||||
|
nestedType.requiresAsynchronousContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MappedNestedListQuery extends ArgumentForQueryRowType {
|
||||||
final NestedResultQuery column;
|
final NestedResultQuery column;
|
||||||
final ExistingQueryRowType nestedType;
|
final QueryRowType nestedType;
|
||||||
|
|
||||||
MappedNestedListQuery(this.column, this.nestedType);
|
MappedNestedListQuery(this.column, this.nestedType);
|
||||||
|
|
||||||
|
// List queries run another statement and always need an asynchronous mapping.
|
||||||
|
@override
|
||||||
|
bool get requiresAsynchronousContext => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Information about a matching table. A table matches a query if a query
|
/// Information about a matching table. A table matches a query if a query
|
||||||
/// selects all columns from that table, and nothing more.
|
/// selects all columns from that table, and nothing more.
|
||||||
///
|
///
|
||||||
/// We still need to handle column aliases.
|
/// We still need to handle column aliases.
|
||||||
class MatchingDriftTable implements ArgumentForExistingQueryRowType {
|
class MatchingDriftTable implements ArgumentForQueryRowType {
|
||||||
final DriftElementWithResultSet table;
|
final DriftElementWithResultSet table;
|
||||||
final Map<String, DriftColumn> aliasToColumn;
|
final Map<String, DriftColumn> aliasToColumn;
|
||||||
|
|
||||||
MatchingDriftTable(this.table, this.aliasToColumn);
|
MatchingDriftTable(this.table, this.aliasToColumn);
|
||||||
|
|
||||||
|
@override
|
||||||
|
// Mapping from tables is currently asynchronous because the existing data
|
||||||
|
// class could be an asynchronous factory.
|
||||||
|
bool get requiresAsynchronousContext => true;
|
||||||
|
|
||||||
/// Whether the column alias can be ignored.
|
/// Whether the column alias can be ignored.
|
||||||
///
|
///
|
||||||
/// This is the case if each result column name maps to a drift column with
|
/// This is the case if each result column name maps to a drift column with
|
||||||
|
@ -554,8 +667,7 @@ class MatchingDriftTable implements ArgumentForExistingQueryRowType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@sealed
|
sealed class ResultColumn {
|
||||||
abstract class ResultColumn {
|
|
||||||
/// A unique name for this column in Dart.
|
/// A unique name for this column in Dart.
|
||||||
String dartGetterName(Iterable<String> existingNames);
|
String dartGetterName(Iterable<String> existingNames);
|
||||||
|
|
||||||
|
@ -567,8 +679,8 @@ abstract class ResultColumn {
|
||||||
bool isCompatibleTo(ResultColumn other);
|
bool isCompatibleTo(ResultColumn other);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScalarResultColumn extends ResultColumn
|
final class ScalarResultColumn extends ResultColumn
|
||||||
implements HasType, ArgumentForExistingQueryRowType {
|
implements HasType, ArgumentForQueryRowType {
|
||||||
final String name;
|
final String name;
|
||||||
@override
|
@override
|
||||||
final DriftSqlType sqlType;
|
final DriftSqlType sqlType;
|
||||||
|
@ -587,6 +699,9 @@ class ScalarResultColumn extends ResultColumn
|
||||||
@override
|
@override
|
||||||
bool get isArray => false;
|
bool get isArray => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get requiresAsynchronousContext => false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String dartGetterName(Iterable<String> existingNames) {
|
String dartGetterName(Iterable<String> existingNames) {
|
||||||
return dartNameForSqlColumn(name, existingNames: existingNames);
|
return dartNameForSqlColumn(name, existingNames: existingNames);
|
||||||
|
@ -610,7 +725,7 @@ class ScalarResultColumn extends ResultColumn
|
||||||
|
|
||||||
/// A nested result, could either be a [NestedResultTable] or a
|
/// A nested result, could either be a [NestedResultTable] or a
|
||||||
/// [NestedResultQuery].
|
/// [NestedResultQuery].
|
||||||
abstract class NestedResult extends ResultColumn {}
|
sealed class NestedResult extends ResultColumn {}
|
||||||
|
|
||||||
/// A nested table extracted from a `**` column.
|
/// A nested table extracted from a `**` column.
|
||||||
///
|
///
|
||||||
|
@ -638,14 +753,24 @@ abstract class NestedResult extends ResultColumn {}
|
||||||
///
|
///
|
||||||
/// Knowing that `User` should be extracted into a field is represented with a
|
/// Knowing that `User` should be extracted into a field is represented with a
|
||||||
/// [NestedResultTable] information as part of the result set.
|
/// [NestedResultTable] information as part of the result set.
|
||||||
class NestedResultTable extends NestedResult
|
final class NestedResultTable extends NestedResult {
|
||||||
implements ArgumentForExistingQueryRowType {
|
|
||||||
final bool isNullable;
|
final bool isNullable;
|
||||||
final NestedStarResultColumn from;
|
final NestedStarResultColumn from;
|
||||||
final String name;
|
final String name;
|
||||||
final DriftElementWithResultSet table;
|
|
||||||
|
|
||||||
NestedResultTable(this.from, this.name, this.table, {this.isNullable = true});
|
/// The inner result set, e.g. the table or subquery/table-valued function
|
||||||
|
/// that the [from] column resolves to.
|
||||||
|
final InferredResultSet innerResultSet;
|
||||||
|
|
||||||
|
final String nameForGeneratedRowClass;
|
||||||
|
|
||||||
|
NestedResultTable({
|
||||||
|
required this.from,
|
||||||
|
required this.name,
|
||||||
|
required this.innerResultSet,
|
||||||
|
required this.nameForGeneratedRowClass,
|
||||||
|
this.isNullable = true,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String dartGetterName(Iterable<String> existingNames) {
|
String dartGetterName(Iterable<String> existingNames) {
|
||||||
|
@ -655,7 +780,7 @@ class NestedResultTable extends NestedResult
|
||||||
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
|
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
|
||||||
@override
|
@override
|
||||||
int get compatibilityHashCode {
|
int get compatibilityHashCode {
|
||||||
return Object.hash(name, table);
|
return Object.hash(name, innerResultSet.compatibilityHashCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks whether this is compatible to the [other] nested result, which is
|
/// Checks whether this is compatible to the [other] nested result, which is
|
||||||
|
@ -665,12 +790,12 @@ class NestedResultTable extends NestedResult
|
||||||
if (other is! NestedResultTable) return false;
|
if (other is! NestedResultTable) return false;
|
||||||
|
|
||||||
return other.name == name &&
|
return other.name == name &&
|
||||||
other.table == table &&
|
other.innerResultSet.isCompatibleTo(other.innerResultSet) &&
|
||||||
other.isNullable == isNullable;
|
other.isNullable == isNullable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NestedResultQuery extends NestedResult {
|
final class NestedResultQuery extends NestedResult {
|
||||||
final NestedQueryColumn from;
|
final NestedQueryColumn from;
|
||||||
|
|
||||||
final SqlSelectQuery query;
|
final SqlSelectQuery query;
|
||||||
|
|
|
@ -179,11 +179,16 @@ DialectOptions _$DialectOptionsFromJson(Map json) => $checkedCreate(
|
||||||
($checkedConvert) {
|
($checkedConvert) {
|
||||||
$checkKeys(
|
$checkKeys(
|
||||||
json,
|
json,
|
||||||
allowedKeys: const ['dialect', 'options'],
|
allowedKeys: const ['dialect', 'dialects', 'options'],
|
||||||
);
|
);
|
||||||
final val = DialectOptions(
|
final val = DialectOptions(
|
||||||
$checkedConvert(
|
$checkedConvert(
|
||||||
'dialect', (v) => $enumDecode(_$SqlDialectEnumMap, v)),
|
'dialect', (v) => $enumDecodeNullable(_$SqlDialectEnumMap, v)),
|
||||||
|
$checkedConvert(
|
||||||
|
'dialects',
|
||||||
|
(v) => (v as List<dynamic>?)
|
||||||
|
?.map((e) => $enumDecode(_$SqlDialectEnumMap, e))
|
||||||
|
.toList()),
|
||||||
$checkedConvert(
|
$checkedConvert(
|
||||||
'options',
|
'options',
|
||||||
(v) =>
|
(v) =>
|
||||||
|
@ -195,7 +200,9 @@ DialectOptions _$DialectOptionsFromJson(Map json) => $checkedCreate(
|
||||||
|
|
||||||
Map<String, dynamic> _$DialectOptionsToJson(DialectOptions instance) =>
|
Map<String, dynamic> _$DialectOptionsToJson(DialectOptions instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'dialect': _$SqlDialectEnumMap[instance.dialect]!,
|
'dialect': _$SqlDialectEnumMap[instance.dialect],
|
||||||
|
'dialects':
|
||||||
|
instance.dialects?.map((e) => _$SqlDialectEnumMap[e]!).toList(),
|
||||||
'options': instance.options?.toJson(),
|
'options': instance.options?.toJson(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -220,25 +220,37 @@ class DatabaseWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
static String createTrigger(Scope scope, DriftTrigger entity) {
|
static String createTrigger(Scope scope, DriftTrigger entity) {
|
||||||
final sql = scope.sqlCode(entity.parsedStatement!);
|
final (sql, dialectSpecific) = scope.sqlByDialect(entity.parsedStatement!);
|
||||||
final trigger = scope.drift('Trigger');
|
final trigger = scope.drift('Trigger');
|
||||||
|
|
||||||
return '$trigger(${asDartLiteral(sql)}, ${asDartLiteral(entity.schemaName)})';
|
if (dialectSpecific) {
|
||||||
|
return '$trigger.byDialect(${asDartLiteral(entity.schemaName)}, $sql)';
|
||||||
|
} else {
|
||||||
|
return '$trigger($sql, ${asDartLiteral(entity.schemaName)})';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String createIndex(Scope scope, DriftIndex entity) {
|
static String createIndex(Scope scope, DriftIndex entity) {
|
||||||
final sql = scope.sqlCode(entity.parsedStatement!);
|
final (sql, dialectSpecific) = scope.sqlByDialect(entity.parsedStatement!);
|
||||||
final index = scope.drift('Index');
|
final index = scope.drift('Index');
|
||||||
|
|
||||||
return '$index(${asDartLiteral(entity.schemaName)}, ${asDartLiteral(sql)})';
|
if (dialectSpecific) {
|
||||||
|
return '$index.byDialect(${asDartLiteral(entity.schemaName)}, $sql)';
|
||||||
|
} else {
|
||||||
|
return '$index(${asDartLiteral(entity.schemaName)}, $sql)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static String createOnCreate(
|
static String createOnCreate(
|
||||||
Scope scope, DefinedSqlQuery query, SqlQuery resolved) {
|
Scope scope, DefinedSqlQuery query, SqlQuery resolved) {
|
||||||
final sql = scope.sqlCode(resolved.root!);
|
final (sql, dialectSpecific) = scope.sqlByDialect(resolved.root!);
|
||||||
final onCreate = scope.drift('OnCreateQuery');
|
final onCreate = scope.drift('OnCreateQuery');
|
||||||
|
|
||||||
return '$onCreate(${asDartLiteral(sql)})';
|
if (dialectSpecific) {
|
||||||
|
return '$onCreate.byDialect($sql)';
|
||||||
|
} else {
|
||||||
|
return '$onCreate($sql)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:recase/recase.dart';
|
import 'package:recase/recase.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
||||||
|
|
||||||
|
@ -13,6 +14,16 @@ import 'utils.dart';
|
||||||
|
|
||||||
const highestAssignedIndexVar = '\$arrayStartIndex';
|
const highestAssignedIndexVar = '\$arrayStartIndex';
|
||||||
|
|
||||||
|
typedef _ArgumentContext = ({
|
||||||
|
// Indicates that the argument is available under a prefix in SQL, probably
|
||||||
|
// because it comes from a [NestedResultTable].
|
||||||
|
String? sqlPrefix,
|
||||||
|
// Indicates that, even if the argument appears to be non-nullable by itself,
|
||||||
|
// it comes from a [NestedResultTable] part of an outer join that could make
|
||||||
|
// the entire structure nullable.
|
||||||
|
bool isNullable,
|
||||||
|
});
|
||||||
|
|
||||||
/// Writes the handling code for a query. The code emitted will be a method that
|
/// Writes the handling code for a query. The code emitted will be a method that
|
||||||
/// should be included in a generated database or dao class.
|
/// should be included in a generated database or dao class.
|
||||||
class QueryWriter {
|
class QueryWriter {
|
||||||
|
@ -74,83 +85,83 @@ class QueryWriter {
|
||||||
|
|
||||||
/// Writes the function literal that turns a "QueryRow" into the desired
|
/// Writes the function literal that turns a "QueryRow" into the desired
|
||||||
/// custom return type of a query.
|
/// custom return type of a query.
|
||||||
void _writeMappingLambda(SqlQuery query) {
|
void _writeMappingLambda(InferredResultSet resultSet, QueryRowType rowClass) {
|
||||||
final resultSet = query.resultSet!;
|
|
||||||
final queryRow = _emitter.drift('QueryRow');
|
final queryRow = _emitter.drift('QueryRow');
|
||||||
final existingRowType = resultSet.existingRowType;
|
final asyncModifier = rowClass.requiresAsynchronousContext ? 'async' : '';
|
||||||
final asyncModifier = query.needsAsyncMapping ? 'async' : '';
|
|
||||||
|
|
||||||
if (existingRowType != null) {
|
// We can write every available mapping as a Dart expression via
|
||||||
_emitter.write('($queryRow row) $asyncModifier => ');
|
// _writeArgumentExpression. This can be turned into a lambda by appending
|
||||||
_writeArgumentExpression(existingRowType, resultSet);
|
// it with `(QueryRow row) => $expression`. That's also what we're doing,
|
||||||
} else if (resultSet.singleColumn) {
|
// but if we'll just call mapFromRow in there, we can just tear that method
|
||||||
final column = resultSet.scalarColumns.single;
|
// off instead. This is just an optimization.
|
||||||
_emitter.write('($queryRow row) => ');
|
final singleValue = rowClass.singleValue;
|
||||||
_readScalar(column);
|
if (singleValue is MatchingDriftTable && singleValue.effectivelyNoAlias) {
|
||||||
} else if (resultSet.matchingTable != null) {
|
|
||||||
final match = resultSet.matchingTable!;
|
|
||||||
|
|
||||||
if (match.effectivelyNoAlias) {
|
|
||||||
// Tear-off mapFromRow method on table
|
// Tear-off mapFromRow method on table
|
||||||
_emitter.write('${match.table.dbGetterName}.mapFromRow');
|
_emitter.write('${singleValue.table.dbGetterName}.mapFromRow');
|
||||||
} else {
|
} else {
|
||||||
_emitter.write('($queryRow row) => ');
|
// In all other cases, we're off to write the expression.
|
||||||
_writeArgumentExpression(match, resultSet);
|
_emitter.write('($queryRow row) $asyncModifier => ');
|
||||||
}
|
_writeArgumentExpression(
|
||||||
} else {
|
rowClass, resultSet, (sqlPrefix: null, isNullable: false));
|
||||||
_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}');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes code that will read the [argument] for an existing row type from
|
/// Writes code that will read the [argument] for an existing row type from
|
||||||
/// the raw `QueryRow`.
|
/// the raw `QueryRow`.
|
||||||
void _writeArgumentExpression(
|
void _writeArgumentExpression(
|
||||||
ArgumentForExistingQueryRowType argument, InferredResultSet resultSet) {
|
ArgumentForQueryRowType argument,
|
||||||
if (argument is MappedNestedListQuery) {
|
InferredResultSet resultSet,
|
||||||
final queryRow = _emitter.drift('QueryRow');
|
_ArgumentContext context,
|
||||||
|
) {
|
||||||
|
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 ');
|
_buffer.write('await ');
|
||||||
_writeCustomSelectStatement(argument.column.query,
|
final query = argument.column.query;
|
||||||
includeMappingToDart: false);
|
_writeCustomSelectStatement(query, argument.nestedType);
|
||||||
_buffer.write('.map(');
|
_buffer.write('.get()');
|
||||||
_buffer.write('($queryRow row) => ');
|
case QueryRowType():
|
||||||
_writeArgumentExpression(argument.nestedType, resultSet);
|
|
||||||
_buffer.write(').get()');
|
|
||||||
} else if (argument is ExistingQueryRowType) {
|
|
||||||
final singleValue = argument.singleValue;
|
final singleValue = argument.singleValue;
|
||||||
if (singleValue != null) {
|
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) {
|
if (!argument.isRecord) {
|
||||||
// We're writing a constructor, so let's start with the class name.
|
// We're writing a constructor, so let's start with the class name.
|
||||||
_emitter.writeDart(argument.rowType);
|
_emitter.writeDart(argument.rowType);
|
||||||
|
@ -165,41 +176,40 @@ class QueryWriter {
|
||||||
|
|
||||||
_buffer.write('(');
|
_buffer.write('(');
|
||||||
for (final positional in argument.positionalArguments) {
|
for (final positional in argument.positionalArguments) {
|
||||||
_writeArgumentExpression(positional, resultSet);
|
_writeArgumentExpression(positional, resultSet, childContext);
|
||||||
_buffer.write(', ');
|
_buffer.write(', ');
|
||||||
}
|
}
|
||||||
argument.namedArguments.forEach((name, parameter) {
|
argument.namedArguments.forEach((name, parameter) {
|
||||||
_buffer.write('$name: ');
|
_buffer.write('$name: ');
|
||||||
_writeArgumentExpression(parameter, resultSet);
|
_writeArgumentExpression(parameter, resultSet, childContext);
|
||||||
_buffer.write(', ');
|
_buffer.write(', ');
|
||||||
});
|
});
|
||||||
|
|
||||||
_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`
|
/// Writes Dart code that, given a variable of type `QueryRow` named `row`
|
||||||
/// in the same scope, reads the [column] from that row and brings it into a
|
/// in the same scope, reads the [column] from that row and brings it into a
|
||||||
/// suitable type.
|
/// suitable type.
|
||||||
void _readScalar(ScalarResultColumn column) {
|
void _readScalar(ScalarResultColumn column, _ArgumentContext context) {
|
||||||
final specialName = _transformer.newNameFor(column.sqlParserColumn!);
|
final specialName = _transformer.newNameFor(column.sqlParserColumn!);
|
||||||
|
final isNullable = context.isNullable || column.nullable;
|
||||||
|
|
||||||
final dartLiteral = asDartLiteral(specialName ?? column.name);
|
var name = specialName ?? column.name;
|
||||||
final method = column.nullable ? 'readNullable' : 'read';
|
if (context.sqlPrefix != null) {
|
||||||
|
name = '${context.sqlPrefix}.$name';
|
||||||
|
}
|
||||||
|
|
||||||
|
final dartLiteral = asDartLiteral(name);
|
||||||
|
final method = isNullable ? 'readNullable' : 'read';
|
||||||
final rawDartType =
|
final rawDartType =
|
||||||
_emitter.dartCode(AnnotatedDartCode([dartTypeNames[column.sqlType]!]));
|
_emitter.dartCode(AnnotatedDartCode([dartTypeNames[column.sqlType]!]));
|
||||||
var code = 'row.$method<$rawDartType>($dartLiteral)';
|
var code = 'row.$method<$rawDartType>($dartLiteral)';
|
||||||
|
|
||||||
final converter = column.typeConverter;
|
final converter = column.typeConverter;
|
||||||
if (converter != null) {
|
if (converter != null) {
|
||||||
if (converter.canBeSkippedForNulls && column.nullable) {
|
if (converter.canBeSkippedForNulls && isNullable) {
|
||||||
// The type converter maps non-nullable types, but the column may be
|
// The type converter maps non-nullable types, but the column may be
|
||||||
// nullable in SQL => just map null to null and only invoke the type
|
// nullable in SQL => just map null to null and only invoke the type
|
||||||
// converter for non-null values.
|
// converter for non-null values.
|
||||||
|
@ -214,36 +224,52 @@ class QueryWriter {
|
||||||
_emitter.write(code);
|
_emitter.write(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _readMatchingTable(MatchingDriftTable match) {
|
void _readMatchingTable(MatchingDriftTable match, _ArgumentContext context) {
|
||||||
// note that, even if the result set has a matching table, we can't just
|
// note that, even if the result set has a matching table, we can't just
|
||||||
// use the mapFromRow() function of that table - the column names might
|
// use the mapFromRow() function of that table - the column names might
|
||||||
// be different!
|
// be different!
|
||||||
final table = match.table;
|
final table = match.table;
|
||||||
|
|
||||||
if (match.effectivelyNoAlias) {
|
if (match.effectivelyNoAlias) {
|
||||||
_emitter.write('${table.dbGetterName}.mapFromRow(row)');
|
final mappingMethod =
|
||||||
|
context.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
|
||||||
|
final sqlPrefix = context.sqlPrefix;
|
||||||
|
|
||||||
|
_emitter.write('await ${table.dbGetterName}.$mappingMethod(row');
|
||||||
|
if (sqlPrefix != null) {
|
||||||
|
_emitter.write(', tablePrefix: ${asDartLiteral(sqlPrefix)}');
|
||||||
|
}
|
||||||
|
|
||||||
|
_emitter.write(')');
|
||||||
} else {
|
} else {
|
||||||
|
// If the entire table can be nullable, we can check whether a non-nullable
|
||||||
|
// column from the table is null. If it is, the entire table is null. This
|
||||||
|
// can happen when the table comes from an outer join.
|
||||||
|
if (context.isNullable) {
|
||||||
|
for (final MapEntry(:key, :value) in match.aliasToColumn.entries) {
|
||||||
|
if (!value.nullable) {
|
||||||
|
final mapKey = context.applyPrefix(key);
|
||||||
|
|
||||||
|
_emitter
|
||||||
|
.write('row.data[${asDartLiteral(mapKey)}] == null ? null : ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_emitter.write('${table.dbGetterName}.mapFromRowWithAlias(row, const {');
|
_emitter.write('${table.dbGetterName}.mapFromRowWithAlias(row, const {');
|
||||||
|
|
||||||
for (final alias in match.aliasToColumn.entries) {
|
for (final alias in match.aliasToColumn.entries) {
|
||||||
_emitter
|
_emitter
|
||||||
..write(asDartLiteral(alias.key))
|
..write(asDartLiteral(context.applyPrefix(alias.key)))
|
||||||
..write(': ')
|
..write(': ')
|
||||||
..write(asDartLiteral(alias.value.nameInSql))
|
..write(asDartLiteral(alias.value.nameInSql))
|
||||||
..write(', ');
|
..write(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
_emitter.write('})');
|
_emitter.write('})');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _readNestedTable(NestedResultTable table, String prefix) {
|
|
||||||
final tableGetter = table.table.dbGetterName;
|
|
||||||
final mappingMethod = table.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
|
|
||||||
|
|
||||||
_emitter.write('await $tableGetter.$mappingMethod(row, '
|
|
||||||
'tablePrefix: ${asDartLiteral(prefix)})');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Writes a method returning a `Selectable<T>`, where `T` is the return type
|
/// Writes a method returning a `Selectable<T>`, where `T` is the return type
|
||||||
/// of the custom query.
|
/// of the custom query.
|
||||||
void _writeSelectStatementCreator(SqlSelectQuery select) {
|
void _writeSelectStatementCreator(SqlSelectQuery select) {
|
||||||
|
@ -270,22 +296,22 @@ class QueryWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeCustomSelectStatement(SqlSelectQuery select,
|
void _writeCustomSelectStatement(SqlSelectQuery select,
|
||||||
{bool includeMappingToDart = true}) {
|
[QueryRowType? resultType]) {
|
||||||
_buffer.write(' customSelect(${_queryCode(select)}, ');
|
_buffer.write(' customSelect(${_queryCode(select)}, ');
|
||||||
_writeVariables(select);
|
_writeVariables(select);
|
||||||
_buffer.write(', ');
|
_buffer.write(', ');
|
||||||
_writeReadsFrom(select);
|
_writeReadsFrom(select);
|
||||||
|
|
||||||
if (includeMappingToDart) {
|
final resultSet = select.resultSet;
|
||||||
if (select.needsAsyncMapping) {
|
resultType ??= select.queryRowType(options);
|
||||||
|
|
||||||
|
if (resultType.requiresAsynchronousContext) {
|
||||||
_buffer.write(').asyncMap(');
|
_buffer.write(').asyncMap(');
|
||||||
} else {
|
} else {
|
||||||
_buffer.write(').map(');
|
_buffer.write(').map(');
|
||||||
}
|
}
|
||||||
|
|
||||||
_writeMappingLambda(select);
|
_writeMappingLambda(resultSet, resultType);
|
||||||
}
|
|
||||||
|
|
||||||
_buffer.write(')');
|
_buffer.write(')');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -311,13 +337,17 @@ class QueryWriter {
|
||||||
_writeCommonUpdateParameters(update);
|
_writeCommonUpdateParameters(update);
|
||||||
|
|
||||||
_buffer.write(').then((rows) => ');
|
_buffer.write(').then((rows) => ');
|
||||||
if (update.needsAsyncMapping) {
|
|
||||||
|
final resultSet = update.resultSet!;
|
||||||
|
final rowType = update.queryRowType(options);
|
||||||
|
|
||||||
|
if (rowType.requiresAsynchronousContext) {
|
||||||
_buffer.write('Future.wait(rows.map(');
|
_buffer.write('Future.wait(rows.map(');
|
||||||
_writeMappingLambda(update);
|
_writeMappingLambda(resultSet, rowType);
|
||||||
_buffer.write('))');
|
_buffer.write('))');
|
||||||
} else {
|
} else {
|
||||||
_buffer.write('rows.map(');
|
_buffer.write('rows.map(');
|
||||||
_writeMappingLambda(update);
|
_writeMappingLambda(resultSet, rowType);
|
||||||
_buffer.write(').toList()');
|
_buffer.write(').toList()');
|
||||||
}
|
}
|
||||||
_buffer.write(');\n}');
|
_buffer.write(');\n}');
|
||||||
|
@ -466,7 +496,45 @@ class QueryWriter {
|
||||||
/// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned
|
/// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned
|
||||||
/// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'.
|
/// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'.
|
||||||
String _queryCode(SqlQuery query) {
|
String _queryCode(SqlQuery query) {
|
||||||
return SqlWriter(scope.options, query: query).write();
|
final dialectForCode = <String, List<SqlDialect>>{};
|
||||||
|
|
||||||
|
for (final dialect in scope.options.supportedDialects) {
|
||||||
|
final code =
|
||||||
|
SqlWriter(scope.options, dialect: dialect, query: query).write();
|
||||||
|
|
||||||
|
dialectForCode.putIfAbsent(code, () => []).add(dialect);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialectForCode.length == 1) {
|
||||||
|
// All supported dialects use the same SQL syntax, so we can just use that
|
||||||
|
return dialectForCode.keys.single;
|
||||||
|
} else {
|
||||||
|
// Create a switch expression matching over the dialect of the database
|
||||||
|
// we're connected to.
|
||||||
|
final buffer = StringBuffer('switch (executor.dialect) {');
|
||||||
|
final dialectEnum = scope.drift('SqlDialect');
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
for (final MapEntry(key: code, value: dialects)
|
||||||
|
in dialectForCode.entries) {
|
||||||
|
index++;
|
||||||
|
|
||||||
|
buffer
|
||||||
|
.write(dialects.map((e) => '$dialectEnum.${e.name}').join(' || '));
|
||||||
|
if (index == dialectForCode.length) {
|
||||||
|
// In the last branch, match all dialects as a fallback
|
||||||
|
buffer.write(' || _ ');
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer
|
||||||
|
..write(' => ')
|
||||||
|
..write(code)
|
||||||
|
..write(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.writeln('}');
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _writeReadsFrom(SqlSelectQuery select) {
|
void _writeReadsFrom(SqlSelectQuery select) {
|
||||||
|
@ -789,9 +857,14 @@ String? _defaultForDartPlaceholder(
|
||||||
if (kind is ExpressionDartPlaceholderType && kind.defaultValue != null) {
|
if (kind is ExpressionDartPlaceholderType && kind.defaultValue != null) {
|
||||||
// Wrap the default expression in parentheses to avoid issues with
|
// Wrap the default expression in parentheses to avoid issues with
|
||||||
// the surrounding precedence in SQL.
|
// the surrounding precedence in SQL.
|
||||||
final sql = SqlWriter(scope.options)
|
final (sql, dialectSpecific) =
|
||||||
.writeNodeIntoStringLiteral(Parentheses(kind.defaultValue!));
|
scope.sqlByDialect(Parentheses(kind.defaultValue!));
|
||||||
|
|
||||||
|
if (dialectSpecific) {
|
||||||
|
return 'const ${scope.drift('CustomExpression')}.dialectSpecific($sql)';
|
||||||
|
} else {
|
||||||
return 'const ${scope.drift('CustomExpression')}($sql)';
|
return 'const ${scope.drift('CustomExpression')}($sql)';
|
||||||
|
}
|
||||||
} else if (kind is SimpleDartPlaceholderType &&
|
} else if (kind is SimpleDartPlaceholderType &&
|
||||||
kind.kind == SimpleDartPlaceholderKind.orderBy) {
|
kind.kind == SimpleDartPlaceholderKind.orderBy) {
|
||||||
return 'const ${scope.drift('OrderBy')}.nothing()';
|
return 'const ${scope.drift('OrderBy')}.nothing()';
|
||||||
|
@ -800,3 +873,12 @@ String? _defaultForDartPlaceholder(
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on _ArgumentContext {
|
||||||
|
String applyPrefix(String originalName) {
|
||||||
|
return switch (sqlPrefix) {
|
||||||
|
null => originalName,
|
||||||
|
var s => '$s.$originalName',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,20 +5,23 @@ import '../writer.dart';
|
||||||
|
|
||||||
/// Writes a class holding the result of an sql query into Dart.
|
/// Writes a class holding the result of an sql query into Dart.
|
||||||
class ResultSetWriter {
|
class ResultSetWriter {
|
||||||
final SqlQuery query;
|
final InferredResultSet resultSet;
|
||||||
|
final String resultClassName;
|
||||||
final Scope scope;
|
final Scope scope;
|
||||||
|
|
||||||
ResultSetWriter(this.query, this.scope);
|
ResultSetWriter(SqlQuery query, this.scope)
|
||||||
|
: resultSet = query.resultSet!,
|
||||||
|
resultClassName = query.resultClassName;
|
||||||
|
|
||||||
|
ResultSetWriter.fromResultSetAndClassName(
|
||||||
|
this.resultSet, this.resultClassName, this.scope);
|
||||||
|
|
||||||
void write() {
|
void write() {
|
||||||
final className = query.resultClassName;
|
|
||||||
final fields = <EqualityField>[];
|
final fields = <EqualityField>[];
|
||||||
final nonNullableFields = <String>{};
|
final nonNullableFields = <String>{};
|
||||||
final into = scope.leaf();
|
final into = scope.leaf();
|
||||||
|
|
||||||
final resultSet = query.resultSet!;
|
into.write('class $resultClassName ');
|
||||||
|
|
||||||
into.write('class $className ');
|
|
||||||
if (scope.options.rawResultSetData) {
|
if (scope.options.rawResultSetData) {
|
||||||
into.write('extends CustomResultSet {\n');
|
into.write('extends CustomResultSet {\n');
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,6 +42,12 @@ class ResultSetWriter {
|
||||||
fields.add(EqualityField(fieldName, isList: column.isUint8ListInDart));
|
fields.add(EqualityField(fieldName, isList: column.isUint8ListInDart));
|
||||||
if (!column.nullable) nonNullableFields.add(fieldName);
|
if (!column.nullable) nonNullableFields.add(fieldName);
|
||||||
} else if (column is NestedResultTable) {
|
} else if (column is NestedResultTable) {
|
||||||
|
if (column.innerResultSet.needsOwnClass) {
|
||||||
|
ResultSetWriter.fromResultSetAndClassName(
|
||||||
|
column.innerResultSet, column.nameForGeneratedRowClass, scope)
|
||||||
|
.write();
|
||||||
|
}
|
||||||
|
|
||||||
into
|
into
|
||||||
..write('$modifier ')
|
..write('$modifier ')
|
||||||
..writeDart(
|
..writeDart(
|
||||||
|
@ -66,9 +75,9 @@ class ResultSetWriter {
|
||||||
|
|
||||||
// write the constructor
|
// write the constructor
|
||||||
if (scope.options.rawResultSetData) {
|
if (scope.options.rawResultSetData) {
|
||||||
into.write('$className({required QueryRow row,');
|
into.write('$resultClassName({required QueryRow row,');
|
||||||
} else {
|
} else {
|
||||||
into.write('$className({');
|
into.write('$resultClassName({');
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final column in fields) {
|
for (final column in fields) {
|
||||||
|
@ -90,9 +99,9 @@ class ResultSetWriter {
|
||||||
writeHashCode(fields, into);
|
writeHashCode(fields, into);
|
||||||
into.write(';\n');
|
into.write(';\n');
|
||||||
|
|
||||||
overrideEquals(fields, className, into);
|
overrideEquals(fields, resultClassName, into);
|
||||||
overrideToString(
|
overrideToString(
|
||||||
className, fields.map((f) => f.lexeme).toList(), into.buffer);
|
resultClassName, fields.map((f) => f.lexeme).toList(), into.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
into.write('}\n');
|
into.write('}\n');
|
||||||
|
|
|
@ -26,8 +26,9 @@ String placeholderContextName(FoundDartPlaceholder placeholder) {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ToSqlText on AstNode {
|
extension ToSqlText on AstNode {
|
||||||
String toSqlWithoutDriftSpecificSyntax(DriftOptions options) {
|
String toSqlWithoutDriftSpecificSyntax(
|
||||||
final writer = SqlWriter(options, escapeForDart: false);
|
DriftOptions options, SqlDialect dialect) {
|
||||||
|
final writer = SqlWriter(options, dialect: dialect, escapeForDart: false);
|
||||||
return writer.writeSql(this);
|
return writer.writeSql(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,17 +37,19 @@ class SqlWriter extends NodeSqlBuilder {
|
||||||
final StringBuffer _out;
|
final StringBuffer _out;
|
||||||
final SqlQuery? query;
|
final SqlQuery? query;
|
||||||
final DriftOptions options;
|
final DriftOptions options;
|
||||||
|
final SqlDialect dialect;
|
||||||
final Map<NestedStarResultColumn, NestedResultTable> _starColumnToResolved;
|
final Map<NestedStarResultColumn, NestedResultTable> _starColumnToResolved;
|
||||||
|
|
||||||
bool get _isPostgres => options.effectiveDialect == SqlDialect.postgres;
|
bool get _isPostgres => dialect == SqlDialect.postgres;
|
||||||
|
|
||||||
SqlWriter._(this.query, this.options, this._starColumnToResolved,
|
SqlWriter._(this.query, this.options, this.dialect,
|
||||||
StringBuffer out, bool escapeForDart)
|
this._starColumnToResolved, StringBuffer out, bool escapeForDart)
|
||||||
: _out = out,
|
: _out = out,
|
||||||
super(escapeForDart ? _DartEscapingSink(out) : out);
|
super(escapeForDart ? _DartEscapingSink(out) : out);
|
||||||
|
|
||||||
factory SqlWriter(
|
factory SqlWriter(
|
||||||
DriftOptions options, {
|
DriftOptions options, {
|
||||||
|
required SqlDialect dialect,
|
||||||
SqlQuery? query,
|
SqlQuery? query,
|
||||||
bool escapeForDart = true,
|
bool escapeForDart = true,
|
||||||
StringBuffer? buffer,
|
StringBuffer? buffer,
|
||||||
|
@ -61,7 +64,7 @@ class SqlWriter extends NodeSqlBuilder {
|
||||||
if (nestedResult is NestedResultTable) nestedResult.from: nestedResult
|
if (nestedResult is NestedResultTable) nestedResult.from: nestedResult
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return SqlWriter._(query, options, doubleStarColumnToResolvedTable,
|
return SqlWriter._(query, options, dialect, doubleStarColumnToResolvedTable,
|
||||||
buffer ?? StringBuffer(), escapeForDart);
|
buffer ?? StringBuffer(), escapeForDart);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +87,7 @@ class SqlWriter extends NodeSqlBuilder {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool isKeyword(String lexeme) {
|
bool isKeyword(String lexeme) {
|
||||||
switch (options.effectiveDialect) {
|
switch (dialect) {
|
||||||
case SqlDialect.postgres:
|
case SqlDialect.postgres:
|
||||||
return isKeywordLexeme(lexeme) || isPostgresKeywordLexeme(lexeme);
|
return isKeywordLexeme(lexeme) || isPostgresKeywordLexeme(lexeme);
|
||||||
default:
|
default:
|
||||||
|
@ -194,14 +197,14 @@ class SqlWriter extends NodeSqlBuilder {
|
||||||
// Convert foo.** to "foo.a" AS "nested_0.a", ... for all columns in foo
|
// Convert foo.** to "foo.a" AS "nested_0.a", ... for all columns in foo
|
||||||
var isFirst = true;
|
var isFirst = true;
|
||||||
|
|
||||||
for (final column in result.table.columns) {
|
for (final column in result.innerResultSet.scalarColumns) {
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
isFirst = false;
|
isFirst = false;
|
||||||
} else {
|
} else {
|
||||||
_out.write(', ');
|
_out.write(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
final columnName = column.nameInSql;
|
final columnName = column.name;
|
||||||
_out.write('"$table"."$columnName" AS "$prefix.$columnName"');
|
_out.write('"$table"."$columnName" AS "$prefix.$columnName"');
|
||||||
}
|
}
|
||||||
} else if (e is DartPlaceholder) {
|
} else if (e is DartPlaceholder) {
|
||||||
|
|
|
@ -162,7 +162,7 @@ abstract class TableOrViewWriter {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the Dart type and the Dart expression creating a `GeneratedColumn`
|
/// Returns the Dart type and the Dart expression creating a `GeneratedColumn`
|
||||||
/// instance in drift for the givne [column].
|
/// instance in drift for the given [column].
|
||||||
static (String, String) instantiateColumn(
|
static (String, String) instantiateColumn(
|
||||||
DriftColumn column,
|
DriftColumn column,
|
||||||
TextEmitter emitter, {
|
TextEmitter emitter, {
|
||||||
|
@ -173,6 +173,10 @@ abstract class TableOrViewWriter {
|
||||||
final expressionBuffer = StringBuffer();
|
final expressionBuffer = StringBuffer();
|
||||||
final constraints = defaultConstraints(column);
|
final constraints = defaultConstraints(column);
|
||||||
|
|
||||||
|
// Remove dialect-specific constraints for dialects we don't care about.
|
||||||
|
constraints.removeWhere(
|
||||||
|
(key, _) => !emitter.writer.options.supportedDialects.contains(key));
|
||||||
|
|
||||||
for (final constraint in column.constraints) {
|
for (final constraint in column.constraints) {
|
||||||
if (constraint is LimitingTextLength) {
|
if (constraint is LimitingTextLength) {
|
||||||
final buffer =
|
final buffer =
|
||||||
|
|
|
@ -75,18 +75,29 @@ class ViewWriter extends TableOrViewWriter {
|
||||||
..write('@override\n String get entityName=>'
|
..write('@override\n String get entityName=>'
|
||||||
' ${asDartLiteral(view.schemaName)};\n');
|
' ${asDartLiteral(view.schemaName)};\n');
|
||||||
|
|
||||||
|
emitter
|
||||||
|
..writeln('@override')
|
||||||
|
..write('Map<${emitter.drift('SqlDialect')}, String>')
|
||||||
|
..write(source is! SqlViewSource ? '?' : '')
|
||||||
|
..write('get createViewStatements => ');
|
||||||
if (source is SqlViewSource) {
|
if (source is SqlViewSource) {
|
||||||
final astNode = source.parsedStatement;
|
final astNode = source.parsedStatement;
|
||||||
|
|
||||||
emitter.write('@override\nString get createViewStmt =>');
|
|
||||||
if (astNode != null) {
|
if (astNode != null) {
|
||||||
emitter.writeSqlAsDartLiteral(astNode);
|
emitter.writeSqlByDialectMap(astNode);
|
||||||
} else {
|
} else {
|
||||||
emitter.write(asDartLiteral(source.sqlCreateViewStmt));
|
final firstDialect = scope.options.supportedDialects.first;
|
||||||
|
|
||||||
|
emitter
|
||||||
|
..write('{')
|
||||||
|
..writeDriftRef('SqlDialect')
|
||||||
|
..write('.${firstDialect.name}: ')
|
||||||
|
..write(asDartLiteral(source.sqlCreateViewStmt))
|
||||||
|
..write('}');
|
||||||
}
|
}
|
||||||
buffer.writeln(';');
|
buffer.writeln(';');
|
||||||
} else {
|
} else {
|
||||||
buffer.write('@override\n String? get createViewStmt => null;\n');
|
buffer.writeln('null;');
|
||||||
}
|
}
|
||||||
|
|
||||||
writeAsDslTable();
|
writeAsDslTable();
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:recase/recase.dart';
|
import 'package:recase/recase.dart';
|
||||||
import 'package:sqlparser/sqlparser.dart' as sql;
|
import 'package:sqlparser/sqlparser.dart' as sql;
|
||||||
import 'package:path/path.dart' show url;
|
import 'package:path/path.dart' show url;
|
||||||
|
@ -228,8 +229,50 @@ abstract class _NodeOrWriter {
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
String sqlCode(sql.AstNode node) {
|
String sqlCode(sql.AstNode node, SqlDialect dialect) {
|
||||||
return SqlWriter(writer.options, escapeForDart: false).writeSql(node);
|
return SqlWriter(writer.options, dialect: dialect, escapeForDart: false)
|
||||||
|
.writeSql(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a Dart expression writing the [node] into a Dart string.
|
||||||
|
///
|
||||||
|
/// If the code for [node] depends on the dialect, the code returned evaluates
|
||||||
|
/// to a `Map<SqlDialect, String>`. Otherwise, the code is a direct string
|
||||||
|
/// literal.
|
||||||
|
///
|
||||||
|
/// The boolean component in the record describes whether the code will be
|
||||||
|
/// dialect specific.
|
||||||
|
(String, bool) sqlByDialect(sql.AstNode node) {
|
||||||
|
final dialects = writer.options.supportedDialects;
|
||||||
|
|
||||||
|
if (dialects.length == 1) {
|
||||||
|
return (
|
||||||
|
SqlWriter(writer.options, dialect: dialects.single)
|
||||||
|
.writeNodeIntoStringLiteral(node),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
_writeSqlByDialectMap(node, buffer);
|
||||||
|
return (buffer.toString(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _writeSqlByDialectMap(sql.AstNode node, StringBuffer buffer) {
|
||||||
|
buffer.write('{');
|
||||||
|
|
||||||
|
for (final dialect in writer.options.supportedDialects) {
|
||||||
|
buffer
|
||||||
|
..write(drift('SqlDialect'))
|
||||||
|
..write(".${dialect.name}: '");
|
||||||
|
|
||||||
|
SqlWriter(writer.options, dialect: dialect, buffer: buffer)
|
||||||
|
.writeSql(node);
|
||||||
|
|
||||||
|
buffer.writeln("',");
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.write('}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,16 +345,18 @@ class TextEmitter extends _Node {
|
||||||
|
|
||||||
void writeDart(AnnotatedDartCode code) => write(dartCode(code));
|
void writeDart(AnnotatedDartCode code) => write(dartCode(code));
|
||||||
|
|
||||||
void writeSql(sql.AstNode node, {bool escapeForDartString = true}) {
|
void writeSql(sql.AstNode node,
|
||||||
SqlWriter(writer.options,
|
{required SqlDialect dialect, bool escapeForDartString = true}) {
|
||||||
escapeForDart: escapeForDartString, buffer: buffer)
|
SqlWriter(
|
||||||
.writeSql(node);
|
writer.options,
|
||||||
|
dialect: dialect,
|
||||||
|
escapeForDart: escapeForDartString,
|
||||||
|
buffer: buffer,
|
||||||
|
).writeSql(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeSqlAsDartLiteral(sql.AstNode node) {
|
void writeSqlByDialectMap(sql.AstNode node) {
|
||||||
buffer.write("'");
|
_writeSqlByDialectMap(node, buffer);
|
||||||
writeSql(node);
|
|
||||||
buffer.write("'");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,8 @@ SELECT rowid, highlight(example_table_search, 0, '[match]', '[match]') name,
|
||||||
{'a|lib/a.drift': 'CREATE VIRTUAL TABLE demo USING spellfix1;'},
|
{'a|lib/a.drift': 'CREATE VIRTUAL TABLE demo USING spellfix1;'},
|
||||||
options: DriftOptions.defaults(
|
options: DriftOptions.defaults(
|
||||||
dialect: DialectOptions(
|
dialect: DialectOptions(
|
||||||
SqlDialect.sqlite,
|
null,
|
||||||
|
[SqlDialect.sqlite],
|
||||||
SqliteAnalysisOptions(
|
SqliteAnalysisOptions(
|
||||||
modules: [SqlModule.spellfix1],
|
modules: [SqlModule.spellfix1],
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
|
import 'package:drift_dev/src/analysis/options.dart';
|
||||||
import 'package:drift_dev/src/analysis/results/results.dart';
|
import 'package:drift_dev/src/analysis/results/results.dart';
|
||||||
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../../test_utils.dart';
|
import '../../test_utils.dart';
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('recognizes existing row classes', () async {
|
test('recognizes existing row classes', () async {
|
||||||
|
@ -224,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({
|
final state = TestBackend.inTest({
|
||||||
'a|lib/a.drift': '''
|
'a|lib/a.drift': '''
|
||||||
import 'a.dart';
|
import 'a.dart';
|
||||||
|
@ -260,79 +460,7 @@ class MyQueryRow {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('nested - table', () async {
|
test('custom result set with 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, 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 {
|
|
||||||
final state = TestBackend.inTest({
|
final state = TestBackend.inTest({
|
||||||
'a|lib/a.drift': '''
|
'a|lib/a.drift': '''
|
||||||
import 'a.dart';
|
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(
|
final state = TestBackend.inTest(
|
||||||
{
|
{
|
||||||
'a|lib/a.drift': '''
|
'a|lib/a.drift': '''
|
||||||
|
@ -414,6 +542,7 @@ class MyRow {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('into record', () async {
|
test('into record', () async {
|
||||||
final state = TestBackend.inTest(
|
final state = TestBackend.inTest(
|
||||||
|
@ -536,7 +665,12 @@ class MyRow {
|
||||||
isExistingRowType(type: 'MyRow', positional: [
|
isExistingRowType(type: 'MyRow', positional: [
|
||||||
scalarColumn('name'),
|
scalarColumn('name'),
|
||||||
], named: {
|
], named: {
|
||||||
'otherUser': nestedTableColumm('otherUser'),
|
'otherUser': structedFromNested(
|
||||||
|
isExistingRowType(
|
||||||
|
type: 'MyUser',
|
||||||
|
singleValue: isA<MatchingDriftTable>(),
|
||||||
|
),
|
||||||
|
),
|
||||||
'nested': nestedListQuery(
|
'nested': nestedListQuery(
|
||||||
'nested',
|
'nested',
|
||||||
isExistingRowType(
|
isExistingRowType(
|
||||||
|
@ -723,46 +857,3 @@ class MyRow {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
TypeMatcher<ScalarResultColumn> scalarColumn(String name) =>
|
|
||||||
isA<ScalarResultColumn>().having((e) => e.name, 'name', name);
|
|
||||||
|
|
||||||
TypeMatcher nestedTableColumm(String name) =>
|
|
||||||
isA<NestedResultTable>().having((e) => e.name, 'name', name);
|
|
||||||
|
|
||||||
TypeMatcher<MappedNestedListQuery> nestedListQuery(
|
|
||||||
String columnName, TypeMatcher<ExistingQueryRowType> nestedType) {
|
|
||||||
return isA<MappedNestedListQuery>()
|
|
||||||
.having((e) => e.column.filedName(), 'column', columnName)
|
|
||||||
.having((e) => e.nestedType, 'nestedType', nestedType);
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeMatcher<ExistingQueryRowType> isExistingRowType({
|
|
||||||
String? type,
|
|
||||||
String? constructorName,
|
|
||||||
Object? singleValue,
|
|
||||||
Object? positional,
|
|
||||||
Object? named,
|
|
||||||
}) {
|
|
||||||
var matcher = isA<ExistingQueryRowType>();
|
|
||||||
|
|
||||||
if (type != null) {
|
|
||||||
matcher = matcher.having((e) => e.rowType.toString(), 'rowType', type);
|
|
||||||
}
|
|
||||||
if (constructorName != null) {
|
|
||||||
matcher = matcher.having(
|
|
||||||
(e) => e.constructorName, 'constructorName', constructorName);
|
|
||||||
}
|
|
||||||
if (singleValue != null) {
|
|
||||||
matcher = matcher.having((e) => e.singleValue, 'singleValue', singleValue);
|
|
||||||
}
|
|
||||||
if (positional != null) {
|
|
||||||
matcher = matcher.having(
|
|
||||||
(e) => e.positionalArguments, 'positionalArguments', positional);
|
|
||||||
}
|
|
||||||
if (named != null) {
|
|
||||||
matcher = matcher.having((e) => e.namedArguments, 'namedArguments', named);
|
|
||||||
}
|
|
||||||
|
|
||||||
return matcher;
|
|
||||||
}
|
|
||||||
|
|
|
@ -75,22 +75,6 @@ q: SELECT * FROM t WHERE i IN ?1;
|
||||||
expect(result.allErrors, isEmpty);
|
expect(result.allErrors, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('warns when nested results refer to table-valued functions', () async {
|
|
||||||
final result = await TestBackend.analyzeSingle(
|
|
||||||
"a: SELECT json_each.** FROM json_each('');",
|
|
||||||
options: DriftOptions.defaults(modules: [SqlModule.json1]),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
result.allErrors,
|
|
||||||
[
|
|
||||||
isDriftError(
|
|
||||||
contains('Nested star columns must refer to a table directly.'))
|
|
||||||
.withSpan('json_each.**')
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('warns about default values outside of expressions', () async {
|
test('warns about default values outside of expressions', () async {
|
||||||
final state = TestBackend.inTest({
|
final state = TestBackend.inTest({
|
||||||
'foo|lib/a.drift': r'''
|
'foo|lib/a.drift': r'''
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../../test_utils.dart';
|
import '../../test_utils.dart';
|
||||||
import 'existing_row_classes_test.dart';
|
import 'utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('respects explicit type arguments', () async {
|
test('respects explicit type arguments', () async {
|
||||||
|
@ -90,10 +90,52 @@ query: SELECT foo.**, bar.** FROM my_view foo, my_view bar;
|
||||||
final query = file.fileAnalysis!.resolvedQueries.values.single;
|
final query = file.fileAnalysis!.resolvedQueries.values.single;
|
||||||
|
|
||||||
expect(query.resultSet!.nestedResults, hasLength(2));
|
expect(query.resultSet!.nestedResults, hasLength(2));
|
||||||
|
|
||||||
|
final isFromView = isExistingRowType(
|
||||||
|
type: 'MyViewData',
|
||||||
|
singleValue: isA<MatchingDriftTable>()
|
||||||
|
.having((e) => e.table.schemaName, 'table.schemaName', 'my_view'),
|
||||||
|
);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
query.resultSet!.nestedResults,
|
query.resultSet!.mappingToRowClass('', const DriftOptions.defaults()),
|
||||||
everyElement(isA<NestedResultTable>()
|
isExistingRowType(
|
||||||
.having((e) => e.table.schemaName, 'table.schemName', 'my_view')));
|
named: {
|
||||||
|
'foo': structedFromNested(isFromView),
|
||||||
|
'bar': structedFromNested(isFromView),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('infers nested result sets for custom result sets', () async {
|
||||||
|
final state = TestBackend.inTest({
|
||||||
|
'foo|lib/main.drift': r'''
|
||||||
|
query: SELECT 1 AS a, b.** FROM (SELECT 2 AS b, 3 AS c) AS b;
|
||||||
|
''',
|
||||||
|
});
|
||||||
|
|
||||||
|
final file = await state.analyze('package:foo/main.drift');
|
||||||
|
state.expectNoErrors();
|
||||||
|
|
||||||
|
final query = file.fileAnalysis!.resolvedQueries.values.single;
|
||||||
|
|
||||||
|
expect(
|
||||||
|
query.resultSet!.mappingToRowClass('Row', const DriftOptions.defaults()),
|
||||||
|
isExistingRowType(
|
||||||
|
type: 'Row',
|
||||||
|
named: {
|
||||||
|
'a': scalarColumn('a'),
|
||||||
|
'b': structedFromNested(isExistingRowType(
|
||||||
|
type: 'QueryNestedColumn0',
|
||||||
|
named: {
|
||||||
|
'b': scalarColumn('b'),
|
||||||
|
'c': scalarColumn('c'),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (final dateTimeAsText in [false, true]) {
|
for (final dateTimeAsText in [false, true]) {
|
||||||
|
@ -178,7 +220,7 @@ FROM routes
|
||||||
expect(
|
expect(
|
||||||
resultSet.nestedResults
|
resultSet.nestedResults
|
||||||
.cast<NestedResultTable>()
|
.cast<NestedResultTable>()
|
||||||
.map((e) => e.table.schemaName),
|
.map((e) => e.innerResultSet.matchingTable!.table.schemaName),
|
||||||
['points', 'points'],
|
['points', 'points'],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:drift_dev/src/analysis/results/results.dart';
|
import 'package:drift_dev/src/analysis/results/results.dart';
|
||||||
|
import 'package:test/expect.dart';
|
||||||
|
|
||||||
import '../../test_utils.dart';
|
import '../../test_utils.dart';
|
||||||
|
|
||||||
|
@ -10,3 +11,52 @@ Future<SqlQuery> analyzeSingleQueryInDriftFile(String driftFile) async {
|
||||||
Future<SqlQuery> analyzeQuery(String sql) async {
|
Future<SqlQuery> analyzeQuery(String sql) async {
|
||||||
return analyzeSingleQueryInDriftFile('a: $sql');
|
return analyzeSingleQueryInDriftFile('a: $sql');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TypeMatcher<ScalarResultColumn> scalarColumn(String name) =>
|
||||||
|
isA<ScalarResultColumn>().having((e) => e.name, 'name', name);
|
||||||
|
|
||||||
|
TypeMatcher<StructuredFromNestedColumn> structedFromNested(
|
||||||
|
TypeMatcher<QueryRowType> nestedType) =>
|
||||||
|
isA<StructuredFromNestedColumn>()
|
||||||
|
.having((e) => e.nestedType, 'nestedType', nestedType);
|
||||||
|
|
||||||
|
TypeMatcher<MappedNestedListQuery> nestedListQuery(
|
||||||
|
String columnName, TypeMatcher<QueryRowType> nestedType) {
|
||||||
|
return isA<MappedNestedListQuery>()
|
||||||
|
.having((e) => e.column.filedName(), 'column', columnName)
|
||||||
|
.having((e) => e.nestedType, 'nestedType', nestedType);
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeMatcher<QueryRowType> isExistingRowType({
|
||||||
|
String? type,
|
||||||
|
String? constructorName,
|
||||||
|
Object? singleValue,
|
||||||
|
Object? positional,
|
||||||
|
Object? named,
|
||||||
|
Object? isRecord,
|
||||||
|
}) {
|
||||||
|
var matcher = isA<QueryRowType>();
|
||||||
|
|
||||||
|
if (type != null) {
|
||||||
|
matcher = matcher.having((e) => e.rowType.toString(), 'rowType', type);
|
||||||
|
}
|
||||||
|
if (constructorName != null) {
|
||||||
|
matcher = matcher.having(
|
||||||
|
(e) => e.constructorName, 'constructorName', constructorName);
|
||||||
|
}
|
||||||
|
if (singleValue != null) {
|
||||||
|
matcher = matcher.having((e) => e.singleValue, 'singleValue', singleValue);
|
||||||
|
}
|
||||||
|
if (positional != null) {
|
||||||
|
matcher = matcher.having(
|
||||||
|
(e) => e.positionalArguments, 'positionalArguments', positional);
|
||||||
|
}
|
||||||
|
if (named != null) {
|
||||||
|
matcher = matcher.having((e) => e.namedArguments, 'namedArguments', named);
|
||||||
|
}
|
||||||
|
if (isRecord != null) {
|
||||||
|
matcher = matcher.having((e) => e.isRecord, 'isRecord', isRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matcher;
|
||||||
|
}
|
||||||
|
|
|
@ -307,7 +307,7 @@ TypeConverter<Object, int> myConverter() => throw UnimplementedError();
|
||||||
'a|lib/a.drift.dart': decodedMatches(
|
'a|lib/a.drift.dart': decodedMatches(
|
||||||
allOf(
|
allOf(
|
||||||
contains(
|
contains(
|
||||||
''''CREATE VIEW my_view AS SELECT CAST(1 AS INT) AS c1, CAST(\\'bar\\' AS TEXT) AS c2, 1 AS c3, NULLIF(1, 2) AS c4';'''),
|
''''CREATE VIEW my_view AS SELECT CAST(1 AS INT) AS c1, CAST(\\'bar\\' AS TEXT) AS c2, 1 AS c3, NULLIF(1, 2) AS c4','''),
|
||||||
contains(r'$converterc1 ='),
|
contains(r'$converterc1 ='),
|
||||||
contains(r'$converterc2 ='),
|
contains(r'$converterc2 ='),
|
||||||
contains(r'$converterc3 ='),
|
contains(r'$converterc3 ='),
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import 'package:build_test/build_test.dart';
|
import 'package:build_test/build_test.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift_dev/src/analysis/options.dart';
|
import 'package:drift_dev/src/analysis/options.dart';
|
||||||
import 'package:drift_dev/src/writer/import_manager.dart';
|
import 'package:drift_dev/src/writer/import_manager.dart';
|
||||||
import 'package:drift_dev/src/writer/queries/query_writer.dart';
|
import 'package:drift_dev/src/writer/queries/query_writer.dart';
|
||||||
import 'package:drift_dev/src/writer/writer.dart';
|
import 'package:drift_dev/src/writer/writer.dart';
|
||||||
|
import 'package:sqlparser/sqlparser.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../../analysis/test_utils.dart';
|
import '../../analysis/test_utils.dart';
|
||||||
|
@ -10,13 +12,16 @@ import '../../utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
Future<String> generateForQueryInDriftFile(String driftFile,
|
Future<String> generateForQueryInDriftFile(String driftFile,
|
||||||
{DriftOptions options = const DriftOptions.defaults()}) async {
|
{DriftOptions options = const DriftOptions.defaults(
|
||||||
|
generateNamedParameters: true,
|
||||||
|
)}) async {
|
||||||
final state =
|
final state =
|
||||||
TestBackend.inTest({'a|lib/main.drift': driftFile}, options: options);
|
TestBackend.inTest({'a|lib/main.drift': driftFile}, options: options);
|
||||||
final file = await state.analyze('package:a/main.drift');
|
final file = await state.analyze('package:a/main.drift');
|
||||||
|
state.expectNoErrors();
|
||||||
|
|
||||||
final writer = Writer(
|
final writer = Writer(
|
||||||
const DriftOptions.defaults(generateNamedParameters: true),
|
options,
|
||||||
generationOptions: GenerationOptions(
|
generationOptions: GenerationOptions(
|
||||||
imports: ImportManagerForPartFiles(),
|
imports: ImportManagerForPartFiles(),
|
||||||
),
|
),
|
||||||
|
@ -55,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('''
|
final generated = await generateForQueryInDriftFile('''
|
||||||
CREATE TABLE tbl (
|
CREATE TABLE tbl (
|
||||||
id INTEGER NULL
|
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('''
|
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 (
|
CREATE TABLE tbl (
|
||||||
id INTEGER,
|
id INTEGER,
|
||||||
text TEXT
|
text TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
query: INSERT INTO tbl (id, text) VALUES(10, 'test') RETURNING id;
|
query: INSERT INTO tbl (id, text) VALUES(10, 'test') RETURNING id;
|
||||||
''');
|
''',
|
||||||
|
options: const DriftOptions.defaults(
|
||||||
|
sqliteAnalysisOptions:
|
||||||
|
// Assuming 3.35 because dso that returning works.
|
||||||
|
SqliteAnalysisOptions(version: SqliteVersion.v3(35)),
|
||||||
|
),
|
||||||
|
);
|
||||||
expect(generated, contains('.toList()'));
|
expect(generated, contains('.toList()'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -346,8 +431,7 @@ failQuery:
|
||||||
],
|
],
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
t,
|
t,
|
||||||
}).asyncMap((i0.QueryRow row) async {
|
}).asyncMap((i0.QueryRow row) async => FailQueryResult(
|
||||||
return FailQueryResult(
|
|
||||||
a: row.readNullable<double>('a'),
|
a: row.readNullable<double>('a'),
|
||||||
b: row.readNullable<int>('b'),
|
b: row.readNullable<int>('b'),
|
||||||
nestedQuery0: await customSelect(
|
nestedQuery0: await customSelect(
|
||||||
|
@ -358,8 +442,8 @@ failQuery:
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
t,
|
t,
|
||||||
}).asyncMap(t.mapFromRow).get(),
|
}).asyncMap(t.mapFromRow).get(),
|
||||||
);
|
));
|
||||||
});
|
}
|
||||||
'''))
|
'''))
|
||||||
}, outputs.dartOutputs, outputs);
|
}, outputs.dartOutputs, outputs);
|
||||||
});
|
});
|
||||||
|
@ -447,4 +531,26 @@ class ADrift extends i1.ModularAccessor {
|
||||||
}'''))
|
}'''))
|
||||||
}, outputs.dartOutputs, outputs);
|
}, outputs.dartOutputs, outputs);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('creates dialect-specific query code', () async {
|
||||||
|
final result = await generateForQueryInDriftFile(
|
||||||
|
r'''
|
||||||
|
query (:foo AS TEXT): SELECT :foo;
|
||||||
|
''',
|
||||||
|
options: const DriftOptions.defaults(
|
||||||
|
dialect: DialectOptions(
|
||||||
|
null, [SqlDialect.sqlite, SqlDialect.postgres], null),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
result,
|
||||||
|
contains(
|
||||||
|
'switch (executor.dialect) {'
|
||||||
|
"SqlDialect.sqlite => 'SELECT ?1 AS _c0', "
|
||||||
|
"SqlDialect.postgres || _ => 'SELECT \\\$1 AS _c0', "
|
||||||
|
'}',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,18 @@ import 'package:sqlparser/sqlparser.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
void check(String sql, String expectedDart,
|
void check(
|
||||||
{DriftOptions options = const DriftOptions.defaults()}) {
|
String sql,
|
||||||
|
String expectedDart, {
|
||||||
|
DriftOptions options = const DriftOptions.defaults(),
|
||||||
|
SqlDialect dialect = SqlDialect.sqlite,
|
||||||
|
}) {
|
||||||
final engine = SqlEngine();
|
final engine = SqlEngine();
|
||||||
final context = engine.analyze(sql);
|
final context = engine.analyze(sql);
|
||||||
final query = SqlSelectQuery('name', context, context.root, [], [],
|
final query = SqlSelectQuery('name', context, context.root, [], [],
|
||||||
InferredResultSet(null, []), null, null);
|
InferredResultSet(null, []), null, null);
|
||||||
|
|
||||||
final result = SqlWriter(options, query: query).write();
|
final result = SqlWriter(options, dialect: dialect, query: query).write();
|
||||||
|
|
||||||
expect(result, expectedDart);
|
expect(result, expectedDart);
|
||||||
}
|
}
|
||||||
|
@ -33,7 +37,6 @@ void main() {
|
||||||
test('escapes postgres keywords', () {
|
test('escapes postgres keywords', () {
|
||||||
check('SELECT * FROM user', "'SELECT * FROM user'");
|
check('SELECT * FROM user', "'SELECT * FROM user'");
|
||||||
check('SELECT * FROM user', "'SELECT * FROM \"user\"'",
|
check('SELECT * FROM user', "'SELECT * FROM \"user\"'",
|
||||||
options: DriftOptions.defaults(
|
dialect: SqlDialect.postgres);
|
||||||
dialect: DialectOptions(SqlDialect.postgres, null)));
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,8 +36,7 @@ class MyApp extends StatelessWidget {
|
||||||
primarySwatch: Colors.amber,
|
primarySwatch: Colors.amber,
|
||||||
typography: Typography.material2018(),
|
typography: Typography.material2018(),
|
||||||
),
|
),
|
||||||
routeInformationParser: _router.routeInformationParser,
|
routerConfig: _router,
|
||||||
routerDelegate: _router.routerDelegate,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ dependencies:
|
||||||
file_picker: ^5.2.5
|
file_picker: ^5.2.5
|
||||||
flutter_colorpicker: ^1.0.3
|
flutter_colorpicker: ^1.0.3
|
||||||
flutter_riverpod: ^2.3.0
|
flutter_riverpod: ^2.3.0
|
||||||
go_router: ^9.0.0
|
go_router: ^10.0.0
|
||||||
intl: ^0.18.0
|
intl: ^0.18.0
|
||||||
sqlite3_flutter_libs: ^0.5.5
|
sqlite3_flutter_libs: ^0.5.5
|
||||||
sqlite3: ^2.0.0
|
sqlite3: ^2.0.0
|
||||||
|
|
|
@ -602,8 +602,10 @@ class PopularUsers extends i0.ViewInfo<i1.PopularUsers, i1.PopularUser>
|
||||||
@override
|
@override
|
||||||
String get entityName => 'popular_users';
|
String get entityName => 'popular_users';
|
||||||
@override
|
@override
|
||||||
String get createViewStmt =>
|
Map<i0.SqlDialect, String> get createViewStatements => {
|
||||||
'CREATE VIEW popular_users AS SELECT * FROM users ORDER BY (SELECT count(*) FROM follows WHERE followed = users.id)';
|
i0.SqlDialect.sqlite:
|
||||||
|
'CREATE VIEW popular_users AS SELECT * FROM users ORDER BY (SELECT count(*) FROM follows WHERE followed = users.id)',
|
||||||
|
};
|
||||||
@override
|
@override
|
||||||
PopularUsers get asDslTable => this;
|
PopularUsers get asDslTable => this;
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -9,9 +9,7 @@ targets:
|
||||||
raw_result_set_data: false
|
raw_result_set_data: false
|
||||||
named_parameters: false
|
named_parameters: false
|
||||||
sql:
|
sql:
|
||||||
# As sqlite3 is compatible with the postgres dialect (but not vice-versa), we're
|
dialects: [sqlite, postgres]
|
||||||
# using this dialect so that we can run the tests on postgres as well.
|
|
||||||
dialect: postgres
|
|
||||||
options:
|
options:
|
||||||
version: "3.37"
|
version: "3.37"
|
||||||
modules:
|
modules:
|
||||||
|
|
|
@ -336,7 +336,6 @@ class $FriendshipsTable extends Friendships
|
||||||
requiredDuringInsert: false,
|
requiredDuringInsert: false,
|
||||||
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
|
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
|
||||||
SqlDialect.sqlite: 'CHECK ("really_good_friends" IN (0, 1))',
|
SqlDialect.sqlite: 'CHECK ("really_good_friends" IN (0, 1))',
|
||||||
SqlDialect.mysql: '',
|
|
||||||
SqlDialect.postgres: '',
|
SqlDialect.postgres: '',
|
||||||
}),
|
}),
|
||||||
defaultValue: const Constant(false));
|
defaultValue: const Constant(false));
|
||||||
|
@ -554,7 +553,13 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
late final $FriendshipsTable friendships = $FriendshipsTable(this);
|
late final $FriendshipsTable friendships = $FriendshipsTable(this);
|
||||||
Selectable<User> mostPopularUsers(int amount) {
|
Selectable<User> mostPopularUsers(int amount) {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
|
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',
|
'SELECT * FROM users AS u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT \$1',
|
||||||
|
},
|
||||||
variables: [
|
variables: [
|
||||||
Variable<int>(amount)
|
Variable<int>(amount)
|
||||||
],
|
],
|
||||||
|
@ -566,7 +571,13 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
|
|
||||||
Selectable<int> amountOfGoodFriends(int user) {
|
Selectable<int> amountOfGoodFriends(int user) {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
|
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)',
|
'SELECT COUNT(*) AS _c0 FROM friendships AS f WHERE f.really_good_friends = TRUE AND(f.first_user = \$1 OR f.second_user = \$1)',
|
||||||
|
},
|
||||||
variables: [
|
variables: [
|
||||||
Variable<int>(user)
|
Variable<int>(user)
|
||||||
],
|
],
|
||||||
|
@ -577,19 +588,23 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
|
|
||||||
Selectable<FriendshipsOfResult> friendshipsOf(int user) {
|
Selectable<FriendshipsOfResult> friendshipsOf(int user) {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
|
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)',
|
'SELECT f.really_good_friends,"user"."id" AS "nested_0.id", "user"."name" AS "nested_0.name", "user"."birth_date" AS "nested_0.birth_date", "user"."profile_picture" AS "nested_0.profile_picture", "user"."preferences" AS "nested_0.preferences" FROM friendships AS f INNER JOIN users AS "user" ON "user".id IN (f.first_user, f.second_user) AND "user".id != \$1 WHERE(f.first_user = \$1 OR f.second_user = \$1)',
|
||||||
|
},
|
||||||
variables: [
|
variables: [
|
||||||
Variable<int>(user)
|
Variable<int>(user)
|
||||||
],
|
],
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
friendships,
|
friendships,
|
||||||
users,
|
users,
|
||||||
}).asyncMap((QueryRow row) async {
|
}).asyncMap((QueryRow row) async => FriendshipsOfResult(
|
||||||
return FriendshipsOfResult(
|
|
||||||
reallyGoodFriends: row.read<bool>('really_good_friends'),
|
reallyGoodFriends: row.read<bool>('really_good_friends'),
|
||||||
user: await users.mapFromRow(row, tablePrefix: 'nested_0'),
|
user: await users.mapFromRow(row, tablePrefix: 'nested_0'),
|
||||||
);
|
));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Selectable<int> userCount() {
|
Selectable<int> userCount() {
|
||||||
|
@ -601,7 +616,13 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
Selectable<Preferences?> settingsFor(int user) {
|
Selectable<Preferences?> settingsFor(int user) {
|
||||||
return customSelect('SELECT preferences FROM users WHERE id = \$1',
|
return customSelect(
|
||||||
|
switch (executor.dialect) {
|
||||||
|
SqlDialect.sqlite => 'SELECT preferences FROM users WHERE id = ?1',
|
||||||
|
SqlDialect.postgres ||
|
||||||
|
_ =>
|
||||||
|
'SELECT preferences FROM users WHERE id = \$1',
|
||||||
|
},
|
||||||
variables: [
|
variables: [
|
||||||
Variable<int>(user)
|
Variable<int>(user)
|
||||||
],
|
],
|
||||||
|
@ -626,7 +647,13 @@ abstract class _$Database extends GeneratedDatabase {
|
||||||
|
|
||||||
Future<List<Friendship>> returning(int var1, int var2, bool var3) {
|
Future<List<Friendship>> returning(int var1, int var2, bool var3) {
|
||||||
return customWriteReturning(
|
return customWriteReturning(
|
||||||
|
switch (executor.dialect) {
|
||||||
|
SqlDialect.sqlite =>
|
||||||
|
'INSERT INTO friendships VALUES (?1, ?2, ?3) RETURNING *',
|
||||||
|
SqlDialect.postgres ||
|
||||||
|
_ =>
|
||||||
'INSERT INTO friendships VALUES (\$1, \$2, \$3) RETURNING *',
|
'INSERT INTO friendships VALUES (\$1, \$2, \$3) RETURNING *',
|
||||||
|
},
|
||||||
variables: [
|
variables: [
|
||||||
Variable<int>(var1),
|
Variable<int>(var1),
|
||||||
Variable<int>(var2),
|
Variable<int>(var2),
|
||||||
|
|
|
@ -4,7 +4,7 @@ version: 1.0.0
|
||||||
# homepage: https://www.example.com
|
# homepage: https://www.example.com
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.17.0 <3.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
drift: ^2.0.0-0
|
drift: ^2.0.0-0
|
||||||
|
|
|
@ -50,6 +50,13 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
|
||||||
currentColumnIndex += child.scope.expansionOfStarColumn?.length ?? 1;
|
currentColumnIndex += child.scope.expansionOfStarColumn?.length ?? 1;
|
||||||
} else if (child is NestedQueryColumn) {
|
} else if (child is NestedQueryColumn) {
|
||||||
visit(child.select, arg);
|
visit(child.select, arg);
|
||||||
|
} else if (child is NestedStarResultColumn) {
|
||||||
|
final columns = child.resultSet?.resolvedColumns;
|
||||||
|
if (columns != null) {
|
||||||
|
for (final column in columns) {
|
||||||
|
_handleColumn(column, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
visit(child, arg);
|
visit(child, arg);
|
||||||
|
|
|
@ -145,7 +145,7 @@ class SqlEngine {
|
||||||
|
|
||||||
/// Parses multiple [sql] statements, separated by a semicolon.
|
/// Parses multiple [sql] statements, separated by a semicolon.
|
||||||
///
|
///
|
||||||
/// You can use the [AstNode.children] of the returned [ParseResult.rootNode]
|
/// You can use the [AstNode.childNodes] of the returned [ParseResult.rootNode]
|
||||||
/// to inspect the returned statements.
|
/// to inspect the returned statements.
|
||||||
ParseResult parseMultiple(String sql) {
|
ParseResult parseMultiple(String sql) {
|
||||||
final tokens = tokenize(sql);
|
final tokens = tokenize(sql);
|
||||||
|
|
|
@ -189,9 +189,14 @@ class Parser {
|
||||||
final first = _peek;
|
final first = _peek;
|
||||||
final statements = <Statement>[];
|
final statements = <Statement>[];
|
||||||
while (!_isAtEnd) {
|
while (!_isAtEnd) {
|
||||||
|
final firstForStatement = _peek;
|
||||||
final statement = _parseAsStatement(_statementWithoutSemicolon);
|
final statement = _parseAsStatement(_statementWithoutSemicolon);
|
||||||
|
|
||||||
if (statement != null) {
|
if (statement != null) {
|
||||||
statements.add(statement);
|
statements.add(statement);
|
||||||
|
} else {
|
||||||
|
statements
|
||||||
|
.add(InvalidStatement()..setSpan(firstForStatement, _previous));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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