diff --git a/extras/integration_tests/tests/lib/database/database.g.dart b/extras/integration_tests/tests/lib/database/database.g.dart index 3f6477c3..51eba060 100644 --- a/extras/integration_tests/tests/lib/database/database.g.dart +++ b/extras/integration_tests/tests/lib/database/database.g.dart @@ -147,6 +147,14 @@ class UsersCompanion extends UpdateCompanion { 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 id, Value 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 { 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 firstUser, Value 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 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> 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> watchMostPopularUsers(int amount) { + return mostPopularUsersQuery(amount).watch(); + } + + AmountOfGoodFriendsResult _rowToAmountOfGoodFriendsResult(QueryRow row) { + return AmountOfGoodFriendsResult( + count: row.readInt('COUNT(*)'), + ); + } + + Selectable 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> 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> watchAmountOfGoodFriends(int user) { + return amountOfGoodFriendsQuery(user).watch(); + } + + Selectable 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> 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> watchFriendsOf(int user) { + return friendsOfQuery(user).watch(); + } + + UserCountResult _rowToUserCountResult(QueryRow row) { + return UserCountResult( + cOUNTid: row.readInt('COUNT(id)'), + ); + } + + Selectable 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> userCount( + {@Deprecated('No longer needed with Moor 1.6 - see the changelog for details') + QueryEngine operateOn}) { + return userCountQuery(operateOn: operateOn).get(); + } + + Stream> watchUserCount() { + return userCountQuery().watch(); + } + + SettingsForResult _rowToSettingsForResult(QueryRow row) { + return SettingsForResult( + preferences: + $UsersTable.$converter0.mapToDart(row.readString('preferences')), + ); + } + + Selectable 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> 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> watchSettingsFor(int user) { + return settingsForQuery(user).watch(); + } + + @override + List 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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 get allTables => [users, friendships]; -} diff --git a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart index 21bf136d..e9381760 100644 --- a/moor_generator/lib/src/analyzer/moor/create_table_reader.dart +++ b/moor_generator/lib/src/analyzer/moor/create_table_reader.dart @@ -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 = {}; final primaryKey = {}; diff --git a/moor_generator/lib/src/analyzer/moor/parser.dart b/moor_generator/lib/src/analyzer/moor/parser.dart index 6be86cdb..316ba9f9 100644 --- a/moor_generator/lib/src/analyzer/moor/parser.dart +++ b/moor_generator/lib/src/analyzer/moor/parser.dart @@ -10,31 +10,27 @@ class MoorParser { MoorParser(this.task); Future 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 = []; - 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 ? [] : 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().toList(); - - final parsedFile = - ParsedMoorFile(tokens, statements, declaredTables: createdTables); - - return Future.value(parsedFile); + return Future.value(ParsedMoorFile(createdTables)); } } diff --git a/sqlparser/lib/src/ast/ast.dart b/sqlparser/lib/src/ast/ast.dart index 403ac59d..2734145b 100644 --- a/sqlparser/lib/src/ast/ast.dart +++ b/sqlparser/lib/src/ast/ast.dart @@ -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 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 extends AstVisitor { @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) { diff --git a/sqlparser/lib/src/ast/moor/declared_statement.dart b/sqlparser/lib/src/ast/moor/declared_statement.dart new file mode 100644 index 00000000..bb73cae8 --- /dev/null +++ b/sqlparser/lib/src/ast/moor/declared_statement.dart @@ -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(AstVisitor visitor) => + visitor.visitMoorDeclaredStatement(this); + + @override + Iterable get childNodes => [statement]; + + @override + bool contentEquals(DeclaredStatement other) { + return other.name == name; + } +} diff --git a/sqlparser/lib/src/ast/moor/import_statement.dart b/sqlparser/lib/src/ast/moor/import_statement.dart index 31115896..bdbfc8d8 100644 --- a/sqlparser/lib/src/ast/moor/import_statement.dart +++ b/sqlparser/lib/src/ast/moor/import_statement.dart @@ -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; diff --git a/sqlparser/lib/src/ast/moor/moor_file.dart b/sqlparser/lib/src/ast/moor/moor_file.dart new file mode 100644 index 00000000..d451d14f --- /dev/null +++ b/sqlparser/lib/src/ast/moor/moor_file.dart @@ -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 statements; + + MoorFile(this.statements); + + @override + T accept(AstVisitor visitor) => visitor.visitMoorFile(this); + + @override + Iterable get childNodes => statements; + + @override + bool contentEquals(MoorFile other) => true; +} diff --git a/sqlparser/lib/src/ast/statements/create_table.dart b/sqlparser/lib/src/ast/statements/create_table.dart index 961a9659..cf7b900f 100644 --- a/sqlparser/lib/src/ast/statements/create_table.dart +++ b/sqlparser/lib/src/ast/statements/create_table.dart @@ -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 columns; diff --git a/sqlparser/lib/src/engine/autocomplete/descriptions/description.dart b/sqlparser/lib/src/engine/autocomplete/descriptions/description.dart new file mode 100644 index 00000000..cef121de --- /dev/null +++ b/sqlparser/lib/src/engine/autocomplete/descriptions/description.dart @@ -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 types) = + TokensDescription; + + factory HintDescription.token(TokenType type) = TokensDescription.single; + + Iterable suggest(CalculationRequest request); +} diff --git a/sqlparser/lib/src/engine/autocomplete/descriptions/static.dart b/sqlparser/lib/src/engine/autocomplete/descriptions/static.dart new file mode 100644 index 00000000..8587eae6 --- /dev/null +++ b/sqlparser/lib/src/engine/autocomplete/descriptions/static.dart @@ -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 types; + + const TokensDescription(this.types); + TokensDescription.single(TokenType type) : types = [type]; + + @override + Iterable suggest(CalculationRequest request) sync* { + final code = types + .map((type) => reverseKeywords[type]) + .where((k) => k != null) + .join(' '); + + yield Suggestion(code, 0); + } +} diff --git a/sqlparser/lib/src/engine/autocomplete/engine.dart b/sqlparser/lib/src/engine/autocomplete/engine.dart new file mode 100644 index 00000000..f8335ff4 --- /dev/null +++ b/sqlparser/lib/src/engine/autocomplete/engine.dart @@ -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 get foundHints => _hintsView; + // hints are always sorted by their offset + final List _hints = []; + UnmodifiableListView _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 {} diff --git a/sqlparser/lib/src/engine/autocomplete/suggestion.dart b/sqlparser/lib/src/engine/autocomplete/suggestion.dart new file mode 100644 index 00000000..8b92dc90 --- /dev/null +++ b/sqlparser/lib/src/engine/autocomplete/suggestion.dart @@ -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 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); +} diff --git a/sqlparser/lib/src/engine/sql_engine.dart b/sqlparser/lib/src/engine/sql_engine.dart index ad93896e..e978e75c 100644 --- a/sqlparser/lib/src/engine/sql_engine.dart +++ b/sqlparser/lib/src/engine/sql_engine.dart @@ -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 parseMultiple(List 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); } diff --git a/sqlparser/lib/src/reader/parser/parser.dart b/sqlparser/lib/src/reader/parser/parser.dart index 340ee8bf..007392a3 100644 --- a/sqlparser/lib/src/reader/parser/parser.dart +++ b/sqlparser/lib/src/reader/parser/parser.dart @@ -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 tokens; final List 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 tokens, {bool useMoor = false}) : super(tokens, useMoor); + Parser(List 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 = []; + + // 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 statements() { - final stmts = []; - 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 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() { diff --git a/sqlparser/lib/src/reader/parser/schema.dart b/sqlparser/lib/src/reader/parser/schema.dart index e33e7335..e0fe8cbc 100644 --- a/sqlparser/lib/src/reader/parser/schema.dart +++ b/sqlparser/lib/src/reader/parser/schema.dart @@ -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; diff --git a/sqlparser/lib/src/reader/tokenizer/token.dart b/sqlparser/lib/src/reader/tokenizer/token.dart index c7ea7f00..8f1a71fb 100644 --- a/sqlparser/lib/src/reader/tokenizer/token.dart +++ b/sqlparser/lib/src/reader/tokenizer/token.dart @@ -237,6 +237,12 @@ const Map 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 moorKeywords = { 'MAPPED': TokenType.mapped, 'IMPORT': TokenType.import, diff --git a/sqlparser/test/engine/autocomplete/static_test.dart b/sqlparser/test/engine/autocomplete/static_test.dart new file mode 100644 index 00000000..9c347f43 --- /dev/null +++ b/sqlparser/test/engine/autocomplete/static_test.dart @@ -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); + } +} diff --git a/sqlparser/test/parser/multiple_statements.dart b/sqlparser/test/parser/multiple_statements.dart index 43b0e1f7..0842b81b 100644 --- a/sqlparser/test/parser/multiple_statements.dart +++ b/sqlparser/test/parser/multiple_statements.dart @@ -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)], + ), + ), + ); }); }