mirror of https://github.com/AMT-Cheif/drift.git
sqlparser: Parse CREATE INDEX statements
This commit is contained in:
parent
fc0949ebd1
commit
6924543a47
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue