Parse CREATE TABLE statements

This commit is contained in:
Simon Binder 2019-07-26 13:35:21 +02:00
parent 888e429467
commit dee9993c83
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
5 changed files with 337 additions and 31 deletions

View File

@ -65,11 +65,11 @@ package to generate type-safe methods from sql.
Most on this list is just not supported yet because I didn't found a use case for Most on this list is just not supported yet because I didn't found a use case for
them yet. If you need them, just leave an issue and I'll try to implement them soon. them yet. If you need them, just leave an issue and I'll try to implement them soon.
- For now, only `INSERT` statements are not supported, but they will be soon - For now, `INSERT` statements are not supported, but they will be soon.
- Windowing is not supported yet - Windowing is not supported yet
- Compound select statements (`UNION` / `INTERSECT`) are not supported yet - Compound select statements (`UNION` / `INTERSECT`) are not supported yet
- Common table expressions are not supported - Common table expressions are not supported
- Some advanced expressions, like `COLLATE` or `CAST`s aren't supported yet. - Some advanced expressions, like `CAST`s aren't supported yet.
If you run into parsing errors with what you think is valid sql, please create an issue. If you run into parsing errors with what you think is valid sql, please create an issue.

View File

@ -27,6 +27,10 @@ class ColumnDefinition extends AstNode {
abstract class ColumnConstraint extends AstNode { abstract class ColumnConstraint extends AstNode {
// todo foreign key clause // todo foreign key clause
final String name;
ColumnConstraint(this.name);
@override @override
T accept<T>(AstVisitor<T> visitor) => visitor.visitColumnConstraint(this); T accept<T>(AstVisitor<T> visitor) => visitor.visitColumnConstraint(this);
@ -54,43 +58,60 @@ abstract class ColumnConstraint extends AstNode {
throw Exception('Did not expect $runtimeType as a ColumnConstraint'); throw Exception('Did not expect $runtimeType as a ColumnConstraint');
} }
} }
@visibleForOverriding
bool _equalToConstraint(covariant ColumnConstraint other);
@override
bool contentEquals(ColumnConstraint other) {
return other.name == name && _equalToConstraint(other);
}
} }
enum ConflictClause { rollback, abort, fail, ignore, replace } enum ConflictClause { rollback, abort, fail, ignore, replace }
class NotNull extends ColumnConstraint { class NotNull extends ColumnConstraint {
final ConflictClause onConflict;
NotNull(String name, {this.onConflict}) : super(name);
@override @override
final Iterable<AstNode> childNodes = const []; final Iterable<AstNode> childNodes = const [];
@override @override
bool contentEquals(NotNull other) => true; bool _equalToConstraint(NotNull other) => onConflict == other.onConflict;
} }
class PrimaryKey extends ColumnConstraint { class PrimaryKey extends ColumnConstraint {
final bool autoIncrement; final bool autoIncrement;
final ConflictClause onConflict;
final OrderingMode mode; final OrderingMode mode;
PrimaryKey(this.autoIncrement, this.mode); PrimaryKey(String name,
{this.autoIncrement = false, this.mode, this.onConflict})
: super(name);
@override @override
Iterable<AstNode> get childNodes => null; Iterable<AstNode> get childNodes => const [];
@override @override
bool contentEquals(PrimaryKey other) { bool _equalToConstraint(PrimaryKey other) {
return other.autoIncrement == autoIncrement && other.mode == mode; return other.autoIncrement == autoIncrement &&
other.mode == mode &&
other.onConflict == onConflict;
} }
} }
class Unique extends ColumnConstraint { class Unique extends ColumnConstraint {
final ConflictClause onConflict; final ConflictClause onConflict;
Unique(this.onConflict); Unique(String name, this.onConflict) : super(name);
@override @override
Iterable<AstNode> get childNodes => const []; Iterable<AstNode> get childNodes => const [];
@override @override
bool contentEquals(Unique other) { bool _equalToConstraint(Unique other) {
return other.onConflict == onConflict; return other.onConflict == onConflict;
} }
} }
@ -98,35 +119,35 @@ class Unique extends ColumnConstraint {
class Check extends ColumnConstraint { class Check extends ColumnConstraint {
final Expression expression; final Expression expression;
Check(this.expression); Check(String name, this.expression) : super(name);
@override @override
Iterable<AstNode> get childNodes => [expression]; Iterable<AstNode> get childNodes => [expression];
@override @override
bool contentEquals(Check other) => true; bool _equalToConstraint(Check other) => true;
} }
class Default extends ColumnConstraint { class Default extends ColumnConstraint {
final Expression expression; final Expression expression;
Default(this.expression); Default(String name, this.expression) : super(name);
@override @override
Iterable<AstNode> get childNodes => [expression]; Iterable<AstNode> get childNodes => [expression];
@override @override
bool contentEquals(Default other) => true; bool _equalToConstraint(Default other) => true;
} }
class CollateConstraint extends ColumnConstraint { class CollateConstraint extends ColumnConstraint {
final String collation; final String collation;
CollateConstraint(this.collation); CollateConstraint(String name, this.collation) : super(name);
@override @override
final Iterable<AstNode> childNodes = const []; final Iterable<AstNode> childNodes = const [];
@override @override
bool contentEquals(CollateConstraint other) => true; bool _equalToConstraint(CollateConstraint other) => true;
} }

View File

@ -52,7 +52,7 @@ class Parser {
Token get _peekNext => tokens[_current + 1]; Token get _peekNext => tokens[_current + 1];
Token get _previous => tokens[_current - 1]; Token get _previous => tokens[_current - 1];
bool _match(List<TokenType> types) { bool _match(Iterable<TokenType> types) {
for (var type in types) { for (var type in types) {
if (_check(type)) { if (_check(type)) {
_advance(); _advance();
@ -110,7 +110,7 @@ class Parser {
} }
Statement statement() { Statement statement() {
final stmt = select() ?? _deleteStmt() ?? _update(); final stmt = select() ?? _deleteStmt() ?? _update() ?? _createTable();
_matchOne(TokenType.semicolon); _matchOne(TokenType.semicolon);
if (!_isAtEnd) { if (!_isAtEnd) {
@ -390,14 +390,17 @@ class Parser {
OrderingTerm _orderingTerm() { OrderingTerm _orderingTerm() {
final expr = expression(); final expr = expression();
return OrderingTerm(expression: expr, orderingMode: _orderingModeOrNull());
}
OrderingMode _orderingModeOrNull() {
if (_match(const [TokenType.asc, TokenType.desc])) { if (_match(const [TokenType.asc, TokenType.desc])) {
final mode = _previous.type == TokenType.asc final mode = _previous.type == TokenType.asc
? OrderingMode.ascending ? OrderingMode.ascending
: OrderingMode.descending; : OrderingMode.descending;
return OrderingTerm(expression: expr, orderingMode: mode); return mode;
} }
return null;
return OrderingTerm(expression: expr);
} }
/// Parses a [Limit] clause, or returns null if there is no limit token after /// Parses a [Limit] clause, or returns null if there is no limit token after
@ -463,6 +466,177 @@ class Parser {
or: failureMode, table: table, set: set, where: where); or: failureMode, table: table, set: set, where: where);
} }
CreateTableStatement _createTable() {
if (!_matchOne(TokenType.create)) return null;
final first = _previous;
_consume(TokenType.table, 'Expected TABLE keyword here');
var ifNotExists = false;
if (_matchOne(TokenType.$if)) {
_consume(TokenType.not, 'Expected IF to be followed by NOT EXISTS');
_consume(TokenType.exists, 'Expected IF NOT to be followed by EXISTS');
ifNotExists = true;
}
final tableIdentifier =
_consume(TokenType.identifier, 'Expected a table name')
as IdentifierToken;
// we don't currently support CREATE TABLE x AS SELECT ... statements
_consume(
TokenType.leftParen, 'Expected opening parenthesis to list columns');
final columns = <ColumnDefinition>[];
do {
columns.add(_columnDefinition());
} while (_matchOne(TokenType.comma));
// todo parse table constraints
_consume(TokenType.rightParen, 'Expected closing parenthesis');
var withoutRowId = false;
if (_matchOne(TokenType.without)) {
_consume(
TokenType.rowid, 'Expected ROWID to complete the WITHOUT ROWID part');
withoutRowId = true;
}
return CreateTableStatement(
ifNotExists: ifNotExists,
tableName: tableIdentifier.identifier,
withoutRowId: withoutRowId,
columns: columns,
)..setSpan(first, _previous);
}
ColumnDefinition _columnDefinition() {
final name = _consume(TokenType.identifier, 'Expected a column name')
as IdentifierToken;
IdentifierToken typeName;
if (_matchOne(TokenType.identifier)) {
typeName = _previous as IdentifierToken;
}
final constraints = <ColumnConstraint>[];
ColumnConstraint constraint;
while ((constraint = _columnConstraint(orNull: true)) != null) {
constraints.add(constraint);
}
return ColumnDefinition(
columnName: name.identifier,
typeName: typeName?.identifier,
constraints: constraints,
)..setSpan(name, _previous);
}
ColumnConstraint _columnConstraint({bool orNull = false}) {
Token first;
IdentifierToken name;
if (_matchOne(TokenType.constraint)) {
first = _previous;
name = _consume(
TokenType.identifier, 'Expect a name for the constraint here')
as IdentifierToken;
}
final resolvedName = name?.identifier;
if (_matchOne(TokenType.primary)) {
// set reference to first token in this constraint if not set because of
// the CONSTRAINT token
first ??= _previous;
_consume(TokenType.key, 'Expected KEY to complete PRIMARY KEY clause');
final mode = _orderingModeOrNull();
final conflict = _conflictClauseOrNull();
final hasAutoInc = _matchOne(TokenType.autoincrement);
return PrimaryKey(resolvedName,
autoIncrement: hasAutoInc, mode: mode, onConflict: conflict)
..setSpan(first, _previous);
}
if (_matchOne(TokenType.not)) {
first ??= _previous;
_consume(TokenType.$null, 'Expected NULL to complete NOT NULL');
return NotNull(resolvedName, onConflict: _conflictClauseOrNull())
..setSpan(first, _previous);
}
if (_matchOne(TokenType.unique)) {
first ??= _previous;
return Unique(resolvedName, _conflictClauseOrNull())
..setSpan(first, _previous);
}
if (_matchOne(TokenType.check)) {
first ??= _previous;
_consume(TokenType.leftParen, 'Expected opening parenthesis');
final expr = expression();
_consume(TokenType.rightParen, 'Expected closing parenthesis');
return Check(resolvedName, expr)..setSpan(first, _previous);
}
if (_matchOne(TokenType.$default)) {
first ??= _previous;
Expression expr = _literalOrNull();
if (expr == null) {
// no literal, expect (expression)
_consume(TokenType.leftParen,
'Expected opening parenthesis before expression');
expr = expression();
_consume(TokenType.rightParen, 'Expected closing parenthesis');
}
return Default(resolvedName, expr);
}
if (_matchOne(TokenType.collate)) {
first ??= _previous;
final collation =
_consume(TokenType.identifier, 'Expected the collation name')
as IdentifierToken;
return CollateConstraint(resolvedName, collation.identifier)
..setSpan(first, _previous);
}
// todo foreign key clauses
// no known column constraint matched. If orNull is set and we're not
// guaranteed to be in a constraint clause (started with CONSTRAINT), we
// can return null
if (orNull && name == null) {
return null;
}
_error('Expected a constraint (primary key, nullability, etc.)');
}
ConflictClause _conflictClauseOrNull() {
if (_matchOne(TokenType.on)) {
_consume(TokenType.conflict,
'Expected CONFLICT to complete ON CONFLICT clause');
const modes = {
TokenType.rollback: ConflictClause.rollback,
TokenType.abort: ConflictClause.abort,
TokenType.fail: ConflictClause.fail,
TokenType.ignore: ConflictClause.ignore,
TokenType.replace: ConflictClause.replace,
};
if (_match(modes.keys)) {
return modes[_previous.type];
} else {
_error('Expected a conflict handler (rollback, abort, etc.) here');
}
}
return null;
}
/* We parse expressions here. /* We parse expressions here.
* Operators have the following precedence: * Operators have the following precedence:
* - + ~ NOT (unary) * - + ~ NOT (unary)
@ -681,21 +855,41 @@ class Parser {
return expression; return expression;
} }
Literal _literalOrNull() {
final token = _peek;
Literal _parseInner() {
if (_matchOne(TokenType.numberLiteral)) {
return NumericLiteral(_parseNumber(token.lexeme), token);
}
if (_matchOne(TokenType.stringLiteral)) {
return StringLiteral(token as StringLiteralToken);
}
if (_matchOne(TokenType.$null)) {
return NullLiteral(token);
}
if (_matchOne(TokenType.$true)) {
return BooleanLiteral.withTrue(token);
}
if (_matchOne(TokenType.$false)) {
return BooleanLiteral.withFalse(token);
}
// todo CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP
return null;
}
final literal = _parseInner();
literal?.setSpan(token, token);
return literal;
}
Expression _primary() { Expression _primary() {
final literal = _literalOrNull();
if (literal != null) return literal;
final token = _advance(); final token = _advance();
final type = token.type; final type = token.type;
switch (type) { switch (type) {
case TokenType.numberLiteral:
return NumericLiteral(_parseNumber(token.lexeme), token);
case TokenType.stringLiteral:
return StringLiteral(token as StringLiteralToken);
case TokenType.$null:
return NullLiteral(token);
case TokenType.$true:
return BooleanLiteral.withTrue(token);
case TokenType.$false:
return BooleanLiteral.withFalse(token);
// todo CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP
case TokenType.leftParen: case TokenType.leftParen:
// Opening brackets could be three things: An inner select statement // Opening brackets could be three things: An inner select statement
// (SELECT ...), a parenthesised expression, or a tuple of expressions // (SELECT ...), a parenthesised expression, or a tuple of expressions

View File

@ -100,6 +100,14 @@ enum TokenType {
$if, $if,
without, without,
rowid, rowid,
constraint,
autoincrement,
primary,
key,
unique,
check,
$default,
conflict,
semicolon, semicolon,
eof, eof,
@ -165,6 +173,14 @@ const Map<String, TokenType> keywords = {
'IF': TokenType.$if, 'IF': TokenType.$if,
'WITHOUT': TokenType.without, 'WITHOUT': TokenType.without,
'ROWID': TokenType.rowid, 'ROWID': TokenType.rowid,
'CONSTRAINT': TokenType.constraint,
'AUTOINCREMENT': TokenType.autoincrement,
'PRIMARY': TokenType.primary,
'KEY': TokenType.key,
'UNIQUE': TokenType.unique,
'CHECK': TokenType.check,
'DEFAULT': TokenType.$default,
'CONFLICT': TokenType.conflict,
}; };
class Token { class Token {

View File

@ -0,0 +1,75 @@
import 'package:sqlparser/src/ast/ast.dart';
import 'utils.dart';
final statement = '''
CREATE TABLE IF NOT EXISTS users (
id INT NOT NULL PRIMARY KEY DESC ON CONFLICT ROLLBACK AUTOINCREMENT,
email VARCHAR NOT NULL UNIQUE ON CONFLICT ABORT,
score INT CONSTRAINT "score set" NOT NULL DEFAULT 420 CHECK (score > 0),
display_name VARCHAR COLLATE BINARY
)
''';
void main() {
testStatement(
statement,
CreateTableStatement(
tableName: 'users',
ifNotExists: true,
withoutRowId: false,
columns: [
ColumnDefinition(
columnName: 'id',
typeName: 'INT',
constraints: [
NotNull(null),
PrimaryKey(
null,
autoIncrement: true,
onConflict: ConflictClause.rollback,
mode: OrderingMode.descending,
),
],
),
ColumnDefinition(
columnName: 'email',
typeName: 'VARCHAR',
constraints: [
NotNull(null),
Unique(null, ConflictClause.abort),
],
),
ColumnDefinition(
columnName: 'score',
typeName: 'INT',
constraints: [
NotNull('score set'),
Default(null, NumericLiteral(420, token(TokenType.numberLiteral))),
Check(
null,
BinaryExpression(
Reference(columnName: 'score'),
token(TokenType.more),
NumericLiteral(
0,
token(TokenType.numberLiteral),
),
),
),
],
),
ColumnDefinition(
columnName: 'display_name',
typeName: 'VARCHAR',
constraints: [
CollateConstraint(
null,
'BINARY',
),
],
)
],
),
);
}