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, synctactic,
unknownFunction, unknownFunction,
compoundColumnCountMismatch,
other, other,
} }

View File

@ -13,7 +13,8 @@ class TableColumn extends Column {
@override @override
final String name; 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; final ResolvedType type;
/// The column constraints set on this column. /// The column constraints set on this column.
@ -39,3 +40,15 @@ class ExpressionColumn extends Column {
ExpressionColumn({@required this.name, this.expression}); 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); 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 @override
void visitUpdateStatement(UpdateStatement e) { void visitUpdateStatement(UpdateStatement e) {
final table = _resolveTableReference(e.table); final table = _resolveTableReference(e.table);

View File

@ -63,6 +63,9 @@ class TypeResolver {
return ResolveResult(column.type); return ResolveResult(column.type);
} else if (column is ExpressionColumn) { } else if (column is ExpressionColumn) {
return resolveOrInfer(column.expression); 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'); throw StateError('Unknown column $column');

View File

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

View File

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