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