mirror of https://github.com/AMT-Cheif/drift.git
Parse table constraints
This commit is contained in:
parent
7e0bfa9cf9
commit
0bad842735
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue