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
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
- Compound select statements (`UNION` / `INTERSECT`) are not supported yet
- 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.

View File

@ -27,6 +27,10 @@ class ColumnDefinition extends AstNode {
abstract class ColumnConstraint extends AstNode {
// todo foreign key clause
final String name;
ColumnConstraint(this.name);
@override
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');
}
}
@visibleForOverriding
bool _equalToConstraint(covariant ColumnConstraint other);
@override
bool contentEquals(ColumnConstraint other) {
return other.name == name && _equalToConstraint(other);
}
}
enum ConflictClause { rollback, abort, fail, ignore, replace }
class NotNull extends ColumnConstraint {
final ConflictClause onConflict;
NotNull(String name, {this.onConflict}) : super(name);
@override
final Iterable<AstNode> childNodes = const [];
@override
bool contentEquals(NotNull other) => true;
bool _equalToConstraint(NotNull other) => onConflict == other.onConflict;
}
class PrimaryKey extends ColumnConstraint {
final bool autoIncrement;
final ConflictClause onConflict;
final OrderingMode mode;
PrimaryKey(this.autoIncrement, this.mode);
PrimaryKey(String name,
{this.autoIncrement = false, this.mode, this.onConflict})
: super(name);
@override
Iterable<AstNode> get childNodes => null;
Iterable<AstNode> get childNodes => const [];
@override
bool contentEquals(PrimaryKey other) {
return other.autoIncrement == autoIncrement && other.mode == mode;
bool _equalToConstraint(PrimaryKey other) {
return other.autoIncrement == autoIncrement &&
other.mode == mode &&
other.onConflict == onConflict;
}
}
class Unique extends ColumnConstraint {
final ConflictClause onConflict;
Unique(this.onConflict);
Unique(String name, this.onConflict) : super(name);
@override
Iterable<AstNode> get childNodes => const [];
@override
bool contentEquals(Unique other) {
bool _equalToConstraint(Unique other) {
return other.onConflict == onConflict;
}
}
@ -98,35 +119,35 @@ class Unique extends ColumnConstraint {
class Check extends ColumnConstraint {
final Expression expression;
Check(this.expression);
Check(String name, this.expression) : super(name);
@override
Iterable<AstNode> get childNodes => [expression];
@override
bool contentEquals(Check other) => true;
bool _equalToConstraint(Check other) => true;
}
class Default extends ColumnConstraint {
final Expression expression;
Default(this.expression);
Default(String name, this.expression) : super(name);
@override
Iterable<AstNode> get childNodes => [expression];
@override
bool contentEquals(Default other) => true;
bool _equalToConstraint(Default other) => true;
}
class CollateConstraint extends ColumnConstraint {
final String collation;
CollateConstraint(this.collation);
CollateConstraint(String name, this.collation) : super(name);
@override
final Iterable<AstNode> childNodes = const [];
@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 _previous => tokens[_current - 1];
bool _match(List<TokenType> types) {
bool _match(Iterable<TokenType> types) {
for (var type in types) {
if (_check(type)) {
_advance();
@ -110,7 +110,7 @@ class Parser {
}
Statement statement() {
final stmt = select() ?? _deleteStmt() ?? _update();
final stmt = select() ?? _deleteStmt() ?? _update() ?? _createTable();
_matchOne(TokenType.semicolon);
if (!_isAtEnd) {
@ -390,14 +390,17 @@ class Parser {
OrderingTerm _orderingTerm() {
final expr = expression();
return OrderingTerm(expression: expr, orderingMode: _orderingModeOrNull());
}
OrderingMode _orderingModeOrNull() {
if (_match(const [TokenType.asc, TokenType.desc])) {
final mode = _previous.type == TokenType.asc
? OrderingMode.ascending
: OrderingMode.descending;
return OrderingTerm(expression: expr, orderingMode: mode);
return mode;
}
return OrderingTerm(expression: expr);
return null;
}
/// 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);
}
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.
* Operators have the following precedence:
* - + ~ NOT (unary)
@ -681,21 +855,41 @@ class Parser {
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() {
final literal = _literalOrNull();
if (literal != null) return literal;
final token = _advance();
final type = token.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:
// Opening brackets could be three things: An inner select statement
// (SELECT ...), a parenthesised expression, or a tuple of expressions

View File

@ -100,6 +100,14 @@ enum TokenType {
$if,
without,
rowid,
constraint,
autoincrement,
primary,
key,
unique,
check,
$default,
conflict,
semicolon,
eof,
@ -165,6 +173,14 @@ const Map<String, TokenType> keywords = {
'IF': TokenType.$if,
'WITHOUT': TokenType.without,
'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 {

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',
),
],
)
],
),
);
}