Merge branch 'develop' into analyzer-plugin

# Conflicts:
#	moor_generator/lib/src/analyzer/moor/parser.dart
#	sqlparser/lib/src/engine/sql_engine.dart
This commit is contained in:
Simon Binder 2019-09-07 23:05:36 +02:00
commit 7354b56712
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
18 changed files with 607 additions and 225 deletions

View File

@ -147,6 +147,14 @@ class UsersCompanion extends UpdateCompanion<User> {
this.profilePicture = const Value.absent(),
this.preferences = const Value.absent(),
});
UsersCompanion.insert({
this.id = const Value.absent(),
@required String name,
@required DateTime birthDate,
this.profilePicture = const Value.absent(),
this.preferences = const Value.absent(),
}) : name = Value(name),
birthDate = Value(birthDate);
UsersCompanion copyWith(
{Value<int> id,
Value<String> name,
@ -172,7 +180,8 @@ class $UsersTable extends Users with TableInfo<$UsersTable, User> {
@override
GeneratedIntColumn get id => _id ??= _constructId();
GeneratedIntColumn _constructId() {
return GeneratedIntColumn('id', $tableName, false, hasAutoIncrement: true);
return GeneratedIntColumn('id', $tableName, false,
hasAutoIncrement: true, declaredAsPrimaryKey: true);
}
final VerificationMeta _nameMeta = const VerificationMeta('name');
@ -402,6 +411,12 @@ class FriendshipsCompanion extends UpdateCompanion<Friendship> {
this.secondUser = const Value.absent(),
this.reallyGoodFriends = const Value.absent(),
});
FriendshipsCompanion.insert({
@required int firstUser,
@required int secondUser,
this.reallyGoodFriends = const Value.absent(),
}) : firstUser = Value(firstUser),
secondUser = Value(secondUser);
FriendshipsCompanion copyWith(
{Value<int> firstUser,
Value<int> secondUser,
@ -520,6 +535,165 @@ class $FriendshipsTable extends Friendships
}
}
abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
$UsersTable _users;
$UsersTable get users => _users ??= $UsersTable(this);
$FriendshipsTable _friendships;
$FriendshipsTable get friendships => _friendships ??= $FriendshipsTable(this);
User _rowToUser(QueryRow row) {
return User(
id: row.readInt('id'),
name: row.readString('name'),
birthDate: row.readDateTime('birth_date'),
profilePicture: row.readBlob('profile_picture'),
preferences:
$UsersTable.$converter0.mapToDart(row.readString('preferences')),
);
}
Selectable<User> mostPopularUsersQuery(
int amount,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelectQuery(
'SELECT * FROM users u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT :amount',
variables: [
Variable.withInt(amount),
],
readsFrom: {
users,
friendships
}).map(_rowToUser);
}
Future<List<User>> mostPopularUsers(
int amount,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return mostPopularUsersQuery(amount, operateOn: operateOn).get();
}
Stream<List<User>> watchMostPopularUsers(int amount) {
return mostPopularUsersQuery(amount).watch();
}
AmountOfGoodFriendsResult _rowToAmountOfGoodFriendsResult(QueryRow row) {
return AmountOfGoodFriendsResult(
count: row.readInt('COUNT(*)'),
);
}
Selectable<AmountOfGoodFriendsResult> amountOfGoodFriendsQuery(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelectQuery(
'SELECT COUNT(*) FROM friendships f WHERE f.really_good_friends AND (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
],
readsFrom: {
friendships
}).map(_rowToAmountOfGoodFriendsResult);
}
Future<List<AmountOfGoodFriendsResult>> amountOfGoodFriends(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return amountOfGoodFriendsQuery(user, operateOn: operateOn).get();
}
Stream<List<AmountOfGoodFriendsResult>> watchAmountOfGoodFriends(int user) {
return amountOfGoodFriendsQuery(user).watch();
}
Selectable<User> friendsOfQuery(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelectQuery(
'SELECT u.* FROM friendships f\n INNER JOIN users u ON u.id IN (f.first_user, f.second_user) AND\n u.id != :user\n WHERE (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
],
readsFrom: {
friendships,
users
}).map(_rowToUser);
}
Future<List<User>> friendsOf(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return friendsOfQuery(user, operateOn: operateOn).get();
}
Stream<List<User>> watchFriendsOf(int user) {
return friendsOfQuery(user).watch();
}
UserCountResult _rowToUserCountResult(QueryRow row) {
return UserCountResult(
cOUNTid: row.readInt('COUNT(id)'),
);
}
Selectable<UserCountResult> userCountQuery(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelectQuery('SELECT COUNT(id) FROM users',
variables: [], readsFrom: {users}).map(_rowToUserCountResult);
}
Future<List<UserCountResult>> userCount(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return userCountQuery(operateOn: operateOn).get();
}
Stream<List<UserCountResult>> watchUserCount() {
return userCountQuery().watch();
}
SettingsForResult _rowToSettingsForResult(QueryRow row) {
return SettingsForResult(
preferences:
$UsersTable.$converter0.mapToDart(row.readString('preferences')),
);
}
Selectable<SettingsForResult> settingsForQuery(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelectQuery(
'SELECT preferences FROM users WHERE id = :user',
variables: [
Variable.withInt(user),
],
readsFrom: {
users
}).map(_rowToSettingsForResult);
}
Future<List<SettingsForResult>> settingsFor(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return settingsForQuery(user, operateOn: operateOn).get();
}
Stream<List<SettingsForResult>> watchSettingsFor(int user) {
return settingsForQuery(user).watch();
}
@override
List<TableInfo> get allTables => [users, friendships];
}
class AmountOfGoodFriendsResult {
final int count;
AmountOfGoodFriendsResult({
@ -540,145 +714,3 @@ class SettingsForResult {
this.preferences,
});
}
abstract class _$Database extends GeneratedDatabase {
_$Database(QueryExecutor e) : super(const SqlTypeSystem.withDefaults(), e);
$UsersTable _users;
$UsersTable get users => _users ??= $UsersTable(this);
$FriendshipsTable _friendships;
$FriendshipsTable get friendships => _friendships ??= $FriendshipsTable(this);
User _rowToUser(QueryRow row) {
return User(
id: row.readInt('id'),
name: row.readString('name'),
birthDate: row.readDateTime('birth_date'),
profilePicture: row.readBlob('profile_picture'),
preferences:
$UsersTable.$converter0.mapToDart(row.readString('preferences')),
);
}
Future<List<User>> mostPopularUsers(
int amount,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT * FROM users u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT :amount',
variables: [
Variable.withInt(amount),
]).then((rows) => rows.map(_rowToUser).toList());
}
Stream<List<User>> watchMostPopularUsers(int amount) {
return customSelectStream(
'SELECT * FROM users u ORDER BY (SELECT COUNT(*) FROM friendships WHERE first_user = u.id OR second_user = u.id) DESC LIMIT :amount',
variables: [
Variable.withInt(amount),
],
readsFrom: {
users,
friendships
}).map((rows) => rows.map(_rowToUser).toList());
}
AmountOfGoodFriendsResult _rowToAmountOfGoodFriendsResult(QueryRow row) {
return AmountOfGoodFriendsResult(
count: row.readInt('COUNT(*)'),
);
}
Future<List<AmountOfGoodFriendsResult>> amountOfGoodFriends(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT COUNT(*) FROM friendships f WHERE f.really_good_friends AND (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
]).then((rows) => rows.map(_rowToAmountOfGoodFriendsResult).toList());
}
Stream<List<AmountOfGoodFriendsResult>> watchAmountOfGoodFriends(int user) {
return customSelectStream(
'SELECT COUNT(*) FROM friendships f WHERE f.really_good_friends AND (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
],
readsFrom: {
friendships
}).map((rows) => rows.map(_rowToAmountOfGoodFriendsResult).toList());
}
Future<List<User>> friendsOf(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT u.* FROM friendships f\n INNER JOIN users u ON u.id IN (f.first_user, f.second_user) AND\n u.id != :user\n WHERE (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
]).then((rows) => rows.map(_rowToUser).toList());
}
Stream<List<User>> watchFriendsOf(int user) {
return customSelectStream(
'SELECT u.* FROM friendships f\n INNER JOIN users u ON u.id IN (f.first_user, f.second_user) AND\n u.id != :user\n WHERE (f.first_user = :user OR f.second_user = :user)',
variables: [
Variable.withInt(user),
],
readsFrom: {
friendships,
users
}).map((rows) => rows.map(_rowToUser).toList());
}
UserCountResult _rowToUserCountResult(QueryRow row) {
return UserCountResult(
cOUNTid: row.readInt('COUNT(id)'),
);
}
Future<List<UserCountResult>> userCount(
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect('SELECT COUNT(id) FROM users',
variables: []).then((rows) => rows.map(_rowToUserCountResult).toList());
}
Stream<List<UserCountResult>> watchUserCount() {
return customSelectStream('SELECT COUNT(id) FROM users',
variables: [], readsFrom: {users})
.map((rows) => rows.map(_rowToUserCountResult).toList());
}
SettingsForResult _rowToSettingsForResult(QueryRow row) {
return SettingsForResult(
preferences:
$UsersTable.$converter0.mapToDart(row.readString('preferences')),
);
}
Future<List<SettingsForResult>> settingsFor(
int user,
{@Deprecated('No longer needed with Moor 1.6 - see the changelog for details')
QueryEngine operateOn}) {
return (operateOn ?? this).customSelect(
'SELECT preferences FROM users WHERE id = :user',
variables: [
Variable.withInt(user),
]).then((rows) => rows.map(_rowToSettingsForResult).toList());
}
Stream<List<SettingsForResult>> watchSettingsFor(int user) {
return customSelectStream('SELECT preferences FROM users WHERE id = :user',
variables: [
Variable.withInt(user),
],
readsFrom: {
users
}).map((rows) => rows.map(_rowToSettingsForResult).toList());
}
@override
List<TableInfo> get allTables => [users, friendships];
}

View File

@ -9,13 +9,12 @@ import 'package:sqlparser/sqlparser.dart';
class CreateTableReader {
/// The AST of this `CREATE TABLE` statement.
final ParseResult ast;
final CreateTableStatement stmt;
CreateTableReader(this.ast);
CreateTableReader(this.stmt);
SpecifiedTable extractTable(TypeMapper mapper) {
final table =
SchemaFromCreateTable().read(ast.rootNode as CreateTableStatement);
final table = SchemaFromCreateTable().read(stmt);
final foundColumns = <String, SpecifiedColumn>{};
final primaryKey = <SpecifiedColumn>{};

View File

@ -10,31 +10,27 @@ class MoorParser {
MoorParser(this.task);
Future<ParsedMoorFile> parseAndAnalyze() {
final engine = SqlEngine(useMoorExtensions: true);
final tokens = engine.tokenize(task.content);
final results =
SqlEngine(useMoorExtensions: true).parseMultiple(tokens, task.content);
final result =
SqlEngine(useMoorExtensions: true).parseMoorFile(task.content);
final parsedFile = result.rootNode as MoorFile;
final createdReaders = <CreateTableReader>[];
for (var parsedStmt in results) {
if (parsedStmt.rootNode is ImportStatement) {
final importStmt = (parsedStmt.rootNode) as ImportStatement;
for (var parsedStmt in parsedFile.statements) {
if (parsedStmt is ImportStatement) {
final importStmt = parsedStmt;
task.inlineDartResolver.importStatements.add(importStmt.importedFile);
} else if (parsedStmt.rootNode is CreateTableStatement) {
} else if (parsedStmt is CreateTableStatement) {
createdReaders.add(CreateTableReader(parsedStmt));
} else {
task.reportError(ErrorInMoorFile(
span: parsedStmt.rootNode.span,
span: parsedStmt.span,
message: 'At the moment, only CREATE TABLE statements are supported'
'in .moor files'));
}
}
// all results have the same list of errors
final sqlErrors = results.isEmpty ? <ParsingError>[] : results.first.errors;
for (var error in sqlErrors) {
for (var error in result.errors) {
task.reportError(ErrorInMoorFile(
span: error.token.span,
message: error.message,
@ -44,12 +40,6 @@ class MoorParser {
final createdTables =
createdReaders.map((r) => r.extractTable(task.mapper)).toList();
final statements =
results.map((r) => r.rootNode).cast<Statement>().toList();
final parsedFile =
ParsedMoorFile(tokens, statements, declaredTables: createdTables);
return Future.value(parsedFile);
return Future.value(ParsedMoorFile(createdTables));
}
}

View File

@ -21,7 +21,9 @@ part 'expressions/subquery.dart';
part 'expressions/tuple.dart';
part 'expressions/variables.dart';
part 'moor/declared_statement.dart';
part 'moor/import_statement.dart';
part 'moor/moor_file.dart';
part 'schema/column_definition.dart';
part 'schema/table_definition.dart';
@ -177,7 +179,9 @@ abstract class AstVisitor<T> {
T visitNumberedVariable(NumberedVariable e);
T visitNamedVariable(ColonNamedVariable e);
T visitMoorFile(MoorFile e);
T visitMoorImportStatement(ImportStatement e);
T visitMoorDeclaredStatement(DeclaredStatement e);
}
/// Visitor that walks down the entire tree, visiting all children in order.
@ -290,9 +294,15 @@ class RecursiveVisitor<T> extends AstVisitor<T> {
@override
T visitFrameSpec(FrameSpec e) => visitChildren(e);
@override
T visitMoorFile(MoorFile e) => visitChildren(e);
@override
T visitMoorImportStatement(ImportStatement e) => visitChildren(e);
@override
T visitMoorDeclaredStatement(DeclaredStatement e) => visitChildren(e);
@protected
T visitChildren(AstNode e) {
for (var child in e.childNodes) {

View File

@ -0,0 +1,25 @@
part of '../ast.dart';
/// A declared statement inside a `.moor` file. It consists of an identifier,
/// followed by a colon and the query to run.
class DeclaredStatement extends Statement implements PartOfMoorFile {
final String name;
final CrudStatement statement;
IdentifierToken identifier;
Token colon;
DeclaredStatement(this.name, this.statement);
@override
T accept<T>(AstVisitor<T> visitor) =>
visitor.visitMoorDeclaredStatement(this);
@override
Iterable<AstNode> get childNodes => [statement];
@override
bool contentEquals(DeclaredStatement other) {
return other.name == name;
}
}

View File

@ -1,6 +1,7 @@
part of '../ast.dart';
class ImportStatement extends Statement {
/// An `import "file.dart";` statement that can appear inside a moor file.
class ImportStatement extends Statement implements PartOfMoorFile {
Token importToken;
StringLiteralToken importString;
final String importedFile;

View File

@ -0,0 +1,19 @@
part of '../ast.dart';
/// Something that can appear as a top-level declaration inside a `.moor` file.
abstract class PartOfMoorFile implements Statement {}
class MoorFile extends AstNode {
final List<PartOfMoorFile> statements;
MoorFile(this.statements);
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitMoorFile(this);
@override
Iterable<AstNode> get childNodes => statements;
@override
bool contentEquals(MoorFile other) => true;
}

View File

@ -2,7 +2,9 @@ part of '../ast.dart';
/// A "CREATE TABLE" statement, see https://www.sqlite.org/lang_createtable.html
/// for the individual components.
class CreateTableStatement extends Statement with SchemaStatement {
class CreateTableStatement extends Statement
with SchemaStatement
implements PartOfMoorFile {
final bool ifNotExists;
final String tableName;
final List<ColumnDefinition> columns;

View File

@ -0,0 +1,14 @@
part of '../engine.dart';
/// Attached to a hint. A [HintDescription], together with additional context,
/// can be used to compute the available autocomplete suggestions.
abstract class HintDescription {
const HintDescription();
const factory HintDescription.tokens(List<TokenType> types) =
TokensDescription;
factory HintDescription.token(TokenType type) = TokensDescription.single;
Iterable<Suggestion> suggest(CalculationRequest request);
}

View File

@ -0,0 +1,20 @@
part of '../engine.dart';
/// Suggestion that just inserts a bunch of token types with whitespace in
/// between.
class TokensDescription extends HintDescription {
final List<TokenType> types;
const TokensDescription(this.types);
TokensDescription.single(TokenType type) : types = [type];
@override
Iterable<Suggestion> suggest(CalculationRequest request) sync* {
final code = types
.map((type) => reverseKeywords[type])
.where((k) => k != null)
.join(' ');
yield Suggestion(code, 0);
}
}

View File

@ -0,0 +1,82 @@
import 'package:collection/collection.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
part 'descriptions/description.dart';
part 'descriptions/static.dart';
part 'suggestion.dart';
/// Helper to provide context aware auto-complete suggestions inside a sql
/// query.
///
/// While parsing a query, the parser will yield a bunch of [Hint]s that are
/// specific to a specific location. Each hint contains the current position and
/// a [HintDescription] of what can appear behind that position.
/// To obtain suggestions for a specific cursor position, we then go back from
/// that position to the last [Hint] found and populate it.
class AutoCompleteEngine {
/// The found hints.
UnmodifiableListView<Hint> get foundHints => _hintsView;
// hints are always sorted by their offset
final List<Hint> _hints = [];
UnmodifiableListView<Hint> _hintsView;
void addHint(Hint hint) {
_hints.insert(_lastHintBefore(hint.offset), hint);
}
AutoCompleteEngine() {
_hintsView = UnmodifiableListView(_hints);
}
/// Suggest completions at a specific position.
///
/// This api will change in the future.
ComputedSuggestions suggestCompletions(int offset) {
if (_hints.isEmpty) {
return ComputedSuggestions(-1, -1, []);
}
final hint = foundHints[_lastHintBefore(offset)];
final suggestions = hint.description.suggest(CalculationRequest()).toList();
return ComputedSuggestions(hint.offset, offset - hint.offset, suggestions);
}
int _lastHintBefore(int offset) {
// find the last hint that appears before offset
var min = 0;
var max = foundHints.length;
while (min < max) {
final mid = min + ((max - min) >> 1);
final hint = _hints[mid];
final offsetOfMid = hint.offset;
if (offsetOfMid == offset) {
return mid;
} else if (offsetOfMid < offset) {
min = mid + 1;
} else {
max = mid - 1;
}
}
return min;
}
}
class Hint {
/// The token that appears just before this hint, or `null` if the hint
/// appears at the beginning of the file.
final Token before;
int get offset => before?.span?.end?.offset ?? 0;
final HintDescription description;
Hint(this.before, this.description);
}
class CalculationRequest {}

View File

@ -0,0 +1,30 @@
part of 'engine.dart';
/// The result of suggesting auto-complete at a specific location.
class ComputedSuggestions {
/// The offset from the source file from which the suggestion should be
/// applied. Effectively, the range from [anchor] to `anchor + lengthBefore`
/// will be replaced with the suggestion.
final int anchor;
/// The amount of chars that have already been typed and would be replaced
/// when applying a suggestion.
final int lengthBefore;
/// The actual suggestions which are relevant here.
final List<Suggestion> suggestions;
ComputedSuggestions(this.anchor, this.lengthBefore, this.suggestions);
}
/// A single auto-complete suggestion.
class Suggestion {
/// The code inserted.
final String code;
/// The relevance of this suggestion, where more relevant suggestions have a
/// higher [relevance].
final int relevance;
Suggestion(this.code, this.relevance);
}

View File

@ -1,5 +1,6 @@
import 'package:sqlparser/src/analysis/analysis.dart';
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/engine/autocomplete/engine.dart';
import 'package:sqlparser/src/reader/parser/parser.dart';
import 'package:sqlparser/src/reader/tokenizer/scanner.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
@ -49,24 +50,21 @@ class SqlEngine {
final parser = Parser(tokens, useMoor: useMoorExtensions);
final stmt = parser.statement();
return ParseResult._(stmt, tokens, parser.errors, sql);
return ParseResult._(stmt, tokens, parser.errors, sql, null);
}
/// Parses multiple sql statements, separated by a semicolon. All
/// [ParseResult] entries will have the same [ParseResult.errors], but the
/// [ParseResult.sql] will only refer to the substring creating a statement.
List<ParseResult> parseMultiple(List<Token> tokens, String sql) {
final parser = Parser(tokens);
/// Parses a `.moor` file, which can consist of multiple statements and
/// additional components like import statements.
ParseResult parseMoorFile(String content) {
assert(useMoorExtensions);
final stmts = parser.statements();
final autoComplete = AutoCompleteEngine();
final tokens = tokenize(content);
final parser = Parser(tokens, useMoor: true, autoComplete: autoComplete);
return stmts.map((statement) {
final first = statement.firstPosition;
final last = statement.lastPosition;
final moorFile = parser.moorFile();
final source = sql.substring(first, last);
return ParseResult._(statement, tokens, parser.errors, source);
}).toList();
return ParseResult._(moorFile, tokens, parser.errors, content, autoComplete);
}
/// Parses and analyzes the [sql] statement. The [AnalysisContext] returned
@ -128,5 +126,9 @@ class ParseResult {
/// The sql source that created the AST at [rootNode].
final String sql;
ParseResult._(this.rootNode, this.tokens, this.errors, this.sql);
/// The engine which can be used to handle auto-complete requests on this
/// result.
final AutoCompleteEngine autoCompleteEngine;
ParseResult._(this.rootNode, this.tokens, this.errors, this.sql, this.autoCompleteEngine);
}

View File

@ -1,5 +1,6 @@
import 'package:meta/meta.dart';
import 'package:sqlparser/src/ast/ast.dart';
import 'package:sqlparser/src/engine/autocomplete/engine.dart';
import 'package:sqlparser/src/reader/tokenizer/token.dart';
part 'crud.dart';
@ -43,13 +44,19 @@ class ParsingError implements Exception {
abstract class ParserBase {
final List<Token> tokens;
final List<ParsingError> errors = [];
final AutoCompleteEngine autoComplete;
/// Whether to enable the extensions moor makes to the sql grammar.
final bool enableMoorExtensions;
int _current = 0;
ParserBase(this.tokens, this.enableMoorExtensions);
ParserBase(this.tokens, this.enableMoorExtensions, this.autoComplete);
void _suggestHint(HintDescription description) {
final tokenBefore = _current == 0 ? null : _previous;
autoComplete?.addHint(Hint(tokenBefore, description));
}
bool get _isAtEnd => _peek.type == TokenType.eof;
Token get _peek => tokens[_current];
@ -153,18 +160,17 @@ abstract class ParserBase {
class Parser extends ParserBase
with ExpressionParser, SchemaParser, CrudParser {
Parser(List<Token> tokens, {bool useMoor = false}) : super(tokens, useMoor);
Parser(List<Token> tokens,
{bool useMoor = false, AutoCompleteEngine autoComplete})
: super(tokens, useMoor, autoComplete);
Statement statement({bool expectEnd = true}) {
Statement statement() {
final first = _peek;
var stmt = select() ??
_deleteStmt() ??
_update() ??
_insertStmt() ??
_createTable();
Statement stmt = _crud();
stmt ??= _createTable();
if (enableMoorExtensions) {
stmt ??= _import();
stmt ??= _import() ?? _declaredStatement();
}
if (stmt == null) {
@ -175,12 +181,61 @@ class Parser extends ParserBase
stmt.semicolon = _previous;
}
if (!_isAtEnd && expectEnd) {
if (!_isAtEnd) {
_error('Expected the statement to finish here');
}
return stmt..setSpan(first, _previous);
}
CrudStatement _crud() {
// writing select() ?? _deleteStmt() and so on doesn't cast to CrudStatement
// for some reason.
CrudStatement stmt = select();
stmt ??= _deleteStmt();
stmt ??= _update();
stmt ??= _insertStmt();
return stmt;
}
MoorFile moorFile() {
final first = _peek;
final foundComponents = <PartOfMoorFile>[];
// first, parse import statements
for (var stmt = _parseAsStatement(_import);
stmt != null;
stmt = _parseAsStatement(_import)) {
foundComponents.add(stmt);
}
// next, table declarations
for (var stmt = _parseAsStatement(_createTable);
stmt != null;
stmt = _parseAsStatement(_createTable)) {
foundComponents.add(stmt);
}
// finally, declared statements
for (var stmt = _parseAsStatement(_declaredStatement);
stmt != null;
stmt = _parseAsStatement(_declaredStatement)) {
foundComponents.add(stmt);
}
if (!_isAtEnd) {
_error('Expected the file to end here.');
}
final file = MoorFile(foundComponents);
if (foundComponents.isNotEmpty) {
file.setSpan(first, _previous);
} else {
file.setSpan(first, first); // empty file
}
return file;
}
ImportStatement _import() {
if (_matchOne(TokenType.import)) {
final importToken = _previous;
@ -195,18 +250,44 @@ class Parser extends ParserBase
return null;
}
List<Statement> statements() {
final stmts = <Statement>[];
while (!_isAtEnd) {
try {
stmts.add(statement(expectEnd: false));
} on ParsingError catch (_) {
// the error is added to the list errors, so ignore. We skip to the next
// semicolon to parse the next statement.
_synchronize();
}
DeclaredStatement _declaredStatement() {
if (_check(TokenType.identifier) || _peek is KeywordToken) {
final name = _consumeIdentifier('Expected a name for a declared query',
lenient: true);
final colon =
_consume(TokenType.colon, 'Expected colon (:) followed by a query');
final stmt = _crud();
return DeclaredStatement(name.identifier, stmt)
..identifier = name
..colon = colon;
}
return stmts;
return null;
}
/// Invokes [parser], sets the appropriate source span and attaches a
/// semicolon if one exists.
T _parseAsStatement<T extends Statement>(T Function() parser) {
final first = _peek;
T result;
try {
result = parser();
} on ParsingError catch (_) {
// the error is added to the list errors, so ignore. We skip to the next
// semicolon to parse the next statement.
_synchronize();
}
if (result == null) return null;
if (_matchOne(TokenType.semicolon)) {
result.semicolon = _previous;
}
result.setSpan(first, _previous);
return result;
}
void _synchronize() {

View File

@ -2,9 +2,12 @@ part of 'parser.dart';
mixin SchemaParser on ParserBase {
CreateTableStatement _createTable() {
_suggestHint(
const HintDescription.tokens([TokenType.create, TokenType.table]));
if (!_matchOne(TokenType.create)) return null;
final first = _previous;
_suggestHint(HintDescription.token(TokenType.table));
_consume(TokenType.table, 'Expected TABLE keyword here');
var ifNotExists = false;

View File

@ -237,6 +237,12 @@ const Map<String, TokenType> keywords = {
'VALUES': TokenType.$values,
};
/// Maps [TokenType]s which are keywords to their lexeme.
final reverseKeywords = {
for (var entry in keywords.entries) entry.value: entry.key,
for (var entry in moorKeywords.entries) entry.value: entry.key,
};
const Map<String, TokenType> moorKeywords = {
'MAPPED': TokenType.mapped,
'IMPORT': TokenType.import,

View File

@ -0,0 +1,43 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/src/engine/autocomplete/engine.dart';
import 'package:test/test.dart';
void main() {
test('suggests a CREATE TABLE statements for an empty file', () {
final engine = SqlEngine(useMoorExtensions: true);
final parseResult = engine.parseMoorFile('');
final suggestions = parseResult.autoCompleteEngine.suggestCompletions(0);
expect(suggestions.anchor, 0);
expect(suggestions.suggestions, contains(hasCode('CREATE TABLE')));
});
test('suggests completions for started expressions', () {
final engine = SqlEngine(useMoorExtensions: true);
final parseResult = engine.parseMoorFile('creat');
final suggestions = parseResult.autoCompleteEngine.suggestCompletions(0);
expect(suggestions.anchor, 0);
expect(suggestions.suggestions, contains(hasCode('CREATE TABLE')));
});
}
dynamic hasCode(code) => SuggestionWithCode(code);
class SuggestionWithCode extends Matcher {
final Matcher codeMatcher;
SuggestionWithCode(dynamic code) : codeMatcher = wrapMatcher(code);
@override
Description describe(Description description) {
return description.add('suggests ').addDescriptionOf(codeMatcher);
}
@override
bool matches(item, Map matchState) {
return item is Suggestion && codeMatcher.matches(item.code, matchState);
}
}

View File

@ -6,54 +6,65 @@ import 'package:test/test.dart';
void main() {
test('can parse multiple statements', () {
final sql = 'UPDATE tbl SET a = b; SELECT * FROM tbl;';
final sql = 'a: UPDATE tbl SET a = b; b: SELECT * FROM tbl;';
final tokens = Scanner(sql).scanTokens();
final statements = Parser(tokens).statements();
final moorFile = Parser(tokens).moorFile();
final statements = moorFile.statements;
enforceEqual(
statements[0],
UpdateStatement(
table: TableReference('tbl', null),
set: [
SetComponent(
column: Reference(columnName: 'a'),
expression: Reference(columnName: 'b'),
),
],
DeclaredStatement(
'a',
UpdateStatement(
table: TableReference('tbl', null),
set: [
SetComponent(
column: Reference(columnName: 'a'),
expression: Reference(columnName: 'b'),
),
],
),
),
);
enforceEqual(
statements[1],
SelectStatement(
columns: [StarResultColumn(null)],
from: [TableReference('tbl', null)],
DeclaredStatement(
'b',
SelectStatement(
columns: [StarResultColumn(null)],
from: [TableReference('tbl', null)],
),
),
);
});
test('recovers from invalid statements', () {
final sql = 'UPDATE tbl SET a = * d; SELECT * FROM tbl;';
final sql = 'a: UPDATE tbl SET a = * d; b: SELECT * FROM tbl;';
final tokens = Scanner(sql).scanTokens();
final statements = Parser(tokens).statements();
final statements = Parser(tokens).moorFile().statements;
expect(statements, hasLength(1));
enforceEqual(
statements[0],
SelectStatement(
columns: [StarResultColumn(null)],
from: [TableReference('tbl', null)],
DeclaredStatement(
'b',
SelectStatement(
columns: [StarResultColumn(null)],
from: [TableReference('tbl', null)],
),
),
);
});
test('parses import directives in moor mode', () {
test('parses imports and declared statements in moor mode', () {
final sql = r'''
import 'test.dart';
SELECT * FROM tbl;
query: SELECT * FROM tbl;
''';
final tokens = Scanner(sql, scanMoorTokens: true).scanTokens();
final statements = Parser(tokens, useMoor: true).statements();
final statements = Parser(tokens, useMoor: true).moorFile().statements;
expect(statements, hasLength(2));
@ -62,5 +73,17 @@ void main() {
expect(parsedImport.importToken, tokens[0]);
expect(parsedImport.importString, tokens[1]);
expect(parsedImport.semicolon, tokens[2]);
final declared = statements[1] as DeclaredStatement;
enforceEqual(
declared,
DeclaredStatement(
'query',
SelectStatement(
columns: [StarResultColumn(null)],
from: [TableReference('tbl', null)],
),
),
);
});
}