sqlparser: Support upsert clauses (#367)

This commit is contained in:
Simon Binder 2020-02-03 21:43:18 +01:00
parent a5451104a0
commit 0c171c3b81
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
15 changed files with 292 additions and 28 deletions

View File

@ -89,8 +89,8 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
@override
void visitCrudStatement(CrudStatement stmt, TypeExpectation arg) {
if (stmt is HasWhereClause) {
final typedStmt = stmt as HasWhereClause;
if (stmt is StatementWithWhere) {
final typedStmt = stmt as StatementWithWhere;
_handleWhereClause(typedStmt);
visitExcept(stmt, typedStmt.where, arg);
} else {
@ -561,7 +561,7 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
}
}
void _handleWhereClause(HasWhereClause stmt) {
void _handleWhereClause(StatementWithWhere stmt) {
if (stmt.where != null) {
// assume that a where statement is a boolean expression. Sqlite
// internally casts (https://www.sqlite.org/lang_expr.html#booleanexpr),

View File

@ -8,6 +8,7 @@ import 'package:sqlparser/src/utils/meta.dart';
part 'clauses/limit.dart';
part 'clauses/ordering.dart';
part 'clauses/upsert.dart';
part 'clauses/with.dart';
part 'common/queryables.dart';
part 'common/renamable.dart';
@ -156,3 +157,8 @@ abstract class AstNode with HasMetaMixin implements SyntacticEntity {
return super.toString();
}
}
/// Common interface for every node that has a `where` clause.
abstract class HasWhereClause implements AstNode {
Expression get where;
}

View File

@ -0,0 +1,62 @@
part of '../ast.dart';
class UpsertClause extends AstNode implements HasWhereClause {
final List<IndexedColumn> /*?*/ onColumns;
@override
final Expression where;
final UpsertAction action;
UpsertClause({this.onColumns, this.where, @required this.action});
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
return visitor.visitUpsertClause(this, arg);
}
@override
Iterable<AstNode> get childNodes {
return [
if (onColumns != null) ...onColumns,
if (where != null) where,
action,
];
}
@override
bool contentEquals(UpsertClause other) => true;
}
abstract class UpsertAction extends AstNode {}
class DoNothing extends UpsertAction {
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
return visitor.visitDoNothing(this, arg);
}
@override
Iterable<AstNode> get childNodes => const [];
@override
bool contentEquals(DoNothing other) => true;
}
class DoUpdate extends UpsertAction implements HasWhereClause {
final List<SetComponent> set;
@override
final Expression where;
DoUpdate(this.set, {this.where});
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
return visitor.visitDoUpdate(this, arg);
}
@override
Iterable<AstNode> get childNodes => [...set, if (where != null) where];
@override
bool contentEquals(DoUpdate other) => true;
}

View File

@ -1,7 +1,7 @@
part of '../ast.dart';
class CreateIndexStatement extends Statement
implements CreatingStatement, HasWhereClause {
implements CreatingStatement, StatementWithWhere {
final String indexName;
final bool unique;
final bool ifNotExists;

View File

@ -1,6 +1,6 @@
part of '../ast.dart';
class DeleteStatement extends CrudStatement implements HasWhereClause {
class DeleteStatement extends CrudStatement implements StatementWithWhere {
final TableReference from;
@override
final Expression where;

View File

@ -15,6 +15,7 @@ class InsertStatement extends CrudStatement {
final TableReference table;
final List<Reference> targetColumns;
final InsertSource source;
final UpsertClause upsert;
List<Column> get resolvedTargetColumns {
if (targetColumns.isNotEmpty) {
@ -25,14 +26,13 @@ class InsertStatement extends CrudStatement {
}
}
// todo parse upsert clauses
InsertStatement(
{WithClause withClause,
this.mode = InsertMode.insert,
@required this.table,
@required this.targetColumns,
@required this.source})
@required this.source,
this.upsert})
: super._(withClause);
@override
@ -46,6 +46,7 @@ class InsertStatement extends CrudStatement {
yield table;
yield* targetColumns;
yield* source.childNodes;
if (upsert != null) yield upsert;
}
@override

View File

@ -9,7 +9,8 @@ abstract class BaseSelectStatement extends CrudStatement with ResultSet {
BaseSelectStatement._(WithClause withClause) : super._(withClause);
}
class SelectStatement extends BaseSelectStatement implements HasWhereClause {
class SelectStatement extends BaseSelectStatement
implements StatementWithWhere {
final bool distinct;
final List<ResultColumn> columns;
final List<Queryable> from;

View File

@ -14,9 +14,7 @@ abstract class CrudStatement extends Statement {
/// Interface for statements that have a primary where clause (select, update,
/// delete).
abstract class HasWhereClause extends Statement {
Expression get where;
}
abstract class StatementWithWhere extends Statement implements HasWhereClause {}
/// Marker interface for statements that change the table structure.
abstract class SchemaStatement extends Statement implements PartOfMoorFile {}

View File

@ -16,7 +16,7 @@ const Map<TokenType, FailureMode> _tokensToMode = {
TokenType.ignore: FailureMode.ignore,
};
class UpdateStatement extends CrudStatement implements HasWhereClause {
class UpdateStatement extends CrudStatement implements StatementWithWhere {
final FailureMode or;
final TableReference table;
final List<SetComponent> set;

View File

@ -14,6 +14,7 @@ abstract class AstVisitor<A, R> {
R visitCreateIndexStatement(CreateIndexStatement e, A arg);
R visitWithClause(WithClause e, A arg);
R visitUpsertClause(UpsertClause e, A arg);
R visitCommonTableExpression(CommonTableExpression e, A arg);
R visitOrderBy(OrderBy e, A arg);
R visitOrderingTerm(OrderingTerm e, A arg);
@ -22,6 +23,9 @@ abstract class AstVisitor<A, R> {
R visitJoin(Join e, A arg);
R visitGroupBy(GroupBy e, A arg);
R visitDoNothing(DoNothing e, A arg);
R visitDoUpdate(DoUpdate e, A arg);
R visitSetComponent(SetComponent e, A arg);
R visitColumnDefinition(ColumnDefinition e, A arg);
@ -158,6 +162,25 @@ class RecursiveVisitor<A, R> implements AstVisitor<A, R> {
return visitChildren(e, arg);
}
@override
R visitUpsertClause(UpsertClause e, A arg) {
return visitChildren(e, arg);
}
@override
R visitDoNothing(DoNothing e, A arg) {
return defaultUpsertAction(e, arg);
}
@override
R visitDoUpdate(DoUpdate e, A arg) {
return defaultUpsertAction(e, arg);
}
R defaultUpsertAction(UpsertAction e, A arg) {
return visitChildren(e, arg);
}
@override
R visitCommonTableExpression(CommonTableExpression e, A arg) {
return visitChildren(e, arg);

View File

@ -562,6 +562,19 @@ mixin CrudParser on ParserBase {
final table = _tableReference();
_consume(TokenType.set, 'Expected SET after the table name');
final set = _setComponents();
final where = _where();
return UpdateStatement(
withClause: withClause,
or: failureMode,
table: table,
set: set,
where: where,
)..setSpan(withClause?.first ?? updateToken, _previous);
}
List<SetComponent> _setComponents() {
final set = <SetComponent>[];
do {
final columnName =
@ -576,14 +589,7 @@ mixin CrudParser on ParserBase {
..setSpan(columnName, _previous));
} while (_matchOne(TokenType.comma));
final where = _where();
return UpdateStatement(
withClause: withClause,
or: failureMode,
table: table,
set: set,
where: where,
)..setSpan(withClause?.first ?? updateToken, _previous);
return set;
}
InsertStatement _insertStmt([WithClause withClause]) {
@ -632,6 +638,7 @@ mixin CrudParser on ParserBase {
'Expected clpsing parenthesis after column list');
}
final source = _insertSource();
final upsert = _upsertClauseOrNull();
return InsertStatement(
withClause: withClause,
@ -639,6 +646,7 @@ mixin CrudParser on ParserBase {
table: table,
targetColumns: targetColumns,
source: source,
upsert: upsert,
)..setSpan(withClause?.first ?? firstToken, _previous);
}
@ -659,6 +667,54 @@ mixin CrudParser on ParserBase {
}
}
UpsertClause _upsertClauseOrNull() {
if (!_matchOne(TokenType.on)) return null;
final first = _previous;
_consume(TokenType.conflict, 'Expected CONFLICT keyword for upsert clause');
List<IndexedColumn> indexedColumns;
Expression where;
if (_matchOne(TokenType.leftParen)) {
indexedColumns = _indexedColumns();
_consume(TokenType.rightParen, 'Expected closing paren here');
if (_matchOne(TokenType.where)) {
where = expression();
}
}
_consume(TokenType.$do,
'Expected DO, followed by the action (NOTHING or UPDATE SET)');
UpsertAction action;
if (_matchOne(TokenType.nothing)) {
action = DoNothing()..setSpan(_previous, _previous);
} else if (_check(TokenType.update)) {
action = _doUpdate();
}
return UpsertClause(
onColumns: indexedColumns,
where: where,
action: action,
)..setSpan(first, _previous);
}
DoUpdate _doUpdate() {
_consume(TokenType.update, 'Expected UPDATE SET keyword here');
final first = _previous;
_consume(TokenType.set, 'Expected UPDATE SET keyword here');
final set = _setComponents();
Expression where;
if (_matchOne(TokenType.where)) {
where = expression();
}
return DoUpdate(set, where: where)..setSpan(first, _previous);
}
@override
WindowDefinition _windowDefinition() {
_consume(TokenType.leftParen, 'Expected opening parenthesis');

View File

@ -204,6 +204,8 @@ abstract class ParserBase {
/// Parses function parameters, without the surrounding parentheses.
FunctionParameters _functionParameters();
List<IndexedColumn> _indexedColumns();
/// Skips all tokens until it finds one with [type]. If [skipTarget] is true,
/// that token will be skipped as well.
///

View File

@ -267,13 +267,7 @@ mixin SchemaParser on ParserBase {
_consume(TokenType.leftParen, 'Expected indexed columns in parentheses');
final indexes = <IndexedColumn>[];
do {
final expr = expression();
final mode = _orderingModeOrNull();
indexes.add(IndexedColumn(expr, mode)..setSpan(expr.first, _previous));
} while (_matchOne(TokenType.comma));
final indexes = _indexedColumns();
_consume(TokenType.rightParen, 'Expected closing bracket');
@ -294,6 +288,19 @@ mixin SchemaParser on ParserBase {
..setSpan(create, _previous);
}
@override
List<IndexedColumn> _indexedColumns() {
final indexes = <IndexedColumn>[];
do {
final expr = expression();
final mode = _orderingModeOrNull();
indexes.add(IndexedColumn(expr, mode)..setSpan(expr.first, _previous));
} while (_matchOne(TokenType.comma));
return indexes;
}
/// Parses `IF NOT EXISTS` | epsilon
bool _ifNotExists() {
if (_matchOne(TokenType.$if)) {

View File

@ -6,6 +6,7 @@ enum TokenType {
rightParen,
comma,
dot,
$do,
doublePipe,
star,
slash,
@ -27,6 +28,7 @@ enum TokenType {
$is,
$in,
not,
nothing,
like,
glob,
match,
@ -171,6 +173,7 @@ const Map<String, TokenType> keywords = {
'INTO': TokenType.into,
'COLLATE': TokenType.collate,
'DISTINCT': TokenType.distinct,
'DO': TokenType.$do,
'UPDATE': TokenType.update,
'ALL': TokenType.all,
'AND': TokenType.and,
@ -206,6 +209,7 @@ const Map<String, TokenType> keywords = {
'REGEXP': TokenType.regexp,
'ESCAPE': TokenType.escape,
'NOT': TokenType.not,
'NOTHING': TokenType.nothing,
'TRUE': TokenType.$true,
'FALSE': TokenType.$false,
'NULL': TokenType.$null,

View File

@ -54,4 +54,108 @@ void main() {
),
);
});
group('parses upsert clauses', () {
const prefix = 'INSERT INTO tbl DEFAULT VALUES ON CONFLICT';
test('without listing indexed columns', () {
testStatement(
'$prefix DO NOTHING',
InsertStatement(
table: TableReference('tbl'),
targetColumns: const [],
source: const DefaultValues(),
upsert: UpsertClause(action: DoNothing()),
),
);
});
test('listing indexed columns without where clause', () {
testStatement(
'$prefix (foo, bar DESC) DO NOTHING',
InsertStatement(
table: TableReference('tbl'),
targetColumns: const [],
source: const DefaultValues(),
upsert: UpsertClause(
onColumns: [
IndexedColumn(Reference(columnName: 'foo')),
IndexedColumn(
Reference(columnName: 'bar'),
OrderingMode.descending,
),
],
action: DoNothing(),
),
),
);
});
test('listing indexed columns and where clause', () {
testStatement(
'$prefix (foo, bar) WHERE 2 = foo DO NOTHING',
InsertStatement(
table: TableReference('tbl'),
targetColumns: const [],
source: const DefaultValues(),
upsert: UpsertClause(
onColumns: [
IndexedColumn(Reference(columnName: 'foo')),
IndexedColumn(Reference(columnName: 'bar')),
],
where: BinaryExpression(
NumericLiteral(2, token(TokenType.numberLiteral)),
token(TokenType.equal),
Reference(columnName: 'foo'),
),
action: DoNothing(),
),
),
);
});
test('having an update action without where', () {
testStatement(
'$prefix DO UPDATE SET foo = 2',
InsertStatement(
table: TableReference('tbl'),
targetColumns: const [],
source: const DefaultValues(),
upsert: UpsertClause(
action: DoUpdate(
[
SetComponent(
column: Reference(columnName: 'foo'),
expression: NumericLiteral(2, token(TokenType.numberLiteral)),
),
],
),
),
),
);
});
test('having an update action with where', () {
testStatement(
'$prefix DO UPDATE SET foo = 2 WHERE ?',
InsertStatement(
table: TableReference('tbl'),
targetColumns: const [],
source: const DefaultValues(),
upsert: UpsertClause(
action: DoUpdate(
[
SetComponent(
column: Reference(columnName: 'foo'),
expression: NumericLiteral(2, token(TokenType.numberLiteral)),
),
],
where: NumberedVariable(
QuestionMarkVariableToken(fakeSpan('?'), null),
),
),
),
),
);
});
});
}