Parse table constraints

This commit is contained in:
Simon Binder 2019-07-28 21:01:48 +02:00
parent 7e0bfa9cf9
commit 0bad842735
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
8 changed files with 252 additions and 94 deletions

View File

@ -20,7 +20,7 @@ part 'expressions/tuple.dart';
part 'expressions/variables.dart';
part 'schema/column_definition.dart';
part 'schema/table_definitions.dart';
part 'schema/table_definition.dart';
part 'statements/create_table.dart';
part 'statements/delete.dart';
@ -144,6 +144,7 @@ abstract class AstVisitor<T> {
T visitColumnDefinition(ColumnDefinition e);
T visitColumnConstraint(ColumnConstraint e);
T visitTableConstraint(TableConstraint e);
T visitForeignKeyClause(ForeignKeyClause e);
T visitBinaryExpression(BinaryExpression e);
@ -254,6 +255,9 @@ class RecursiveVisitor<T> extends AstVisitor<T> {
@override
T visitColumnDefinition(ColumnDefinition e) => visitChildren(e);
@override
T visitTableConstraint(TableConstraint e) => visitChildren(e);
@override
T visitColumnConstraint(ColumnConstraint e) => visitChildren(e);

View File

@ -34,8 +34,8 @@ abstract class ColumnConstraint extends AstNode {
T when<T>({
T Function(NotNull n) notNull,
T Function(PrimaryKey) primaryKey,
T Function(Unique) unique,
T Function(PrimaryKeyColumn) primaryKey,
T Function(UniqueColumn) unique,
T Function(CheckColumn) check,
T Function(Default) isDefault,
T Function(CollateConstraint) collate,
@ -43,10 +43,10 @@ abstract class ColumnConstraint extends AstNode {
}) {
if (this is NotNull) {
return notNull?.call(this as NotNull);
} else if (this is PrimaryKey) {
return primaryKey?.call(this as PrimaryKey);
} else if (this is Unique) {
return unique?.call(this as Unique);
} else if (this is PrimaryKeyColumn) {
return primaryKey?.call(this as PrimaryKeyColumn);
} else if (this is UniqueColumn) {
return unique?.call(this as UniqueColumn);
} else if (this is CheckColumn) {
return check?.call(this as CheckColumn);
} else if (this is Default) {
@ -83,12 +83,12 @@ class NotNull extends ColumnConstraint {
bool _equalToConstraint(NotNull other) => onConflict == other.onConflict;
}
class PrimaryKey extends ColumnConstraint {
class PrimaryKeyColumn extends ColumnConstraint {
final bool autoIncrement;
final ConflictClause onConflict;
final OrderingMode mode;
PrimaryKey(String name,
PrimaryKeyColumn(String name,
{this.autoIncrement = false, this.mode, this.onConflict})
: super(name);
@ -96,23 +96,23 @@ class PrimaryKey extends ColumnConstraint {
Iterable<AstNode> get childNodes => const [];
@override
bool _equalToConstraint(PrimaryKey other) {
bool _equalToConstraint(PrimaryKeyColumn other) {
return other.autoIncrement == autoIncrement &&
other.mode == mode &&
other.onConflict == onConflict;
}
}
class Unique extends ColumnConstraint {
class UniqueColumn extends ColumnConstraint {
final ConflictClause onConflict;
Unique(String name, this.onConflict) : super(name);
UniqueColumn(String name, this.onConflict) : super(name);
@override
Iterable<AstNode> get childNodes => const [];
@override
bool _equalToConstraint(Unique other) {
bool _equalToConstraint(UniqueColumn other) {
return other.onConflict == onConflict;
}
}

View File

@ -0,0 +1,93 @@
part of '../ast.dart';
enum ReferenceAction { setNull, setDefault, cascade, restrict, noAction }
class ForeignKeyClause extends AstNode {
final TableReference foreignTable;
final List<Reference> columnNames;
final ReferenceAction onDelete;
final ReferenceAction onUpdate;
ForeignKeyClause(
{@required this.foreignTable,
@required this.columnNames,
this.onDelete,
this.onUpdate});
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitForeignKeyClause(this);
@override
Iterable<AstNode> get childNodes => [foreignTable, ...columnNames];
@override
bool contentEquals(ForeignKeyClause other) {
return other.onDelete == onDelete && other.onUpdate == onUpdate;
}
}
abstract class TableConstraint extends AstNode {
final String name;
TableConstraint(this.name);
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitTableConstraint(this);
@override
bool contentEquals(TableConstraint other) {
return other.name == name && _constraintEquals(other);
}
@visibleForOverriding
bool _constraintEquals(covariant TableConstraint other);
}
class KeyClause extends TableConstraint {
final bool isPrimaryKey;
final List<Reference> indexedColumns;
final ConflictClause onConflict;
bool get isUnique => !isPrimaryKey;
KeyClause(String name,
{@required this.isPrimaryKey,
@required this.indexedColumns,
this.onConflict})
: super(name);
@override
bool _constraintEquals(KeyClause other) {
return other.isPrimaryKey == isPrimaryKey && other.onConflict == onConflict;
}
@override
Iterable<AstNode> get childNodes => indexedColumns;
}
class CheckTable extends TableConstraint {
final Expression expression;
CheckTable(String name, this.expression) : super(name);
@override
bool _constraintEquals(CheckTable other) => true;
@override
Iterable<AstNode> get childNodes => [expression];
}
class ForeignKeyTableConstraint extends TableConstraint {
final List<Reference> columns;
final ForeignKeyClause clause;
ForeignKeyTableConstraint(String name,
{@required this.columns, @required this.clause})
: super(name);
@override
bool _constraintEquals(ForeignKeyTableConstraint other) => true;
@override
Iterable<AstNode> get childNodes => [...columns, clause];
}

View File

@ -1,27 +0,0 @@
part of '../ast.dart';
enum ReferenceAction { setNull, setDefault, cascade, restrict, noAction }
class ForeignKeyClause extends AstNode {
final TableReference foreignTable;
final List<Reference> columnNames;
final ReferenceAction onDelete;
final ReferenceAction onUpdate;
ForeignKeyClause(
{@required this.foreignTable,
@required this.columnNames,
this.onDelete,
this.onUpdate});
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitForeignKeyClause(this);
@override
Iterable<AstNode> get childNodes => [foreignTable, ...columnNames];
@override
bool contentEquals(ForeignKeyClause other) {
return other.onDelete == onDelete && other.onUpdate == onUpdate;
}
}

View File

@ -6,19 +6,21 @@ class CreateTableStatement extends Statement {
final bool ifNotExists;
final String tableName;
final List<ColumnDefinition> columns;
final List<TableConstraint> tableConstraints;
final bool withoutRowId;
CreateTableStatement(
{this.ifNotExists = false,
@required this.tableName,
this.columns,
this.columns = const [],
this.tableConstraints = const [],
this.withoutRowId = false});
@override
T accept<T>(AstVisitor<T> visitor) => visitor.visitCreateTableStatement(this);
@override
Iterable<AstNode> get childNodes => columns;
Iterable<AstNode> get childNodes => [...columns, ...tableConstraints];
@override
bool contentEquals(CreateTableStatement other) {

View File

@ -15,19 +15,31 @@ mixin SchemaParser on ParserBase {
ifNotExists = true;
}
final tableIdentifier =
_consume(TokenType.identifier, 'Expected a table name')
as IdentifierToken;
final tableIdentifier = _consumeIdentifier('Expected a table name');
// we don't currently support CREATE TABLE x AS SELECT ... statements
_consume(
TokenType.leftParen, 'Expected opening parenthesis to list columns');
final columns = <ColumnDefinition>[];
final tableConstraints = <TableConstraint>[];
// the columns must come before the table constraints!
var encounteredTableConstraint = false;
do {
columns.add(_columnDefinition());
final tableConstraint = _tableConstraintOrNull();
if (tableConstraint != null) {
encounteredTableConstraint = true;
tableConstraints.add(tableConstraint);
} else {
if (encounteredTableConstraint) {
_error('Expected another table constraint');
} else {
columns.add(_columnDefinition());
}
}
} while (_matchOne(TokenType.comma));
// todo parse table constraints
_consume(TokenType.rightParen, 'Expected closing parenthesis');
@ -43,6 +55,7 @@ mixin SchemaParser on ParserBase {
tableName: tableIdentifier.identifier,
withoutRowId: withoutRowId,
columns: columns,
tableConstraints: tableConstraints,
)..setSpan(first, _previous);
}
@ -69,74 +82,50 @@ mixin SchemaParser on ParserBase {
}
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 first = _peek;
final resolvedName = name?.identifier;
final resolvedName = _constraintNameOrNull();
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,
return PrimaryKeyColumn(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())
return UniqueColumn(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');
final expr = _expressionInParentheses();
return CheckColumn(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');
}
// when not a literal, expect an expression in parentheses
expr ??= _expressionInParentheses();
return Default(resolvedName, expr);
}
if (_matchOne(TokenType.collate)) {
first ??= _previous;
final collation = _consumeIdentifier('Expected the collation name');
return CollateConstraint(resolvedName, collation.identifier)
..setSpan(first, _previous);
}
if (_peek.type == TokenType.references) {
first ??= _peek;
final clause = _foreignKeyClause();
return ForeignKeyColumnConstraint(resolvedName, clause)
..setSpan(first, _previous);
@ -145,12 +134,66 @@ mixin SchemaParser on ParserBase {
// 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) {
if (orNull && resolvedName == null) {
return null;
}
_error('Expected a constraint (primary key, nullability, etc.)');
}
TableConstraint _tableConstraintOrNull() {
final first = _peek;
final name = _constraintNameOrNull();
if (_match([TokenType.unique, TokenType.primary])) {
final isPrimaryKey = _previous.type == TokenType.primary;
if (isPrimaryKey) {
_consume(TokenType.key, 'Expected KEY to start PRIMARY KEY clause');
}
final columns = _listColumnsInParentheses(allowEmpty: false);
final conflictClause = _conflictClauseOrNull();
return KeyClause(name,
isPrimaryKey: isPrimaryKey,
indexedColumns: columns,
onConflict: conflictClause)
..setSpan(first, _previous);
} else if (_matchOne(TokenType.check)) {
final expr = _expressionInParentheses();
return CheckTable(name, expr)..setSpan(first, _previous);
} else if (_matchOne(TokenType.foreign)) {
_consume(TokenType.key, 'Expected KEY to start FOREIGN KEY clause');
final columns = _listColumnsInParentheses(allowEmpty: false);
final clause = _foreignKeyClause();
return ForeignKeyTableConstraint(name, columns: columns, clause: clause)
..setSpan(first, _previous);
}
if (name != null) {
// if a constraint was started with CONSTRAINT <name> but then we didn't
// find a constraint, that's an syntax error
_error('Expected a table constraint (e.g. a primary key)');
}
return null;
}
String _constraintNameOrNull() {
if (_matchOne(TokenType.constraint)) {
final name = _consumeIdentifier('Expect a name for the constraint here');
return name.identifier;
}
return null;
}
Expression _expressionInParentheses() {
_consume(TokenType.leftParen, 'Expected opening parenthesis');
final expr = expression();
_consume(TokenType.rightParen, 'Expected closing parenthesis');
return expr;
}
ConflictClause _conflictClauseOrNull() {
if (_matchOne(TokenType.on)) {
_consume(TokenType.conflict,
@ -183,18 +226,7 @@ mixin SchemaParser on ParserBase {
final foreignTableName = TableReference(foreignTable.identifier, null)
..setSpan(foreignTable, foreignTable);
final columnNames = <Reference>[];
if (_matchOne(TokenType.leftParen)) {
do {
final referenceId = _consumeIdentifier('Expected a column name');
final reference = Reference(columnName: referenceId.identifier)
..setSpan(referenceId, referenceId);
columnNames.add(reference);
} while (_matchOne(TokenType.comma));
_consume(TokenType.rightParen,
'Expected closing paranthesis after column names');
}
final columnNames = _listColumnsInParentheses(allowEmpty: true);
ReferenceAction onDelete, onUpdate;
@ -236,4 +268,25 @@ mixin SchemaParser on ParserBase {
_error('Not a valid action, expected CASCADE, SET NULL, etc..');
}
}
List<Reference> _listColumnsInParentheses({bool allowEmpty = false}) {
final columnNames = <Reference>[];
if (_matchOne(TokenType.leftParen)) {
do {
final referenceId = _consumeIdentifier('Expected a column name');
final reference = Reference(columnName: referenceId.identifier)
..setSpan(referenceId, referenceId);
columnNames.add(reference);
} while (_matchOne(TokenType.comma));
_consume(TokenType.rightParen,
'Expected closing paranthesis after column names');
} else {
if (!allowEmpty) {
_error('Expected a list of columns in parantheses');
}
}
return columnNames;
}
}

View File

@ -103,6 +103,7 @@ enum TokenType {
constraint,
autoincrement,
primary,
foreign,
key,
unique,
check,
@ -181,6 +182,7 @@ const Map<String, TokenType> keywords = {
'CONSTRAINT': TokenType.constraint,
'AUTOINCREMENT': TokenType.autoincrement,
'PRIMARY': TokenType.primary,
'FOREIGN': TokenType.foreign,
'KEY': TokenType.key,
'UNIQUE': TokenType.unique,
'CHECK': TokenType.check,

View File

@ -8,7 +8,11 @@ CREATE TABLE IF NOT EXISTS users (
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
REFERENCES some(thing) ON UPDATE CASCADE ON DELETE SET NULL
REFERENCES some(thing) ON UPDATE CASCADE ON DELETE SET NULL,
UNIQUE (score, display_name) ON CONFLICT ABORT,
FOREIGN KEY (id, email) REFERENCES another (a, b)
ON DELETE NO ACTION ON UPDATE RESTRICT
)
''';
@ -25,7 +29,7 @@ void main() {
typeName: 'INT',
constraints: [
NotNull(null),
PrimaryKey(
PrimaryKeyColumn(
null,
autoIncrement: true,
onConflict: ConflictClause.rollback,
@ -38,7 +42,7 @@ void main() {
typeName: 'VARCHAR',
constraints: [
NotNull(null),
Unique(null, ConflictClause.abort),
UniqueColumn(null, ConflictClause.abort),
],
),
ColumnDefinition(
@ -80,6 +84,33 @@ void main() {
],
)
],
tableConstraints: [
KeyClause(
null,
isPrimaryKey: false,
indexedColumns: [
Reference(columnName: 'score'),
Reference(columnName: 'display_name'),
],
onConflict: ConflictClause.abort,
),
ForeignKeyTableConstraint(
null,
columns: [
Reference(columnName: 'id'),
Reference(columnName: 'email'),
],
clause: ForeignKeyClause(
foreignTable: TableReference('another', null),
columnNames: [
Reference(columnName: 'a'),
Reference(columnName: 'b'),
],
onDelete: ReferenceAction.noAction,
onUpdate: ReferenceAction.restrict,
),
)
],
),
);
}