Parse and analyze VALUES clause for selects

This commit is contained in:
Simon Binder 2020-04-16 22:41:21 +02:00
parent c007e1f9ac
commit 6b2bd27d4d
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
15 changed files with 277 additions and 10 deletions

View File

@ -5,6 +5,7 @@
tables with a comma will now be parsed as a `JoinClause`.
- Changed `SelectStatementAsSource.statement` from `SelectStatement` to `BaseSelectStatement` and allow
compound select statements to appear in a `FROM` clause
- Support the `VALUES` clause as select statement
- The new type inference engine is now enabled by default and the `enableExperimentalTypeInference` option
has been removed. To continue using the old engine, the `useLegacyTypeInference` flag can be used.

View File

@ -61,5 +61,6 @@ enum AnalysisErrorType {
unknownFunction,
compoundColumnCountMismatch,
cteColumnCountMismatch,
valuesSelectCountMismatch,
other,
}

View File

@ -173,3 +173,18 @@ class CommonTableExpressionColumn extends Column with DelegatedColumn {
CommonTableExpressionColumn(this.name, this.innerColumn);
}
/// Result column coming from a `VALUES` select statement.
class ValuesSelectColumn extends Column {
@override
final String name;
/// The expressions from a `VALUES` clause contributing to this column.
///
/// Essentially, each [ValuesSelectColumn] consists of a column in the values
/// of a [ValuesSelectStatement].
final List<Expression> expressions;
ValuesSelectColumn(this.name, this.expressions)
: assert(expressions.isNotEmpty);
}

View File

@ -25,6 +25,14 @@ class ColumnResolver extends RecursiveVisitor<void, void> {
_resolveCompoundSelect(e);
}
@override
void visitValuesSelectStatement(ValuesSelectStatement e, void arg) {
// visit children to resolve CTEs
visitChildren(e, arg);
_resolveValuesSelect(e);
}
@override
void visitCommonTableExpression(CommonTableExpression e, void arg) {
visitChildren(e, arg);
@ -116,6 +124,8 @@ class ColumnResolver extends RecursiveVisitor<void, void> {
_resolveCompoundSelect(stmt);
} else if (stmt is SelectStatement) {
_resolveSelect(stmt);
} else if (stmt is ValuesSelectStatement) {
_resolveValuesSelect(stmt);
} else {
throw AssertionError('Unknown type of select statement: $stmt');
}
@ -255,6 +265,32 @@ class ColumnResolver extends RecursiveVisitor<void, void> {
statement.resolvedColumns = resolved;
}
void _resolveValuesSelect(ValuesSelectStatement statement) {
// ideally all tuples should have the same arity, but the parser doesn't
// enforce that.
final amountOfColumns =
statement.values.fold<int>(null, (maxLength, tuple) {
final lengthHere = tuple.expressions.length;
return maxLength == null ? lengthHere : max(maxLength, lengthHere);
});
final columns = <Column>[];
for (var i = 0; i < amountOfColumns; i++) {
// Columns in a VALUES clause appear to be named "Column$i", where i is a
// one-based index.
final columnName = 'Column${i + 1}';
final expressions = statement.values
.where((tuple) => tuple.expressions.length > i)
.map((tuple) => tuple.expressions[i])
.toList();
columns.add(ValuesSelectColumn(columnName, expressions));
}
statement.resolvedColumns = columns;
}
String _nameOfResultColumn(ExpressionResultColumn c) {
if (c.as != null) return c.as;

View File

@ -17,4 +17,24 @@ class LintingVisitor extends RecursiveVisitor<void, void> {
visitChildren(e, arg);
}
@override
void visitValuesSelectStatement(ValuesSelectStatement e, void arg) {
final expectedColumns = e.resolvedColumns.length;
for (final tuple in e.values) {
final elementsInTuple = tuple.expressions.length;
if (elementsInTuple != expectedColumns) {
context.reportError(AnalysisError(
type: AnalysisErrorType.valuesSelectCountMismatch,
relevantNode: tuple,
message: 'The surrounding VALUES clause has $expectedColumns '
'columns, but this tuple only has $elementsInTuple',
));
}
}
visitChildren(e, arg);
}
}

View File

@ -28,6 +28,8 @@ class ExactTypeExpectation extends TypeExpectation {
/// When false, we can report a compile-time error for a type mismatch.
final bool lax;
const ExactTypeExpectation._(this.type, this.lax);
const ExactTypeExpectation(this.type) : lax = false;
const ExactTypeExpectation.laxly(this.type) : lax = true;
@ -77,3 +79,17 @@ class SelectTypeExpectation extends TypeExpectation {
SelectTypeExpectation(this.columnExpectations);
}
extension on TypeExpectation {
TypeExpectation clearArray() {
if (this is ExactTypeExpectation) {
final expectation = this as ExactTypeExpectation;
if (expectation.type.isArray) {
return ExactTypeExpectation._(
expectation.type.toArray(false), expectation.lax);
}
}
return this;
}
}

View File

@ -231,7 +231,7 @@ class _ResolvedVariables {
}
}
extension on ResolvedType {
extension ResolvedTypeUtils on ResolvedType {
ResolvedType cast(CastMode mode) {
switch (mode) {
case CastMode.numeric:

View File

@ -229,6 +229,9 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
@override
void visitTuple(Tuple e, TypeExpectation arg) {
final expectationForChildren = arg.clearArray();
visitChildren(e, expectationForChildren);
// make children non-arrays
for (final child in e.childNodes) {
session._addRelation(CopyTypeFrom(child, e, array: false));
@ -593,6 +596,8 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
} else if (column is CompoundSelectColumn) {
session._addRelation(CopyEncapsulating(column, column.columns));
column.columns.forEach(_handleColumn);
} else if (column is ValuesSelectColumn) {
session._addRelation(CopyEncapsulating(column, column.expressions));
} else if (column is DelegatedColumn && column.innerColumn != null) {
_handleColumn(column.innerColumn);
_lazyCopy(column, column.innerColumn);

View File

@ -9,8 +9,12 @@ abstract class BaseSelectStatement extends CrudStatement with ResultSet {
BaseSelectStatement._(WithClause withClause) : super._(withClause);
}
/// Marker interface for classes that are a [BaseSelectStatement] but aren't a
/// [CompoundSelectStatement].
abstract class SelectStatementNoCompound implements BaseSelectStatement {}
class SelectStatement extends BaseSelectStatement
implements StatementWithWhere {
implements StatementWithWhere, SelectStatementNoCompound {
final bool distinct;
final List<ResultColumn> columns;
final Queryable /*?*/ from;
@ -61,7 +65,7 @@ class SelectStatement extends BaseSelectStatement
}
class CompoundSelectStatement extends BaseSelectStatement {
final SelectStatement base;
final SelectStatementNoCompound base;
final List<CompoundSelectPart> additional;
// the grammar under https://www.sqlite.org/syntax/compound-select-stmt.html
@ -91,6 +95,26 @@ class CompoundSelectStatement extends BaseSelectStatement {
}
}
/// A select statement of the form `VALUES (expr-list), ..., (expr-list-N)`.
class ValuesSelectStatement extends BaseSelectStatement
implements SelectStatementNoCompound {
final List<Tuple> values;
ValuesSelectStatement(this.values, {WithClause withClause})
: super._(withClause);
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
return visitor.visitValuesSelectStatement(this, arg);
}
@override
Iterable<AstNode> get childNodes => values;
@override
bool contentEquals(ValuesSelectStatement other) => true;
}
abstract class ResultColumn extends AstNode {
@override
R accept<A, R>(AstVisitor<A, R> visitor, A arg) {
@ -164,7 +188,7 @@ enum CompoundSelectMode {
class CompoundSelectPart extends AstNode {
final CompoundSelectMode mode;
final SelectStatement select;
final SelectStatementNoCompound select;
/// The first token of this statement, so either union, intersect or except.
Token firstModeToken;

View File

@ -4,6 +4,7 @@ abstract class AstVisitor<A, R> {
R visitSelectStatement(SelectStatement e, A arg);
R visitCompoundSelectStatement(CompoundSelectStatement e, A arg);
R visitCompoundSelectPart(CompoundSelectPart e, A arg);
R visitValuesSelectStatement(ValuesSelectStatement e, A arg);
R visitResultColumn(ResultColumn e, A arg);
R visitInsertStatement(InsertStatement e, A arg);
R visitDeleteStatement(DeleteStatement e, A arg);
@ -85,6 +86,11 @@ class RecursiveVisitor<A, R> implements AstVisitor<A, R> {
return visitBaseSelectStatement(e, arg);
}
@override
R visitValuesSelectStatement(ValuesSelectStatement e, A arg) {
return visitBaseSelectStatement(e, arg);
}
@override
R visitCompoundSelectStatement(CompoundSelectStatement e, A arg) {
return visitBaseSelectStatement(e, arg);

View File

@ -13,7 +13,7 @@ mixin CrudParser on ParserBase {
CrudStatement _crud() {
final withClause = _withClause();
if (_check(TokenType.select)) {
if (_check(TokenType.select) || _check(TokenType.$values)) {
return select(withClause: withClause);
} else if (_check(TokenType.delete)) {
return _deleteStmt(withClause);
@ -128,7 +128,8 @@ mixin CrudParser on ParserBase {
}
}
SelectStatement _selectNoCompound([WithClause withClause]) {
SelectStatementNoCompound _selectNoCompound([WithClause withClause]) {
if (_peek.type == TokenType.$values) return _valuesSelect(withClause);
if (!_match(const [TokenType.select])) return null;
final selectToken = _previous;
@ -166,6 +167,19 @@ mixin CrudParser on ParserBase {
)..setSpan(first, _previous);
}
ValuesSelectStatement _valuesSelect([WithClause withClause]) {
if (!_matchOne(TokenType.$values)) return null;
final firstToken = _previous;
final tuples = <Tuple>[];
do {
tuples.add(_consumeTuple() as Tuple);
} while (_matchOne(TokenType.comma));
return ValuesSelectStatement(tuples, withClause: withClause)
..setSpan(firstToken, _previous);
}
CompoundSelectPart _compoundSelectPart() {
if (_match(
const [TokenType.union, TokenType.intersect, TokenType.except])) {

View File

@ -84,4 +84,32 @@ INSERT INTO demo VALUES (?, ?)
expect(context.errors, isEmpty);
});
test('columns from values statement', () {
final context = engine.analyze("VALUES ('foo', 3), ('bar', 5)");
expect(context.errors, isEmpty);
final columns = (context.root as ResultSet).resolvedColumns;
expect(columns.map((e) => e.name), ['Column1', 'Column2']);
expect(columns.map((e) => context.typeOf(e).type?.type),
[BasicType.text, BasicType.int]);
});
test('columns from nested VALUES', () {
final context = engine.analyze('SELECT Column1 FROM (VALUES (3))');
expect(context.errors, isEmpty);
});
test('gracefully handles tuples of different lengths in VALUES', () {
final context = engine.analyze("VALUES ('foo', 3), ('bar')");
expect(context.errors, isNotEmpty);
final columns = (context.root as ResultSet).resolvedColumns;
expect(columns.map((e) => e.name), ['Column1', 'Column2']);
expect(columns.map((e) => context.typeOf(e).type?.type),
[BasicType.text, BasicType.int]);
});
}

View File

@ -4,13 +4,14 @@ import 'package:test/test.dart';
void main() {
test('reports error if LIMIT is used before last part', () {
final engine = SqlEngine();
final analyzed = engine.analyze('SELECT 1 ORDER BY 3 UNION SELECT 2');
final analyzed = engine.analyze('SELECT 1 LIMIT 3 UNION SELECT 2');
expect(analyzed.errors, hasLength(1));
final error = analyzed.errors.single;
expect(error.type, AnalysisErrorType.synctactic);
final wrongLimit = (analyzed.root as CompoundSelectStatement).base.orderBy;
final problematicSelect = (analyzed.root as CompoundSelectStatement).base;
final wrongLimit = (problematicSelect as SelectStatement).limit;
expect(error.relevantNode, wrongLimit);
});
@ -26,8 +27,9 @@ void main() {
final error = analyzed.errors.single;
expect(error.type, AnalysisErrorType.synctactic);
final wrongOrderBy =
(analyzed.root as CompoundSelectStatement).additional[0].select.orderBy;
final problematicSelect =
(analyzed.root as CompoundSelectStatement).additional[0].select;
final wrongOrderBy = (problematicSelect as SelectStatement).orderBy;
expect(error.relevantNode, wrongOrderBy);
});
}

View File

@ -0,0 +1,21 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
void main() {
test('reports error when using different-length tuples in VALUES', () {
final stmt = SqlEngine().analyze('VALUES (1, 2), (3)');
expect(
stmt.errors,
contains(
isA<AnalysisError>()
.having((e) => e.type, 'type',
AnalysisErrorType.valuesSelectCountMismatch)
.having((e) => e.message, 'message',
allOf(contains('1'), contains('2')))
.having((e) => e.relevantNode.span.text, 'relevantNode.span.text',
'(3)'),
),
);
});
}

View File

@ -0,0 +1,78 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import '../utils.dart';
void main() {
test('parses VALUES select statement', () {
testStatement(
"VALUES ('foo', 'bar'), (1, 2)",
ValuesSelectStatement(
[
Tuple(
expressions: [
StringLiteral(stringLiteral('foo')),
StringLiteral(stringLiteral('bar')),
],
),
Tuple(
expressions: [
NumericLiteral(1, token(TokenType.numberLiteral)),
NumericLiteral(2, token(TokenType.numberLiteral)),
],
),
],
),
);
});
test('can select FROM VALUES', () {
testStatement(
'SELECT * FROM (VALUES(1, 2))',
SelectStatement(
columns: [StarResultColumn()],
from: SelectStatementAsSource(
statement: ValuesSelectStatement(
[
Tuple(
expressions: [
NumericLiteral(1, token(TokenType.numberLiteral)),
NumericLiteral(2, token(TokenType.numberLiteral)),
],
),
],
),
),
),
);
});
test('can use WITH clause on VALUES', () {
testStatement(
'WITH foo AS (VALUES (3)) VALUES(1, 2)',
ValuesSelectStatement(
[
Tuple(
expressions: [
NumericLiteral(1, token(TokenType.numberLiteral)),
NumericLiteral(2, token(TokenType.numberLiteral)),
],
),
],
withClause: WithClause(
recursive: false,
ctes: [
CommonTableExpression(
cteTableName: 'foo',
as: ValuesSelectStatement([
Tuple(expressions: [
NumericLiteral(3, token(TokenType.numberLiteral))
]),
]),
),
],
),
),
);
});
}