From 2a782a010e75a415ff26370168b51d4eec0fff0e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 25 Sep 2019 19:46:39 +0200 Subject: [PATCH] Resolve types of columns in compound select statements --- sqlparser/lib/src/analysis/error.dart | 1 + sqlparser/lib/src/analysis/schema/column.dart | 15 +++++++- .../src/analysis/steps/column_resolver.dart | 38 +++++++++++++++++++ .../lib/src/analysis/types/resolver.dart | 3 ++ .../lib/src/reader/parser/expressions.dart | 5 ++- sqlparser/lib/src/reader/parser/parser.dart | 16 ++++---- 6 files changed, 68 insertions(+), 10 deletions(-) diff --git a/sqlparser/lib/src/analysis/error.dart b/sqlparser/lib/src/analysis/error.dart index 3d8e65a7..8c78c3ee 100644 --- a/sqlparser/lib/src/analysis/error.dart +++ b/sqlparser/lib/src/analysis/error.dart @@ -59,5 +59,6 @@ enum AnalysisErrorType { synctactic, unknownFunction, + compoundColumnCountMismatch, other, } diff --git a/sqlparser/lib/src/analysis/schema/column.dart b/sqlparser/lib/src/analysis/schema/column.dart index dc4c05ac..9aff6d23 100644 --- a/sqlparser/lib/src/analysis/schema/column.dart +++ b/sqlparser/lib/src/analysis/schema/column.dart @@ -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 columns; + + CompoundSelectColumn(this.columns); + + @override + String get name => columns.first.name; +} diff --git a/sqlparser/lib/src/analysis/steps/column_resolver.dart b/sqlparser/lib/src/analysis/steps/column_resolver.dart index 1f00ae02..facddc1f 100644 --- a/sqlparser/lib/src/analysis/steps/column_resolver.dart +++ b/sqlparser/lib/src/analysis/steps/column_resolver.dart @@ -15,6 +15,44 @@ class ColumnResolver extends RecursiveVisitor { 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 = []; + + // 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); diff --git a/sqlparser/lib/src/analysis/types/resolver.dart b/sqlparser/lib/src/analysis/types/resolver.dart index 4289a813..0bcbae1e 100644 --- a/sqlparser/lib/src/analysis/types/resolver.dart +++ b/sqlparser/lib/src/analysis/types/resolver.dart @@ -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'); diff --git a/sqlparser/lib/src/reader/parser/expressions.dart b/sqlparser/lib/src/reader/parser/expressions.dart index 3d7db099..51dc7851 100644 --- a/sqlparser/lib/src/reader/parser/expressions.dart +++ b/sqlparser/lib/src/reader/parser/expressions.dart @@ -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); diff --git a/sqlparser/lib/src/reader/parser/parser.dart b/sqlparser/lib/src/reader/parser/parser.dart index 13ea1892..edec32e3 100644 --- a/sqlparser/lib/src/reader/parser/parser.dart +++ b/sqlparser/lib/src/reader/parser/parser.dart @@ -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;