diff --git a/sqlparser/lib/src/analysis/steps/reference_resolver.dart b/sqlparser/lib/src/analysis/steps/reference_resolver.dart index 8090f91c..046a18e9 100644 --- a/sqlparser/lib/src/analysis/steps/reference_resolver.dart +++ b/sqlparser/lib/src/analysis/steps/reference_resolver.dart @@ -8,6 +8,10 @@ class ReferenceResolver extends RecursiveVisitor { @override void visitReference(Reference e) { + if (e.resolved != null) { + return super.visitReference(e); + } + final scope = e.scope; if (e.tableName != null) { @@ -65,7 +69,7 @@ class ReferenceResolver extends RecursiveVisitor { @override void visitAggregateExpression(AggregateExpression e) { - if (e.windowName != null) { + if (e.windowName != null && e.resolved != null) { final resolved = e.scope.resolve(e.windowName); e.resolved = resolved; } diff --git a/sqlparser/lib/src/ast/ast.dart b/sqlparser/lib/src/ast/ast.dart index 86e970b9..b7b218f4 100644 --- a/sqlparser/lib/src/ast/ast.dart +++ b/sqlparser/lib/src/ast/ast.dart @@ -26,6 +26,7 @@ part 'schema/table_definition.dart'; part 'statements/create_table.dart'; part 'statements/delete.dart'; +part 'statements/insert.dart'; part 'statements/select.dart'; part 'statements/statement.dart'; part 'statements/update.dart'; @@ -133,6 +134,7 @@ abstract class AstNode { abstract class AstVisitor { T visitSelectStatement(SelectStatement e); T visitResultColumn(ResultColumn e); + T visitInsertStatement(InsertStatement e); T visitDeleteStatement(DeleteStatement e); T visitUpdateStatement(UpdateStatement e); T visitCreateTableStatement(CreateTableStatement e); @@ -248,6 +250,9 @@ class RecursiveVisitor extends AstVisitor { @override T visitSelectStatement(SelectStatement e) => visitChildren(e); + @override + T visitInsertStatement(InsertStatement e) => visitChildren(e); + @override T visitDeleteStatement(DeleteStatement e) => visitChildren(e); diff --git a/sqlparser/lib/src/ast/statements/insert.dart b/sqlparser/lib/src/ast/statements/insert.dart new file mode 100644 index 00000000..d7550fd9 --- /dev/null +++ b/sqlparser/lib/src/ast/statements/insert.dart @@ -0,0 +1,90 @@ +part of '../ast.dart'; + +enum InsertMode { + insert, + replace, + insertOrReplace, + insertOrRollback, + insertOrAbort, + insertOrFail, + insertOrIgnore +} + +class InsertStatement extends Statement with CrudStatement { + final InsertMode mode; + final TableReference table; + final List targetColumns; + final InsertSource source; + + // todo parse upsert clauses + + InsertStatement( + {this.mode = InsertMode.insert, + @required this.table, + @required this.targetColumns, + @required this.source}); + + @override + T accept(AstVisitor visitor) => visitor.visitInsertStatement(this); + + @override + Iterable get childNodes sync* { + yield table; + yield* targetColumns; + yield* source.childNodes; + } + + @override + bool contentEquals(InsertStatement other) { + return other.mode == mode && other.source.runtimeType == source.runtimeType; + } +} + +abstract class InsertSource { + Iterable get childNodes; + + const InsertSource(); + + T when( + {T Function(ValuesSource) isValues, + T Function(SelectInsertSource) isSelect, + T Function(DefaultValues) isDefaults}) { + if (this is ValuesSource) { + return isValues?.call(this as ValuesSource); + } else if (this is SelectInsertSource) { + return isSelect?.call(this as SelectInsertSource); + } else if (this is DefaultValues) { + return isDefaults?.call(this as DefaultValues); + } else { + throw StateError('Did not expect $runtimeType as InsertSource'); + } + } +} + +/// Uses a list of values for an insert statement (`VALUES (a, b, c)`). +class ValuesSource extends InsertSource { + final List values; + + ValuesSource(this.values); + + @override + Iterable get childNodes => values; +} + +/// Inserts the rows returned by [stmt]. +class SelectInsertSource extends InsertSource { + final SelectStatement stmt; + + SelectInsertSource(this.stmt); + + @override + Iterable get childNodes => [stmt]; +} + +/// Use `DEFAULT VALUES` for an insert statement. +class DefaultValues extends InsertSource { + const DefaultValues(); + + @override + final Iterable childNodes = const []; +} diff --git a/sqlparser/lib/src/reader/parser/crud.dart b/sqlparser/lib/src/reader/parser/crud.dart index 9545eb84..e0ba177f 100644 --- a/sqlparser/lib/src/reader/parser/crud.dart +++ b/sqlparser/lib/src/reader/parser/crud.dart @@ -368,6 +368,75 @@ mixin CrudParser on ParserBase { or: failureMode, table: table, set: set, where: where); } + InsertStatement _insertStmt() { + if (!_match(const [TokenType.insert, TokenType.replace])) return null; + + final firstToken = _previous; + InsertMode insertMode; + if (_previous.type == TokenType.insert) { + // insert modes can have a failure clause (INSERT OR xxx) + if (_matchOne(TokenType.or)) { + const tokensToModes = { + TokenType.replace: InsertMode.insertOrReplace, + TokenType.rollback: InsertMode.insertOrRollback, + TokenType.abort: InsertMode.insertOrAbort, + TokenType.fail: InsertMode.insertOrFail, + TokenType.ignore: InsertMode.insertOrIgnore + }; + + if (_match(tokensToModes.keys)) { + insertMode = tokensToModes[_previous.type]; + } else { + _error( + 'After the INSERT OR, expected an insert mode (REPLACE, ROLLBACK, etc.)'); + } + } else { + insertMode = InsertMode.insert; + } + } else { + // is it wasn't an insert, it must have been a replace + insertMode = InsertMode.replace; + } + assert(insertMode != null); + _consume(TokenType.into, 'Expected INSERT INTO'); + + final table = _tableReference(); + final targetColumns = []; + + if (_matchOne(TokenType.leftParen)) { + do { + final columnRef = _consumeIdentifier('Expected a column'); + targetColumns.add(Reference(columnName: columnRef.identifier)); + } while (_matchOne(TokenType.comma)); + + _consume(TokenType.rightParen, + 'Expected clpsing parenthesis after column list'); + } + final source = _insertSource(); + + return InsertStatement( + mode: insertMode, + table: table, + targetColumns: targetColumns, + source: source, + )..setSpan(firstToken, _previous); + } + + InsertSource _insertSource() { + if (_matchOne(TokenType.$values)) { + final values = []; + do { + values.add(_consumeTuple()); + } while (_matchOne(TokenType.comma)); + return ValuesSource(values); + } else if (_matchOne(TokenType.$default)) { + _consume(TokenType.$values, 'Expected DEFAULT VALUES'); + return const DefaultValues(); + } else { + return SelectInsertSource(select()); + } + } + @override WindowDefinition _windowDefinition() { _consume(TokenType.leftParen, 'Expected opening parenthesis'); diff --git a/sqlparser/lib/src/reader/parser/expressions.dart b/sqlparser/lib/src/reader/parser/expressions.dart index 61227a0a..594f0a6e 100644 --- a/sqlparser/lib/src/reader/parser/expressions.dart +++ b/sqlparser/lib/src/reader/parser/expressions.dart @@ -391,4 +391,19 @@ mixin ExpressionParser on ParserBase { windowName: windowName, )..setSpan(name, _previous); } + + TupleExpression _consumeTuple() { + final firstToken = + _consume(TokenType.leftParen, 'Expected opening parenthesis for tuple'); + final expressions = []; + + do { + expressions.add(expression()); + } while (_matchOne(TokenType.comma)); + + _consume(TokenType.rightParen, 'Expected right parenthesis to close tuple'); + + return TupleExpression(expressions: expressions) + ..setSpan(firstToken, _previous); + } } diff --git a/sqlparser/lib/src/reader/parser/parser.dart b/sqlparser/lib/src/reader/parser/parser.dart index 8fc1de35..05a39cbd 100644 --- a/sqlparser/lib/src/reader/parser/parser.dart +++ b/sqlparser/lib/src/reader/parser/parser.dart @@ -129,6 +129,7 @@ abstract class ParserBase { // Common operations that we are referenced very often Expression expression(); + TupleExpression _consumeTuple(); /// Parses a [SelectStatement], or returns null if there is no select token /// after the current position. @@ -153,7 +154,11 @@ class Parser extends ParserBase Statement statement({bool expectEnd = true}) { final first = _peek; - final stmt = select() ?? _deleteStmt() ?? _update() ?? _createTable(); + final stmt = select() ?? + _deleteStmt() ?? + _update() ?? + _insertStmt() ?? + _createTable(); if (stmt == null) { _error('Expected a sql statement to start here'); diff --git a/sqlparser/lib/src/reader/tokenizer/token.dart b/sqlparser/lib/src/reader/tokenizer/token.dart index b7c7251d..ebc12054 100644 --- a/sqlparser/lib/src/reader/tokenizer/token.dart +++ b/sqlparser/lib/src/reader/tokenizer/token.dart @@ -57,6 +57,8 @@ enum TokenType { select, delete, update, + insert, + into, distinct, all, from, @@ -124,6 +126,7 @@ enum TokenType { unique, check, $default, + $values, conflict, references, cascade, @@ -141,6 +144,8 @@ enum TokenType { const Map keywords = { 'SELECT': TokenType.select, + 'INSERT': TokenType.insert, + 'INTO': TokenType.into, 'COLLATE': TokenType.collate, 'DISTINCT': TokenType.distinct, 'UPDATE': TokenType.update, @@ -228,6 +233,7 @@ const Map keywords = { 'OTHERS': TokenType.others, 'TIES': TokenType.ties, 'WINDOW': TokenType.window, + 'VALUES': TokenType.$values, }; const Map moorKeywords = { diff --git a/sqlparser/test/parser/insert_test.dart b/sqlparser/test/parser/insert_test.dart new file mode 100644 index 00000000..75580cb5 --- /dev/null +++ b/sqlparser/test/parser/insert_test.dart @@ -0,0 +1,57 @@ +import 'package:test/test.dart'; +import 'package:sqlparser/sqlparser.dart'; + +import 'utils.dart'; + +void main() { + test('parses insert statements', () { + testStatement( + 'INSERT OR REPLACE INTO tbl (a, b, c) VALUES (d, e, f)', + InsertStatement( + mode: InsertMode.insertOrReplace, + table: TableReference('tbl', null), + targetColumns: [ + Reference(columnName: 'a'), + Reference(columnName: 'b'), + Reference(columnName: 'c'), + ], + source: ValuesSource([ + TupleExpression(expressions: [ + Reference(columnName: 'd'), + Reference(columnName: 'e'), + Reference(columnName: 'f'), + ]), + ]), + ), + ); + }); + + test('insert statement with default values', () { + testStatement( + 'INSERT INTO tbl DEFAULT VALUES', + InsertStatement( + mode: InsertMode.insert, + table: TableReference('tbl', null), + targetColumns: const [], + source: const DefaultValues(), + ), + ); + }); + + test('insert statement with select as source', () { + testStatement( + 'REPLACE INTO tbl SELECT * FROM tbl', + InsertStatement( + mode: InsertMode.replace, + table: TableReference('tbl', null), + targetColumns: const [], + source: SelectInsertSource( + SelectStatement( + columns: [StarResultColumn(null)], + from: [TableReference('tbl', null)], + ), + ), + ), + ); + }); +}