diff --git a/drift_dev/pubspec.yaml b/drift_dev/pubspec.yaml index c305bcd0..875af92c 100644 --- a/drift_dev/pubspec.yaml +++ b/drift_dev/pubspec.yaml @@ -32,7 +32,7 @@ dependencies: # Drift-specific analysis and apis drift: '>=2.10.0 <2.11.0' sqlite3: '>=0.1.6 <3.0.0' - sqlparser: '^0.30.0' + sqlparser: '^0.31.0' # Dart analysis analyzer: ^5.12.0 diff --git a/sqlparser/CHANGELOG.md b/sqlparser/CHANGELOG.md index f51d4af4..d0163490 100644 --- a/sqlparser/CHANGELOG.md +++ b/sqlparser/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.31.0-dev + +- Add `SqlEngine.parseMultiple` to parse multiple statements into one AST. + ## 0.30.3 - Fix `WITH` clauses not being resolved for compound select statements. diff --git a/sqlparser/lib/src/ast/statements/statement.dart b/sqlparser/lib/src/ast/statements/statement.dart index b3a11d9a..9012ccb1 100644 --- a/sqlparser/lib/src/ast/statements/statement.dart +++ b/sqlparser/lib/src/ast/statements/statement.dart @@ -8,6 +8,26 @@ abstract class Statement extends AstNode { Token? semicolon; } +/// A list of statements, separated by semicolons. +class SemicolonSeparatedStatements extends AstNode { + List statements; + + SemicolonSeparatedStatements(this.statements); + + @override + R accept(AstVisitor visitor, A arg) { + return visitor.visitSemicolonSeparatedStatements(this, arg); + } + + @override + Iterable get childNodes => statements; + + @override + void transformChildren(Transformer transformer, A arg) { + statements = transformer.transformChildren(statements, this, arg); + } +} + /// A statement that reads from an existing table structure and has an optional /// `WITH` clause. abstract class CrudStatement extends Statement { diff --git a/sqlparser/lib/src/ast/visitor.dart b/sqlparser/lib/src/ast/visitor.dart index 72f1adb2..48adfb97 100644 --- a/sqlparser/lib/src/ast/visitor.dart +++ b/sqlparser/lib/src/ast/visitor.dart @@ -92,6 +92,7 @@ abstract class AstVisitor { R visitNamedVariable(ColonNamedVariable e, A arg); R visitBlock(Block block, A arg); + R visitSemicolonSeparatedStatements(SemicolonSeparatedStatements e, A arg); R visitBeginTransaction(BeginTransactionStatement e, A arg); R visitCommitStatement(CommitStatement e, A arg); @@ -382,6 +383,11 @@ class RecursiveVisitor implements AstVisitor { return defaultNode(e, arg); } + @override + R? visitSemicolonSeparatedStatements(SemicolonSeparatedStatements e, A arg) { + return defaultNode(e, arg); + } + @override R? visitBeginTransaction(BeginTransactionStatement e, A arg) { return visitStatement(e, arg); diff --git a/sqlparser/lib/src/engine/sql_engine.dart b/sqlparser/lib/src/engine/sql_engine.dart index 62ae4ada..6a96d229 100644 --- a/sqlparser/lib/src/engine/sql_engine.dart +++ b/sqlparser/lib/src/engine/sql_engine.dart @@ -143,6 +143,18 @@ class SqlEngine { return ParseResult._(stmt, tokens, parser.errors, sql, null); } + /// Parses multiple [sql] statements, separated by a semicolon. + /// + /// You can use the [AstNode.children] of the returned [ParseResult.rootNode] + /// to inspect the returned statements. + ParseResult parseMultiple(String sql) { + final tokens = tokenize(sql); + final parser = _createParser(tokens); + + final ast = parser.safeStatements(); + return ParseResult._(ast, tokens, parser.errors, sql, null); + } + /// Parses [sql] as a list of column constraints. /// /// The [ParseResult.rootNode] will be a [ColumnDefinition] with the parsed diff --git a/sqlparser/lib/src/reader/parser.dart b/sqlparser/lib/src/reader/parser.dart index 767285f2..a272b58a 100644 --- a/sqlparser/lib/src/reader/parser.dart +++ b/sqlparser/lib/src/reader/parser.dart @@ -185,6 +185,19 @@ class Parser { InvalidStatement(); } + SemicolonSeparatedStatements safeStatements() { + final first = _peek; + final statements = []; + while (!_isAtEnd) { + final statement = _parseAsStatement(_statementWithoutSemicolon); + if (statement != null) { + statements.add(statement); + } + } + + return SemicolonSeparatedStatements(statements)..setSpan(first, _previous); + } + /// Parses the remaining input as column constraints. List columnConstraintsUntilEnd() { final constraints = []; diff --git a/sqlparser/lib/src/utils/ast_equality.dart b/sqlparser/lib/src/utils/ast_equality.dart index 79a0374d..10efa09e 100644 --- a/sqlparser/lib/src/utils/ast_equality.dart +++ b/sqlparser/lib/src/utils/ast_equality.dart @@ -600,6 +600,13 @@ class EqualityEnforcingVisitor implements AstVisitor { _checkChildren(e); } + @override + void visitSemicolonSeparatedStatements( + SemicolonSeparatedStatements e, void arg) { + _currentAs(e); + _checkChildren(e); + } + @override void visitSetComponent(SetComponent e, void arg) { _currentAs(e); diff --git a/sqlparser/lib/utils/node_to_text.dart b/sqlparser/lib/utils/node_to_text.dart index 5a5aa90d..eae4eeb1 100644 --- a/sqlparser/lib/utils/node_to_text.dart +++ b/sqlparser/lib/utils/node_to_text.dart @@ -1046,6 +1046,16 @@ class NodeSqlBuilder extends AstVisitor { } } + @override + void visitSemicolonSeparatedStatements( + SemicolonSeparatedStatements e, void arg) { + for (final stmt in e.statements) { + visit(stmt, arg); + buffer.writeln(';'); + needsSpace = false; + } + } + @override void visitSetComponent(SetComponent e, void arg) { visit(e.column, arg); diff --git a/sqlparser/pubspec.yaml b/sqlparser/pubspec.yaml index 3ab512b2..38286c9a 100644 --- a/sqlparser/pubspec.yaml +++ b/sqlparser/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlparser description: Parses sqlite statements and performs static analysis on them -version: 0.30.3 +version: 0.31.0-dev homepage: https://github.com/simolus3/drift/tree/develop/sqlparser repository: https://github.com/simolus3/drift #homepage: https://drift.simonbinder.eu/ diff --git a/sqlparser/test/parser/multiple_statements.dart b/sqlparser/test/parser/multiple_statements_test.dart similarity index 58% rename from sqlparser/test/parser/multiple_statements.dart rename to sqlparser/test/parser/multiple_statements_test.dart index f6c6a8a7..1f999404 100644 --- a/sqlparser/test/parser/multiple_statements.dart +++ b/sqlparser/test/parser/multiple_statements_test.dart @@ -83,4 +83,64 @@ void main() { ), ); }); + + test('parses multiple statements with parseMultiple()', () { + const sql = ''' + CREATE TABLE users ( + id INTEGER PRIMARY KEY, + name TEXT, + email TEXT + ); + + SELECT * FROM users WHERE id = 1; + + INSERT INTO users (name, email) VALUES ('John Doe', 'john@example.com'); + '''; + + final engine = SqlEngine(); + final ast = engine.parseMultiple(sql).rootNode; + enforceHasSpan(ast); + + enforceEqual( + ast, + SemicolonSeparatedStatements( + [ + CreateTableStatement( + tableName: 'users', + columns: [ + ColumnDefinition( + columnName: 'id', + typeName: 'INTEGER', + constraints: [PrimaryKeyColumn(null)], + ), + ColumnDefinition(columnName: 'name', typeName: 'TEXT'), + ColumnDefinition(columnName: 'email', typeName: 'TEXT'), + ], + ), + SelectStatement( + columns: [StarResultColumn()], + from: TableReference('users'), + where: BinaryExpression( + Reference(columnName: 'id'), + token(TokenType.equal), + NumericLiteral(1), + ), + ), + InsertStatement( + table: TableReference('users'), + targetColumns: [ + Reference(columnName: 'name'), + Reference(columnName: 'email') + ], + source: ValuesSource([ + Tuple(expressions: [ + StringLiteral('John Doe'), + StringLiteral('john@example.com'), + ]), + ]), + ), + ], + ), + ); + }); } diff --git a/sqlparser/test/utils/node_to_text_test.dart b/sqlparser/test/utils/node_to_text_test.dart index 875ce22d..184f9c6e 100644 --- a/sqlparser/test/utils/node_to_text_test.dart +++ b/sqlparser/test/utils/node_to_text_test.dart @@ -3,7 +3,11 @@ import 'package:sqlparser/src/utils/ast_equality.dart'; import 'package:sqlparser/utils/node_to_text.dart'; import 'package:test/test.dart'; -enum _ParseKind { statement, driftFile } +enum _ParseKind { + statement, + driftFile, + multipleStatements, +} void main() { final engine = @@ -21,6 +25,9 @@ void main() { case _ParseKind.driftFile: result = engine.parseDriftFile(input); break; + case _ParseKind.multipleStatements: + result = engine.parseMultiple(input); + break; } if (result.errors.isNotEmpty) { @@ -599,4 +606,19 @@ COMMIT TRANSACTION; test('does not format invalid statements', () { expect(InvalidStatement().toSql, throwsUnsupportedError); }); + + test('multiple statements', () { + testFormat(''' +CREATE TABLE my_table ( + id INTEGER NOT NULL PRIMARY KEY, + another TEXT +) STRICT; + +BEGIN; + +INSERT INTO foo (bar, baz) VALUES ('hi', 3); + +COMMIT; +''', kind: _ParseKind.multipleStatements); + }); }