Add code generation for nested result queries

This commit is contained in:
Daniel Brauner 2022-01-17 22:19:31 +01:00
parent ca8482be71
commit 23de4c5cee
15 changed files with 341 additions and 196 deletions

View File

@ -1,6 +1,8 @@
// Mega compilation unit that includes all Dart apis related to generating SQL // Mega compilation unit that includes all Dart apis related to generating SQL
// at runtime. // at runtime.
import 'dart:async';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift/sqlite_keywords.dart'; import 'package:drift/sqlite_keywords.dart';

View File

@ -250,28 +250,29 @@ abstract class Selectable<T>
/// ///
/// Each entry emitted by this [Selectable] will be transformed by the /// Each entry emitted by this [Selectable] will be transformed by the
/// [mapper] and then emitted to the selectable returned. /// [mapper] and then emitted to the selectable returned.
Selectable<N> map<N>(N Function(T) mapper) { Selectable<N> map<N>(FutureOr<N> Function(T) mapper) {
return _MappedSelectable<T, N>(this, mapper); return _MappedSelectable<T, N>(this, mapper);
} }
} }
class _MappedSelectable<S, T> extends Selectable<T> { class _MappedSelectable<S, T> extends Selectable<T> {
final Selectable<S> _source; final Selectable<S> _source;
final T Function(S) _mapper; final FutureOr<T> Function(S) _mapper;
_MappedSelectable(this._source, this._mapper); _MappedSelectable(this._source, this._mapper);
@override @override
Future<List<T>> get() { Future<List<T>> get() async {
return _source.get().then(_mapResults); return _source.get().then(_mapResults);
} }
@override @override
Stream<List<T>> watch() { Stream<List<T>> watch() {
return _source.watch().map(_mapResults); return _source.watch().asyncMap(_mapResults);
} }
List<T> _mapResults(List<S> results) => results.map(_mapper).toList(); Future<List<T>> _mapResults(List<S> results) async =>
[for (final result in results) await _mapper(result)];
} }
/// Mixin for a [Query] that operates on a single primary table only. /// Mixin for a [Query] that operates on a single primary table only.

View File

@ -167,8 +167,8 @@ class SqlAnalyzer extends BaseAnalyzer {
try { try {
final handled = final handled =
QueryHandler(query, context, mapper, requiredVariables: variables) QueryHandler(context, mapper, requiredVariables: variables)
.handle() .handle(query)
..declaredInMoorFile = query is DeclaredMoorQuery; ..declaredInMoorFile = query is DeclaredMoorQuery;
foundQueries.add(handled); foundQueries.add(handled);
} catch (e, s) { } catch (e, s) {

View File

@ -11,7 +11,6 @@ import 'required_variables.dart';
/// generator package by determining its type, return columns, variables and so /// generator package by determining its type, return columns, variables and so
/// on. /// on.
class QueryHandler { class QueryHandler {
final DeclaredQuery source;
final AnalysisContext context; final AnalysisContext context;
final TypeMapper mapper; final TypeMapper mapper;
final RequiredVariables requiredVariables; final RequiredVariables requiredVariables;
@ -23,19 +22,30 @@ class QueryHandler {
Iterable<FoundVariable> get _foundVariables => Iterable<FoundVariable> get _foundVariables =>
_foundElements.whereType<FoundVariable>(); _foundElements.whereType<FoundVariable>();
BaseSelectStatement get _select => context.root as BaseSelectStatement; QueryHandler(
this.context,
this.mapper, {
this.requiredVariables = RequiredVariables.empty,
});
QueryHandler(this.source, this.context, this.mapper, SqlQuery handle(DeclaredQuery source) {
{this.requiredVariables = RequiredVariables.empty});
String get name => source.name;
SqlQuery handle() {
_foundElements = _foundElements =
mapper.extractElements(context, required: requiredVariables); mapper.extractElements(context, required: requiredVariables);
_verifyNoSkippedIndexes(); _verifyNoSkippedIndexes();
final query = _mapToMoor();
final String? requestedResultClass;
if (source is DeclaredMoorQuery) {
requestedResultClass = source.astNode.as;
} else {
requestedResultClass = null;
}
final query = _mapToMoor(
queryName: source.name,
requestedResultClass: requestedResultClass,
root: context.root,
);
final linter = Linter.forHandler(this); final linter = Linter.forHandler(this);
linter.reportLints(); linter.reportLints();
@ -44,14 +54,21 @@ class QueryHandler {
return query; return query;
} }
SqlQuery _mapToMoor() { SqlQuery _mapToMoor({
final root = context.root; required String queryName,
required String? requestedResultClass,
required AstNode root,
}) {
if (root is BaseSelectStatement) { if (root is BaseSelectStatement) {
return _handleSelect(); return _handleSelect(
queryName: queryName,
requestedResultClass: requestedResultClass,
select: root,
);
} else if (root is UpdateStatement || } else if (root is UpdateStatement ||
root is DeleteStatement || root is DeleteStatement ||
root is InsertStatement) { root is InsertStatement) {
return _handleUpdate(); return _handleUpdate(queryName, root);
} else { } else {
throw StateError('Unexpected sql: Got $root, expected insert, select, ' throw StateError('Unexpected sql: Got $root, expected insert, select, '
'update or delete'); 'update or delete');
@ -63,25 +80,25 @@ class QueryHandler {
_foundViews = visitor.foundViews; _foundViews = visitor.foundViews;
} }
UpdatingQuery _handleUpdate() { UpdatingQuery _handleUpdate(String queryName, AstNode root) {
final updatedFinder = UpdatedTablesVisitor(); final updatedFinder = UpdatedTablesVisitor();
context.root.acceptWithoutArg(updatedFinder); root.acceptWithoutArg(updatedFinder);
_applyFoundTables(updatedFinder); _applyFoundTables(updatedFinder);
final root = context.root;
final isInsert = root is InsertStatement; final isInsert = root is InsertStatement;
InferredResultSet? resultSet; InferredResultSet? resultSet;
if (root is StatementReturningColumns) { if (root is StatementReturningColumns) {
final columns = root.returnedResultSet?.resolvedColumns; final columns = root.returnedResultSet?.resolvedColumns;
if (columns != null) { if (columns != null) {
resultSet = _inferResultSet(columns); resultSet = _inferResultSet(root, columns);
} }
} }
return UpdatingQuery( return UpdatingQuery(
name, queryName,
context, context,
root,
_foundElements, _foundElements,
updatedFinder.writtenTables updatedFinder.writtenTables
.map(mapper.writtenToMoor) .map(mapper.writtenToMoor)
@ -93,9 +110,14 @@ class QueryHandler {
); );
} }
SqlSelectQuery _handleSelect() { SqlSelectQuery _handleSelect({
required String queryName,
required String? requestedResultClass,
required BaseSelectStatement select,
}) {
final tableFinder = ReferencedTablesVisitor(); final tableFinder = ReferencedTablesVisitor();
_select.acceptWithoutArg(tableFinder); select.acceptWithoutArg(tableFinder);
// fine
_applyFoundTables(tableFinder); _applyFoundTables(tableFinder);
final moorTables = final moorTables =
@ -105,22 +127,18 @@ class QueryHandler {
final moorEntities = [...moorTables, ...moorViews]; final moorEntities = [...moorTables, ...moorViews];
String? requestedName;
if (source is DeclaredMoorQuery) {
requestedName = (source as DeclaredMoorQuery).astNode.as;
}
return SqlSelectQuery( return SqlSelectQuery(
name, queryName,
context, context,
select,
_foundElements, _foundElements,
moorEntities, moorEntities,
_inferResultSet(_select.resolvedColumns!), _inferResultSet(select, select.resolvedColumns!),
requestedName, requestedResultClass,
); );
} }
InferredResultSet _inferResultSet(List<Column> rawColumns) { InferredResultSet _inferResultSet(AstNode select, List<Column> rawColumns) {
final candidatesForSingleTable = {..._foundTables, ..._foundViews}; final candidatesForSingleTable = {..._foundTables, ..._foundViews};
final columns = <ResultColumn>[]; final columns = <ResultColumn>[];
@ -140,7 +158,7 @@ class QueryHandler {
candidatesForSingleTable.removeWhere((t) => t != resultSet); candidatesForSingleTable.removeWhere((t) => t != resultSet);
} }
final nestedResults = _findNestedResultTables(); final nestedResults = _findNestedResultTables(select);
if (nestedResults.isNotEmpty) { if (nestedResults.isNotEmpty) {
// The single table optimization doesn't make sense when nested result // The single table optimization doesn't make sense when nested result
// sets are present. // sets are present.
@ -202,12 +220,11 @@ class QueryHandler {
return InferredResultSet(null, columns, nestedResults: nestedResults); return InferredResultSet(null, columns, nestedResults: nestedResults);
} }
List<NestedResultTable> _findNestedResultTables() { List<NestedResult> _findNestedResultTables(AstNode query) {
final query = context.root;
// We don't currently support nested results for compound statements // We don't currently support nested results for compound statements
if (query is! SelectStatement) return const []; if (query is! SelectStatement) return const [];
final nestedTables = <NestedResultTable>[]; final nestedTables = <NestedResult>[];
final analysis = JoinModel.of(query); final analysis = JoinModel.of(query);
for (final column in query.columns) { for (final column in query.columns) {
@ -225,6 +242,15 @@ class QueryHandler {
moorTable, moorTable,
isNullable: isNullable, isNullable: isNullable,
)); ));
} else if (column is NestedQueryColumn) {
nestedTables.add(NestedResultQuery(
from: column,
query: _handleSelect(
queryName: 'nested',
requestedResultClass: column.as,
select: column.select,
),
));
} }
} }

View File

@ -1,5 +1,4 @@
import 'package:build/build.dart'; import 'package:build/build.dart';
import 'package:drift_dev/moor_generator.dart';
import 'package:drift_dev/src/backends/build/moor_builder.dart'; import 'package:drift_dev/src/backends/build/moor_builder.dart';
import 'package:drift_dev/src/utils/type_utils.dart'; import 'package:drift_dev/src/utils/type_utils.dart';
import 'package:drift_dev/writer.dart'; import 'package:drift_dev/writer.dart';
@ -32,9 +31,8 @@ class DaoGenerator extends Generator implements BaseGenerator {
'$infoType get $getterName => attachedDatabase.$getterName;\n'); '$infoType get $getterName => attachedDatabase.$getterName;\n');
} }
for (final query in dao.queries ?? const <SqlQuery>[]) { dao.queries
QueryWriter(query, classScope.child()).write(); ?.forEach((query) => QueryWriter(classScope.child()).write(query));
}
classScope.leaf().write('}'); classScope.leaf().write('}');
} }

View File

@ -68,6 +68,7 @@ abstract class SqlQuery {
final String name; final String name;
AnalysisContext? get fromContext; AnalysisContext? get fromContext;
AstNode? get root;
List<AnalysisError>? lints; List<AnalysisError>? lints;
/// Whether this query was declared in a `.moor` file. /// Whether this query was declared in a `.moor` file.
@ -165,6 +166,8 @@ class SqlSelectQuery extends SqlQuery {
final InferredResultSet resultSet; final InferredResultSet resultSet;
@override @override
final AnalysisContext fromContext; final AnalysisContext fromContext;
@override
final AstNode root;
/// The name of the result class, as requested by the user. /// The name of the result class, as requested by the user.
// todo: Allow custom result classes for RETURNING as well? // todo: Allow custom result classes for RETURNING as well?
@ -173,6 +176,7 @@ class SqlSelectQuery extends SqlQuery {
SqlSelectQuery( SqlSelectQuery(
String name, String name,
this.fromContext, this.fromContext,
this.root,
List<FoundElement> elements, List<FoundElement> elements,
this.readsFrom, this.readsFrom,
this.resultSet, this.resultSet,
@ -196,6 +200,7 @@ class SqlSelectQuery extends SqlQuery {
return SqlSelectQuery( return SqlSelectQuery(
name, name,
fromContext, fromContext,
root,
elements, elements,
readsFrom, readsFrom,
resultSet, resultSet,
@ -211,6 +216,8 @@ class UpdatingQuery extends SqlQuery {
final InferredResultSet? resultSet; final InferredResultSet? resultSet;
@override @override
final AnalysisContext fromContext; final AnalysisContext fromContext;
@override
final AstNode root;
bool get isOnlyDelete => updates.every((w) => w.kind == UpdateKind.delete); bool get isOnlyDelete => updates.every((w) => w.kind == UpdateKind.delete);
@ -219,6 +226,7 @@ class UpdatingQuery extends SqlQuery {
UpdatingQuery( UpdatingQuery(
String name, String name,
this.fromContext, this.fromContext,
this.root,
List<FoundElement> elements, List<FoundElement> elements,
this.updates, { this.updates, {
this.isInsert = false, this.isInsert = false,
@ -239,6 +247,9 @@ class InTransactionQuery extends SqlQuery {
@override @override
AnalysisContext? get fromContext => null; AnalysisContext? get fromContext => null;
@override
AstNode? get root => null;
} }
class InferredResultSet { class InferredResultSet {
@ -249,9 +260,9 @@ class InferredResultSet {
/// Tables in the result set that should appear as a class. /// Tables in the result set that should appear as a class.
/// ///
/// See [NestedResultTable] for further discussion and examples. /// See [NestedResult] for further discussion and examples.
final List<NestedResultTable> nestedResults; final List<NestedResult> nestedResults;
Map<NestedResultTable, String>? _expandedNestedPrefixes; Map<NestedResult, String>? _expandedNestedPrefixes;
final List<ResultColumn> columns; final List<ResultColumn> columns;
final Map<ResultColumn, String> _dartNames = {}; final Map<ResultColumn, String> _dartNames = {};
@ -293,7 +304,7 @@ class InferredResultSet {
bool get singleColumn => bool get singleColumn =>
matchingTable == null && nestedResults.isEmpty && columns.length == 1; matchingTable == null && nestedResults.isEmpty && columns.length == 1;
String? nestedPrefixFor(NestedResultTable table) { String? nestedPrefixFor(NestedResult table) {
if (_expandedNestedPrefixes == null) { if (_expandedNestedPrefixes == null) {
var index = 0; var index = 0;
_expandedNestedPrefixes = { _expandedNestedPrefixes = {
@ -329,12 +340,17 @@ class InferredResultSet {
}); });
} }
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
int get compatibilityHashCode => Object.hash(
Object.hashAll(columns.map((e) => e.compatibilityHashCode)),
Object.hashAll(nestedResults.map((e) => e.compatibilityHashCode)),
);
/// Checks whether this and the [other] result set have the same columns and /// Checks whether this and the [other] result set have the same columns and
/// nested result sets. /// nested result sets.
bool isCompatibleTo(InferredResultSet other) { bool isCompatibleTo(InferredResultSet other) {
const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality()); const columnsEquality = UnorderedIterableEquality(_ResultColumnEquality());
const nestedEquality = const nestedEquality = UnorderedIterableEquality(_NestedResultEquality());
UnorderedIterableEquality(_NestedResultTableEquality());
return columnsEquality.equals(columns, other.columns) && return columnsEquality.equals(columns, other.columns) &&
nestedEquality.equals(nestedResults, other.nestedResults); nestedEquality.equals(nestedResults, other.nestedResults);
@ -406,6 +422,15 @@ class ResultColumn implements HasType {
} }
} }
/// A nested result, could either be a NestedResultTable or a NestedQueryResult.
abstract class NestedResult {
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
int get compatibilityHashCode;
/// Checks whether this is compatible to the [other] nested result.
bool isCompatibleTo(NestedResult other);
}
/// A nested table extracted from a `**` column. /// A nested table extracted from a `**` column.
/// ///
/// For instance, consider this query: /// For instance, consider this query:
@ -432,7 +457,7 @@ class ResultColumn implements HasType {
/// ///
/// Knowing that `User` should be extracted into a field is represented with a /// Knowing that `User` should be extracted into a field is represented with a
/// [NestedResultTable] information as part of the result set. /// [NestedResultTable] information as part of the result set.
class NestedResultTable { class NestedResultTable extends NestedResult {
final bool isNullable; final bool isNullable;
final NestedStarResultColumn from; final NestedStarResultColumn from;
final String name; final String name;
@ -443,19 +468,67 @@ class NestedResultTable {
String get dartFieldName => ReCase(name).camelCase; String get dartFieldName => ReCase(name).camelCase;
/// [hashCode] that matches [isCompatibleTo] instead of `==`. /// [hashCode] that matches [isCompatibleTo] instead of `==`.
@override
int get compatibilityHashCode { int get compatibilityHashCode {
return Object.hash(name, table); return Object.hash(name, table);
} }
/// Checks whether this is compatible to the [other] nested result, which is /// Checks whether this is compatible to the [other] nested result, which is
/// the case iff they have the same and read from the same table. /// the case iff they have the same and read from the same table.
bool isCompatibleTo(NestedResultTable other) { @override
bool isCompatibleTo(NestedResult other) {
if (other is! NestedResultTable) return false;
return other.name == name && return other.name == name &&
other.table == table && other.table == table &&
other.isNullable == isNullable; other.isNullable == isNullable;
} }
} }
class NestedResultQuery extends NestedResult {
final NestedQueryColumn from;
final SqlSelectQuery query;
NestedResultQuery({
required this.from,
required this.query,
});
String filedName(String? nestedPrefix) {
if (from.as != null) {
return from.as!;
}
final name = ReCase(query.resultClassName).camelCase;
if (nestedPrefix != null) {
return name + nestedPrefix;
}
return name;
}
String resultTypeCode(String parentResultClassName) {
if (query.resultSet.needsOwnClass) {
return parentResultClassName + query.resultClassName;
} else {
return query.resultTypeCode();
}
}
// Every query should be unique.
/// [hashCode] that matches [isCompatibleTo] instead of `==`.
@override
int get compatibilityHashCode => hashCode;
/// Checks whether this is compatible to the [other] nested result, which is
/// the case iff they have the same and read from the same table.
@override
bool isCompatibleTo(NestedResult other) => this == other;
}
/// Something in the query that needs special attention when generating code, /// Something in the query that needs special attention when generating code,
/// such as variables or Dart placeholders. /// such as variables or Dart placeholders.
abstract class FoundElement { abstract class FoundElement {
@ -742,16 +815,16 @@ class _ResultColumnEquality implements Equality<ResultColumn> {
bool isValidKey(Object? e) => e is ResultColumn; bool isValidKey(Object? e) => e is ResultColumn;
} }
class _NestedResultTableEquality implements Equality<NestedResultTable> { class _NestedResultEquality implements Equality<NestedResult> {
const _NestedResultTableEquality(); const _NestedResultEquality();
@override @override
bool equals(NestedResultTable e1, NestedResultTable e2) { bool equals(NestedResult e1, NestedResult e2) {
return e1.isCompatibleTo(e2); return e1.isCompatibleTo(e2);
} }
@override @override
int hash(NestedResultTable e) => e.compatibilityHashCode; int hash(NestedResult e) => e.compatibilityHashCode;
@override @override
bool isValidKey(Object? e) => e is NestedResultTable; bool isValidKey(Object? e) => e is NestedResultTable;

View File

@ -117,9 +117,7 @@ class DatabaseWriter {
} }
// Write implementation for query methods // Write implementation for query methods
for (final query in db.queries ?? const <Never>[]) { db.queries?.forEach((query) => QueryWriter(dbScope.child()).write(query));
QueryWriter(query, dbScope.child()).write();
}
// Write List of tables // Write List of tables
final schemaScope = dbScope.leaf(); final schemaScope = dbScope.leaf();

View File

@ -18,25 +18,16 @@ int _compareNodes(AstNode a, AstNode b) =>
/// Writes the handling code for a query. The code emitted will be a method that /// Writes the handling code for a query. The code emitted will be a method that
/// should be included in a generated database or dao class. /// should be included in a generated database or dao class.
class QueryWriter { class QueryWriter {
final SqlQuery query;
final Scope scope; final Scope scope;
late ExplicitAliasTransformer _transformer; late final ExplicitAliasTransformer _transformer;
final StringBuffer _buffer;
SqlSelectQuery get _select => query as SqlSelectQuery;
UpdatingQuery get _update => query as UpdatingQuery;
MoorOptions get options => scope.writer.options; MoorOptions get options => scope.writer.options;
late StringBuffer _buffer;
bool get _newSelectableMode => QueryWriter(this.scope) : _buffer = scope.leaf();
query.declaredInMoorFile || options.compactQueryMethods;
QueryWriter(this.query, this.scope) { void write(SqlQuery query) {
_buffer = scope.leaf();
}
void write() {
// Note that writing queries can have a result set if they use a RETURNING // Note that writing queries can have a result set if they use a RETURNING
// clause. // clause.
final resultSet = query.resultSet; final resultSet = query.resultSet;
@ -54,40 +45,40 @@ class QueryWriter {
// analysis, Dart getter names stay the same. // analysis, Dart getter names stay the same.
if (resultSet != null && options.newSqlCodeGeneration) { if (resultSet != null && options.newSqlCodeGeneration) {
_transformer = ExplicitAliasTransformer(); _transformer = ExplicitAliasTransformer();
_transformer.rewrite(query.fromContext!.root); _transformer.rewrite(query.root!);
} }
if (query is SqlSelectQuery) { if (query is SqlSelectQuery) {
_writeSelect(); _writeSelect(query);
} else if (query is UpdatingQuery) { } else if (query is UpdatingQuery) {
if (resultSet != null) { if (resultSet != null) {
_writeUpdatingQueryWithReturning(); _writeUpdatingQueryWithReturning(query);
} else { } else {
_writeUpdatingQuery(); _writeUpdatingQuery(query);
} }
} }
} }
void _writeSelect() { void _writeSelect(SqlSelectQuery select) {
_writeSelectStatementCreator(); _writeSelectStatementCreator(select);
if (!_newSelectableMode) { if (!select.declaredInMoorFile && !options.compactQueryMethods) {
_writeOneTimeReader(); _writeOneTimeReader(select);
_writeStreamReader(); _writeStreamReader(select);
} }
} }
String _nameOfCreationMethod() { String _nameOfCreationMethod(SqlSelectQuery select) {
if (_newSelectableMode) { if (!select.declaredInMoorFile && !options.compactQueryMethods) {
return query.name; return select.name;
} else { } else {
return '${query.name}Query'; return '${select.name}Query';
} }
} }
/// Writes the function literal that turns a "QueryRow" into the desired /// Writes the function literal that turns a "QueryRow" into the desired
/// custom return type of a query. /// custom return type of a query.
void _writeMappingLambda() { void _writeMappingLambda(SqlQuery query) {
final resultSet = query.resultSet!; final resultSet = query.resultSet!;
if (resultSet.singleColumn) { if (resultSet.singleColumn) {
@ -119,7 +110,7 @@ class QueryWriter {
_buffer.write('})'); _buffer.write('})');
} }
} else { } else {
_buffer.write('(QueryRow row) { return ${query.resultClassName}('); _buffer.write('(QueryRow row) async { return ${query.resultClassName}(');
if (options.rawResultSetData) { if (options.rawResultSetData) {
_buffer.write('row: row,\n'); _buffer.write('row: row,\n');
@ -131,17 +122,35 @@ class QueryWriter {
'${readingCode(column, scope.generationOptions, options)},'); '${readingCode(column, scope.generationOptions, options)},');
} }
for (final nested in resultSet.nestedResults) { for (final nested in resultSet.nestedResults) {
final prefix = resultSet.nestedPrefixFor(nested); if (nested is NestedResultTable) {
if (prefix == null) continue; final prefix = resultSet.nestedPrefixFor(nested);
if (prefix == null) continue;
final fieldName = nested.dartFieldName; final fieldName = nested.dartFieldName;
final tableGetter = nested.table.dbGetterName; final tableGetter = nested.table.dbGetterName;
final mappingMethod = final mappingMethod =
nested.isNullable ? 'mapFromRowOrNull' : 'mapFromRow'; nested.isNullable ? 'mapFromRowOrNull' : 'mapFromRow';
_buffer.write('$fieldName: $tableGetter.$mappingMethod(row, ' _buffer.write('$fieldName: $tableGetter.$mappingMethod(row, '
'tablePrefix: ${asDartLiteral(prefix)}),'); 'tablePrefix: ${asDartLiteral(prefix)}),');
} else if (nested is NestedResultQuery) {
if (!scope.options.newSqlCodeGeneration) {
throw UnsupportedError(
'To use nested result queries enable new_sql_code_generation',
);
}
final prefix = resultSet.nestedPrefixFor(nested);
if (prefix == null) continue;
final fieldName = nested.filedName(prefix);
_buffer.write('$fieldName: await ');
_writeCustomSelectStatement(nested.query);
_buffer.write('.get()');
}
} }
_buffer.write(');\n}'); _buffer.write(');\n}');
} }
@ -180,101 +189,107 @@ class QueryWriter {
/// Writes a method returning a `Selectable<T>`, where `T` is the return type /// Writes a method returning a `Selectable<T>`, where `T` is the return type
/// of the custom query. /// of the custom query.
void _writeSelectStatementCreator() { void _writeSelectStatementCreator(SqlSelectQuery select) {
final returnType = final returnType =
'Selectable<${_select.resultTypeCode(scope.generationOptions)}>'; 'Selectable<${select.resultTypeCode(scope.generationOptions)}>';
final methodName = _nameOfCreationMethod(); final methodName = _nameOfCreationMethod(select);
_buffer.write('$returnType $methodName('); _buffer.write('$returnType $methodName(');
_writeParameters(); _writeParameters(select);
_buffer.write(') {\n'); _buffer.write(') {\n');
_writeExpandedDeclarations(); _writeExpandedDeclarations(select);
_buffer.write('return customSelect(${_queryCode()}, '); _buffer.write('return');
_writeVariables(); _writeCustomSelectStatement(select);
_buffer.write(', '); _buffer.write(';\n}\n');
_writeReadsFrom();
_buffer.write(').map(');
_writeMappingLambda();
_buffer.write(');\n}\n');
} }
void _writeOneTimeReader() { void _writeCustomSelectStatement(SqlSelectQuery select) {
_buffer.write(' customSelect(${_queryCode(select)}, ');
_writeVariables(select);
_buffer.write(', ');
_writeReadsFrom(select);
_buffer.write(').map(');
_writeMappingLambda(select);
_buffer.write(')');
}
void _writeOneTimeReader(SqlSelectQuery select) {
final returnType = final returnType =
'Future<List<${_select.resultTypeCode(scope.generationOptions)}>>'; 'Future<List<${select.resultTypeCode(scope.generationOptions)}>>';
_buffer.write('$returnType ${query.name}('); _buffer.write('$returnType ${select.name}(');
_writeParameters(); _writeParameters(select);
_buffer _buffer
..write(') {\n') ..write(') {\n')
..write('return ${_nameOfCreationMethod()}('); ..write('return ${_nameOfCreationMethod(select)}(');
_writeUseParameters(); _writeUseParameters(select);
_buffer.write(').get();\n}\n'); _buffer.write(').get();\n}\n');
} }
void _writeStreamReader() { void _writeStreamReader(SqlSelectQuery select) {
final upperQueryName = ReCase(query.name).pascalCase; final upperQueryName = ReCase(select.name).pascalCase;
String methodName; String methodName;
// turning the query name into pascal case will remove underscores, add the // turning the query name into pascal case will remove underscores, add the
// "private" modifier back in // "private" modifier back in
if (query.name.startsWith('_')) { if (select.name.startsWith('_')) {
methodName = '_watch$upperQueryName'; methodName = '_watch$upperQueryName';
} else { } else {
methodName = 'watch$upperQueryName'; methodName = 'watch$upperQueryName';
} }
final returnType = final returnType =
'Stream<List<${_select.resultTypeCode(scope.generationOptions)}>>'; 'Stream<List<${select.resultTypeCode(scope.generationOptions)}>>';
_buffer.write('$returnType $methodName('); _buffer.write('$returnType $methodName(');
_writeParameters(); _writeParameters(select);
_buffer _buffer
..write(') {\n') ..write(') {\n')
..write('return ${_nameOfCreationMethod()}('); ..write('return ${_nameOfCreationMethod(select)}(');
_writeUseParameters(); _writeUseParameters(select);
_buffer.write(').watch();\n}\n'); _buffer.write(').watch();\n}\n');
} }
void _writeUpdatingQueryWithReturning() { void _writeUpdatingQueryWithReturning(UpdatingQuery update) {
final type = query.resultTypeCode(scope.generationOptions); final type = update.resultTypeCode(scope.generationOptions);
_buffer.write('Future<List<$type>> ${query.name}('); _buffer.write('Future<List<$type>> ${update.name}(');
_writeParameters(); _writeParameters(update);
_buffer.write(') {\n'); _buffer.write(') {\n');
_writeExpandedDeclarations(); _writeExpandedDeclarations(update);
_buffer.write('return customWriteReturning(${_queryCode()},'); _buffer.write('return customWriteReturning(${_queryCode(update)},');
_writeCommonUpdateParameters(); _writeCommonUpdateParameters(update);
_buffer.write(').then((rows) => rows.map('); _buffer.write(').then((rows) => rows.map(');
_writeMappingLambda(); _writeMappingLambda(update);
_buffer.write(').toList());\n}'); _buffer.write(').toList());\n}');
} }
void _writeUpdatingQuery() { void _writeUpdatingQuery(UpdatingQuery update) {
/* /*
Future<int> test() { Future<int> test() {
return customUpdate('', variables: [], updates: {}); return customUpdate('', variables: [], updates: {});
} }
*/ */
final implName = _update.isInsert ? 'customInsert' : 'customUpdate'; final implName = update.isInsert ? 'customInsert' : 'customUpdate';
_buffer.write('Future<int> ${query.name}('); _buffer.write('Future<int> ${update.name}(');
_writeParameters(); _writeParameters(update);
_buffer.write(') {\n'); _buffer.write(') {\n');
_writeExpandedDeclarations(); _writeExpandedDeclarations(update);
_buffer.write('return $implName(${_queryCode()},'); _buffer.write('return $implName(${_queryCode(update)},');
_writeCommonUpdateParameters(); _writeCommonUpdateParameters(update);
_buffer.write(',);\n}\n'); _buffer.write(',);\n}\n');
} }
void _writeCommonUpdateParameters() { void _writeCommonUpdateParameters(UpdatingQuery update) {
_writeVariables(); _writeVariables(update);
_buffer.write(','); _buffer.write(',');
_writeUpdates(); _writeUpdates(update);
_writeUpdateKind(); _writeUpdateKind(update);
} }
void _writeParameters() { void _writeParameters(SqlQuery query) {
final namedElements = <FoundElement>[]; final namedElements = <FoundElement>[];
String typeFor(FoundElement element) { String typeFor(FoundElement element) {
@ -407,35 +422,37 @@ class QueryWriter {
/// Writes code that uses the parameters as declared by [_writeParameters], /// Writes code that uses the parameters as declared by [_writeParameters],
/// assuming that for each parameter, a variable with the same name exists /// assuming that for each parameter, a variable with the same name exists
/// in the current scope. /// in the current scope.
void _writeUseParameters() { void _writeUseParameters(SqlQuery query) {
final parameters = query.elements.map((e) => e.dartParameterName); final parameters = query.elements.map((e) => e.dartParameterName);
_buffer.write(parameters.join(', ')); _buffer.write(parameters.join(', '));
} }
void _writeExpandedDeclarations() { void _writeExpandedDeclarations(SqlQuery query) {
_ExpandedDeclarationWriter(query, options, _buffer) _ExpandedDeclarationWriter(query, options, _buffer)
.writeExpandedDeclarations(); .writeExpandedDeclarations();
} }
void _writeVariables() { void _writeVariables(SqlQuery query) {
_ExpandedVariableWriter(query, scope, _buffer).writeVariables(); _ExpandedVariableWriter(query, scope, _buffer).writeVariables();
} }
/// Returns a Dart string literal representing the query after variables have /// Returns a Dart string literal representing the query after variables have
/// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned /// been expanded. For instance, 'SELECT * FROM t WHERE x IN ?' will be turned
/// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'. /// into 'SELECT * FROM t WHERE x IN ($expandedVar1)'.
String _queryCode() { String _queryCode(SqlQuery query) {
if (scope.options.newSqlCodeGeneration) { if (scope.options.newSqlCodeGeneration) {
return SqlWriter(scope.options, query: query).write(); return SqlWriter(scope.options, query: query).write();
} else { } else {
return _legacyQueryCode(); return _legacyQueryCode(query);
} }
} }
String _legacyQueryCode() { String _legacyQueryCode(SqlQuery query) {
final context = query.fromContext!; final root = query.root!;
final sql = query.fromContext!.sql;
// sort variables and placeholders by the order in which they appear // sort variables and placeholders by the order in which they appear
final toReplace = context.root.allDescendants final toReplace = root.allDescendants
.where((node) => .where((node) =>
node is Variable || node is Variable ||
node is DartPlaceholder || node is DartPlaceholder ||
@ -450,17 +467,17 @@ class QueryWriter {
const <NestedStarResultColumn, NestedResultTable>{}; const <NestedStarResultColumn, NestedResultTable>{};
if (query is SqlSelectQuery) { if (query is SqlSelectQuery) {
doubleStarColumnToResolvedTable = { doubleStarColumnToResolvedTable = {
for (final nestedResult in _select.resultSet.nestedResults) for (final nestedResult in query.resultSet.nestedResults)
nestedResult.from: nestedResult if (nestedResult is NestedResultTable) nestedResult.from: nestedResult
}; };
} }
var lastIndex = context.root.firstPosition; var lastIndex = root.firstPosition;
void replaceNode(AstNode node, String content) { void replaceNode(AstNode node, String content) {
// write everything that comes before this var into the buffer // write everything that comes before this var into the buffer
final currentIndex = node.firstPosition; final currentIndex = node.firstPosition;
final queryPart = context.sql.substring(lastIndex, currentIndex); final queryPart = sql.substring(lastIndex, currentIndex);
buffer.write(escapeForDart(queryPart)); buffer.write(escapeForDart(queryPart));
lastIndex = node.lastPosition; lastIndex = node.lastPosition;
@ -486,7 +503,9 @@ class QueryWriter {
final result = doubleStarColumnToResolvedTable[rewriteTarget]; final result = doubleStarColumnToResolvedTable[rewriteTarget];
if (result == null) continue; if (result == null) continue;
final prefix = _select.resultSet.nestedPrefixFor(result); // weird cast here :O
final prefix =
(query as SqlSelectQuery).resultSet.nestedPrefixFor(result);
final table = rewriteTarget.tableName; final table = rewriteTarget.tableName;
// Convert foo.** to "foo.a" AS "nested_0.a", ... for all columns in foo // Convert foo.** to "foo.a" AS "nested_0.a", ... for all columns in foo
@ -509,40 +528,40 @@ class QueryWriter {
} }
// write the final part after the last variable, plus the ending ' // write the final part after the last variable, plus the ending '
final lastPosition = context.root.lastPosition; final lastPosition = root.lastPosition;
buffer buffer
..write(escapeForDart(context.sql.substring(lastIndex, lastPosition))) ..write(escapeForDart(sql.substring(lastIndex, lastPosition)))
..write("'"); ..write("'");
return buffer.toString(); return buffer.toString();
} }
void _writeReadsFrom() { void _writeReadsFrom(SqlSelectQuery select) {
_buffer.write('readsFrom: {'); _buffer.write('readsFrom: {');
for (final table in _select.readsFromTables) { for (final table in select.readsFromTables) {
_buffer.write('${table.dbGetterName},'); _buffer.write('${table.dbGetterName},');
} }
for (final element in query.elements.whereType<FoundDartPlaceholder>()) { for (final element in select.elements.whereType<FoundDartPlaceholder>()) {
_buffer.write('...${placeholderContextName(element)}.watchedTables,'); _buffer.write('...${placeholderContextName(element)}.watchedTables,');
} }
_buffer.write('}'); _buffer.write('}');
} }
void _writeUpdates() { void _writeUpdates(UpdatingQuery update) {
final from = _update.updates.map((t) => t.table.dbGetterName).join(', '); final from = update.updates.map((t) => t.table.dbGetterName).join(', ');
_buffer _buffer
..write('updates: {') ..write('updates: {')
..write(from) ..write(from)
..write('}'); ..write('}');
} }
void _writeUpdateKind() { void _writeUpdateKind(UpdatingQuery update) {
if (_update.isOnlyDelete) { if (update.isOnlyDelete) {
_buffer.write(', updateKind: UpdateKind.delete'); _buffer.write(', updateKind: UpdateKind.delete');
} else if (_update.isOnlyUpdate) { } else if (update.isOnlyUpdate) {
_buffer.write(', updateKind: UpdateKind.update'); _buffer.write(', updateKind: UpdateKind.update');
} }
} }

View File

@ -39,17 +39,32 @@ class ResultSetWriter {
} }
for (final nested in resultSet.nestedResults) { for (final nested in resultSet.nestedResults) {
var typeName = nested.table.dartTypeCode(scope.generationOptions); if (nested is NestedResultTable) {
final fieldName = nested.dartFieldName; var typeName = nested.table.dartTypeCode(scope.generationOptions);
final fieldName = nested.dartFieldName;
if (nested.isNullable) { if (nested.isNullable) {
typeName = scope.nullableType(typeName); typeName = scope.nullableType(typeName);
}
into.write('$modifier $typeName $fieldName;\n');
fieldNames.add(fieldName);
if (!nested.isNullable) nonNullableFields.add(fieldName);
} else if (nested is NestedResultQuery) {
final nestedPrefix = resultSet.nestedPrefixFor(nested);
final fieldName = nested.filedName(nestedPrefix);
final typeName = nested.resultTypeCode(className);
if (nested.query.resultSet.needsOwnClass) {
ResultSetWriter(nested.query, scope).write();
}
into.write('$modifier List<$typeName> $fieldName;\n');
fieldNames.add(fieldName);
nonNullableFields.add(fieldName);
} }
into.write('$modifier $typeName $fieldName;\n');
fieldNames.add(fieldName);
if (!nested.isNullable) nonNullableFields.add(fieldName);
} }
// write the constructor // write the constructor

View File

@ -46,7 +46,7 @@ class SqlWriter extends NodeSqlBuilder {
if (query is SqlSelectQuery) { if (query is SqlSelectQuery) {
doubleStarColumnToResolvedTable = { doubleStarColumnToResolvedTable = {
for (final nestedResult in query.resultSet.nestedResults) for (final nestedResult in query.resultSet.nestedResults)
nestedResult.from: nestedResult if (nestedResult is NestedResultTable) nestedResult.from: nestedResult
}; };
} }
return SqlWriter._(query, options, doubleStarColumnToResolvedTable, return SqlWriter._(query, options, doubleStarColumnToResolvedTable,
@ -54,7 +54,7 @@ class SqlWriter extends NodeSqlBuilder {
} }
String write() { String write() {
return writeNodeIntoStringLiteral(query!.fromContext!.root); return writeNodeIntoStringLiteral(query!.root!);
} }
String writeNodeIntoStringLiteral(AstNode node) { String writeNodeIntoStringLiteral(AstNode node) {

View File

@ -17,7 +17,7 @@ void main() {
test('warns when a result column is unresolved', () { test('warns when a result column is unresolved', () {
final result = engine.analyze('SELECT ?;'); final result = engine.analyze('SELECT ?;');
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle(); final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
expect(moorQuery.lints, expect(moorQuery.lints,
anyElement((AnalysisError q) => q.message.contains('unknown type'))); anyElement((AnalysisError q) => q.message.contains('unknown type')));
@ -25,7 +25,7 @@ void main() {
test('warns when the result depends on a Dart template', () { test('warns when the result depends on a Dart template', () {
final result = engine.analyze(r"SELECT 'string' = $expr;"); final result = engine.analyze(r"SELECT 'string' = $expr;");
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle(); final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
expect(moorQuery.lints, expect(moorQuery.lints,
anyElement((AnalysisError q) => q.message.contains('Dart template'))); anyElement((AnalysisError q) => q.message.contains('Dart template')));
@ -33,7 +33,7 @@ void main() {
test('warns when nested results refer to table-valued functions', () { test('warns when nested results refer to table-valued functions', () {
final result = engine.analyze("SELECT json_each.** FROM json_each('')"); final result = engine.analyze("SELECT json_each.** FROM json_each('')");
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle(); final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
expect( expect(
moorQuery.lints, moorQuery.lints,
@ -95,7 +95,7 @@ in: INSERT INTO foo (id) $placeholder;
group('warns about wrong types in subexpressions', () { group('warns about wrong types in subexpressions', () {
test('strings in arithmetic', () { test('strings in arithmetic', () {
final result = engine.analyze("SELECT 'foo' + 3;"); final result = engine.analyze("SELECT 'foo' + 3;");
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle(); final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
expect( expect(
moorQuery.lints, moorQuery.lints,
@ -106,14 +106,14 @@ in: INSERT INTO foo (id) $placeholder;
test('allows numerics in arithmetic', () { test('allows numerics in arithmetic', () {
final result = engine.analyze('SELECT 3.6 * 3;'); final result = engine.analyze('SELECT 3.6 * 3;');
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle(); final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
expect(moorQuery.lints, isEmpty); expect(moorQuery.lints, isEmpty);
}); });
test('real in binary', () { test('real in binary', () {
final result = engine.analyze('SELECT 3.5 | 3;'); final result = engine.analyze('SELECT 3.5 | 3;');
final moorQuery = QueryHandler(fakeQuery, result, mapper).handle(); final moorQuery = QueryHandler(result, mapper).handle(fakeQuery);
expect( expect(
moorQuery.lints, moorQuery.lints,

View File

@ -43,7 +43,7 @@ Future<void> main() async {
SqlQuery parse(String sql) { SqlQuery parse(String sql) {
final parsed = engine.analyze(sql); final parsed = engine.analyze(sql);
final fakeQuery = DeclaredDartQuery('query', sql); final fakeQuery = DeclaredDartQuery('query', sql);
return QueryHandler(fakeQuery, parsed, mapper).handle(); return QueryHandler(parsed, mapper).handle(fakeQuery);
} }
group('detects whether multiple tables are referenced', () { group('detects whether multiple tables are referenced', () {
@ -105,9 +105,16 @@ FROM routes
expect(resultSet.columns.map((e) => e.name), ['id', 'from', 'to']); expect(resultSet.columns.map((e) => e.name), ['id', 'from', 'to']);
expect(resultSet.matchingTable, isNull); expect(resultSet.matchingTable, isNull);
expect(resultSet.nestedResults.map((e) => e.name), ['from', 'to']); expect(
expect(resultSet.nestedResults.map((e) => e.table.displayName), resultSet.nestedResults.cast<NestedResultTable>().map((e) => e.name),
['points', 'points']); ['from', 'to'],
);
expect(
resultSet.nestedResults
.cast<NestedResultTable>()
.map((e) => e.table.displayName),
['points', 'points'],
);
}); });
test('resolves nullability of aliases in nested result sets', () async { test('resolves nullability of aliases in nested result sets', () async {
@ -144,9 +151,14 @@ LEFT JOIN tableB1 AS tableB2 -- nullable
final resultSet = (query as SqlSelectQuery).resultSet; final resultSet = (query as SqlSelectQuery).resultSet;
final nested = resultSet.nestedResults; final nested = resultSet.nestedResults;
expect(nested.map((e) => e.name), expect(
['tableA1', 'tableA2', 'tableB1', 'tableB2']); nested.cast<NestedResultTable>().map((e) => e.name),
expect(nested.map((e) => e.isNullable), [false, true, false, true]); ['tableA1', 'tableA2', 'tableB1', 'tableB2'],
);
expect(
nested.cast<NestedResultTable>().map((e) => e.isNullable),
[false, true, false, true],
);
}); });
test('infers result set for views', () async { test('infers result set for views', () async {

View File

@ -24,7 +24,7 @@ void main() {
final writer = Writer( final writer = Writer(
const MoorOptions.defaults(generateNamedParameters: true), const MoorOptions.defaults(generateNamedParameters: true),
generationOptions: const GenerationOptions(nnbd: true)); generationOptions: const GenerationOptions(nnbd: true));
QueryWriter(fileState.resolvedQueries!.single, writer.child()).write(); QueryWriter(writer.child()).write(fileState.resolvedQueries!.single);
expect(writer.writeGenerated(), contains('required List<int?> idList')); expect(writer.writeGenerated(), contains('required List<int?> idList'));
}); });
@ -47,7 +47,7 @@ void main() {
final writer = Writer( final writer = Writer(
const MoorOptions.defaults(newSqlCodeGeneration: true), const MoorOptions.defaults(newSqlCodeGeneration: true),
generationOptions: const GenerationOptions(nnbd: true)); generationOptions: const GenerationOptions(nnbd: true));
QueryWriter(fileState.resolvedQueries!.single, writer.child()).write(); QueryWriter(writer.child()).write(fileState.resolvedQueries!.single);
expect( expect(
writer.writeGenerated(), writer.writeGenerated(),
@ -76,7 +76,7 @@ void main() {
final writer = Writer( final writer = Writer(
const MoorOptions.defaults(newSqlCodeGeneration: true), const MoorOptions.defaults(newSqlCodeGeneration: true),
generationOptions: const GenerationOptions(nnbd: true)); generationOptions: const GenerationOptions(nnbd: true));
QueryWriter(fileState.resolvedQueries!.single, writer.child()).write(); QueryWriter(writer.child()).write(fileState.resolvedQueries!.single);
expect( expect(
writer.writeGenerated(), writer.writeGenerated(),
@ -116,7 +116,7 @@ void main() {
options, options,
generationOptions: const GenerationOptions(nnbd: true), generationOptions: const GenerationOptions(nnbd: true),
); );
QueryWriter(fileState.resolvedQueries!.single, writer.child()).write(); QueryWriter(writer.child()).write(fileState.resolvedQueries!.single);
expect(writer.writeGenerated(), expectation); expect(writer.writeGenerated(), expectation);
} }

View File

@ -9,8 +9,8 @@ void main() {
void check(String sql, String expectedDart) { void check(String sql, String expectedDart) {
final engine = SqlEngine(); final engine = SqlEngine();
final context = engine.analyze(sql); final context = engine.analyze(sql);
final query = SqlSelectQuery( final query = SqlSelectQuery('name', context, context.root, [], [],
'name', context, [], [], InferredResultSet(null, []), null); InferredResultSet(null, []), null);
final result = final result =
SqlWriter(const MoorOptions.defaults(), query: query).write(); SqlWriter(const MoorOptions.defaults(), query: query).write();

View File

@ -1,5 +1,6 @@
import '../../../sqlparser.dart'; import '../../analysis/analysis.dart';
import '../ast.dart' show ResultColumn, Renamable; import '../ast.dart'
show StarResultColumn, ResultColumn, Renamable, SelectStatement;
import '../node.dart'; import '../node.dart';
import '../visitor.dart'; import '../visitor.dart';
import 'moor_file.dart'; import 'moor_file.dart';