mirror of https://github.com/AMT-Cheif/drift.git
Update documentation, fix multiline parsing
This commit is contained in:
parent
b2f79e97e6
commit
a45d73a6cf
|
@ -141,3 +141,6 @@ now run your queries with fluent Dart code:
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
Visit the detailed [documentation](https://moor.simonbinder.eu/) to learn about advanced
|
||||
features like transactions, DAOs, custom queries and more.
|
|
@ -5,7 +5,7 @@ the sqlite dialect and some advanced features aren't supported yet.
|
|||
|
||||
## Features
|
||||
This library can parse most statements and perform type analysis for parameters and returned
|
||||
columns. It supports joins, `group by`, nested sql statements, updates and deletes and more.
|
||||
columns. It supports joins, `group by`, nested sql statements, updates and deletes, and more.
|
||||
### Just parsing
|
||||
You can parse the abstract syntax tree of sqlite statements with `SqlEngine.parse`.
|
||||
```dart
|
||||
|
@ -30,7 +30,7 @@ Given information about all tables and a sql statement, this library can:
|
|||
2. Make an educated guess about what type the variables in the query should have (it's not really
|
||||
possible to be 100% accurate about this because sqlite is very flexible at types, but this library
|
||||
gets it mostly right)
|
||||
3. Issue warnings about queries that are syntactically valid but won't run (references unknown
|
||||
3. Issue basic warnings about queries that are syntactically valid but won't run (references unknown
|
||||
tables / columns, uses undefined functions, etc.)
|
||||
|
||||
To use the analyzer, first register all known tables via `SqlEngine.registerTable`. Then,
|
||||
|
|
|
@ -1 +1,81 @@
|
|||
// todo write example
|
||||
import 'package:sqlparser/sqlparser.dart';
|
||||
|
||||
// Example that parses a select statement on some tables defined below and
|
||||
// prints what columns would be returned by that statement.
|
||||
void main() {
|
||||
final engine = SqlEngine()
|
||||
..registerTable(frameworks)
|
||||
..registerTable(languages)
|
||||
..registerTable(frameworkToLanguage);
|
||||
|
||||
final result = engine.analyze('''
|
||||
SELECT f.* FROM frameworks f
|
||||
INNER JOIN uses_language ul ON ul.framework = f.id
|
||||
INNER JOIN languages l ON l.id = ul.language
|
||||
WHERE l.name = 'Dart'
|
||||
ORDER BY f.name ASC, f.popularity DESC
|
||||
LIMIT 5 OFFSET 5 * 3
|
||||
''');
|
||||
|
||||
for (var error in result.errors) {
|
||||
print(error);
|
||||
}
|
||||
|
||||
final select = result.root as SelectStatement;
|
||||
final columns = select.resolvedColumns;
|
||||
|
||||
print('the query returns ${columns.length} columns');
|
||||
|
||||
for (var column in columns) {
|
||||
final type = result.typeOf(column);
|
||||
print('${column.name}, which will be a $type');
|
||||
}
|
||||
}
|
||||
|
||||
// declare some tables. I know this is verbose and boring, but it's needed so
|
||||
// that the analyzer knows what's going on.
|
||||
final Table frameworks = Table(
|
||||
name: 'frameworks',
|
||||
resolvedColumns: [
|
||||
TableColumn(
|
||||
'id',
|
||||
const ResolvedType(type: BasicType.int),
|
||||
),
|
||||
TableColumn(
|
||||
'name',
|
||||
const ResolvedType(type: BasicType.text),
|
||||
),
|
||||
TableColumn(
|
||||
'popularity',
|
||||
const ResolvedType(type: BasicType.real),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final Table languages = Table(
|
||||
name: 'languages',
|
||||
resolvedColumns: [
|
||||
TableColumn(
|
||||
'id',
|
||||
const ResolvedType(type: BasicType.int),
|
||||
),
|
||||
TableColumn(
|
||||
'name',
|
||||
const ResolvedType(type: BasicType.text),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final Table frameworkToLanguage = Table(
|
||||
name: 'uses_language',
|
||||
resolvedColumns: [
|
||||
TableColumn(
|
||||
'framework',
|
||||
const ResolvedType(type: BasicType.int),
|
||||
),
|
||||
TableColumn(
|
||||
'language',
|
||||
const ResolvedType(type: BasicType.int),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -6,6 +6,19 @@ class AnalysisError {
|
|||
final AnalysisErrorType type;
|
||||
|
||||
AnalysisError({@required this.type, this.message, this.relevantNode});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final first = relevantNode?.first?.span;
|
||||
final last = relevantNode?.last?.span;
|
||||
|
||||
if (first != null && last != null) {
|
||||
final span = first.expand(last);
|
||||
return span.message(message ?? type.toString(), color: true);
|
||||
} else {
|
||||
return 'Error: $type: $message at $relevantNode';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum AnalysisErrorType {
|
||||
|
|
|
@ -2,7 +2,6 @@ part of '../ast.dart';
|
|||
|
||||
/// Marker interface for something that can appear after a "FROM" in a select
|
||||
/// statement.
|
||||
@sealed
|
||||
abstract class Queryable extends AstNode {
|
||||
@override
|
||||
T accept<T>(AstVisitor<T> visitor) => visitor.visitQueryable(this);
|
||||
|
@ -143,7 +142,6 @@ class Join extends AstNode {
|
|||
}
|
||||
|
||||
/// https://www.sqlite.org/syntax/join-constraint.html
|
||||
@sealed
|
||||
abstract class JoinConstraint {}
|
||||
|
||||
class OnConstraint extends JoinConstraint {
|
||||
|
|
|
@ -55,11 +55,15 @@ class SqlEngine {
|
|||
final context = AnalysisContext(node, sql);
|
||||
final scope = _constructRootScope();
|
||||
|
||||
ReferenceFinder(globalScope: scope).start(node);
|
||||
node
|
||||
..accept(ColumnResolver(context))
|
||||
..accept(ReferenceResolver(context))
|
||||
..accept(TypeResolvingVisitor(context));
|
||||
try {
|
||||
ReferenceFinder(globalScope: scope).start(node);
|
||||
node
|
||||
..accept(ColumnResolver(context))
|
||||
..accept(ReferenceResolver(context))
|
||||
..accept(TypeResolvingVisitor(context));
|
||||
} catch (e) {
|
||||
// todo should we do now? Mostly, everything
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
|
|
@ -667,15 +667,18 @@ class Parser {
|
|||
_consume(TokenType.identifier, 'Expected a column name here')
|
||||
as IdentifierToken;
|
||||
return Reference(
|
||||
tableName: first.identifier, columnName: second.identifier);
|
||||
tableName: first.identifier, columnName: second.identifier)
|
||||
..setSpan(first, second);
|
||||
} else if (_matchOne(TokenType.leftParen)) {
|
||||
final parameters = _functionParameters();
|
||||
_consume(TokenType.rightParen,
|
||||
final rightParen = _consume(TokenType.rightParen,
|
||||
'Expected closing bracket after argument list');
|
||||
|
||||
return FunctionExpression(
|
||||
name: first.identifier, parameters: parameters);
|
||||
name: first.identifier, parameters: parameters)
|
||||
..setSpan(first, rightParen);
|
||||
} else {
|
||||
return Reference(columnName: first.identifier);
|
||||
return Reference(columnName: first.identifier)..setSpan(first, first);
|
||||
}
|
||||
break;
|
||||
case TokenType.questionMark:
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:sqlparser/src/reader/tokenizer/utils.dart';
|
|||
|
||||
class Scanner {
|
||||
final String source;
|
||||
final SourceFile _file;
|
||||
|
||||
final List<Token> tokens = [];
|
||||
final List<TokenizerError> errors = [];
|
||||
|
@ -12,20 +13,15 @@ class Scanner {
|
|||
int _currentOffset = 0;
|
||||
bool get _isAtEnd => _currentOffset >= source.length;
|
||||
|
||||
SourceSpan get _currentSpan {
|
||||
return SourceSpanWithContext(_startLocation, _currentLocation,
|
||||
source.substring(_startOffset, _currentOffset), source);
|
||||
}
|
||||
|
||||
SourceLocation get _startLocation {
|
||||
return SourceLocation(_startOffset);
|
||||
FileSpan get _currentSpan {
|
||||
return _file.span(_startOffset, _currentOffset);
|
||||
}
|
||||
|
||||
SourceLocation get _currentLocation {
|
||||
return SourceLocation(_currentOffset);
|
||||
return _file.location(_currentOffset);
|
||||
}
|
||||
|
||||
Scanner(this.source);
|
||||
Scanner(this.source) : _file = SourceFile.fromString(source);
|
||||
|
||||
List<Token> scanTokens() {
|
||||
while (!_isAtEnd) {
|
||||
|
@ -33,8 +29,8 @@ class Scanner {
|
|||
_scanToken();
|
||||
}
|
||||
|
||||
final endLoc = SourceLocation(source.length);
|
||||
tokens.add(Token(TokenType.eof, SourceSpan(endLoc, endLoc, '')));
|
||||
final endSpan = _file.span(source.length);
|
||||
tokens.add(Token(TokenType.eof, endSpan));
|
||||
return tokens;
|
||||
}
|
||||
|
||||
|
|
|
@ -157,7 +157,7 @@ const Map<String, TokenType> keywords = {
|
|||
class Token {
|
||||
final TokenType type;
|
||||
|
||||
final SourceSpan span;
|
||||
final FileSpan span;
|
||||
String get lexeme => span.text;
|
||||
|
||||
const Token(this.type, this.span);
|
||||
|
@ -174,7 +174,7 @@ class StringLiteralToken extends Token {
|
|||
/// sqlite allows binary strings (x'literal') which are interpreted as blobs.
|
||||
final bool binary;
|
||||
|
||||
const StringLiteralToken(this.value, SourceSpan span, {this.binary = false})
|
||||
const StringLiteralToken(this.value, FileSpan span, {this.binary = false})
|
||||
: super(TokenType.stringLiteral, span);
|
||||
}
|
||||
|
||||
|
@ -190,7 +190,7 @@ class IdentifierToken extends Token {
|
|||
}
|
||||
}
|
||||
|
||||
const IdentifierToken(this.escaped, SourceSpan span)
|
||||
const IdentifierToken(this.escaped, FileSpan span)
|
||||
: super(TokenType.identifier, span);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
name: sqlparser
|
||||
description: Parsing and analysis for sql queries
|
||||
description: Parses sqlite statements and performs static analysis on them
|
||||
version: 0.1.0
|
||||
repository: https://github.com/simolus3/moor/tree/develop/sqlparser
|
||||
homepage: https://github.com/simolus3/moor/tree/develop/sqlparser
|
||||
#homepage: https://moor.simonbinder.eu/
|
||||
issue_tracker: https://github.com/simolus3/moor/issues
|
||||
author: Simon Binder <oss@simonbinder.eu>
|
||||
|
||||
environment:
|
||||
sdk: '>=2.2.2 <3.0.0'
|
||||
meta: ^1.1.7
|
||||
|
||||
dependencies:
|
||||
meta: ^1.1.0
|
||||
collection: ^1.14.11
|
||||
source_span: ^1.5.5
|
||||
|
||||
dev_dependencies:
|
||||
test: ^1.0.0
|
||||
test: ^1.6.0
|
||||
|
|
Loading…
Reference in New Issue