Option to generate functions for Dart placeholders

This commit is contained in:
Simon Binder 2021-06-04 18:28:03 +02:00
parent e87e4d7a7a
commit eb362effe8
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
15 changed files with 255 additions and 34 deletions

View File

@ -13,6 +13,7 @@ targets:
generate_values_in_copy_with: true
named_parameters: true
new_sql_code_generation: true
scoped_dart_components: true
sqlite:
version: "3.35"
modules:

View File

@ -1650,11 +1650,11 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
}
Selectable<Config> readMultiple(List<String> var1,
{OrderBy clause = const OrderBy.nothing()}) {
{OrderBy Function(ConfigTable config) clause = _$moor$default$0}) {
var $arrayStartIndex = 1;
final expandedvar1 = $expandVar($arrayStartIndex, var1.length);
$arrayStartIndex += var1.length;
final generatedclause = $write(clause);
final generatedclause = $write(clause(this.config));
$arrayStartIndex += generatedclause.amountOfVariables;
return customSelect(
'SELECT * FROM config WHERE config_key IN ($expandedvar1) ${generatedclause.sql}',
@ -1668,17 +1668,18 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
}
Selectable<Config> readDynamic(
{Expression<bool> predicate = const CustomExpression('(TRUE)')}) {
final generatedpredicate = $write(predicate);
{Expression<bool> Function(ConfigTable config) predicate =
_$moor$default$1}) {
final generatedpredicate = $write(predicate(this.config));
return customSelect('SELECT * FROM config WHERE ${generatedpredicate.sql}',
variables: [...generatedpredicate.introducedVariables],
readsFrom: {config}).map(config.mapFromRow);
}
Selectable<String> typeConverterVar(SyncType? var1, List<SyncType?> var2,
{Expression<bool> pred = const CustomExpression('(TRUE)')}) {
{Expression<bool> Function(ConfigTable config) pred = _$moor$default$2}) {
var $arrayStartIndex = 2;
final generatedpred = $write(pred);
final generatedpred = $write(pred(this.config));
$arrayStartIndex += generatedpred.amountOfVariables;
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
$arrayStartIndex += var2.length;
@ -1721,8 +1722,13 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
});
}
Selectable<MultipleResult> multiple({required Expression<bool> predicate}) {
final generatedpredicate = $write(predicate, hasMultipleTables: true);
Selectable<MultipleResult> multiple(
{required Expression<bool> Function(WithDefaults d, WithConstraints c)
predicate}) {
final generatedpredicate = $write(
predicate(
alias(this.withDefaults, 'd'), alias(this.withConstraints, 'c')),
hasMultipleTables: true);
return customSelect(
'SELECT d.*,"c"."a" AS "nested_0.a", "c"."b" AS "nested_0.b", "c"."c" AS "nested_0.c" FROM with_defaults AS d LEFT OUTER JOIN with_constraints AS c ON d.a = c.a AND d.b = c.b WHERE ${generatedpredicate.sql}',
variables: [...generatedpredicate.introducedVariables],
@ -1743,8 +1749,9 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
readsFrom: {email}).map(email.mapFromRow);
}
Selectable<ReadRowIdResult> readRowId({required Expression<int> expr}) {
final generatedexpr = $write(expr);
Selectable<ReadRowIdResult> readRowId(
{required Expression<int> Function(ConfigTable config) expr}) {
final generatedexpr = $write(expr(this.config));
return customSelect(
'SELECT oid, * FROM config WHERE _rowid_ = ${generatedexpr.sql}',
variables: [...generatedexpr.introducedVariables],
@ -1837,6 +1844,12 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
);
}
OrderBy _$moor$default$0(ConfigTable _) => const OrderBy.nothing();
Expression<bool> _$moor$default$1(ConfigTable _) =>
const CustomExpression('(TRUE)');
Expression<bool> _$moor$default$2(ConfigTable _) =>
const CustomExpression('(TRUE)');
class JsonResult extends CustomResultSet {
final String key;
final String? value;

View File

@ -8,7 +8,7 @@ CREATE TABLE no_ids (
CREATE TABLE with_defaults (
a TEXT DEFAULT 'something',
b INT UNIQUE
) ;
);
CREATE TABLE with_constraints (
a TEXT,

View File

@ -103,7 +103,8 @@ void main() {
test('runs queries with arrays and Dart templates', () async {
await db.readMultiple(['a', 'b'],
clause: OrderBy([OrderingTerm(expression: db.config.configKey)])).get();
clause: (config) =>
OrderBy([OrderingTerm(expression: config.configKey)])).get();
verify(mock.runSelect(
'SELECT * FROM config WHERE config_key IN (?1, ?2) '
@ -118,7 +119,7 @@ void main() {
.thenAnswer((_) => Future.value([mockResponse]));
final parsed = await db
.readDynamic(predicate: db.config.configKey.equals('key'))
.readDynamic(predicate: (config) => config.configKey.equals('key'))
.getSingle();
verify(
@ -133,9 +134,9 @@ void main() {
});
test('columns use table names in queries with multiple tables', () async {
await db.multiple(predicate: db.withDefaults.a.equals('foo')).get();
await db.multiple(predicate: (d, c) => d.a.equals('foo')).get();
verify(mock.runSelect(argThat(contains('with_defaults.a')), any));
verify(mock.runSelect(argThat(contains('d.a = ?')), any));
});
test('order by-params are ignored by default', () async {
@ -156,8 +157,9 @@ void main() {
return Future.value([row]);
});
final result =
await db.multiple(predicate: const Constant(true)).getSingle();
final result = await db
.multiple(predicate: (_, __) => const Constant(true))
.getSingle();
expect(
result,
@ -183,8 +185,9 @@ void main() {
return Future.value([row]);
});
final result =
await db.multiple(predicate: const Constant(true)).getSingle();
final result = await db
.multiple(predicate: (_, __) => const Constant(true))
.getSingle();
expect(
result,

View File

@ -91,6 +91,9 @@ class MoorOptions {
@JsonKey(name: 'new_sql_code_generation', defaultValue: false)
final bool newSqlCodeGeneration;
@JsonKey(name: 'scoped_dart_components', defaultValue: false)
final bool scopedDartComponents;
@internal
const MoorOptions.defaults({
this.generateFromJsonStringConstructor = false,
@ -108,6 +111,7 @@ class MoorOptions {
this.generateValuesInCopyWith = false,
this.generateNamedParameters = false,
this.newSqlCodeGeneration = false,
this.scopedDartComponents = false,
this.modules = const [],
this.sqliteAnalysisOptions,
});
@ -128,6 +132,7 @@ class MoorOptions {
required this.generateValuesInCopyWith,
required this.generateNamedParameters,
required this.newSqlCodeGeneration,
required this.scopedDartComponents,
required this.modules,
required this.sqliteAnalysisOptions,
}) {

View File

@ -25,7 +25,8 @@ MoorOptions _$MoorOptionsFromJson(Map json) {
'apply_converters_on_variables',
'generate_values_in_copy_with',
'named_parameters',
'new_sql_code_generation'
'new_sql_code_generation',
'scoped_dart_components'
]);
final val = MoorOptions(
generateFromJsonStringConstructor: $checkedConvert(
@ -73,6 +74,9 @@ MoorOptions _$MoorOptionsFromJson(Map json) {
newSqlCodeGeneration:
$checkedConvert(json, 'new_sql_code_generation', (v) => v as bool?) ??
false,
scopedDartComponents:
$checkedConvert(json, 'scoped_dart_components', (v) => v as bool?) ??
false,
modules: $checkedConvert(
json,
'sqlite_modules',
@ -102,6 +106,7 @@ MoorOptions _$MoorOptionsFromJson(Map json) {
'generateValuesInCopyWith': 'generate_values_in_copy_with',
'generateNamedParameters': 'named_parameters',
'newSqlCodeGeneration': 'new_sql_code_generation',
'scopedDartComponents': 'scoped_dart_components',
'modules': 'sqlite_modules',
'sqliteAnalysisOptions': 'sqlite'
});

View File

@ -278,7 +278,38 @@ class TypeMapper {
},
);
return FoundDartPlaceholder(type, name)..astNode = placeholder;
final availableResults =
placeholder.scope.allOf<ResultSetAvailableInStatement>();
final availableMoorResults = <AvailableMoorResultSet>[];
for (final available in availableResults) {
final aliasedResultSet = available.resultSet.resultSet;
final resultSet = aliasedResultSet?.unalias();
String name;
if (aliasedResultSet is NamedResultSet) {
name = aliasedResultSet.name;
} else {
// If we don't have a name we can't include this result set.
continue;
}
MoorEntityWithResultSet moorEntity;
if (resultSet is Table) {
moorEntity = tableToMoor(resultSet);
} else if (resultSet is View) {
moorEntity = viewToMoor(resultSet);
} else {
// If this result set is an inner select statement or anything else we
// can't represent it in Dart.
continue;
}
availableMoorResults
.add(AvailableMoorResultSet(name, moorEntity, available));
}
return FoundDartPlaceholder(type, name, availableMoorResults)
..astNode = placeholder;
}
MoorTable tableToMoor(Table table) {

View File

@ -36,4 +36,9 @@ abstract class MoorEntityWithResultSet extends MoorSchemaEntity {
/// The name of the Dart class storing additional properties like type
/// converters.
String get entityInfoName;
/// The name of the Dart class storing the right column getters for this type.
///
/// This class is equal to, or a superclass of, [entityInfoName].
String get dslName => entityInfoName;
}

View File

@ -2,6 +2,7 @@
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:moor/moor.dart' show $mrjf, $mrjc, UpdateKind;
import 'package:moor_generator/src/analyzer/options.dart';
import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:moor_generator/src/model/base_entity.dart';
import 'package:moor_generator/src/utils/hash.dart';
@ -599,10 +600,41 @@ class InsertableDartPlaceholderType extends DartPlaceholderType {
}
}
/// A Dart placeholder that will be bound at runtime.
/// A Dart placeholder that will be bound to a dynamically-generated SQL node
/// at runtime.
///
/// Moor supports injecting expressions, order by terms and clauses and limit
/// clauses as placeholders. For insert statements, companions can be used
/// as a Dart placeholder too.
class FoundDartPlaceholder extends FoundElement {
final DartPlaceholderType type;
/// All result sets that are available for this Dart placeholder.
///
/// When queries are operating on multiple tables, especially if some of those
/// tables have aliases, it may be hard to reflect the name of those tables
/// at runtime.
/// For instance, consider this query:
///
/// ```sql
/// myQuery: SELECT a.**, b.** FROM users a
/// INNER JOIN friends f ON f.a_id = a.id
/// INNER JOIN users b ON b.id = f.b_id
/// WHERE $expression;
/// ```
///
/// Here `$expression` is a Dart-defined expression evaluating to an sql
/// boolean.
/// Moor uses to add a `Expression<bool>` parameter to the generated query
/// method. Unfortunately, this puts the burden of picking the right table
/// name on the user. For instance, they may have to use
/// `alias('a', users).someColumn` to avoid getting an runtime exception.
/// With a new build option, moor instead generates a
/// `Expression<bool> Function(Users a, Users b, Friends f)` function as a
/// parameter. This allows users to access the right aliases right away,
/// reducing potential for misuse.
final List<AvailableMoorResultSet> availableResultSets;
@override
final String name;
DartPlaceholder astNode;
@ -611,26 +643,67 @@ class FoundDartPlaceholder extends FoundElement {
type is ExpressionDartPlaceholderType &&
(type as ExpressionDartPlaceholderType).defaultValue != null;
FoundDartPlaceholder(this.type, this.name);
FoundDartPlaceholder(this.type, this.name, this.availableResultSets);
@override
String get dartParameterName => name;
@override
int get hashCode => hashAll([type, name]);
int get hashCode => hashAll([type, name, ...availableResultSets]);
@override
bool operator ==(dynamic other) {
return identical(this, other) ||
other is FoundDartPlaceholder &&
other.type == type &&
other.name == name;
other.name == name &&
const ListEquality()
.equals(other.availableResultSets, availableResultSets);
}
@override
String dartTypeCode([GenerationOptions options = const GenerationOptions()]) {
return type.parameterTypeCode(options);
}
/// Whether we should write this parameter as a function having available
/// result sets as parameters.
bool writeAsScopedFunction(MoorOptions options) {
return options.scopedDartComponents &&
availableResultSets.isNotEmpty &&
// Don't generate scoped functions for insertables, where the Dart type
// already defines which fields are available
type is! InsertableDartPlaceholderType;
}
}
/// A table or view that is available in the position of a
/// [FoundDartPlaceholder].
///
/// For more information, see [FoundDartPlaceholder.availableResultSets].
class AvailableMoorResultSet {
/// The (potentially aliased) name of this result set.
final String name;
/// The table or view that is available.
final MoorEntityWithResultSet entity;
final ResultSetAvailableInStatement source;
AvailableMoorResultSet(this.name, this.entity, [this.source]);
/// The argument type of this result set when used in a scoped function.
String get argumentType => entity.dslName;
@override
int get hashCode => hashAll([name, entity]);
@override
bool operator ==(Object other) {
return other is AvailableMoorResultSet &&
other.name == name &&
other.entity == entity;
}
}
class _ResultColumnEquality implements Equality<ResultColumn> {

View File

@ -41,6 +41,9 @@ class MoorTable implements MoorEntityWithResultSet {
String get _baseName => _overriddenName ?? fromClass.name;
@override
String get dslName => fromClass?.name ?? entityInfoName;
/// The columns declared in this table.
@override
final List<MoorColumn> columns;

View File

@ -281,6 +281,22 @@ class QueryWriter {
void _writeParameters() {
final namedElements = <FoundElement>[];
String typeFor(FoundElement element) {
var type = element.dartTypeCode(scope.generationOptions);
if (element is FoundDartPlaceholder &&
element.writeAsScopedFunction(options)) {
// Generate a function providing result sets that are in scope as args
final args = element.availableResultSets
.map((e) => '${e.argumentType} ${e.name}')
.join(', ');
type = '$type Function($args)';
}
return type;
}
var needsComma = false;
for (final element in query.elements) {
// Placeholders with a default value generate optional (and thus, named)
@ -294,7 +310,7 @@ class QueryWriter {
} else {
if (needsComma) _buffer.write(', ');
final type = element.dartTypeCode(scope.generationOptions);
final type = typeFor(element);
_buffer.write('$type ${element.dartParameterName}');
needsComma = true;
}
@ -325,9 +341,37 @@ class QueryWriter {
kind.kind == SimpleDartPlaceholderKind.orderBy) {
defaultCode = 'const OrderBy.nothing()';
}
// If the parameter is converted to a scoped function, we also need to
// generate a scoped function as a default value. Since defaults have
// to be constants, we generate a top-level function which is then
// used as a tear-off.
if (optional.writeAsScopedFunction(options) && defaultCode != null) {
final root = scope.root;
final counter = root.counter++;
// ignore: prefer_interpolation_to_compose_strings
final functionName = r'_$moor$default$' + counter.toString();
final buffer = root.leaf()
..write(optional.dartTypeCode(scope.generationOptions))
..write(' ')
..write(functionName)
..write('(');
var i = 0;
for (final arg in optional.availableResultSets) {
if (i != 0) buffer.write(', ');
buffer..write(arg.argumentType)..write(' ')..write('_' * (i + 1));
}
buffer..write(') => ')..write(defaultCode)..write(';');
// With the function being written, the default code is just a tear-
// off of that function
defaultCode = functionName;
}
}
final type = optional.dartTypeCode(scope.generationOptions);
final type = typeFor(optional);
// No default value, this element is required if it's not nullable
var isMarkedAsRequired = false;
@ -436,6 +480,26 @@ class QueryWriter {
} else if (element is FoundDartPlaceholder) {
_writeIndexCounterIfNeeded();
String useExpression() {
if (element.writeAsScopedFunction(scope.options)) {
// The parameter is a function type that needs to be evaluated first
final args = element.availableResultSets.map((e) {
final table = 'this.${e.entity.dbGetterName}';
final needsAlias = e.name != e.entity.displayName;
if (needsAlias) {
return 'alias($table, ${asDartLiteral(e.name)})';
} else {
return table;
}
}).join(', ');
return '${element.dartParameterName}($args)';
} else {
// We can just use the parameter directly
return element.dartParameterName;
}
}
_buffer
..write('final ')
..write(placeholderContextName(element))
@ -449,10 +513,10 @@ class QueryWriter {
..write(r'$writeInsertable(this.')
..write(table?.dbGetterName)
..write(', ')
..write(element.dartParameterName)
..write(useExpression())
..write(');\n');
} else {
_buffer..write(r'$write(')..write(element.dartParameterName);
_buffer..write(r'$write(')..write(useExpression());
if (query.hasMultipleTables) {
_buffer.write(', hasMultipleTables: true');
}

View File

@ -54,6 +54,11 @@ class Scope extends _Node {
final DartScope scope;
final Writer writer;
/// An arbitrary counter.
///
/// This can be used to generated methods which must have a unique name-
int counter = 0;
Scope({@required Scope parent, Writer writer})
: scope = parent?.scope?.nextLevel ?? DartScope.library,
writer = writer ?? parent?.writer,

View File

@ -96,10 +96,23 @@ class ProgrammingLanguages extends Table {
expect(importQuery.declaredInMoorFile, isFalse);
expect(importQuery.hasMultipleTables, isFalse);
expect(
importQuery.placeholders,
contains(equals(FoundDartPlaceholder(
importQuery.placeholders,
contains(
equals(
FoundDartPlaceholder(
SimpleDartPlaceholderType(SimpleDartPlaceholderKind.orderBy),
'o'))));
'o',
[
AvailableMoorResultSet(
'programming_languages',
database.tables
.firstWhere((e) => e.sqlName == 'programming_languages'),
)
],
),
),
),
);
final librariesQuery = database.queries
.singleWhere((q) => q.name == 'findLibraries') as SqlSelectQuery;

View File

@ -34,7 +34,7 @@ totalDurationByArtist:
FROM artists a
INNER JOIN albums ON albums.artist = a.id
INNER JOIN tracks ON tracks.album = albums.id
GROUP BY artists.id;
GROUP BY a.id;
'''
}, options: const MoorOptions.defaults());

View File

@ -88,7 +88,7 @@ CREATE TABLE routes (
);
allRoutes: SELECT routes.*, "from".**, "to".**
FROM routes r
FROM routes
INNER JOIN points "from" ON "from".id = routes.from
INNER JOIN points "to" ON "to".id = routes."to";
''',