Run tests for types1 for types2 resolver

This commit is contained in:
Simon Binder 2020-01-16 22:07:02 +01:00
parent 8ae68707f8
commit 4a2184110f
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 278 additions and 101 deletions

View File

@ -7,7 +7,7 @@ targets:
use_column_name_as_json_key_when_defined_in_moor_file: true
generate_connect_constructor: true
write_from_json_string_constructor: true
# use_experimental_inference: true
use_experimental_inference: true
sqlite_modules:
- json1
- fts5

View File

@ -2,7 +2,7 @@ part of '../types.dart';
/// Dependency declaring that [target] is nullable if any in [from] is
/// nullable.
class NullableIfSomeOtherIs extends TypeRelationship
class NullableIfSomeOtherIs extends TypeRelation
implements MultiSourceRelation {
@override
final Typeable target;
@ -13,17 +13,21 @@ class NullableIfSomeOtherIs extends TypeRelationship
}
/// Dependency declaring that [target] has exactly the same type as [other].
class CopyTypeFrom extends TypeRelationship implements DirectedRelation {
class CopyTypeFrom extends TypeRelation implements DirectedRelation {
@override
final Typeable target;
final Typeable other;
CopyTypeFrom(this.target, this.other);
/// When true, [target] will be the array-variant of [other]. When false,
/// [target] will be the scalar variant of [other]. When null, nothing will be
/// transformed.
final bool array;
CopyTypeFrom(this.target, this.other, {this.array});
}
/// Dependency declaring that [target] has a type that matches all of [from].
class CopyEncapsulating extends TypeRelationship
implements MultiSourceRelation {
class CopyEncapsulating extends TypeRelation implements MultiSourceRelation {
@override
final Typeable target;
@override
@ -35,7 +39,7 @@ class CopyEncapsulating extends TypeRelationship
/// Dependency declaring that [first] and [second] have the same type. This is
/// an optional dependency that will only be applied when one type is known and
/// the other is not.
class HaveSameType extends TypeRelationship {
class HaveSameType extends TypeRelation {
final Typeable first;
final Typeable second;
@ -49,7 +53,7 @@ class HaveSameType extends TypeRelationship {
/// Dependency declaring that, if no better option is found, [target] should
/// have the specified [defaultType].
class DefaultType extends TypeRelationship implements DirectedRelation {
class DefaultType extends TypeRelation implements DirectedRelation {
@override
final Typeable target;
final ResolvedType defaultType;
@ -61,7 +65,7 @@ enum CastMode { numeric, boolean }
/// Dependency declaring that [target] has the same type as [other] after
/// casting it with [cast].
class CopyAndCast extends TypeRelationship implements DirectedRelation {
class CopyAndCast extends TypeRelation implements DirectedRelation {
@override
final Typeable target;
final Typeable other;

View File

@ -6,9 +6,9 @@ class TypeGraph {
final Map<Typeable, ResolvedType> _knownTypes = {};
final Map<Typeable, bool> _knownNullability = {};
final List<TypeRelationship> _relationships = [];
final List<TypeRelation> _relations = [];
final Map<Typeable, List<TypeRelationship>> _edges = {};
final Map<Typeable, List<TypeRelation>> _edges = {};
final List<DefaultType> _defaultTypes = [];
TypeGraph();
@ -35,8 +35,8 @@ class TypeGraph {
bool knowsType(Typeable t) => _knownTypes.containsKey(variables.normalize(t));
void addRelation(TypeRelationship relation) {
_relationships.add(relation);
void addRelation(TypeRelation relation) {
_relations.add(relation);
}
void performResolve() {
@ -61,17 +61,19 @@ class TypeGraph {
// propagate changes
for (final edge in _edges[t]) {
if (edge is CopyTypeFrom) {
_copyType(resolved, edge.other, edge.target);
var type = this[edge.other];
if (edge.array != null) {
type = type.toArray(edge.array);
}
_copyType(resolved, edge.other, edge.target, type);
} else if (edge is HaveSameType) {
_copyType(resolved, t, edge.getOther(t));
} else if (edge is CopyAndCast) {
_copyType(resolved, t, edge.target, this[t].cast(edge.cast));
} else if (edge is MultiSourceRelation) {
// handle many-to-one changes, if all targets have been resolved
final typedEdge = edge as MultiSourceRelation;
if (typedEdge.from.every(knowsType)) {
_propagateManyToOne(typedEdge, resolved, t);
if (edge.from.every(knowsType)) {
_propagateManyToOne(edge, resolved, t);
}
}
}
@ -108,17 +110,17 @@ class TypeGraph {
void _indexRelationships() {
_edges.clear();
void put(Typeable t, TypeRelationship r) {
void put(Typeable t, TypeRelation r) {
_edges.putIfAbsent(t, () => []).add(r);
}
void putAll(Iterable<Typeable> t, TypeRelationship r) {
void putAll(Iterable<Typeable> t, TypeRelation r) {
for (final element in t) {
put(element, r);
}
}
for (final relation in _relationships) {
for (final relation in _relations) {
if (relation is NullableIfSomeOtherIs) {
putAll(relation.from, relation);
} else if (relation is CopyTypeFrom) {
@ -139,12 +141,20 @@ class TypeGraph {
}
}
abstract class TypeRelationship {}
/// Describes how the type of different [Typeable] instances has an effect on
/// others.
///
/// Note that all logic is handled in the type graph, these are logic-less model
/// classes only.
abstract class TypeRelation {}
abstract class DirectedRelation {
/// Relation that only has an effect on one [Typeable] -- namely, [target].
abstract class DirectedRelation implements TypeRelation {
/// The only [Typeable] effected by this relation.
Typeable get target;
}
/// Relation where the type of multiple [Typeable] instances must be known.
abstract class MultiSourceRelation implements DirectedRelation {
List<Typeable> get from;
}

View File

@ -20,7 +20,7 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
// node, which means this visitor doesn't find them
root.acceptWithoutArg(_ResultColumnVisitor(this));
session.finish();
session._finish();
}
@override
@ -72,6 +72,9 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
visit(select.stmt, SelectTypeExpectation(expectations));
},
isValues: (values) {
// todo: It would be nice to remove this special case. Can we generalize
// the SelectTypeExpectation so that it works for tuples and just visit
// e.source?
for (final tuple in values.values) {
for (var i = 0; i < tuple.expressions.length; i++) {
final expectation = i < expectations.length
@ -114,31 +117,49 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
}
}
@override
void visitSetComponent(SetComponent e, TypeExpectation arg) {
visit(e.column, const NoTypeExpectation());
_lazyCopy(e.expression, e.column);
visit(e.expression, const NoTypeExpectation());
}
@override
void visitLimit(Limit e, TypeExpectation arg) {
visit(e.count, _expectInt);
visitNullable(e.offset, _expectInt);
}
@override
void visitFrameSpec(FrameSpec e, TypeExpectation arg) {
// handle something like "RANGE BETWEEN ? PRECEDING AND ? FOLLOWING
if (e.start.isExpressionOffset) {
visit(e.start.offset, _expectInt);
}
if (e.end.isExpressionOffset) {
visit(e.end.offset, _expectInt);
}
}
@override
void visitLiteral(Literal e, TypeExpectation arg) {
ResolvedType type;
if (e is NullLiteral) {
type = const ResolvedType(type: BasicType.nullType, nullable: true);
session.hintNullability(e, true);
session._hintNullability(e, true);
} else if (e is StringLiteral) {
type = e.isBinary ? const ResolvedType(type: BasicType.blob) : _textType;
session.hintNullability(e, false);
session._hintNullability(e, false);
} else if (e is BooleanLiteral) {
type = const ResolvedType.bool();
session.hintNullability(e, false);
session._hintNullability(e, false);
} else if (e is NumericLiteral) {
type = e.isInt ? _intType : _realType;
session.hintNullability(e, false);
session._hintNullability(e, false);
}
session.checkAndResolve(e, type, arg);
session._checkAndResolve(e, type, arg);
}
@override
@ -163,9 +184,9 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
resolved ??= _inferFromContext(arg);
if (resolved != null) {
session.checkAndResolve(e, resolved, arg);
session._checkAndResolve(e, resolved, arg);
} else if (arg is RoughTypeExpectation) {
session.addRelationship(DefaultType(e, arg.defaultType()));
session._addRelation(DefaultType(e, arg.defaultType()));
}
visitChildren(e, arg);
@ -177,22 +198,22 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
if (operatorType == TokenType.plus) {
// plus is a no-op, so copy type from child
session.addRelationship(CopyTypeFrom(e, e.inner));
session._addRelation(CopyTypeFrom(e, e.inner));
visit(e.inner, arg);
} else if (operatorType == TokenType.not) {
// unary not expression - boolean, but nullability depends on child node.
session.checkAndResolve(e, const ResolvedType.bool(nullable: null), arg);
session.addRelationship(NullableIfSomeOtherIs(e, [e.inner]));
session._checkAndResolve(e, const ResolvedType.bool(nullable: null), arg);
session._addRelation(NullableIfSomeOtherIs(e, [e.inner]));
visit(e.inner, const ExactTypeExpectation.laxly(ResolvedType.bool()));
} else if (operatorType == TokenType.minus) {
// unary minus - can be int or real depending on child node
session.addRelationship(CopyAndCast(e, e.inner, CastMode.numeric));
session._addRelation(CopyAndCast(e, e.inner, CastMode.numeric));
visit(e.inner, const RoughTypeExpectation.numeric());
} else if (operatorType == TokenType.tilde) {
// bitwise negation - definitely int, but nullability depends on child
session.checkAndResolve(
session._checkAndResolve(
e, const ResolvedType(type: BasicType.int, nullable: null), arg);
session.addRelationship(NullableIfSomeOtherIs(e, [e.inner]));
session._addRelation(NullableIfSomeOtherIs(e, [e.inner]));
visit(e.inner, const NoTypeExpectation());
} else {
@ -201,14 +222,22 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
}
}
@override
void visitTuple(Tuple e, TypeExpectation arg) {
// make children non-arrays
for (final child in e.childNodes) {
session._addRelation(CopyTypeFrom(child, e, array: false));
}
}
@override
void visitBetweenExpression(BetweenExpression e, TypeExpectation arg) {
visitChildren(e, _expectNum);
session
..addRelationship(NullableIfSomeOtherIs(e, e.childNodes))
..addRelationship(HaveSameType(e.lower, e.upper))
..addRelationship(HaveSameType(e.check, e.lower));
.._addRelation(NullableIfSomeOtherIs(e, e.childNodes))
.._addRelation(HaveSameType(e.lower, e.upper))
.._addRelation(HaveSameType(e.check, e.lower));
}
@override
@ -216,8 +245,8 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
switch (e.operator.type) {
case TokenType.and:
case TokenType.or:
session.checkAndResolve(e, const ResolvedType.bool(), arg);
session.addRelationship(NullableIfSomeOtherIs(e, [e.left, e.right]));
session._checkAndResolve(e, const ResolvedType.bool(), arg);
session._addRelation(NullableIfSomeOtherIs(e, [e.left, e.right]));
// logic expressions, so children must be boolean
visitChildren(e, const ExactTypeExpectation.laxly(ResolvedType.bool()));
@ -230,16 +259,16 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
case TokenType.more:
case TokenType.moreEqual:
// comparison. Returns bool, copying nullability from children.
session.checkAndResolve(e, const ResolvedType.bool(), arg);
session.addRelationship(NullableIfSomeOtherIs(e, [e.left, e.right]));
session._checkAndResolve(e, const ResolvedType.bool(), arg);
session._addRelation(NullableIfSomeOtherIs(e, [e.left, e.right]));
// Not technically a requirement, but assume lhs and rhs have the same
// type.
session.addRelationship(HaveSameType(e.left, e.right));
session._addRelation(HaveSameType(e.left, e.right));
visitChildren(e, const NoTypeExpectation());
break;
case TokenType.plus:
case TokenType.minus:
session.addRelationship(CopyEncapsulating(e, [e.left, e.right]));
session._addRelation(CopyEncapsulating(e, [e.left, e.right]));
break;
// all of those only really make sense for integers
case TokenType.shiftLeft:
@ -248,15 +277,15 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
case TokenType.ampersand:
case TokenType.percent:
const type = ResolvedType(type: BasicType.int);
session.checkAndResolve(e, type, arg);
session.addRelationship(NullableIfSomeOtherIs(e, [e.left, e.right]));
session._checkAndResolve(e, type, arg);
session._addRelation(NullableIfSomeOtherIs(e, [e.left, e.right]));
visitChildren(e, const ExactTypeExpectation.laxly(type));
break;
case TokenType.doublePipe:
// string concatenation.
const stringType = ResolvedType(type: BasicType.text);
session.checkAndResolve(e, stringType, arg);
session.addRelationship(NullableIfSomeOtherIs(e, [e.left, e.right]));
session._checkAndResolve(e, stringType, arg);
session._addRelation(NullableIfSomeOtherIs(e, [e.left, e.right]));
const childExpectation = ExactTypeExpectation.laxly(stringType);
visit(e.left, childExpectation);
visit(e.right, childExpectation);
@ -269,27 +298,37 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
@override
void visitIsExpression(IsExpression e, TypeExpectation arg) {
session.checkAndResolve(e, const ResolvedType.bool(), arg);
session.hintNullability(e, false);
session._checkAndResolve(e, const ResolvedType.bool(), arg);
session._hintNullability(e, false);
visitChildren(e, const NoTypeExpectation());
}
@override
void visitIsNullExpression(IsNullExpression e, TypeExpectation arg) {
session.checkAndResolve(e, const ResolvedType.bool(), arg);
session.hintNullability(e, false);
session._checkAndResolve(e, const ResolvedType.bool(), arg);
session._hintNullability(e, false);
visitChildren(e, const NoTypeExpectation());
}
@override
void visitInExpression(InExpression e, TypeExpectation arg) {
session._checkAndResolve(e, const ResolvedType.bool(), arg);
session._addRelation(NullableIfSomeOtherIs(e, e.childNodes));
session._addRelation(CopyTypeFrom(e.inside, e.left, array: true));
visitChildren(e, const NoTypeExpectation());
}
@override
void visitCaseExpression(CaseExpression e, TypeExpectation arg) {
session.addRelationship(CopyEncapsulating(e, [
session._addRelation(CopyEncapsulating(e, [
for (final when in e.whens) when.then,
if (e.elseExpr != null) e.elseExpr,
]));
if (e.base != null) {
session.addRelationship(
session._addRelation(
CopyEncapsulating(e.base, [for (final when in e.whens) when.when]),
);
}
@ -303,7 +342,7 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
final parent = e.parent;
if (parent is CaseExpression && parent.base != null) {
// case expressions with base -> condition is compared to base
session.addRelationship(CopyTypeFrom(e.when, parent.base));
session._addRelation(CopyTypeFrom(e.when, parent.base));
visit(e.when, const NoTypeExpectation());
} else {
// case expression without base -> the conditions are booleans
@ -316,16 +355,16 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
@override
void visitCastExpression(CastExpression e, TypeExpectation arg) {
final type = session.context.schemaSupport.resolveColumnType(e.typeName);
session.checkAndResolve(e, type, arg);
session.addRelationship(NullableIfSomeOtherIs(e, [e.operand]));
session._checkAndResolve(e, type, arg);
session._addRelation(NullableIfSomeOtherIs(e, [e.operand]));
visit(e.operand, const NoTypeExpectation());
}
@override
void visitStringComparison(
StringComparisonExpression e, TypeExpectation arg) {
session.checkAndResolve(e, const ResolvedType(type: BasicType.text), arg);
session.addRelationship(NullableIfSomeOtherIs(
session._checkAndResolve(e, const ResolvedType(type: BasicType.text), arg);
session._addRelation(NullableIfSomeOtherIs(
e,
[
e.left,
@ -352,15 +391,21 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
void visitInvocation(SqlInvocation e, TypeExpectation arg) {
final type = _resolveInvocation(e);
if (type != null) {
session.checkAndResolve(e, type, arg);
session._checkAndResolve(e, type, arg);
}
final visited = _resolveFunctionArguments(e);
for (final child in e.childNodes) {
if (!visited.contains(child)) {
visit(child, const NoTypeExpectation());
}
}
visitChildren(e, const NoTypeExpectation());
}
ResolvedType _resolveInvocation(SqlInvocation e) {
final params = e.expandParameters();
void nullableIfChildIs() {
session.addRelationship(NullableIfSomeOtherIs(e, params));
session._addRelation(NullableIfSomeOtherIs(e, params));
}
final lowercaseName = e.name.toLowerCase();
@ -376,8 +421,8 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
// ignore: dead_code
throw AssertionError(); // required so that this switch compiles
case 'sum':
session.addRelationship(CopyAndCast(e, params.first, CastMode.numeric));
session.addRelationship(DefaultType(e, _realType));
session._addRelation(CopyAndCast(e, params.first, CastMode.numeric));
session._addRelation(DefaultType(e, _realType));
nullableIfChildIs();
return null;
case 'lower':
@ -432,27 +477,27 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
case 'likelihood':
case 'likely':
case 'unlikely':
session.addRelationship(CopyTypeFrom(e, params.first));
session._addRelation(CopyTypeFrom(e, params.first));
return null;
case 'coalesce':
case 'ifnull':
session.addRelationship(CopyEncapsulating(e, params));
session._addRelation(CopyEncapsulating(e, params));
return null;
case 'nullif':
session.hintNullability(e, true);
session.addRelationship(CopyTypeFrom(e, params.first));
session._hintNullability(e, true);
session._addRelation(CopyTypeFrom(e, params.first));
return null;
case 'first_value':
case 'last_value':
case 'lag':
case 'lead':
case 'nth_value':
session.addRelationship(CopyTypeFrom(e, params.first));
session._addRelation(CopyTypeFrom(e, params.first));
return null;
case 'max':
case 'min':
session.hintNullability(e, true);
session.addRelationship(CopyEncapsulating(e, params));
session._hintNullability(e, true);
session._addRelation(CopyEncapsulating(e, params));
return null;
}
@ -495,11 +540,26 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
return null;
}
Set<AstNode> _resolveFunctionArguments(SqlInvocation e) {
final params = e.expandParameters();
final visited = <AstNode>{};
final name = e.name.toLowerCase();
if (name == 'nth_value' && params.length >= 2 && params[1] is Expression) {
// the second argument of nth_value is always an integer
final secondParam = params[1] as Expression;
visit(secondParam, _expectInt);
visited.add(secondParam);
}
return visited;
}
void _handleColumn(Column column) {
if (session.graph.knowsType(column)) return;
if (column is TableColumn) {
session.markTypeResolved(column, column.type);
session._markTypeResolved(column, column.type);
} else if (column is ExpressionColumn) {
_lazyCopy(column, column.expression);
} else if (column is DelegatedColumn && column.innerColumn != null) {
@ -510,9 +570,9 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
void _lazyCopy(Typeable to, Typeable from) {
if (session.graph.knowsType(from)) {
session.markTypeResolved(to, session.typeOf(from));
session._markTypeResolved(to, session.typeOf(from));
} else {
session.addRelationship(CopyTypeFrom(to, from));
session._addRelation(CopyTypeFrom(to, from));
}
}

View File

@ -19,31 +19,41 @@ class TypeInferenceSession {
results = TypeInferenceResults._(this);
}
void markTypeResolved(Typeable t, ResolvedType r) {
void _markTypeResolved(Typeable t, ResolvedType r) {
graph[t] = r;
}
void checkAndResolve(
void _checkAndResolve(
Typeable t, ResolvedType r, TypeExpectation expectation) {
expectIsPossible(r, expectation);
markTypeResolved(t, r);
_expectIsPossible(r, expectation);
_markTypeResolved(t, r);
}
/// Returns the inferred type of [t], or `null` if it couldn't be inferred.
ResolvedType typeOf(Typeable t) {
return graph[t];
}
void addRelationship(TypeRelationship relationship) {
void _addRelation(TypeRelation relationship) {
graph.addRelation(relationship);
}
void expectIsPossible(ResolvedType r, TypeExpectation expectation) {}
/// Check that [r] is compatible with [expectation].
///
/// This is not currently implemented.
void _expectIsPossible(ResolvedType r, TypeExpectation expectation) {}
void hintNullability(Typeable t, bool nullable) {
/// This is not currently implemented.
void _hintNullability(Typeable t, bool nullable) {
assert(nullable != null);
}
void finish() {
/// Asks the underlying [TypeGraph] to propagate known types via known
/// [TypeRelation]s.
///
/// The [SqlEngine] will call this method when analyzing a statement. There's
/// no need to call it from user code.
void _finish() {
graph.performResolve();
}
}

View File

@ -17,7 +17,7 @@ class Tuple extends Expression {
}
@override
Iterable<AstNode> get childNodes => expressions;
List<Expression> get childNodes => expressions;
@override
bool contentEquals(Tuple other) => true;

View File

@ -169,7 +169,7 @@ class InExpression extends Expression {
}
@override
Iterable<AstNode> get childNodes => [left, inside];
List<Expression> get childNodes => [left, inside];
@override
bool contentEquals(InExpression other) => other.not == not;

View File

@ -0,0 +1,53 @@
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import '../data.dart';
// Cases copied from the regular type inference algorithm test
const Map<String, ResolvedType> _types = {
'SELECT * FROM demo WHERE id = ?': ResolvedType(type: BasicType.int),
'SELECT * FROM demo WHERE content = ?': ResolvedType(type: BasicType.text),
'SELECT * FROM demo LIMIT ?': ResolvedType(type: BasicType.int),
'SELECT 1 FROM demo GROUP BY id HAVING COUNT(*) = ?':
ResolvedType(type: BasicType.int),
'SELECT 1 FROM demo WHERE id BETWEEN 3 AND ?':
ResolvedType(type: BasicType.int),
'UPDATE demo SET content = ? WHERE id = 3':
ResolvedType(type: BasicType.text),
'SELECT * FROM demo WHERE content LIKE ?': ResolvedType(type: BasicType.text),
"SELECT * FROM demo WHERE content LIKE '%e' ESCAPE ?":
ResolvedType(type: BasicType.text),
'SELECT * FROM demo WHERE content IN ?':
ResolvedType(type: BasicType.text, isArray: true),
'SELECT * FROM demo WHERE content IN (?)':
ResolvedType(type: BasicType.text, isArray: false),
'SELECT * FROM demo JOIN tbl ON demo.id = tbl.id WHERE date = ?':
ResolvedType(type: BasicType.int, hint: IsDateTime()),
'SELECT row_number() OVER (RANGE ? PRECEDING)':
ResolvedType(type: BasicType.int),
'SELECT ?;': null,
'SELECT CAST(3 AS TEXT) = ?': ResolvedType(type: BasicType.text),
};
SqlEngine _spawnEngine() {
return SqlEngine.withOptions(
EngineOptions(enableExperimentalTypeInference: true))
..registerTable(demoTable)
..registerTable(anotherTable);
}
void main() {
group('miscellaneous type inference cases', () {
_types.forEach((sql, expected) {
test('for $sql', () {
final engine = _spawnEngine();
final content = engine.analyze(sql);
final variable = content.root.allDescendants
.firstWhere((node) => node is Variable) as Typeable;
expect(content.typeOf(variable).type, equals(expected));
});
});
});
}

View File

@ -142,6 +142,23 @@ void main() {
expect(escapedType, const ResolvedType(type: BasicType.text));
});
test('handles nth_value', () {
final resolver = _obtainResolver("SELECT nth_value('string', ?1) = ?2");
final variables = resolver.session.context.root.allDescendants
.whereType<Variable>()
.iterator;
variables.moveNext();
final firstVar = variables.current;
variables.moveNext();
final secondVar = variables.current;
expect(resolver.session.typeOf(firstVar),
equals(const ResolvedType(type: BasicType.int)));
expect(resolver.session.typeOf(secondVar),
equals(const ResolvedType(type: BasicType.text)));
});
group('case expressions', () {
test('infers base clause from when', () {
final type = _resolveFirstVariable("SELECT CASE ? WHEN 1 THEN 'two' END");
@ -163,24 +180,25 @@ void main() {
"WHEN true THEN 'two' ELSE 'three' END;");
expect(type, const ResolvedType(type: BasicType.text));
});
});
test('can select columns', () {
final type = _resolveResultColumn('SELECT id FROM demo;');
expect(type, const ResolvedType(type: BasicType.int));
});
test('can select columns', () {
final type = _resolveResultColumn('SELECT id FROM demo;');
expect(type, const ResolvedType(type: BasicType.int));
});
test('infers types for dart placeholders', () {
final resolver = _obtainResolver(r'SELECT * FROM demo WHERE $pred');
final type = resolver.session.typeOf(resolver
.session.context.root.allDescendants
.firstWhere((e) => e is DartExpressionPlaceholder)
as DartExpressionPlaceholder);
test('infers types for dart placeholders', () {
final resolver = _obtainResolver(r'SELECT * FROM demo WHERE $pred');
final type = resolver.session.typeOf(resolver
.session.context.root.allDescendants
.firstWhere((e) => e is DartExpressionPlaceholder)
as DartExpressionPlaceholder);
expect(type, const ResolvedType.bool());
});
expect(type, const ResolvedType.bool());
});
test('handles recursive CTEs', () {
final type = _resolveResultColumn('''
test('handles recursive CTEs', () {
final type = _resolveResultColumn('''
WITH RECURSIVE
cnt(x) AS (
SELECT 1
@ -191,7 +209,29 @@ WITH RECURSIVE
SELECT x FROM cnt
''');
expect(type, const ResolvedType(type: BasicType.int));
expect(type, const ResolvedType(type: BasicType.int));
});
test('handles set components in updates', () {
final type = _resolveFirstVariable('UPDATE demo SET id = ?');
expect(type, const ResolvedType(type: BasicType.int));
});
test('infers offsets in frame specs', () {
final type =
_resolveFirstVariable('SELECT SUM(id) OVER (ROWS ? PRECEDING)');
expect(type, const ResolvedType(type: BasicType.int));
});
group('IS IN expressions', () {
test('infer the variable as an array type', () {
final type = _resolveFirstVariable('SELECT 3 IN ?');
expect(type, const ResolvedType(type: BasicType.int, isArray: true));
});
test('does not infer the variable as an array when in a tuple', () {
final type = _resolveFirstVariable('SELECT 3 IN (?)');
expect(type, const ResolvedType(type: BasicType.int, isArray: false));
});
});
}