mirror of https://github.com/AMT-Cheif/drift.git
363 lines
12 KiB
Dart
363 lines
12 KiB
Dart
part of 'parser.dart';
|
|
|
|
mixin SchemaParser on ParserBase {
|
|
CreateTableStatement _createTable() {
|
|
_suggestHint(
|
|
const HintDescription.tokens([TokenType.create, TokenType.table]));
|
|
if (!_matchOne(TokenType.create)) return null;
|
|
final first = _previous;
|
|
|
|
_suggestHint(HintDescription.token(TokenType.table));
|
|
_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 = _consumeIdentifier('Expected a table name');
|
|
|
|
// we don't currently support CREATE TABLE x AS SELECT ... statements
|
|
final leftParen = _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 {
|
|
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));
|
|
|
|
final rightParen =
|
|
_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;
|
|
}
|
|
|
|
String overriddenName;
|
|
if (enableMoorExtensions && _matchOne(TokenType.as)) {
|
|
overriddenName =
|
|
_consumeIdentifier('Expected the name for the data class').identifier;
|
|
}
|
|
|
|
return CreateTableStatement(
|
|
ifNotExists: ifNotExists,
|
|
tableName: tableIdentifier.identifier,
|
|
withoutRowId: withoutRowId,
|
|
columns: columns,
|
|
tableConstraints: tableConstraints,
|
|
overriddenDataClassName: overriddenName,
|
|
)
|
|
..setSpan(first, _previous)
|
|
..openingBracket = leftParen
|
|
..closingBracket = rightParen;
|
|
}
|
|
|
|
ColumnDefinition _columnDefinition() {
|
|
final name = _consume(TokenType.identifier, 'Expected a column name')
|
|
as IdentifierToken;
|
|
|
|
final typeTokens = _typeName();
|
|
String typeName;
|
|
|
|
if (typeTokens != null) {
|
|
final typeSpan = typeTokens.first.span.expand(typeTokens.last.span);
|
|
typeName = typeSpan.text;
|
|
}
|
|
|
|
final constraints = <ColumnConstraint>[];
|
|
ColumnConstraint constraint;
|
|
while ((constraint = _columnConstraint(orNull: true)) != null) {
|
|
constraints.add(constraint);
|
|
}
|
|
|
|
return ColumnDefinition(
|
|
columnName: name.identifier,
|
|
typeName: typeName,
|
|
constraints: constraints,
|
|
)
|
|
..setSpan(name, _previous)
|
|
..typeNames = typeTokens;
|
|
}
|
|
|
|
List<Token> _typeName() {
|
|
// sqlite doesn't really define what a type name is and has very loose rules
|
|
// at turning them into a type affinity. We support this pattern:
|
|
// typename = identifier [ "(" { identifier | comma | number_literal } ")" ]
|
|
if (!_matchOne(TokenType.identifier)) return null;
|
|
|
|
final typeNames = [_previous];
|
|
|
|
if (_matchOne(TokenType.leftParen)) {
|
|
typeNames.add(_previous);
|
|
|
|
const inBrackets = [
|
|
TokenType.identifier,
|
|
TokenType.comma,
|
|
TokenType.numberLiteral
|
|
];
|
|
while (_match(inBrackets)) {
|
|
typeNames.add(_previous);
|
|
}
|
|
|
|
_consume(TokenType.rightParen,
|
|
'Expected closing paranthesis to finish type name');
|
|
typeNames.add(_previous);
|
|
}
|
|
|
|
return typeNames;
|
|
}
|
|
|
|
ColumnConstraint _columnConstraint({bool orNull = false}) {
|
|
final first = _peek;
|
|
|
|
final resolvedName = _constraintNameOrNull();
|
|
|
|
if (_matchOne(TokenType.primary)) {
|
|
_suggestHint(HintDescription.token(TokenType.key));
|
|
_consume(TokenType.key, 'Expected KEY to complete PRIMARY KEY clause');
|
|
|
|
final mode = _orderingModeOrNull();
|
|
final conflict = _conflictClauseOrNull();
|
|
|
|
_suggestHint(HintDescription.token(TokenType.autoincrement));
|
|
final hasAutoInc = _matchOne(TokenType.autoincrement);
|
|
|
|
return PrimaryKeyColumn(resolvedName,
|
|
autoIncrement: hasAutoInc, mode: mode, onConflict: conflict)
|
|
..setSpan(first, _previous);
|
|
}
|
|
if (_matchOne(TokenType.not)) {
|
|
_suggestHint(HintDescription.token(TokenType.$null));
|
|
|
|
final notToken = _previous;
|
|
final nullToken =
|
|
_consume(TokenType.$null, 'Expected NULL to complete NOT NULL');
|
|
|
|
return NotNull(resolvedName, onConflict: _conflictClauseOrNull())
|
|
..setSpan(first, _previous)
|
|
..not = notToken
|
|
..$null = nullToken;
|
|
}
|
|
if (_matchOne(TokenType.unique)) {
|
|
return UniqueColumn(resolvedName, _conflictClauseOrNull())
|
|
..setSpan(first, _previous);
|
|
}
|
|
if (_matchOne(TokenType.check)) {
|
|
final expr = _expressionInParentheses();
|
|
return CheckColumn(resolvedName, expr)..setSpan(first, _previous);
|
|
}
|
|
if (_matchOne(TokenType.$default)) {
|
|
Expression expr = _literalOrNull();
|
|
|
|
// when not a literal, expect an expression in parentheses
|
|
expr ??= _expressionInParentheses();
|
|
|
|
return Default(resolvedName, expr)..setSpan(first, _previous);
|
|
}
|
|
if (_matchOne(TokenType.collate)) {
|
|
final collation = _consumeIdentifier('Expected the collation name');
|
|
|
|
return CollateConstraint(resolvedName, collation.identifier)
|
|
..setSpan(first, _previous);
|
|
}
|
|
if (_peek.type == TokenType.references) {
|
|
final clause = _foreignKeyClause();
|
|
return ForeignKeyColumnConstraint(resolvedName, clause)
|
|
..setSpan(first, _previous);
|
|
}
|
|
if (enableMoorExtensions && _matchOne(TokenType.mapped)) {
|
|
_consume(TokenType.by, 'Expected a MAPPED BY constraint');
|
|
|
|
final dartExpr = _consume(
|
|
TokenType.inlineDart, 'Expected Dart expression in backticks');
|
|
|
|
return MappedBy(resolvedName, dartExpr as InlineDartToken)
|
|
..setSpan(first, _previous);
|
|
}
|
|
|
|
// 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 && 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() {
|
|
_suggestHint(HintDescription.token(TokenType.on));
|
|
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,
|
|
};
|
|
_suggestHint(HintDescription.tokens(modes.keys.toList()));
|
|
|
|
if (_match(modes.keys)) {
|
|
return modes[_previous.type];
|
|
} else {
|
|
_error('Expected a conflict handler (rollback, abort, etc.) here');
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
ForeignKeyClause _foreignKeyClause() {
|
|
// https://www.sqlite.org/syntax/foreign-key-clause.html
|
|
_consume(TokenType.references, 'Expected REFERENCES');
|
|
final firstToken = _previous;
|
|
|
|
final foreignTable = _consumeIdentifier('Expected a table name');
|
|
final foreignTableName = TableReference(foreignTable.identifier, null)
|
|
..setSpan(foreignTable, foreignTable);
|
|
|
|
final columnNames = _listColumnsInParentheses(allowEmpty: true);
|
|
|
|
ReferenceAction onDelete, onUpdate;
|
|
|
|
_suggestHint(HintDescription.token(TokenType.on));
|
|
while (_matchOne(TokenType.on)) {
|
|
_suggestHint(
|
|
const HintDescription.tokens([TokenType.delete, TokenType.update]));
|
|
if (_matchOne(TokenType.delete)) {
|
|
onDelete = _referenceAction();
|
|
} else if (_matchOne(TokenType.update)) {
|
|
onUpdate = _referenceAction();
|
|
} else {
|
|
_error('Expected either DELETE or UPDATE');
|
|
}
|
|
}
|
|
|
|
return ForeignKeyClause(
|
|
foreignTable: foreignTableName,
|
|
columnNames: columnNames,
|
|
onUpdate: onUpdate,
|
|
onDelete: onDelete,
|
|
)..setSpan(firstToken, _previous);
|
|
}
|
|
|
|
ReferenceAction _referenceAction() {
|
|
if (_matchOne(TokenType.cascade)) {
|
|
return ReferenceAction.cascade;
|
|
} else if (_matchOne(TokenType.restrict)) {
|
|
return ReferenceAction.restrict;
|
|
} else if (_matchOne(TokenType.no)) {
|
|
_consume(TokenType.action, 'Expect ACTION to complete NO ACTION clause');
|
|
return ReferenceAction.noAction;
|
|
} else if (_matchOne(TokenType.set)) {
|
|
if (_matchOne(TokenType.$null)) {
|
|
return ReferenceAction.setNull;
|
|
} else if (_matchOne(TokenType.$default)) {
|
|
return ReferenceAction.setDefault;
|
|
} else {
|
|
_error('Expected either NULL or DEFAULT as set action here');
|
|
}
|
|
} else {
|
|
_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;
|
|
}
|
|
}
|