mirror of https://github.com/AMT-Cheif/drift.git
Parse BEGIN and COMMIT statements
This commit is contained in:
parent
580d99ce24
commit
3be320d0c5
|
@ -30,6 +30,7 @@ export 'statements/insert.dart';
|
|||
export 'statements/invalid.dart';
|
||||
export 'statements/select.dart';
|
||||
export 'statements/statement.dart';
|
||||
export 'statements/transaction.dart';
|
||||
export 'statements/update.dart';
|
||||
export 'visitor.dart';
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
import '../../reader/tokenizer/token.dart';
|
||||
import '../node.dart';
|
||||
import '../visitor.dart';
|
||||
import 'statement.dart';
|
||||
|
||||
enum TransactionMode { none, deferred, immediate, exclusive }
|
||||
|
||||
class BeginTransactionStatement extends Statement {
|
||||
Token? begin, modeToken, transaction;
|
||||
|
||||
final TransactionMode mode;
|
||||
|
||||
BeginTransactionStatement([this.mode = TransactionMode.none]);
|
||||
|
||||
@override
|
||||
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
||||
return visitor.visitBeginTransaction(this, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<AstNode> get childNodes => const Iterable.empty();
|
||||
|
||||
@override
|
||||
void transformChildren<A>(Transformer<A> transformer, A arg) {}
|
||||
}
|
||||
|
||||
class CommitStatement extends Statement {
|
||||
Token? commitOrEnd, transaction;
|
||||
|
||||
@override
|
||||
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
|
||||
return visitor.visitCommitStatement(this, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<AstNode> get childNodes => const Iterable.empty();
|
||||
|
||||
@override
|
||||
void transformChildren<A>(Transformer<A> transformer, A arg) {}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
import 'ast.dart';
|
||||
|
||||
import 'expressions/raise.dart';
|
||||
|
||||
abstract class AstVisitor<A, R> {
|
||||
R visitSelectStatement(SelectStatement e, A arg);
|
||||
R visitCompoundSelectStatement(CompoundSelectStatement e, A arg);
|
||||
|
@ -92,6 +90,8 @@ abstract class AstVisitor<A, R> {
|
|||
R visitNamedVariable(ColonNamedVariable e, A arg);
|
||||
|
||||
R visitBlock(Block block, A arg);
|
||||
R visitBeginTransaction(BeginTransactionStatement e, A arg);
|
||||
R visitCommitStatement(CommitStatement e, A arg);
|
||||
|
||||
R visitMoorFile(MoorFile e, A arg);
|
||||
R visitMoorImportStatement(ImportStatement e, A arg);
|
||||
|
@ -393,6 +393,16 @@ class RecursiveVisitor<A, R> implements AstVisitor<A, R?> {
|
|||
return defaultNode(e, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
R? visitBeginTransaction(BeginTransactionStatement e, A arg) {
|
||||
return visitStatement(e, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
R? visitCommitStatement(CommitStatement e, A arg) {
|
||||
return visitStatement(e, arg);
|
||||
}
|
||||
|
||||
// Moor-specific additions
|
||||
@override
|
||||
R? visitMoorFile(MoorFile e, A arg) {
|
||||
|
|
|
@ -110,6 +110,11 @@ class Parser {
|
|||
return _peek.type == type;
|
||||
}
|
||||
|
||||
bool _checkAny(Iterable<TokenType> type) {
|
||||
if (_isAtEnd) return false;
|
||||
return type.contains(_peek.type);
|
||||
}
|
||||
|
||||
/// Returns whether the next token is an [TokenType.identifier] or a
|
||||
/// [KeywordToken]. If this method returns true, calling [_consumeIdentifier]
|
||||
/// with same [lenient] parameter will now throw.
|
||||
|
@ -177,19 +182,46 @@ class Parser {
|
|||
InvalidStatement();
|
||||
}
|
||||
|
||||
Statement statement() {
|
||||
final first = _peek;
|
||||
Statement? stmt = _crud();
|
||||
stmt ??= _create();
|
||||
Statement _statementWithoutSemicolon() {
|
||||
if (_checkAny(const [
|
||||
TokenType.$with,
|
||||
TokenType.select,
|
||||
TokenType.$values,
|
||||
TokenType.delete,
|
||||
TokenType.update,
|
||||
TokenType.insert,
|
||||
TokenType.replace,
|
||||
])) {
|
||||
return _crud()!;
|
||||
}
|
||||
|
||||
if (_check(TokenType.create)) {
|
||||
return _create()!;
|
||||
}
|
||||
|
||||
if (_check(TokenType.begin)) {
|
||||
return _beginStatement();
|
||||
}
|
||||
if (_checkAny(const [TokenType.commit, TokenType.end])) {
|
||||
return _commit();
|
||||
}
|
||||
|
||||
if (enableMoorExtensions) {
|
||||
stmt ??= _import() ?? _declaredStatement();
|
||||
if (_check(TokenType.import)) {
|
||||
return _import()!;
|
||||
}
|
||||
if (_check(TokenType.identifier) || _peek is KeywordToken) {
|
||||
return _declaredStatement()!;
|
||||
}
|
||||
}
|
||||
|
||||
if (stmt == null) {
|
||||
_error('Expected a sql statement to start here');
|
||||
}
|
||||
|
||||
Statement statement() {
|
||||
final first = _peek;
|
||||
final stmt = _statementWithoutSemicolon();
|
||||
|
||||
if (_matchOne(TokenType.semicolon)) {
|
||||
stmt.semicolon = _previous;
|
||||
}
|
||||
|
@ -909,6 +941,61 @@ class Parser {
|
|||
}
|
||||
}
|
||||
|
||||
Statement _beginStatement() {
|
||||
final begin = _consume(TokenType.begin);
|
||||
Token? modeToken;
|
||||
var mode = TransactionMode.none;
|
||||
|
||||
if (_match(
|
||||
const [TokenType.deferred, TokenType.immediate, TokenType.exclusive])) {
|
||||
modeToken = _previous;
|
||||
|
||||
switch (modeToken.type) {
|
||||
case TokenType.deferred:
|
||||
mode = TransactionMode.deferred;
|
||||
break;
|
||||
case TokenType.immediate:
|
||||
mode = TransactionMode.immediate;
|
||||
break;
|
||||
case TokenType.exclusive:
|
||||
mode = TransactionMode.exclusive;
|
||||
break;
|
||||
default:
|
||||
throw AssertionError('unreachable');
|
||||
}
|
||||
}
|
||||
|
||||
Token? transaction;
|
||||
if (_matchOne(TokenType.transaction)) {
|
||||
transaction = _previous;
|
||||
}
|
||||
|
||||
return BeginTransactionStatement(mode)
|
||||
..setSpan(begin, _previous)
|
||||
..begin = begin
|
||||
..modeToken = modeToken
|
||||
..transaction = transaction;
|
||||
}
|
||||
|
||||
CommitStatement _commit() {
|
||||
Token commitOrEnd;
|
||||
if (_match(const [TokenType.commit, TokenType.end])) {
|
||||
commitOrEnd = _previous;
|
||||
} else {
|
||||
_error('Expected COMMIT or END here');
|
||||
}
|
||||
|
||||
Token? transaction;
|
||||
if (_matchOne(TokenType.transaction)) {
|
||||
transaction = _previous;
|
||||
}
|
||||
|
||||
return CommitStatement()
|
||||
..setSpan(commitOrEnd, _previous)
|
||||
..commitOrEnd = commitOrEnd
|
||||
..transaction = transaction;
|
||||
}
|
||||
|
||||
CrudStatement? _crud() {
|
||||
final withClause = _withClause();
|
||||
|
||||
|
@ -921,6 +1008,14 @@ class Parser {
|
|||
} else if (_check(TokenType.insert) || _check(TokenType.replace)) {
|
||||
return _insertStmt(withClause);
|
||||
}
|
||||
|
||||
// A WITH clause without a following select, insert, delete or update
|
||||
// is invalid!
|
||||
if (withClause != null) {
|
||||
_error('Expected a SELECT, INSERT, UPDATE or DELETE statement to '
|
||||
'follow this WITH clause.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,25 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:sqlparser/src/ast/ast.dart';
|
||||
|
||||
/// Checks whether [a] and [b] are equal. If they aren't, throws an exception.
|
||||
void enforceEqual(AstNode a, AstNode b) {
|
||||
EqualityEnforcingVisitor(a).visit(b, null);
|
||||
}
|
||||
|
||||
void enforceEqualIterable(Iterable<AstNode> a, Iterable<AstNode> b) {
|
||||
final childrenA = a.iterator;
|
||||
final childrenB = b.iterator;
|
||||
|
||||
// always move both iterators
|
||||
while (childrenA.moveNext() & childrenB.moveNext()) {
|
||||
enforceEqual(childrenA.current, childrenB.current);
|
||||
}
|
||||
|
||||
if (childrenA.moveNext() || childrenB.moveNext()) {
|
||||
throw ArgumentError("$a and $b don't have an equal amount of children");
|
||||
}
|
||||
}
|
||||
|
||||
/// Visitor enforcing the equality of two ast nodes.
|
||||
class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
||||
// The current ast node. Visitor methods will compare the node they receive to
|
||||
|
@ -19,58 +38,6 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
|||
EqualityEnforcingVisitor(this._current, {bool considerChildren = true})
|
||||
: _considerChildren = considerChildren;
|
||||
|
||||
void _check(AstNode? childOfCurrent, AstNode? childOfOther) {
|
||||
if (identical(childOfCurrent, childOfOther)) return;
|
||||
|
||||
if ((childOfCurrent == null) != (childOfOther == null)) {
|
||||
throw NotEqualException('$childOfCurrent and $childOfOther');
|
||||
}
|
||||
|
||||
// Both non nullable here
|
||||
final savedCurrent = _current;
|
||||
_current = childOfCurrent!;
|
||||
visit(childOfOther!, null);
|
||||
_current = savedCurrent;
|
||||
}
|
||||
|
||||
void _checkChildren(AstNode other) {
|
||||
if (!_considerChildren) return;
|
||||
|
||||
final currentChildren = _current.childNodes.iterator;
|
||||
final otherChildren = other.childNodes.iterator;
|
||||
|
||||
while (currentChildren.moveNext()) {
|
||||
if (otherChildren.moveNext()) {
|
||||
_check(currentChildren.current, otherChildren.current);
|
||||
} else {
|
||||
// Current has more elements than other
|
||||
throw NotEqualException(
|
||||
"$_current and $other don't have an equal amount of children");
|
||||
}
|
||||
}
|
||||
|
||||
if (otherChildren.moveNext()) {
|
||||
// Other has more elements than current
|
||||
throw NotEqualException(
|
||||
"$_current and $other don't have an equal amount of children");
|
||||
}
|
||||
}
|
||||
|
||||
Never _notEqual(AstNode other) {
|
||||
throw NotEqualException('$_current and $other');
|
||||
}
|
||||
|
||||
T _currentAs<T extends AstNode>(T context) {
|
||||
final current = _current;
|
||||
if (current is T) return current;
|
||||
|
||||
_notEqual(context);
|
||||
}
|
||||
|
||||
void _assert(bool contentEqual, AstNode context) {
|
||||
if (!contentEqual) _notEqual(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitAggregateExpression(AggregateExpression e, void arg) {
|
||||
final current = _currentAs<AggregateExpression>(e);
|
||||
|
@ -78,6 +45,13 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
|||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitBeginTransaction(BeginTransactionStatement e, void arg) {
|
||||
final current = _currentAs<BeginTransactionStatement>(e);
|
||||
_assert(current.mode == e.mode, e);
|
||||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitBetweenExpression(BetweenExpression e, void arg) {
|
||||
final current = _currentAs<BetweenExpression>(e);
|
||||
|
@ -165,6 +139,12 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
|||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCommitStatement(CommitStatement e, void arg) {
|
||||
_currentAs<CommitStatement>(e);
|
||||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCommonTableExpression(CommonTableExpression e, void arg) {
|
||||
final current = _currentAs<CommonTableExpression>(e);
|
||||
|
@ -348,24 +328,6 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
|||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitInExpression(InExpression e, void arg) {
|
||||
final current = _currentAs<InExpression>(e);
|
||||
_assert(current.not == e.not, e);
|
||||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitRaiseExpression(RaiseExpression e, void arg) {
|
||||
final current = _currentAs<RaiseExpression>(e);
|
||||
_assert(
|
||||
current.raiseKind == e.raiseKind &&
|
||||
current.errorMessage == e.errorMessage,
|
||||
e,
|
||||
);
|
||||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitIndexedColumn(IndexedColumn e, void arg) {
|
||||
final current = _currentAs<IndexedColumn>(e);
|
||||
|
@ -373,6 +335,13 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
|||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitInExpression(InExpression e, void arg) {
|
||||
final current = _currentAs<InExpression>(e);
|
||||
_assert(current.not == e.not, e);
|
||||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitInsertStatement(InsertStatement e, void arg) {
|
||||
final current = _currentAs<InsertStatement>(e);
|
||||
|
@ -544,6 +513,17 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
|||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitRaiseExpression(RaiseExpression e, void arg) {
|
||||
final current = _currentAs<RaiseExpression>(e);
|
||||
_assert(
|
||||
current.raiseKind == e.raiseKind &&
|
||||
current.errorMessage == e.errorMessage,
|
||||
e,
|
||||
);
|
||||
_checkChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitReference(Reference e, void arg) {
|
||||
final current = _currentAs<Reference>(e);
|
||||
|
@ -721,11 +701,58 @@ class EqualityEnforcingVisitor implements AstVisitor<void, void> {
|
|||
_assert(current.recursive == e.recursive, e);
|
||||
_checkChildren(e);
|
||||
}
|
||||
|
||||
void _assert(bool contentEqual, AstNode context) {
|
||||
if (!contentEqual) _notEqual(context);
|
||||
}
|
||||
|
||||
/// Checks whether [a] and [b] are equal. If they aren't, throws an exception.
|
||||
void enforceEqual(AstNode a, AstNode b) {
|
||||
EqualityEnforcingVisitor(a).visit(b, null);
|
||||
void _check(AstNode? childOfCurrent, AstNode? childOfOther) {
|
||||
if (identical(childOfCurrent, childOfOther)) return;
|
||||
|
||||
if ((childOfCurrent == null) != (childOfOther == null)) {
|
||||
throw NotEqualException('$childOfCurrent and $childOfOther');
|
||||
}
|
||||
|
||||
// Both non nullable here
|
||||
final savedCurrent = _current;
|
||||
_current = childOfCurrent!;
|
||||
visit(childOfOther!, null);
|
||||
_current = savedCurrent;
|
||||
}
|
||||
|
||||
void _checkChildren(AstNode other) {
|
||||
if (!_considerChildren) return;
|
||||
|
||||
final currentChildren = _current.childNodes.iterator;
|
||||
final otherChildren = other.childNodes.iterator;
|
||||
|
||||
while (currentChildren.moveNext()) {
|
||||
if (otherChildren.moveNext()) {
|
||||
_check(currentChildren.current, otherChildren.current);
|
||||
} else {
|
||||
// Current has more elements than other
|
||||
throw NotEqualException(
|
||||
"$_current and $other don't have an equal amount of children");
|
||||
}
|
||||
}
|
||||
|
||||
if (otherChildren.moveNext()) {
|
||||
// Other has more elements than current
|
||||
throw NotEqualException(
|
||||
"$_current and $other don't have an equal amount of children");
|
||||
}
|
||||
}
|
||||
|
||||
T _currentAs<T extends AstNode>(T context) {
|
||||
final current = _current;
|
||||
if (current is T) return current;
|
||||
|
||||
_notEqual(context);
|
||||
}
|
||||
|
||||
Never _notEqual(AstNode other) {
|
||||
throw NotEqualException('$_current and $other');
|
||||
}
|
||||
}
|
||||
|
||||
/// Thrown by the [EqualityEnforcingVisitor] when two nodes were determined to
|
||||
|
@ -740,17 +767,3 @@ class NotEqualException implements Exception {
|
|||
return 'Not equal: $message';
|
||||
}
|
||||
}
|
||||
|
||||
void enforceEqualIterable(Iterable<AstNode> a, Iterable<AstNode> b) {
|
||||
final childrenA = a.iterator;
|
||||
final childrenB = b.iterator;
|
||||
|
||||
// always move both iterators
|
||||
while (childrenA.moveNext() & childrenB.moveNext()) {
|
||||
enforceEqual(childrenA.current, childrenB.current);
|
||||
}
|
||||
|
||||
if (childrenA.moveNext() || childrenB.moveNext()) {
|
||||
throw ArgumentError("$a and $b don't have an equal amount of children");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,25 +7,6 @@ import 'package:charcode/charcode.dart';
|
|||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:sqlparser/src/reader/tokenizer/token.dart';
|
||||
|
||||
/// Defines the [toSql] extension method that turns ast nodes into a compatible
|
||||
/// textual representation.
|
||||
///
|
||||
/// Parsing the output of [toSql] will result in an equal AST.
|
||||
extension NodeToText on AstNode {
|
||||
/// Obtains a textual representation for AST nodes.
|
||||
///
|
||||
/// Parsing the output of [toSql] will result in an equal AST. Since only the
|
||||
/// AST is used, the output will not contain comments. It's possible for the
|
||||
/// output to have more than just whitespace changes if there are multiple
|
||||
/// ways to represent an equivalent node (e.g. the no-op `FOR EACH ROW` on
|
||||
/// triggers).
|
||||
String toSql() {
|
||||
final builder = NodeSqlBuilder();
|
||||
builder.visit(this, null);
|
||||
return builder.buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class NodeSqlBuilder extends AstVisitor<void, void> {
|
||||
final StringSink buffer;
|
||||
|
||||
|
@ -34,42 +15,6 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
|
||||
NodeSqlBuilder([StringSink? buffer]) : buffer = buffer ?? StringBuffer();
|
||||
|
||||
void _join(Iterable<AstNode> nodes, String separatingSymbol) {
|
||||
var isFirst = true;
|
||||
|
||||
for (final node in nodes) {
|
||||
if (!isFirst) {
|
||||
_symbol(separatingSymbol, spaceAfter: true);
|
||||
}
|
||||
|
||||
visit(node, null);
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _identifier(String identifier,
|
||||
{bool spaceBefore = true, bool spaceAfter = true}) {
|
||||
if (isKeywordLexeme(identifier) || identifier.contains(' ')) {
|
||||
identifier = '"$identifier"';
|
||||
}
|
||||
|
||||
_symbol(identifier, spaceBefore: spaceBefore, spaceAfter: spaceAfter);
|
||||
}
|
||||
|
||||
void _ifNotExists(bool ifNotExists) {
|
||||
if (ifNotExists) {
|
||||
_keyword(TokenType.$if);
|
||||
_keyword(TokenType.not);
|
||||
_keyword(TokenType.exists);
|
||||
}
|
||||
}
|
||||
|
||||
void _keyword(TokenType type) {
|
||||
_symbol(reverseKeywords[type]!, spaceAfter: true, spaceBefore: true);
|
||||
}
|
||||
|
||||
void _space() => buffer.writeCharCode($space);
|
||||
|
||||
/// Writes a space character if [needsSpace] is set.
|
||||
///
|
||||
/// This also resets [needsSpace] to `false`.
|
||||
|
@ -80,35 +25,6 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
}
|
||||
}
|
||||
|
||||
void _stringLiteral(String content) {
|
||||
final escapedChars = content.replaceAll("'", "''");
|
||||
_symbol("'$escapedChars'", spaceBefore: true, spaceAfter: true);
|
||||
}
|
||||
|
||||
void _symbol(String lexeme,
|
||||
{bool spaceBefore = false, bool spaceAfter = false}) {
|
||||
if (needsSpace && spaceBefore) {
|
||||
_space();
|
||||
}
|
||||
|
||||
buffer.write(lexeme);
|
||||
needsSpace = spaceAfter;
|
||||
}
|
||||
|
||||
void _where(Expression? where) {
|
||||
if (where != null) {
|
||||
_keyword(TokenType.where);
|
||||
visit(where, null);
|
||||
}
|
||||
}
|
||||
|
||||
void _from(Queryable? from) {
|
||||
if (from != null) {
|
||||
_keyword(TokenType.from);
|
||||
visit(from, null);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitAggregateExpression(AggregateExpression e, void arg) {
|
||||
_symbol(e.name);
|
||||
|
@ -134,6 +50,25 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitBeginTransaction(BeginTransactionStatement e, void arg) {
|
||||
_keyword(TokenType.begin);
|
||||
|
||||
switch (e.mode) {
|
||||
case TransactionMode.none:
|
||||
break;
|
||||
case TransactionMode.deferred:
|
||||
_keyword(TokenType.deferred);
|
||||
break;
|
||||
case TransactionMode.immediate:
|
||||
_keyword(TokenType.immediate);
|
||||
break;
|
||||
case TransactionMode.exclusive:
|
||||
_keyword(TokenType.exclusive);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitBetweenExpression(BetweenExpression e, void arg) {
|
||||
visit(e.check, arg);
|
||||
|
@ -229,21 +164,6 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
_identifier(e.collation);
|
||||
}
|
||||
|
||||
void _conflictClause(ConflictClause? clause) {
|
||||
if (clause != null) {
|
||||
_keyword(TokenType.on);
|
||||
_keyword(TokenType.conflict);
|
||||
|
||||
_keyword(const {
|
||||
ConflictClause.rollback: TokenType.rollback,
|
||||
ConflictClause.abort: TokenType.abort,
|
||||
ConflictClause.fail: TokenType.fail,
|
||||
ConflictClause.ignore: TokenType.ignore,
|
||||
ConflictClause.replace: TokenType.replace,
|
||||
}[clause]!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitColumnConstraint(ColumnConstraint e, void arg) {
|
||||
if (e.name != null) {
|
||||
|
@ -305,6 +225,11 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
visitList(e.constraints, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCommitStatement(CommitStatement e, void arg) {
|
||||
_keyword(TokenType.commit);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitCommonTableExpression(CommonTableExpression e, void arg) {
|
||||
_identifier(e.cteTableName);
|
||||
|
@ -549,6 +474,15 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
_join(e.parameters, ',');
|
||||
}
|
||||
|
||||
@override
|
||||
void visitExpressionResultColumn(ExpressionResultColumn e, void arg) {
|
||||
visit(e.expression, arg);
|
||||
if (e.as != null) {
|
||||
_keyword(TokenType.as);
|
||||
_identifier(e.as!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitForeignKeyClause(ForeignKeyClause e, void arg) {
|
||||
_keyword(TokenType.references);
|
||||
|
@ -673,6 +607,12 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitIndexedColumn(IndexedColumn e, void arg) {
|
||||
visit(e.expression, arg);
|
||||
_orderingMode(e.ordering);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitInExpression(InExpression e, void arg) {
|
||||
visit(e.left, arg);
|
||||
|
@ -685,30 +625,6 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
visit(e.inside, arg);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitRaiseExpression(RaiseExpression e, void arg) {
|
||||
_keyword(TokenType.raise);
|
||||
_symbol('(', spaceBefore: true);
|
||||
_keyword(const {
|
||||
RaiseKind.ignore: TokenType.ignore,
|
||||
RaiseKind.rollback: TokenType.rollback,
|
||||
RaiseKind.abort: TokenType.abort,
|
||||
RaiseKind.fail: TokenType.fail,
|
||||
}[e.raiseKind]!);
|
||||
|
||||
if (e.errorMessage != null) {
|
||||
_symbol(',', spaceAfter: true);
|
||||
_stringLiteral(e.errorMessage!);
|
||||
}
|
||||
_symbol(')', spaceAfter: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitIndexedColumn(IndexedColumn e, void arg) {
|
||||
visit(e.expression, arg);
|
||||
_orderingMode(e.ordering);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitInsertStatement(InsertStatement e, void arg) {
|
||||
visitNullable(e.withClause, arg);
|
||||
|
@ -875,6 +791,12 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
_symbol(';', spaceAfter: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitMoorNestedStarResultColumn(NestedStarResultColumn e, void arg) {
|
||||
_identifier(e.tableName);
|
||||
_symbol('.**', spaceAfter: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitMoorStatementParameter(StatementParameter e, void arg) {
|
||||
if (e is VariableTypeHint) {
|
||||
|
@ -937,15 +859,6 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
_join(e.terms, ',');
|
||||
}
|
||||
|
||||
void _orderingMode(OrderingMode? mode) {
|
||||
if (mode != null) {
|
||||
_keyword(const {
|
||||
OrderingMode.ascending: TokenType.asc,
|
||||
OrderingMode.descending: TokenType.desc,
|
||||
}[mode]!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitOrderingTerm(OrderingTerm e, void arg) {
|
||||
visit(e.expression, arg);
|
||||
|
@ -968,6 +881,24 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
_symbol(')');
|
||||
}
|
||||
|
||||
@override
|
||||
void visitRaiseExpression(RaiseExpression e, void arg) {
|
||||
_keyword(TokenType.raise);
|
||||
_symbol('(', spaceBefore: true);
|
||||
_keyword(const {
|
||||
RaiseKind.ignore: TokenType.ignore,
|
||||
RaiseKind.rollback: TokenType.rollback,
|
||||
RaiseKind.abort: TokenType.abort,
|
||||
RaiseKind.fail: TokenType.fail,
|
||||
}[e.raiseKind]!);
|
||||
|
||||
if (e.errorMessage != null) {
|
||||
_symbol(',', spaceAfter: true);
|
||||
_stringLiteral(e.errorMessage!);
|
||||
}
|
||||
_symbol(')', spaceAfter: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitReference(Reference e, void arg) {
|
||||
var didWriteSpaceBefore = false;
|
||||
|
@ -988,31 +919,6 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
spaceAfter: true, spaceBefore: !didWriteSpaceBefore);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitStarResultColumn(StarResultColumn e, void arg) {
|
||||
if (e.tableName != null) {
|
||||
_identifier(e.tableName!);
|
||||
_symbol('.');
|
||||
}
|
||||
|
||||
_symbol('*', spaceAfter: true, spaceBefore: e.tableName == null);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitMoorNestedStarResultColumn(NestedStarResultColumn e, void arg) {
|
||||
_identifier(e.tableName);
|
||||
_symbol('.**', spaceAfter: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitExpressionResultColumn(ExpressionResultColumn e, void arg) {
|
||||
visit(e.expression, arg);
|
||||
if (e.as != null) {
|
||||
_keyword(TokenType.as);
|
||||
_identifier(e.as!);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void visitReturning(Returning e, void arg) {
|
||||
_keyword(TokenType.returning);
|
||||
|
@ -1081,6 +987,16 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
_symbol('*', spaceAfter: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitStarResultColumn(StarResultColumn e, void arg) {
|
||||
if (e.tableName != null) {
|
||||
_identifier(e.tableName!);
|
||||
_symbol('.');
|
||||
}
|
||||
|
||||
_symbol('*', spaceAfter: true, spaceBefore: e.tableName == null);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitStringComparison(StringComparisonExpression e, void arg) {
|
||||
visit(e.left, arg);
|
||||
|
@ -1316,4 +1232,112 @@ class NodeSqlBuilder extends AstVisitor<void, void> {
|
|||
|
||||
_join(e.ctes, ',');
|
||||
}
|
||||
|
||||
void _conflictClause(ConflictClause? clause) {
|
||||
if (clause != null) {
|
||||
_keyword(TokenType.on);
|
||||
_keyword(TokenType.conflict);
|
||||
|
||||
_keyword(const {
|
||||
ConflictClause.rollback: TokenType.rollback,
|
||||
ConflictClause.abort: TokenType.abort,
|
||||
ConflictClause.fail: TokenType.fail,
|
||||
ConflictClause.ignore: TokenType.ignore,
|
||||
ConflictClause.replace: TokenType.replace,
|
||||
}[clause]!);
|
||||
}
|
||||
}
|
||||
|
||||
void _from(Queryable? from) {
|
||||
if (from != null) {
|
||||
_keyword(TokenType.from);
|
||||
visit(from, null);
|
||||
}
|
||||
}
|
||||
|
||||
void _identifier(String identifier,
|
||||
{bool spaceBefore = true, bool spaceAfter = true}) {
|
||||
if (isKeywordLexeme(identifier) || identifier.contains(' ')) {
|
||||
identifier = '"$identifier"';
|
||||
}
|
||||
|
||||
_symbol(identifier, spaceBefore: spaceBefore, spaceAfter: spaceAfter);
|
||||
}
|
||||
|
||||
void _ifNotExists(bool ifNotExists) {
|
||||
if (ifNotExists) {
|
||||
_keyword(TokenType.$if);
|
||||
_keyword(TokenType.not);
|
||||
_keyword(TokenType.exists);
|
||||
}
|
||||
}
|
||||
|
||||
void _join(Iterable<AstNode> nodes, String separatingSymbol) {
|
||||
var isFirst = true;
|
||||
|
||||
for (final node in nodes) {
|
||||
if (!isFirst) {
|
||||
_symbol(separatingSymbol, spaceAfter: true);
|
||||
}
|
||||
|
||||
visit(node, null);
|
||||
isFirst = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _keyword(TokenType type) {
|
||||
_symbol(reverseKeywords[type]!, spaceAfter: true, spaceBefore: true);
|
||||
}
|
||||
|
||||
void _orderingMode(OrderingMode? mode) {
|
||||
if (mode != null) {
|
||||
_keyword(const {
|
||||
OrderingMode.ascending: TokenType.asc,
|
||||
OrderingMode.descending: TokenType.desc,
|
||||
}[mode]!);
|
||||
}
|
||||
}
|
||||
|
||||
void _space() => buffer.writeCharCode($space);
|
||||
|
||||
void _stringLiteral(String content) {
|
||||
final escapedChars = content.replaceAll("'", "''");
|
||||
_symbol("'$escapedChars'", spaceBefore: true, spaceAfter: true);
|
||||
}
|
||||
|
||||
void _symbol(String lexeme,
|
||||
{bool spaceBefore = false, bool spaceAfter = false}) {
|
||||
if (needsSpace && spaceBefore) {
|
||||
_space();
|
||||
}
|
||||
|
||||
buffer.write(lexeme);
|
||||
needsSpace = spaceAfter;
|
||||
}
|
||||
|
||||
void _where(Expression? where) {
|
||||
if (where != null) {
|
||||
_keyword(TokenType.where);
|
||||
visit(where, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the [toSql] extension method that turns ast nodes into a compatible
|
||||
/// textual representation.
|
||||
///
|
||||
/// Parsing the output of [toSql] will result in an equal AST.
|
||||
extension NodeToText on AstNode {
|
||||
/// Obtains a textual representation for AST nodes.
|
||||
///
|
||||
/// Parsing the output of [toSql] will result in an equal AST. Since only the
|
||||
/// AST is used, the output will not contain comments. It's possible for the
|
||||
/// output to have more than just whitespace changes if there are multiple
|
||||
/// ways to represent an equivalent node (e.g. the no-op `FOR EACH ROW` on
|
||||
/// triggers).
|
||||
String toSql() {
|
||||
final builder = NodeSqlBuilder();
|
||||
builder.visit(this, null);
|
||||
return builder.buffer.toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:test/test.dart';
|
||||
|
||||
import 'utils.dart';
|
||||
|
||||
void main() {
|
||||
test('WITH without following statement', () {
|
||||
enforceError('WITH foo AS (SELECT * FROM bar)',
|
||||
contains('to follow this WITH clause'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import 'package:sqlparser/sqlparser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'utils.dart';
|
||||
|
||||
void main() {
|
||||
group('BEGIN', () {
|
||||
test('without mode', () {
|
||||
testStatement('BEGIN;', BeginTransactionStatement());
|
||||
testStatement('BEGIN TRANSACTION;', BeginTransactionStatement());
|
||||
});
|
||||
|
||||
test('deferred', () {
|
||||
testStatement('BEGIN DEFERRED;',
|
||||
BeginTransactionStatement(TransactionMode.deferred));
|
||||
});
|
||||
|
||||
test('immediate', () {
|
||||
testStatement('BEGIN IMMEDIATE;',
|
||||
BeginTransactionStatement(TransactionMode.immediate));
|
||||
});
|
||||
|
||||
test('exclusive', () {
|
||||
testStatement('BEGIN EXCLUSIVE TRANSACTION;',
|
||||
BeginTransactionStatement(TransactionMode.exclusive));
|
||||
});
|
||||
});
|
||||
|
||||
test('COMMIT', () {
|
||||
testStatement('COMMIT', CommitStatement());
|
||||
testStatement('END', CommitStatement());
|
||||
testStatement('COMMIT TRANSACTION', CommitStatement());
|
||||
testStatement('END TRANSACTION', CommitStatement());
|
||||
});
|
||||
}
|
|
@ -66,3 +66,13 @@ void enforceHasSpan(AstNode node) {
|
|||
throw ArgumentError('Node $problematic did not have a span');
|
||||
}
|
||||
}
|
||||
|
||||
void enforceError(String sql, Matcher textMatcher) {
|
||||
final parsed = SqlEngine().parse(sql);
|
||||
|
||||
expect(
|
||||
parsed.errors,
|
||||
contains(
|
||||
isA<ParsingError>().having((e) => e.message, 'message', textMatcher)),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -167,6 +167,17 @@ CREATE UNIQUE INDEX my_idx ON t1 (c1, c2, c3) WHERE c1 < c3;
|
|||
});
|
||||
});
|
||||
|
||||
group('misc', () {
|
||||
test('transactions', () {
|
||||
testFormat('BEGIN DEFERRED TRANSACTION;');
|
||||
testFormat('BEGIN IMMEDIATE');
|
||||
testFormat('BEGIN EXCLUSIVE');
|
||||
|
||||
testFormat('COMMIT');
|
||||
testFormat('END TRANSACTION');
|
||||
});
|
||||
});
|
||||
|
||||
group('query statements', () {
|
||||
group('select', () {
|
||||
test('with common table expressions', () {
|
||||
|
|
Loading…
Reference in New Issue