mirror of https://github.com/AMT-Cheif/drift.git
Parse and analyze VALUES clause for selects
This commit is contained in:
parent
c007e1f9ac
commit
6b2bd27d4d
|
@ -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.
|
||||
|
||||
|
|
|
@ -61,5 +61,6 @@ enum AnalysisErrorType {
|
|||
unknownFunction,
|
||||
compoundColumnCountMismatch,
|
||||
cteColumnCountMismatch,
|
||||
valuesSelectCountMismatch,
|
||||
other,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -231,7 +231,7 @@ class _ResolvedVariables {
|
|||
}
|
||||
}
|
||||
|
||||
extension on ResolvedType {
|
||||
extension ResolvedTypeUtils on ResolvedType {
|
||||
ResolvedType cast(CastMode mode) {
|
||||
switch (mode) {
|
||||
case CastMode.numeric:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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])) {
|
||||
|
|
|
@ -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]);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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)'),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
|
@ -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))
|
||||
]),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue