Write implementation for Dart placeholders

This commit is contained in:
Simon Binder 2019-09-14 16:42:24 +02:00
parent bf7c9feddf
commit 349b245089
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
13 changed files with 349 additions and 166 deletions

View File

@ -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);
}

View File

@ -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) {

View File

@ -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.

View File

@ -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);
}

View File

@ -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($),
],

View File

@ -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: [

View File

@ -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() {

View File

@ -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) {

View File

@ -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;
}

View File

@ -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 '

View File

@ -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);
},
);
}

View File

@ -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);
}
}

View File

@ -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);