Support REQUIRED annotation for query vars

This commit is contained in:
Simon Binder 2021-04-13 22:14:12 +02:00
parent 51d5ada5c9
commit 602212f99d
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
18 changed files with 146 additions and 34 deletions

View File

@ -1679,7 +1679,7 @@ abstract class _$CustomTablesDb extends GeneratedDatabase {
});
}
Selectable<EMail> searchEmails({String? term}) {
Selectable<EMail> searchEmails({required String? term}) {
return customSelect(
'SELECT * FROM email WHERE email MATCH :term ORDER BY rank',
variables: [Variable<String?>(term)],

View File

@ -75,7 +75,7 @@ multiple: SELECT d.*, c.** FROM with_constraints c
ON d.a = c.a AND d.b = c.b
WHERE $predicate;
searchEmails(:term AS TEXT OR NULL): SELECT * FROM email WHERE email MATCH :term ORDER BY rank;
searchEmails(REQUIRED :term AS TEXT OR NULL): SELECT * FROM email WHERE email MATCH :term ORDER BY rank;
readRowId: SELECT oid, * FROM config WHERE _rowid_ = $expr;

View File

@ -13,6 +13,8 @@ import 'package:moor_generator/src/model/view.dart';
import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
import 'package:sqlparser/utils/find_referenced_tables.dart';
import 'required_variables.dart';
abstract class BaseAnalyzer {
final List<MoorTable> tables;
final List<MoorView> views;
@ -102,16 +104,20 @@ class SqlAnalyzer extends BaseAnalyzer {
var declaredInMoor = false;
AnalysisContext context;
var requiredVariables = RequiredVariables.empty;
try {
if (query is DeclaredDartQuery) {
final sql = query.sql;
context = engine.analyze(sql);
} else if (query is DeclaredMoorQuery) {
final options = _createOptionsAndVars(query.astNode);
requiredVariables = options.variables;
context = engine.analyzeNode(
query.query,
query.file.parseResult.sql,
stmtOptions: _createOptions(query.astNode),
stmtOptions: options.options,
);
declaredInMoor = true;
}
@ -129,8 +135,10 @@ class SqlAnalyzer extends BaseAnalyzer {
}
try {
final handled = QueryHandler(query, context, mapper).handle()
..declaredInMoorFile = declaredInMoor;
final handled = QueryHandler(query, context, mapper,
requiredVariables: requiredVariables)
.handle()
..declaredInMoorFile = declaredInMoor;
foundQueries.add(handled);
} catch (e, s) {
// todo remove dependency on build package here
@ -144,33 +152,56 @@ class SqlAnalyzer extends BaseAnalyzer {
}
}
AnalyzeStatementOptions _createOptions(DeclaredStatement stmt) {
_OptionsAndRequiredVariables _createOptionsAndVars(DeclaredStatement stmt) {
final reader = engine.schemaReader;
final indexedHints = <int, ResolvedType>{};
final namedHints = <String, ResolvedType>{};
final defaultValues = <String, Expression>{};
final requiredIndex = <int>{};
final requiredName = <String>{};
for (final parameter in stmt.parameters) {
if (parameter is VariableTypeHint) {
final variable = parameter.variable;
final type = reader
.resolveColumnType(parameter.typeName)
.withNullable(parameter.orNull);
if (variable is ColonNamedVariable) {
namedHints[variable.name] = type;
} else if (variable is NumberedVariable) {
indexedHints[variable.resolvedIndex] = type;
if (parameter.isRequired) {
if (variable is ColonNamedVariable) {
requiredName.add(variable.name);
} else if (variable is NumberedVariable) {
requiredIndex.add(variable.resolvedIndex);
}
}
if (parameter.typeName != null) {
final type = reader
.resolveColumnType(parameter.typeName)
.withNullable(parameter.orNull);
if (variable is ColonNamedVariable) {
namedHints[variable.name] = type;
} else if (variable is NumberedVariable) {
indexedHints[variable.resolvedIndex] = type;
}
}
} else if (parameter is DartPlaceholderDefaultValue) {
defaultValues[parameter.variableName] = parameter.defaultValue;
}
}
return AnalyzeStatementOptions(
indexedVariableTypes: indexedHints,
namedVariableTypes: namedHints,
defaultValuesForPlaceholder: defaultValues,
return _OptionsAndRequiredVariables(
AnalyzeStatementOptions(
indexedVariableTypes: indexedHints,
namedVariableTypes: namedHints,
defaultValuesForPlaceholder: defaultValues,
),
RequiredVariables(requiredIndex, requiredName),
);
}
}
class _OptionsAndRequiredVariables {
final AnalyzeStatementOptions options;
final RequiredVariables variables;
_OptionsAndRequiredVariables(this.options, this.variables);
}

View File

@ -8,6 +8,7 @@ import 'package:sqlparser/sqlparser.dart' hide ResultColumn;
import 'package:sqlparser/utils/find_referenced_tables.dart';
import 'lints/linter.dart';
import 'required_variables.dart';
/// Maps an [AnalysisContext] from the sqlparser to a [SqlQuery] from this
/// generator package by determining its type, return columns, variables and so
@ -16,6 +17,7 @@ class QueryHandler {
final DeclaredQuery source;
final AnalysisContext context;
final TypeMapper mapper;
final RequiredVariables requiredVariables;
Set<Table> _foundTables;
Set<View> _foundViews;
@ -26,12 +28,14 @@ class QueryHandler {
BaseSelectStatement get _select => context.root as BaseSelectStatement;
QueryHandler(this.source, this.context, this.mapper);
QueryHandler(this.source, this.context, this.mapper,
{this.requiredVariables = RequiredVariables.empty});
String get name => source.name;
SqlQuery handle() {
_foundElements = mapper.extractElements(context);
_foundElements =
mapper.extractElements(context, required: requiredVariables);
_verifyNoSkippedIndexes();
final query = _mapToMoor();

View File

@ -0,0 +1,9 @@
class RequiredVariables {
final Set<int> requiredNumberedVariables;
final Set<String> requiredNamedVariables;
const RequiredVariables(
this.requiredNumberedVariables, this.requiredNamedVariables);
static const RequiredVariables empty = RequiredVariables({}, {});
}

View File

@ -7,6 +7,8 @@ import 'package:moor_generator/src/utils/type_converter_hint.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/utils/find_referenced_tables.dart' as s;
import 'required_variables.dart';
/// Converts tables and types between the moor_generator and the sqlparser
/// library.
class TypeMapper {
@ -121,7 +123,8 @@ class TypeMapper {
/// 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) {
List<FoundElement> extractElements(AnalysisContext ctx,
{RequiredVariables required = RequiredVariables.empty}) {
// 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.
@ -153,6 +156,8 @@ class TypeMapper {
final internalType = ctx.typeOf(used);
final type = resolvedToMoor(internalType.type);
final isArray = internalType.type?.isArray ?? false;
final isRequired = required.requiredNamedVariables.contains(name) ||
required.requiredNumberedVariables.contains(used.resolvedIndex);
if (explicitIndex != null && currentIndex >= maxIndex) {
throw ArgumentError(
@ -177,6 +182,7 @@ class TypeMapper {
variable: used,
isArray: isArray,
typeConverter: converter,
isRequired: isRequired,
));
// arrays cannot be indexed explicitly because they're expanded into

View File

@ -469,6 +469,8 @@ class FoundVariable extends FoundElement implements HasType {
/// without having to look at other variables.
final bool isArray;
final bool isRequired;
FoundVariable({
@required this.index,
@required this.name,
@ -476,6 +478,7 @@ class FoundVariable extends FoundElement implements HasType {
@required this.variable,
this.nullable = false,
this.isArray = false,
this.isRequired = false,
this.typeConverter,
}) : assert(variable.resolvedIndex == index);

View File

@ -308,8 +308,14 @@ class QueryWriter {
final type = optional.dartTypeCode(scope.generationOptions);
// No default value, this element is required if it's not nullable
final isNullable = optional is FoundVariable && optional.nullableInDart;
final isRequired = !isNullable && defaultCode == null;
var isMarkedAsRequired = false;
var isNullable = false;
if (optional is FoundVariable) {
isMarkedAsRequired = optional.isRequired;
isNullable = optional.nullableInDart;
}
final isRequired =
(!isNullable || isMarkedAsRequired) && defaultCode == null;
if (isRequired) {
_buffer..write(scope.required)..write(' ');
}

View File

@ -29,4 +29,29 @@ bar(?1 AS TEXT, :foo AS BOOLEAN): SELECT ?, :foo;
expect(resultSet.columns.map((c) => c.type),
[ColumnType.text, ColumnType.boolean]);
});
test('reads REQUIRED syntax', () async {
final state = TestState.withContent({
'foo|lib/main.moor': '''
bar(REQUIRED ?1 AS TEXT OR NULL, REQUIRED :foo AS BOOLEAN): SELECT ?, :foo;
''',
});
await state.runTask('package:foo/main.moor');
final file = state.file('package:foo/main.moor');
state.close();
expect(file.errors.errors, isEmpty);
final content = file.currentResult as ParsedMoorFile;
final query = content.resolvedQueries.single;
expect(
query.variables,
allOf(
hasLength(2),
everyElement(isA<FoundVariable>()
.having((e) => e.isRequired, 'isRequired', isTrue)),
),
);
});
}

View File

@ -1,4 +1,4 @@
## 0.15.1-dev
## 0.16.0-dev
- New analysis checks for `RETURNING`: Disallow `table.*` syntax and aggregate expressions
- Fix resolving columns when `RETURNING` is used in an `UPDATE FROM` statement

View File

@ -109,12 +109,14 @@ abstract class StatementParameter extends AstNode {
/// cases in which the resolver doesn't yield acceptable results.
class VariableTypeHint extends StatementParameter {
Variable variable;
final String typeName;
final bool isRequired;
final String? typeName;
final bool orNull;
Token? as;
VariableTypeHint(this.variable, this.typeName, {this.orNull = false});
VariableTypeHint(this.variable, this.typeName,
{this.orNull = false, this.isRequired = false});
@override
Iterable<AstNode> get childNodes => [variable];

View File

@ -308,13 +308,19 @@ class Parser {
StatementParameter _statementParameter() {
final first = _peek;
final isRequired = _matchOne(TokenType.required);
final variable = _variableOrNull();
if (variable != null) {
// Type hint for a variable
final as = _consume(TokenType.as, 'Expected AS followed by a type');
final typeNameTokens = _typeName() ?? _error('Expected a type name here');
final typeName = typeNameTokens.lexeme;
Token? as;
String? typeName;
if (_matchOne(TokenType.as)) {
as = _previous;
final typeNameTokens =
_typeName() ?? _error('Expected a type name here');
typeName = typeNameTokens.lexeme;
}
var orNull = false;
if (_matchOne(TokenType.or)) {
@ -322,7 +328,8 @@ class Parser {
orNull = true;
}
return VariableTypeHint(variable, typeName, orNull: orNull)
return VariableTypeHint(variable, typeName,
orNull: orNull, isRequired: isRequired)
..as = as
..setSpan(first, _previous);
} else if (_matchOne(TokenType.dollarSignVariable)) {

View File

@ -195,6 +195,7 @@ enum TokenType {
inlineDart,
import,
json,
required,
/// A `**` token. This is only scanned when scanning for moor tokens.
doubleStar,
@ -363,6 +364,7 @@ const Map<String, TokenType> moorKeywords = {
'IMPORT': TokenType.import,
'JSON': TokenType.json,
'MAPPED': TokenType.mapped,
'REQUIRED': TokenType.required,
};
/// A set of [TokenType]s that can be parsed as an identifier.

View File

@ -458,7 +458,11 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
void visitMoorStatementParameter(StatementParameter e, void arg) {
if (e is VariableTypeHint) {
final current = _currentAs<VariableTypeHint>(e);
_assert(current.typeName == e.typeName && current.orNull == e.orNull, e);
_assert(
current.typeName == e.typeName &&
current.orNull == e.orNull &&
current.isRequired == e.isRequired,
e);
} else if (e is DartPlaceholderDefaultValue) {
final current = _currentAs<DartPlaceholderDefaultValue>(e);
_assert(current.variableName == e.variableName, e);

View File

@ -852,9 +852,19 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
@override
void visitMoorStatementParameter(StatementParameter e, void arg) {
if (e is VariableTypeHint) {
if (e.isRequired) _keyword(TokenType.required);
visit(e.variable, arg);
_keyword(TokenType.as);
_symbol(e.typeName, spaceBefore: true, spaceAfter: true);
final typeName = e.typeName;
if (typeName != null) {
_keyword(TokenType.as);
_symbol(typeName, spaceBefore: true, spaceAfter: true);
}
if (e.orNull) {
_keyword(TokenType.or);
_keyword(TokenType.$null);
}
} else if (e is DartPlaceholderDefaultValue) {
_symbol('\$${e.variableName}', spaceAfter: true);
_symbol('=', spaceBefore: true, spaceAfter: true);

View File

@ -1,6 +1,6 @@
name: sqlparser
description: Parses sqlite statements and performs static analysis on them
version: 0.15.0
version: 0.16.0
homepage: https://github.com/simolus3/moor/tree/develop/sqlparser
#homepage: https://moor.simonbinder.eu/
issue_tracker: https://github.com/simolus3/moor/issues

View File

@ -15,7 +15,7 @@ CREATE TABLE tbl (
all: SELECT /* COUNT(*), */ * FROM tbl WHERE $predicate;
@special: SELECT * FROM tbl;
typeHints(:foo AS TEXT OR NULL, $predicate = TRUE):
typeHints(REQUIRED :foo AS TEXT OR NULL, $predicate = TRUE):
SELECT :foo WHERE $predicate;
nested AS MyResultSet: SELECT foo.** FROM tbl foo;
''';
@ -88,6 +88,7 @@ void main() {
),
'TEXT',
orNull: true,
isRequired: true,
),
DartPlaceholderDefaultValue(
'predicate',

View File

@ -387,6 +387,8 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3;
kind: _ParseKind.moorFile);
testFormat('foo: SELECT * FROM bar WHERE :id < 10;',
kind: _ParseKind.moorFile);
testFormat('foo (REQUIRED :x AS TEXT OR NULL): SELECT :x;',
kind: _ParseKind.moorFile);
testFormat(r'foo ($pred = FALSE): SELECT * FROM bar WHERE $pred;',
kind: _ParseKind.moorFile);
});