mirror of https://github.com/AMT-Cheif/drift.git
Write implementation for Dart placeholders
This commit is contained in:
parent
bf7c9feddf
commit
349b245089
|
@ -28,8 +28,16 @@ class GenerationContext {
|
|||
final QueryExecutor executor;
|
||||
|
||||
final List<dynamic> _boundVariables = [];
|
||||
|
||||
/// The values of [introducedVariables] that will be sent to the underlying
|
||||
/// engine.
|
||||
List<dynamic> get boundVariables => _boundVariables;
|
||||
|
||||
/// All variables ("?" in sql) that were added to this context.
|
||||
final List<Variable> introducedVariables = [];
|
||||
|
||||
int get amountOfVariables => boundVariables.length;
|
||||
|
||||
/// The string buffer contains the sql query as it's being constructed.
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
|
||||
|
@ -49,7 +57,8 @@ class GenerationContext {
|
|||
/// that the prepared statement can be executed with the variable. The value
|
||||
/// must be a type that is supported by the sqflite library. A list of
|
||||
/// supported types can be found [here](https://github.com/tekartik/sqflite#supported-sqlite-types).
|
||||
void introduceVariable(dynamic value) {
|
||||
void introduceVariable(Variable v, dynamic value) {
|
||||
introducedVariables.add(v);
|
||||
_boundVariables.add(value);
|
||||
}
|
||||
|
||||
|
|
|
@ -44,10 +44,12 @@ class OrderBy extends Component {
|
|||
OrderBy(this.terms);
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
void writeInto(GenerationContext context, {bool writeOrderBy = true}) {
|
||||
var first = true;
|
||||
|
||||
context.buffer.write('ORDER BY ');
|
||||
if (writeOrderBy) {
|
||||
context.buffer.write('ORDER BY ');
|
||||
}
|
||||
|
||||
for (var term in terms) {
|
||||
if (first) {
|
||||
|
|
|
@ -320,6 +320,22 @@ mixin QueryEngine on DatabaseConnectionUser {
|
|||
QueryEngine engine, Future<T> Function() calculation) {
|
||||
return runZoned(calculation, zoneValues: {_zoneRootUserKey: engine});
|
||||
}
|
||||
|
||||
/// Will be used by generated code to resolve inline Dart expressions in sql.
|
||||
@protected
|
||||
GenerationContext $write(Component component) {
|
||||
final context = GenerationContext.fromDb(this);
|
||||
|
||||
// we don't want ORDER BY clauses to write the ORDER BY tokens because those
|
||||
// are already declared in sql
|
||||
if (component is OrderBy) {
|
||||
component.writeInto(context, writeOrderBy: false);
|
||||
} else {
|
||||
component.writeInto(context);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
/// A base class for all generated databases.
|
||||
|
|
|
@ -45,12 +45,18 @@ class Variable<T, S extends SqlType<T>> extends Expression<T, S> {
|
|||
/// database engine. For instance, a [DateTime] will me mapped to its unix
|
||||
/// timestamp.
|
||||
dynamic mapToSimpleValue(GenerationContext context) {
|
||||
return _mapToSimpleValue(context, value);
|
||||
final type = context.typeSystem.forDartType<T>();
|
||||
return type.mapToSqlVariable(value);
|
||||
}
|
||||
|
||||
@override
|
||||
void writeInto(GenerationContext context) {
|
||||
_writeVariableIntoContext(context, value);
|
||||
if (value != null) {
|
||||
context.buffer.write('?');
|
||||
context.introduceVariable(this, mapToSimpleValue(context));
|
||||
} else {
|
||||
context.buffer.write('NULL');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,17 +77,3 @@ class Constant<T, S extends SqlType<T>> extends Expression<T, S> {
|
|||
context.buffer.write(type.mapToSqlConstant(value));
|
||||
}
|
||||
}
|
||||
|
||||
void _writeVariableIntoContext<T>(GenerationContext context, T value) {
|
||||
if (value != null) {
|
||||
context.buffer.write('?');
|
||||
context.introduceVariable(_mapToSimpleValue<T>(context, value));
|
||||
} else {
|
||||
context.buffer.write('NULL');
|
||||
}
|
||||
}
|
||||
|
||||
dynamic _mapToSimpleValue<T>(GenerationContext context, T value) {
|
||||
final type = context.typeSystem.forDartType<T>();
|
||||
return type.mapToSqlVariable(value);
|
||||
}
|
||||
|
|
|
@ -830,11 +830,13 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
|
|||
}
|
||||
|
||||
Selectable<ConfigData> readMultiple(List<String> var1, OrderBy clause) {
|
||||
var $highestIndex = 1;
|
||||
final expandedvar1 = $expandVar($highestIndex, var1.length);
|
||||
$highestIndex += var1.length;
|
||||
var $arrayStartIndex = 1;
|
||||
final expandedvar1 = $expandVar($arrayStartIndex, var1.length);
|
||||
$arrayStartIndex += var1.length;
|
||||
final generatedclause = $write(clause);
|
||||
$arrayStartIndex += generatedclause.amountOfVariables;
|
||||
return customSelectQuery(
|
||||
'SELECT * FROM config WHERE config_key IN ($expandedvar1) ORDER BY \$clause',
|
||||
'SELECT * FROM config WHERE config_key IN ($expandedvar1) ORDER BY ${generatedclause.sql}',
|
||||
variables: [
|
||||
for (var $ in var1) Variable.withString($),
|
||||
],
|
||||
|
|
|
@ -1351,9 +1351,9 @@ abstract class _$TodoDb extends GeneratedDatabase {
|
|||
}
|
||||
|
||||
Selectable<TodoEntry> withInQuery(String var1, String var2, List<int> var3) {
|
||||
var $highestIndex = 3;
|
||||
final expandedvar3 = $expandVar($highestIndex, var3.length);
|
||||
$highestIndex += var3.length;
|
||||
var $arrayStartIndex = 3;
|
||||
final expandedvar3 = $expandVar($arrayStartIndex, var3.length);
|
||||
$arrayStartIndex += var3.length;
|
||||
return customSelectQuery(
|
||||
'SELECT * FROM todos WHERE title = ?2 OR id IN ($expandedvar3) OR title = ?1',
|
||||
variables: [
|
||||
|
|
|
@ -16,16 +16,16 @@ class QueryHandler {
|
|||
final TypeMapper mapper;
|
||||
|
||||
Set<Table> _foundTables;
|
||||
List<FoundVariable> _foundVariables;
|
||||
List<FoundDartPlaceholder> _foundPlaceholders;
|
||||
List<FoundElement> _foundElements;
|
||||
Iterable<FoundVariable> get _foundVariables =>
|
||||
_foundElements.whereType<FoundVariable>();
|
||||
|
||||
SelectStatement get _select => context.root as SelectStatement;
|
||||
|
||||
QueryHandler(this.name, this.context, this.mapper);
|
||||
|
||||
SqlQuery handle() {
|
||||
_foundVariables = mapper.extractVariables(context);
|
||||
_foundPlaceholders = mapper.extractPlaceholders(context);
|
||||
_foundElements = mapper.extractElements(context);
|
||||
|
||||
_verifyNoSkippedIndexes();
|
||||
final query = _mapToMoor();
|
||||
|
@ -58,7 +58,7 @@ class QueryHandler {
|
|||
|
||||
final isInsert = context.root is InsertStatement;
|
||||
|
||||
return UpdatingQuery(name, context, _foundVariables, _foundPlaceholders,
|
||||
return UpdatingQuery(name, context, _foundElements,
|
||||
_foundTables.map(mapper.tableToMoor).toList(),
|
||||
isInsert: isInsert);
|
||||
}
|
||||
|
@ -69,8 +69,8 @@ class QueryHandler {
|
|||
_foundTables = tableFinder.foundTables;
|
||||
final moorTables = _foundTables.map(mapper.tableToMoor).toList();
|
||||
|
||||
return SqlSelectQuery(name, context, _foundVariables, _foundPlaceholders,
|
||||
moorTables, _inferResultSet());
|
||||
return SqlSelectQuery(
|
||||
name, context, _foundElements, moorTables, _inferResultSet());
|
||||
}
|
||||
|
||||
InferredResultSet _inferResultSet() {
|
||||
|
|
|
@ -72,82 +72,141 @@ class TypeMapper {
|
|||
throw StateError('Unexpected type: $type');
|
||||
}
|
||||
|
||||
List<FoundVariable> extractVariables(AnalysisContext ctx) {
|
||||
/// Extracts variables and Dart templates from the [ctx]. Variables are
|
||||
/// sorted by their ascending index. Placeholders are sorted by the position
|
||||
/// they have in the query. When comparing variables and placeholders, the
|
||||
/// variable comes first if the first variable with the same index appears
|
||||
/// before the placeholder.
|
||||
///
|
||||
/// Additionally, the following assumptions can be made if this method returns
|
||||
/// without throwing:
|
||||
/// - array variables don't have an explicit index
|
||||
/// - if an explicitly indexed variable appears AFTER an array variable or
|
||||
/// a Dart placeholder, its indexed is LOWER than that element. This means
|
||||
/// that elements can be expanded into multiple variables without breaking
|
||||
/// variables that appear after them.
|
||||
List<FoundElement> extractElements(AnalysisContext ctx) {
|
||||
// this contains variable references. For instance, SELECT :a = :a would
|
||||
// contain two entries, both referring to the same variable. To do that,
|
||||
// we use the fact that each variable has a unique index.
|
||||
final usedVars = ctx.root.allDescendants.whereType<Variable>().toList()
|
||||
..sort((a, b) => a.resolvedIndex.compareTo(b.resolvedIndex));
|
||||
final variables = ctx.root.allDescendants.whereType<Variable>().toList();
|
||||
final placeholders =
|
||||
ctx.root.allDescendants.whereType<DartPlaceholder>().toList();
|
||||
|
||||
final foundVariables = <FoundVariable>[];
|
||||
final merged = _mergeVarsAndPlaceholders(variables, placeholders);
|
||||
|
||||
final foundElements = <FoundElement>[];
|
||||
// we don't allow variables with an explicit index after an array. For
|
||||
// instance: SELECT * FROM t WHERE id IN ? OR id = ?2. The reason this is
|
||||
// not allowed is that we expand the first arg into multiple vars at runtime
|
||||
// which would break the index.
|
||||
// which would break the index. The initial high values can be arbitrary.
|
||||
// We've chosen 999 because most sqlite binaries don't allow more variables.
|
||||
var maxIndex = 999;
|
||||
var currentIndex = 0;
|
||||
|
||||
for (var used in usedVars) {
|
||||
if (used.resolvedIndex == currentIndex) {
|
||||
continue; // already handled
|
||||
}
|
||||
for (var used in merged) {
|
||||
if (used is Variable) {
|
||||
if (used.resolvedIndex == currentIndex) {
|
||||
continue; // already handled, we only report a single variable / index
|
||||
}
|
||||
|
||||
currentIndex = used.resolvedIndex;
|
||||
final name = (used is ColonNamedVariable) ? used.name : null;
|
||||
final explicitIndex =
|
||||
(used is NumberedVariable) ? used.explicitIndex : null;
|
||||
final internalType = ctx.typeOf(used);
|
||||
final type = resolvedToMoor(internalType.type);
|
||||
final isArray = internalType.type?.isArray ?? false;
|
||||
currentIndex = used.resolvedIndex;
|
||||
final name = (used is ColonNamedVariable) ? used.name : null;
|
||||
final explicitIndex =
|
||||
(used is NumberedVariable) ? used.explicitIndex : null;
|
||||
final internalType = ctx.typeOf(used);
|
||||
final type = resolvedToMoor(internalType.type);
|
||||
final isArray = internalType.type?.isArray ?? false;
|
||||
|
||||
if (explicitIndex != null && currentIndex >= maxIndex) {
|
||||
throw ArgumentError(
|
||||
'Cannot have a variable with an index lower than that of an array '
|
||||
'appearing after an array!');
|
||||
}
|
||||
if (explicitIndex != null && currentIndex >= maxIndex) {
|
||||
throw ArgumentError(
|
||||
'Cannot have a variable with an index lower than that of an array '
|
||||
'appearing after an array!');
|
||||
}
|
||||
|
||||
foundVariables
|
||||
.add(FoundVariable(currentIndex, name, type, used, isArray));
|
||||
foundElements
|
||||
.add(FoundVariable(currentIndex, name, type, used, isArray));
|
||||
|
||||
// arrays cannot be indexed explicitly because they're expanded into
|
||||
// multiple variables when executed
|
||||
if (isArray && explicitIndex != null) {
|
||||
throw ArgumentError(
|
||||
'Cannot use an array variable with an explicit index');
|
||||
}
|
||||
if (isArray) {
|
||||
maxIndex = used.resolvedIndex;
|
||||
// arrays cannot be indexed explicitly because they're expanded into
|
||||
// multiple variables when executed
|
||||
if (isArray && explicitIndex != null) {
|
||||
throw ArgumentError(
|
||||
'Cannot use an array variable with an explicit index');
|
||||
}
|
||||
if (isArray) {
|
||||
maxIndex = used.resolvedIndex;
|
||||
}
|
||||
} else if (used is DartPlaceholder) {
|
||||
// we don't what index this placeholder has, so we can't allow _any_
|
||||
// explicitly indexed variables coming after this
|
||||
maxIndex = 0;
|
||||
foundElements.add(_extractPlaceholder(ctx, used));
|
||||
}
|
||||
}
|
||||
|
||||
return foundVariables;
|
||||
return foundElements;
|
||||
}
|
||||
|
||||
List<FoundDartPlaceholder> extractPlaceholders(AnalysisContext context) {
|
||||
final placeholders =
|
||||
context.root.allDescendants.whereType<DartPlaceholder>().toList();
|
||||
final found = <FoundDartPlaceholder>[];
|
||||
|
||||
for (var placeholder in placeholders) {
|
||||
ColumnType columnType;
|
||||
final name = placeholder.name;
|
||||
|
||||
final type = placeholder.when(
|
||||
isExpression: (e) {
|
||||
final foundType = context.typeOf(e);
|
||||
if (foundType.type != null) {
|
||||
columnType = resolvedToMoor(foundType.type);
|
||||
}
|
||||
return DartPlaceholderType.expression;
|
||||
},
|
||||
isLimit: (_) => DartPlaceholderType.limit,
|
||||
isOrderBy: (_) => DartPlaceholderType.orderBy,
|
||||
isOrderingTerm: (_) => DartPlaceholderType.orderByTerm,
|
||||
);
|
||||
|
||||
found.add(FoundDartPlaceholder(type, columnType, name));
|
||||
/// Merges [vars] and [placeholders] into a list that satisfies the order
|
||||
/// described in [extractElements].
|
||||
List<dynamic /* Variable|DartPlaceholder */ > _mergeVarsAndPlaceholders(
|
||||
List<Variable> vars, List<DartPlaceholder> placeholders) {
|
||||
final groupVarsByIndex = <int, List<Variable>>{};
|
||||
for (var variable in vars) {
|
||||
groupVarsByIndex
|
||||
.putIfAbsent(variable.resolvedIndex, () => [])
|
||||
.add(variable);
|
||||
}
|
||||
return found;
|
||||
// sort each group by index
|
||||
for (var group in groupVarsByIndex.values) {
|
||||
group..sort((a, b) => a.resolvedIndex.compareTo(b.resolvedIndex));
|
||||
}
|
||||
|
||||
int Function(dynamic, dynamic) comparer;
|
||||
comparer = (dynamic a, dynamic b) {
|
||||
if (a is Variable && b is Variable) {
|
||||
// variables are sorted by their index
|
||||
return a.resolvedIndex.compareTo(b.resolvedIndex);
|
||||
} else if (a is DartPlaceholder && b is DartPlaceholder) {
|
||||
// placeholders by their position
|
||||
return AnalysisContext.compareNodesByOrder(a, b);
|
||||
} else {
|
||||
// ok, one of them is a variable, the other one is a placeholder. Let's
|
||||
// assume a is the variable. If not, we just switch results.
|
||||
if (a is Variable) {
|
||||
final placeholderB = b as DartPlaceholder;
|
||||
final firstWithSameIndex = groupVarsByIndex[a.resolvedIndex].first;
|
||||
|
||||
return firstWithSameIndex.firstPosition
|
||||
.compareTo(placeholderB.firstPosition);
|
||||
} else {
|
||||
return -comparer(b, a);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
final list = vars.cast<dynamic>().followedBy(placeholders).toList();
|
||||
return list..sort(comparer);
|
||||
}
|
||||
|
||||
FoundDartPlaceholder _extractPlaceholder(
|
||||
AnalysisContext context, DartPlaceholder placeholder) {
|
||||
ColumnType columnType;
|
||||
final name = placeholder.name;
|
||||
|
||||
final type = placeholder.when(
|
||||
isExpression: (e) {
|
||||
final foundType = context.typeOf(e);
|
||||
if (foundType.type != null) {
|
||||
columnType = resolvedToMoor(foundType.type);
|
||||
}
|
||||
return DartPlaceholderType.expression;
|
||||
},
|
||||
isLimit: (_) => DartPlaceholderType.limit,
|
||||
isOrderBy: (_) => DartPlaceholderType.orderBy,
|
||||
isOrderingTerm: (_) => DartPlaceholderType.orderByTerm,
|
||||
);
|
||||
|
||||
return FoundDartPlaceholder(type, columnType, name)..astNode = placeholder;
|
||||
}
|
||||
|
||||
SpecifiedTable tableToMoor(Table table) {
|
||||
|
|
|
@ -74,16 +74,23 @@ abstract class SqlQuery {
|
|||
/// if their index is lower than that of the array (e.g `a = ?2 AND b IN ?
|
||||
/// AND c IN ?1`. In other words, we can expand an array without worrying
|
||||
/// about the variables that appear after that array.
|
||||
final List<FoundVariable> variables;
|
||||
List<FoundVariable> variables;
|
||||
|
||||
/// The placeholders in this query which are bound and converted to sql at
|
||||
/// runtime. For instance, in `SELECT * FROM tbl WHERE $expr`, the `expr` is
|
||||
/// going to be a [FoundDartPlaceholder] with the type
|
||||
/// [DartPlaceholderType.expression] and [ColumnType.boolean]. We will
|
||||
/// generate a method which has a `Expression<bool, BoolType> expr` parameter.
|
||||
final List<FoundDartPlaceholder> placeholders;
|
||||
List<FoundDartPlaceholder> placeholders;
|
||||
|
||||
SqlQuery(this.name, this.fromContext, this.variables, this.placeholders);
|
||||
/// Union of [variables] and [elements], but in the order in which they
|
||||
/// appear inside the query.
|
||||
final List<FoundElement> elements;
|
||||
|
||||
SqlQuery(this.name, this.fromContext, this.elements) {
|
||||
variables = elements.whereType<FoundVariable>().toList();
|
||||
placeholders = elements.whereType<FoundDartPlaceholder>().toList();
|
||||
}
|
||||
}
|
||||
|
||||
class SqlSelectQuery extends SqlQuery {
|
||||
|
@ -97,28 +104,19 @@ class SqlSelectQuery extends SqlQuery {
|
|||
return '${ReCase(name).pascalCase}Result';
|
||||
}
|
||||
|
||||
SqlSelectQuery(
|
||||
String name,
|
||||
AnalysisContext fromContext,
|
||||
List<FoundVariable> variables,
|
||||
List<FoundDartPlaceholder> placeholders,
|
||||
this.readsFrom,
|
||||
this.resultSet)
|
||||
: super(name, fromContext, variables, placeholders);
|
||||
SqlSelectQuery(String name, AnalysisContext fromContext,
|
||||
List<FoundElement> elements, this.readsFrom, this.resultSet)
|
||||
: super(name, fromContext, elements);
|
||||
}
|
||||
|
||||
class UpdatingQuery extends SqlQuery {
|
||||
final List<SpecifiedTable> updates;
|
||||
final bool isInsert;
|
||||
|
||||
UpdatingQuery(
|
||||
String name,
|
||||
AnalysisContext fromContext,
|
||||
List<FoundVariable> variables,
|
||||
List<FoundDartPlaceholder> placeholders,
|
||||
this.updates,
|
||||
UpdatingQuery(String name, AnalysisContext fromContext,
|
||||
List<FoundElement> elements, this.updates,
|
||||
{this.isInsert = false})
|
||||
: super(name, fromContext, variables, placeholders);
|
||||
: super(name, fromContext, elements);
|
||||
}
|
||||
|
||||
class InferredResultSet {
|
||||
|
@ -177,12 +175,19 @@ class ResultColumn {
|
|||
ResultColumn(this.name, this.type, this.nullable, {this.converter});
|
||||
}
|
||||
|
||||
/// Something in the query that needs special attention when generating code,
|
||||
/// such as variables or Dart placeholders.
|
||||
abstract class FoundElement {
|
||||
String get dartParameterName;
|
||||
}
|
||||
|
||||
/// A semantic interpretation of a [Variable] in a sql statement.
|
||||
class FoundVariable {
|
||||
class FoundVariable extends FoundElement {
|
||||
/// The (unique) index of this variable in the sql query. For instance, the
|
||||
/// query `SELECT * FROM tbl WHERE a = ? AND b = :xyz OR c = :xyz` contains
|
||||
/// three [Variable]s in its AST, but only two [FoundVariable]s, where the
|
||||
/// `?` will have index 1 and (both) `:xyz` variables will have index 2.
|
||||
/// `?` will have index 1 and (both) `:xyz` variables will have index 2. We
|
||||
/// only report one [FoundVariable] per index.
|
||||
int index;
|
||||
|
||||
/// The name of this variable, or null if it's not a named variable.
|
||||
|
@ -206,6 +211,7 @@ class FoundVariable {
|
|||
assert(variable.resolvedIndex == index);
|
||||
}
|
||||
|
||||
@override
|
||||
String get dartParameterName {
|
||||
if (name != null) {
|
||||
return name.replaceAll(_illegalChars, '');
|
||||
|
@ -223,7 +229,7 @@ enum DartPlaceholderType {
|
|||
}
|
||||
|
||||
/// A Dart placeholder that will be bound at runtime.
|
||||
class FoundDartPlaceholder {
|
||||
class FoundDartPlaceholder extends FoundElement {
|
||||
final DartPlaceholderType type;
|
||||
|
||||
/// If [type] is [DartPlaceholderType.expression] and the expression could be
|
||||
|
@ -231,6 +237,7 @@ class FoundDartPlaceholder {
|
|||
final ColumnType columnType;
|
||||
|
||||
final String name;
|
||||
DartPlaceholder astNode;
|
||||
|
||||
FoundDartPlaceholder(this.type, this.columnType, this.name);
|
||||
|
||||
|
@ -254,4 +261,7 @@ class FoundDartPlaceholder {
|
|||
|
||||
throw AssertionError('cant happen, all branches covered');
|
||||
}
|
||||
|
||||
@override
|
||||
String get dartParameterName => name;
|
||||
}
|
||||
|
|
|
@ -9,13 +9,17 @@ import 'package:moor_generator/src/writer/writer.dart';
|
|||
import 'package:recase/recase.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
const highestAssignedIndexVar = '\$highestIndex';
|
||||
const highestAssignedIndexVar = '\$arrayStartIndex';
|
||||
|
||||
int _compareNodes(AstNode a, AstNode b) =>
|
||||
a.firstPosition.compareTo(b.firstPosition);
|
||||
|
||||
/// 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 {
|
||||
final SqlQuery query;
|
||||
final Scope scope;
|
||||
|
||||
SqlSelectQuery get _select => query as SqlSelectQuery;
|
||||
UpdatingQuery get _update => query as UpdatingQuery;
|
||||
|
||||
|
@ -40,6 +44,10 @@ class QueryWriter {
|
|||
return 'expanded${v.dartParameterName}';
|
||||
}
|
||||
|
||||
String _placeholderContextName(FoundDartPlaceholder placeholder) {
|
||||
return 'generated${placeholder.name}';
|
||||
}
|
||||
|
||||
void write() {
|
||||
if (query is SqlSelectQuery) {
|
||||
final select = query as SqlSelectQuery;
|
||||
|
@ -180,19 +188,19 @@ class QueryWriter {
|
|||
}
|
||||
|
||||
void _writeParameters() {
|
||||
final variableParams = query.variables.map((v) {
|
||||
var dartType = dartTypeNames[v.type];
|
||||
if (v.isArray) {
|
||||
dartType = 'List<$dartType>';
|
||||
final paramList = query.elements.map((e) {
|
||||
if (e is FoundVariable) {
|
||||
var dartType = dartTypeNames[e.type];
|
||||
if (e.isArray) {
|
||||
dartType = 'List<$dartType>';
|
||||
}
|
||||
return '$dartType ${e.dartParameterName}';
|
||||
} else if (e is FoundDartPlaceholder) {
|
||||
return '${e.parameterType} ${e.name}';
|
||||
}
|
||||
return '$dartType ${v.dartParameterName}';
|
||||
});
|
||||
|
||||
final placeholderParams = query.placeholders.map((p) {
|
||||
return '${p.parameterType} ${p.name}';
|
||||
});
|
||||
|
||||
final paramList = variableParams.followedBy(placeholderParams).join(', ');
|
||||
throw AssertionError('Unknown element (not variable of placeholder)');
|
||||
}).join(', ');
|
||||
_buffer.write(paramList);
|
||||
}
|
||||
|
||||
|
@ -200,10 +208,7 @@ class QueryWriter {
|
|||
/// assuming that for each parameter, a variable with the same name exists
|
||||
/// in the current scope.
|
||||
void _writeUseParameters() {
|
||||
final parameters = query.variables
|
||||
.map((v) => v.dartParameterName)
|
||||
.followedBy(query.placeholders.map((p) => p.name));
|
||||
|
||||
final parameters = query.elements.map((e) => e.dartParameterName);
|
||||
_buffer.write(parameters.join(', '));
|
||||
}
|
||||
|
||||
|
@ -218,42 +223,71 @@ class QueryWriter {
|
|||
// We use explicit indexes when expanding so that we don't have to expand the
|
||||
// "vars" variable twice. To do this, a local var called "$currentVarIndex"
|
||||
// keeps track of the highest variable number assigned.
|
||||
// We can use the same mechanism for runtime Dart placeholders, where we
|
||||
// generate a GenerationContext, write the placeholder and finally extract the
|
||||
// variables
|
||||
|
||||
void _writeExpandedDeclarations() {
|
||||
var indexCounterWasDeclared = false;
|
||||
var highestIndexBeforeArray = 0;
|
||||
|
||||
for (var variable in query.variables) {
|
||||
if (variable.isArray) {
|
||||
if (!indexCounterWasDeclared) {
|
||||
// we only need the index counter when the query contains an array.
|
||||
// add +1 because that's going to be the first index of the expanded
|
||||
// array
|
||||
final firstVal = highestIndexBeforeArray + 1;
|
||||
_buffer.write('var $highestAssignedIndexVar = $firstVal;');
|
||||
indexCounterWasDeclared = true;
|
||||
void _writeIndexCounter() {
|
||||
// we only need the index counter when the query contains an expanded
|
||||
// element.
|
||||
// add +1 because that's going to be the first index of this element.
|
||||
final firstVal = highestIndexBeforeArray + 1;
|
||||
_buffer.write('var $highestAssignedIndexVar = $firstVal;');
|
||||
indexCounterWasDeclared = true;
|
||||
}
|
||||
|
||||
void _increaseIndexCounter(String by) {
|
||||
_buffer..write('$highestAssignedIndexVar += ')..write(by)..write(';\n');
|
||||
}
|
||||
|
||||
// query.elements are guaranteed to be sorted in the order in which they're
|
||||
// going to have an effect when expanded. See TypeMapper.extractElements for
|
||||
// the gory details.
|
||||
for (var element in query.elements) {
|
||||
if (element is FoundVariable) {
|
||||
if (element.isArray) {
|
||||
if (!indexCounterWasDeclared) {
|
||||
_writeIndexCounter();
|
||||
}
|
||||
|
||||
// final expandedvar1 = $expandVar(<startIndex>, <amount>);
|
||||
_buffer
|
||||
..write('final ')
|
||||
..write(_expandedName(element))
|
||||
..write(' = ')
|
||||
..write(r'$expandVar(')
|
||||
..write(highestAssignedIndexVar)
|
||||
..write(', ')
|
||||
..write(element.dartParameterName)
|
||||
..write('.length);\n');
|
||||
|
||||
// increase highest index for the next expanded element
|
||||
_increaseIndexCounter('${element.dartParameterName}.length');
|
||||
}
|
||||
|
||||
// final expandedvar1 = $expandVar(<startIndex>, <amount>);
|
||||
if (!indexCounterWasDeclared) {
|
||||
highestIndexBeforeArray = max(highestIndexBeforeArray, element.index);
|
||||
}
|
||||
} else if (element is FoundDartPlaceholder) {
|
||||
if (!indexCounterWasDeclared) {
|
||||
indexCounterWasDeclared = true;
|
||||
}
|
||||
_buffer
|
||||
..write('final ')
|
||||
..write(_expandedName(variable))
|
||||
..write(_placeholderContextName(element))
|
||||
..write(' = ')
|
||||
..write(r'$expandVar(')
|
||||
..write(highestAssignedIndexVar)
|
||||
..write(', ')
|
||||
..write(variable.dartParameterName)
|
||||
..write('.length);\n');
|
||||
..write(r'$write(')
|
||||
..write(element.dartParameterName)
|
||||
..write(');\n');
|
||||
|
||||
// increase highest index for the next array
|
||||
_buffer
|
||||
..write('$highestAssignedIndexVar += ')
|
||||
..write(variable.dartParameterName)
|
||||
..write('.length;');
|
||||
}
|
||||
|
||||
if (!indexCounterWasDeclared) {
|
||||
highestIndexBeforeArray = max(highestIndexBeforeArray, variable.index);
|
||||
// similar to the case for expanded array variables, we need to
|
||||
// increase the index
|
||||
_increaseIndexCounter(
|
||||
'${_placeholderContextName(element)}.amountOfVariables');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -283,29 +317,42 @@ 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() {
|
||||
// sort variables by the order in which they appear
|
||||
final vars = query.fromContext.root.allDescendants
|
||||
.whereType<Variable>()
|
||||
// sort variables and placeholders by the order in which they appear
|
||||
final toReplace = query.fromContext.root.allDescendants
|
||||
.where((node) => node is Variable || node is DartPlaceholder)
|
||||
.toList()
|
||||
..sort((a, b) => a.firstPosition.compareTo(b.firstPosition));
|
||||
..sort(_compareNodes);
|
||||
|
||||
final buffer = StringBuffer("'");
|
||||
|
||||
var lastIndex = query.fromContext.root.firstPosition;
|
||||
|
||||
for (var sqlVar in vars) {
|
||||
final moorVar = query.variables
|
||||
.singleWhere((f) => f.variable.resolvedIndex == sqlVar.resolvedIndex);
|
||||
if (!moorVar.isArray) continue;
|
||||
|
||||
void replaceNode(AstNode node, String content) {
|
||||
// write everything that comes before this var into the buffer
|
||||
final currentIndex = sqlVar.firstPosition;
|
||||
final currentIndex = node.firstPosition;
|
||||
final queryPart = query.sql.substring(lastIndex, currentIndex);
|
||||
buffer.write(escapeForDart(queryPart));
|
||||
lastIndex = sqlVar.lastPosition;
|
||||
lastIndex = node.lastPosition;
|
||||
|
||||
// write the ($expandedVar) par
|
||||
buffer.write('(\$${_expandedName(moorVar)})');
|
||||
// write the replaced content
|
||||
buffer.write(content);
|
||||
}
|
||||
|
||||
for (var rewriteTarget in toReplace) {
|
||||
if (rewriteTarget is Variable) {
|
||||
final moorVar = query.variables.singleWhere(
|
||||
(f) => f.variable.resolvedIndex == rewriteTarget.resolvedIndex);
|
||||
|
||||
if (moorVar.isArray) {
|
||||
replaceNode(rewriteTarget, '(\$${_expandedName(moorVar)})');
|
||||
}
|
||||
} else if (rewriteTarget is DartPlaceholder) {
|
||||
final moorPlaceholder =
|
||||
query.placeholders.singleWhere((p) => p.astNode == rewriteTarget);
|
||||
|
||||
replaceNode(rewriteTarget,
|
||||
'\${${_placeholderContextName(moorPlaceholder)}.sql}');
|
||||
}
|
||||
}
|
||||
|
||||
// write the final part after the last variable, plus the ending '
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
|
||||
import 'package:moor_generator/src/model/sql_query.dart';
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
final _idColumn = TableColumn('id', const ResolvedType(type: BasicType.int));
|
||||
final _titleColumn =
|
||||
TableColumn('title', const ResolvedType(type: BasicType.text));
|
||||
final table = Table(name: 'todos', resolvedColumns: [_idColumn, _titleColumn]);
|
||||
|
||||
void main() {
|
||||
final engine = SqlEngine()..registerTable(table);
|
||||
final mapper = TypeMapper();
|
||||
|
||||
test('extracts variables and sorts them by index', () {
|
||||
final result = engine.analyze(
|
||||
'SELECT * FROM todos WHERE title = ?2 OR id IN ? OR title = ?1');
|
||||
|
||||
final elements = mapper.extractElements(result).cast<FoundVariable>();
|
||||
|
||||
expect(elements.map((v) => v.index), [1, 2, 3]);
|
||||
});
|
||||
|
||||
test('throws when an array with an explicit index is used', () {
|
||||
final result = engine.analyze('SELECT 1 WHERE 1 IN ?1');
|
||||
|
||||
expect(() => mapper.extractElements(result), throwsArgumentError);
|
||||
});
|
||||
|
||||
test(
|
||||
'throws when an explicitly index var with higher index appears after array',
|
||||
() {
|
||||
final result = engine.analyze('SELECT 1 WHERE 1 IN ? OR 2 = ?2');
|
||||
expect(() => mapper.extractElements(result), throwsArgumentError);
|
||||
},
|
||||
);
|
||||
}
|
|
@ -29,4 +29,12 @@ class AnalysisContext {
|
|||
/// Obtains the result of any typeable component. See the information at
|
||||
/// [types] on important [Typeable]s.
|
||||
ResolveResult typeOf(Typeable t) => types.resolveOrInfer(t);
|
||||
|
||||
/// Compares two [AstNode]s by their first position in the query.
|
||||
static int compareNodesByOrder(AstNode first, AstNode second) {
|
||||
if (first.first == null || second.first == null) {
|
||||
return 0; // position not set. should we throw in that case?
|
||||
}
|
||||
return first.firstPosition.compareTo(second.firstPosition);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -286,7 +286,8 @@ mixin CrudParser on ParserBase {
|
|||
// terms).
|
||||
if (terms.length == 1 && terms.single is DartOrderingTermPlaceholder) {
|
||||
final termPlaceholder = terms.single as DartOrderingTermPlaceholder;
|
||||
return DartOrderByPlaceholder(name: termPlaceholder.name);
|
||||
return DartOrderByPlaceholder(name: termPlaceholder.name)
|
||||
..setSpan(termPlaceholder.first, termPlaceholder.last);
|
||||
}
|
||||
|
||||
return OrderBy(terms: terms);
|
||||
|
|
Loading…
Reference in New Issue