mirror of https://github.com/AMT-Cheif/drift.git
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:
commit
7354b56712
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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>{};
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 {}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue