Resolve types of columns in compound select statements

This commit is contained in:
Simon Binder 2019-09-25 19:46:39 +02:00
parent f6a5009380
commit 2a782a010e
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
6 changed files with 68 additions and 10 deletions

View File

@ -59,5 +59,6 @@ enum AnalysisErrorType {
synctactic,
unknownFunction,
compoundColumnCountMismatch,
other,
}

View File

@ -13,7 +13,8 @@ class TableColumn extends Column {
@override
final String name;
/// The type of this column, which is immediately available.
/// The type of this column, which is available before any resolution happens
/// (we know if from the table).
final ResolvedType type;
/// The column constraints set on this column.
@ -39,3 +40,15 @@ class ExpressionColumn extends Column {
ExpressionColumn({@required this.name, this.expression});
}
/// The result column of a [CompoundSelectStatement].
class CompoundSelectColumn extends Column {
/// The column in [CompoundSelectStatement.base] each of the
/// [CompoundSelectStatement.additional] that contributed to this column.
final List<Column> columns;
CompoundSelectColumn(this.columns);
@override
String get name => columns.first.name;
}

View File

@ -15,6 +15,44 @@ class ColumnResolver extends RecursiveVisitor<void> {
visitChildren(e);
}
@override
void visitCompoundSelectStatement(CompoundSelectStatement e) {
// first, visit all children so that the compound parts have their columns
// resolved
visitChildren(e);
final columnSets = [
e.base.resolvedColumns,
for (var part in e.additional) part.select.resolvedColumns
];
// each select statement must return the same amount of columns
final amount = columnSets.first.length;
for (var i = 1; i < columnSets.length; i++) {
if (columnSets[i].length != amount) {
context.reportError(AnalysisError(
type: AnalysisErrorType.compoundColumnCountMismatch,
relevantNode: e,
message: 'The parts of this compound statement return different '
'amount of columns',
));
break;
}
}
final resolved = <CompoundSelectColumn>[];
// merge all columns at each position into a CompoundSelectColumn
for (var i = 0; i < amount; i++) {
final columnsAtThisIndex = [
for (var set in columnSets) if (set.length > i) set[i]
];
resolved.add(CompoundSelectColumn(columnsAtThisIndex));
}
e.resolvedColumns = resolved;
}
@override
void visitUpdateStatement(UpdateStatement e) {
final table = _resolveTableReference(e.table);

View File

@ -63,6 +63,9 @@ class TypeResolver {
return ResolveResult(column.type);
} else if (column is ExpressionColumn) {
return resolveOrInfer(column.expression);
} else if (column is CompoundSelectColumn) {
// todo maybe use a type that matches every column in here?
return resolveColumn(column.columns.first);
}
throw StateError('Unknown column $column');

View File

@ -279,13 +279,14 @@ mixin ExpressionParser on ParserBase {
..token = typedToken
..setSpan(_previous, _previous);
}
} else if (_checkLenientIdentifier()) {
} else if (_checkIdentifier()) {
final first = _consumeIdentifier(
'This error message should never be displayed. Please report.');
// could be table.column, function(...) or just column
if (_matchOne(TokenType.dot)) {
final second = _consumeIdentifier('Expected a column name here');
final second =
_consumeIdentifier('Expected a column name here', lenient: true);
return Reference(
tableName: first.identifier, columnName: second.identifier)
..setSpan(first, second);

View File

@ -103,11 +103,12 @@ abstract class ParserBase {
/// Returns whether the next token is an [TokenType.identifier] or a
/// [KeywordToken]. If this method returns true, calling [_consumeIdentifier]
/// with the lenient parameter will now throw.
bool _checkLenientIdentifier() {
/// with same [lenient] parameter will now throw.
bool _checkIdentifier({bool lenient = false}) {
final next = _peek;
return next.type == TokenType.identifier ||
(next is KeywordToken && next.canConvertToIdentifier());
if (next.type == TokenType.identifier) return true;
return next is KeywordToken && (next.canConvertToIdentifier() || lenient);
}
Token _advance() {
@ -130,10 +131,11 @@ abstract class ParserBase {
}
/// Consumes an identifier.
IdentifierToken _consumeIdentifier(String message) {
IdentifierToken _consumeIdentifier(String message, {bool lenient = false}) {
final next = _peek;
// non-standard keywords can be parsed as an identifier
if (next is KeywordToken && next.canConvertToIdentifier()) {
// non-standard keywords can be parsed as an identifier, we allow all
// keywords when lenient is true
if (next is KeywordToken && (next.canConvertToIdentifier() || lenient)) {
return (_advance() as KeywordToken).convertToIdentifier();
}
return _consume(TokenType.identifier, message) as IdentifierToken;