mirror of https://github.com/AMT-Cheif/drift.git
Support REQUIRED annotation for query vars
This commit is contained in:
parent
51d5ada5c9
commit
602212f99d
|
@ -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)],
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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({}, {});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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(' ');
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue