mirror of https://github.com/AMT-Cheif/drift.git
sqlparser: Support upsert clauses (#367)
This commit is contained in:
parent
a5451104a0
commit
0c171c3b81
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue