sqlparser: Parse CREATE INDEX statements

This commit is contained in:
Simon Binder 2020-01-03 17:14:42 +01:00
parent fc0949ebd1
commit 6924543a47
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
11 changed files with 233 additions and 9 deletions

View File

@ -4,6 +4,7 @@
- Experimental new type inference algorithm
- Support `CAST` expressions.
- Support parsing `CREATE TRIGGER` statements
- Support parsing `CREATE INDEX` statement
## 0.5.0
- Optionally support the `json1` module

View File

@ -11,14 +11,21 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
@override
void visitCrudStatement(CrudStatement stmt, TypeExpectation arg) {
if (stmt is HasWhereClause && stmt.where != null) {
_handleWhereClause(stmt);
_visitExcept(stmt, stmt.where, arg);
if (stmt is HasWhereClause) {
final typedStmt = stmt as HasWhereClause;
_handleWhereClause(typedStmt);
_visitExcept(stmt, typedStmt.where, arg);
} else {
visitChildren(stmt, arg);
}
}
@override
void visitCreateIndexStatement(CreateIndexStatement e, TypeExpectation arg) {
_handleWhereClause(e);
_visitExcept(e, e.where, arg);
}
@override
void visitJoin(Join e, TypeExpectation arg) {
final constraint = e.constraint;
@ -165,9 +172,12 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
}
void _handleWhereClause(HasWhereClause stmt) {
// assume that a where statement is a boolean expression. Sqlite internally
// casts (https://www.sqlite.org/lang_expr.html#booleanexpr), so be lax
visit(stmt.where, const ExactTypeExpectation.laxly(ResolvedType.bool()));
if (stmt.where != null) {
// assume that a where statement is a boolean expression. Sqlite
// internally casts (https://www.sqlite.org/lang_expr.html#booleanexpr),
// so be lax
visit(stmt.where, const ExactTypeExpectation.laxly(ResolvedType.bool()));
}
}
void _visitExcept(AstNode node, AstNode skip, TypeExpectation arg) {

View File

@ -30,6 +30,7 @@ part 'schema/column_definition.dart';
part 'schema/table_definition.dart';
part 'statements/block.dart';
part 'statements/create_table.dart';
part 'statements/create_index.dart';
part 'statements/create_trigger.dart';
part 'statements/delete.dart';
part 'statements/insert.dart';

View File

@ -0,0 +1,64 @@
part of '../ast.dart';
class CreateIndexStatement extends Statement
implements CreatingStatement, HasWhereClause {
final String indexName;
final bool unique;
final bool ifNotExists;
IdentifierToken nameToken;
final TableReference on;
final List<IndexedColumn> columns;
@override
final Expression where;
CreateIndexStatement(
{@required this.indexName,
this.unique = false,
this.ifNotExists = false,
@required this.on,
@required this.columns,
this.where});
@override
String get createdName => indexName;
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
return visitor.visitCreateIndexStatement(this, arg);
}
@override
Iterable<AstNode> get childNodes =>
[on, ...columns, if (where != null) where];
@override
bool contentEquals(CreateIndexStatement other) {
return other.indexName == indexName;
}
}
/// Note that this class matches the productions listed at https://www.sqlite.org/syntax/indexed-column.html
/// We don't have a special case for `column-name` (those are [Reference]s).
/// The `COLLATE` branch is covered by parsing a [CollateExpression] for
/// [expression].
class IndexedColumn extends AstNode {
/// The expression on which the index should be created. Most commonly a
/// [Reference], for simple column names.
final Expression expression;
// nullable
final OrderingMode ordering;
IndexedColumn(this.expression, [this.ordering]);
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
return visitor.visitIndexedColumn(this, arg);
}
@override
Iterable<AstNode> get childNodes => [expression];
@override
bool contentEquals(IndexedColumn other) => other.ordering == ordering;
}

View File

@ -1,7 +1,7 @@
part of '../ast.dart';
abstract class TableInducingStatement extends Statement
implements SchemaStatement {
implements CreatingStatement {
final bool ifNotExists;
final String tableName;
@ -14,6 +14,9 @@ abstract class TableInducingStatement extends Statement
TableInducingStatement._(this.ifNotExists, this.tableName,
[this.overriddenDataClassName]);
@override
String get createdName => tableName;
}
/// A "CREATE TABLE" statement, see https://www.sqlite.org/lang_createtable.html

View File

@ -1,7 +1,7 @@
part of '../ast.dart';
/// A "CREATE TRIGGER" statement, see https://sqlite.org/lang_createtrigger.html
class CreateTriggerStatement extends Statement implements SchemaStatement {
class CreateTriggerStatement extends Statement implements CreatingStatement {
final bool ifNotExists;
final String triggerName;
IdentifierToken triggerNameToken;
@ -23,6 +23,9 @@ class CreateTriggerStatement extends Statement implements SchemaStatement {
this.when,
@required this.action});
@override
String get createdName => triggerName;
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
return visitor.visitCreateTriggerStatement(this, arg);

View File

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

View File

@ -11,6 +11,7 @@ abstract class AstVisitor<A, R> {
R visitCreateTableStatement(CreateTableStatement e, A arg);
R visitCreateVirtualTableStatement(CreateVirtualTableStatement e, A arg);
R visitCreateTriggerStatement(CreateTriggerStatement e, A arg);
R visitCreateIndexStatement(CreateIndexStatement e, A arg);
R visitWithClause(WithClause e, A arg);
R visitCommonTableExpression(CommonTableExpression e, A arg);
@ -47,6 +48,7 @@ abstract class AstVisitor<A, R> {
R visitAggregateExpression(AggregateExpression e, A arg);
R visitWindowDefinition(WindowDefinition e, A arg);
R visitFrameSpec(FrameSpec e, A arg);
R visitIndexedColumn(IndexedColumn e, A arg);
R visitNumberedVariable(NumberedVariable e, A arg);
R visitNamedVariable(ColonNamedVariable e, A arg);
@ -104,6 +106,11 @@ class RecursiveVisitor<A, R> implements AstVisitor<A, R> {
return visitSchemaStatement(e, arg);
}
@override
R visitCreateIndexStatement(CreateIndexStatement e, A arg) {
return visitSchemaStatement(e, arg);
}
R visitBaseSelectStatement(BaseSelectStatement stmt, A arg) {
return visitCrudStatement(stmt, arg);
}
@ -211,6 +218,11 @@ class RecursiveVisitor<A, R> implements AstVisitor<A, R> {
return visitChildren(e, arg);
}
@override
R visitIndexedColumn(IndexedColumn e, A arg) {
return visitChildren(e, arg);
}
@override
R visitBlock(Block e, A arg) {
return visitChildren(e, arg);

View File

@ -8,6 +8,8 @@ mixin SchemaParser on ParserBase {
return _createTable();
} else if (_check(TokenType.trigger)) {
return _createTrigger();
} else if (_check(TokenType.unique) || _check(TokenType.$index)) {
return _createIndex();
}
return null;
@ -234,6 +236,52 @@ mixin SchemaParser on ParserBase {
..triggerNameToken = trigger;
}
/// Parses a [CreateIndexStatement]. The `CREATE` token must have already been
/// accepted.
CreateIndexStatement _createIndex() {
final create = _previous;
assert(create.type == TokenType.create);
final unique = _matchOne(TokenType.unique);
if (!_matchOne(TokenType.$index)) return null;
final ifNotExists = _ifNotExists();
final name = _consumeIdentifier('Expected a name for this index');
_consume(TokenType.on, 'Expected ON table');
final nameToken = _consumeIdentifier('Expected a table name');
final tableRef = TableReference(nameToken.identifier)
..setSpan(nameToken, nameToken);
_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));
_consume(TokenType.rightParen, 'Expected closing bracket');
Expression where;
if (_matchOne(TokenType.where)) {
where = expression();
}
return CreateIndexStatement(
indexName: name.identifier,
unique: unique,
ifNotExists: ifNotExists,
on: tableRef,
columns: indexes,
where: where,
)
..nameToken = name
..setSpan(create, _previous);
}
/// Parses `IF NOT EXISTS` | epsilon
bool _ifNotExists() {
if (_matchOne(TokenType.$if)) {

View File

@ -122,6 +122,7 @@ enum TokenType {
virtual,
table,
trigger,
$index,
$if,
$with,
without,
@ -222,6 +223,7 @@ const Map<String, TokenType> keywords = {
'CREATE': TokenType.create,
'TABLE': TokenType.table,
'TRIGGER': TokenType.trigger,
'INDEX': TokenType.$index,
'IF': TokenType.$if,
'WITH': TokenType.$with,
'WITHOUT': TokenType.without,

View File

@ -0,0 +1,75 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import 'utils.dart';
void main() {
test('parses CREATE INDEX statements', () {
testStatement(
'CREATE INDEX foo ON bar (baz, inga) WHERE TRUE',
CreateIndexStatement(
indexName: 'foo',
on: TableReference('bar'),
columns: [
IndexedColumn(Reference(columnName: 'baz')),
IndexedColumn(Reference(columnName: 'inga')),
],
where: BooleanLiteral.withTrue(token(TokenType.$true)),
),
);
});
test('with unique and IF NOT EXISTS', () {
testStatement(
'CREATE UNIQUE INDEX IF NOT EXISTS foo ON bar (baz);',
CreateIndexStatement(
unique: true,
ifNotExists: true,
indexName: 'foo',
on: TableReference('bar'),
columns: [
IndexedColumn(Reference(columnName: 'baz')),
],
),
);
});
test('can have ordering modes on index expressions', () {
testStatement(
'CREATE INDEX foo ON bar (a + b DESC);',
CreateIndexStatement(
indexName: 'foo',
on: TableReference('bar'),
columns: [
IndexedColumn(
BinaryExpression(
Reference(columnName: 'a'),
token(TokenType.plus),
Reference(columnName: 'b'),
),
OrderingMode.descending,
),
],
),
);
});
test('can have collate expressions', () {
testStatement(
'CREATE INDEX foo ON bar (baz COLLATE RTRIM);',
CreateIndexStatement(
indexName: 'foo',
on: TableReference('bar'),
columns: [
IndexedColumn(
CollateExpression(
inner: Reference(columnName: 'baz'),
operator: token(TokenType.collate),
collateFunction: identifier('RTRIM'),
),
),
],
),
);
});
}