Merge branch 'develop' into Banana-develop

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,7 +17,14 @@ abstract class ViewInfo<Self extends HasResultSet, Row>
/// The `CREATE VIEW` sql statement that can be used to create this view.
///
/// 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()`
///

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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