From 4a2184110fbc18b9d21a2a9c6735a7c5f3357fd0 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Thu, 16 Jan 2020 22:07:02 +0100 Subject: [PATCH] Run tests for types1 for types2 resolver --- moor/build.yaml | 2 +- .../analysis/types2/graph/relationships.dart | 20 ++- .../src/analysis/types2/graph/type_graph.dart | 38 ++-- .../analysis/types2/resolving_visitor.dart | 166 ++++++++++++------ sqlparser/lib/src/analysis/types2/types.dart | 26 ++- sqlparser/lib/src/ast/common/tuple.dart | 2 +- sqlparser/lib/src/ast/expressions/simple.dart | 2 +- .../test/analysis/types2/misc_cases_test.dart | 53 ++++++ .../test/analysis/types2/resolver_test.dart | 70 ++++++-- 9 files changed, 278 insertions(+), 101 deletions(-) create mode 100644 sqlparser/test/analysis/types2/misc_cases_test.dart diff --git a/moor/build.yaml b/moor/build.yaml index 4c88f972..2c59197a 100644 --- a/moor/build.yaml +++ b/moor/build.yaml @@ -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 \ No newline at end of file diff --git a/sqlparser/lib/src/analysis/types2/graph/relationships.dart b/sqlparser/lib/src/analysis/types2/graph/relationships.dart index 29d37e9b..8b017f2c 100644 --- a/sqlparser/lib/src/analysis/types2/graph/relationships.dart +++ b/sqlparser/lib/src/analysis/types2/graph/relationships.dart @@ -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; diff --git a/sqlparser/lib/src/analysis/types2/graph/type_graph.dart b/sqlparser/lib/src/analysis/types2/graph/type_graph.dart index eb7fe563..7183133b 100644 --- a/sqlparser/lib/src/analysis/types2/graph/type_graph.dart +++ b/sqlparser/lib/src/analysis/types2/graph/type_graph.dart @@ -6,9 +6,9 @@ class TypeGraph { final Map _knownTypes = {}; final Map _knownNullability = {}; - final List _relationships = []; + final List _relations = []; - final Map> _edges = {}; + final Map> _edges = {}; final List _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 t, TypeRelationship r) { + void putAll(Iterable 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 get from; } diff --git a/sqlparser/lib/src/analysis/types2/resolving_visitor.dart b/sqlparser/lib/src/analysis/types2/resolving_visitor.dart index cbf175df..58c23cac 100644 --- a/sqlparser/lib/src/analysis/types2/resolving_visitor.dart +++ b/sqlparser/lib/src/analysis/types2/resolving_visitor.dart @@ -20,7 +20,7 @@ class TypeResolver extends RecursiveVisitor { // 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 { 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 { } } + @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 { 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 { 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 { } } + @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 { 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 { 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 { 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 { @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 { 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 { @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 { 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 { // 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 { 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 { return null; } + Set _resolveFunctionArguments(SqlInvocation e) { + final params = e.expandParameters(); + final visited = {}; + 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 { 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)); } } diff --git a/sqlparser/lib/src/analysis/types2/types.dart b/sqlparser/lib/src/analysis/types2/types.dart index 9f7f0b33..0b6d631c 100644 --- a/sqlparser/lib/src/analysis/types2/types.dart +++ b/sqlparser/lib/src/analysis/types2/types.dart @@ -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(); } } diff --git a/sqlparser/lib/src/ast/common/tuple.dart b/sqlparser/lib/src/ast/common/tuple.dart index 8e0bdd6d..14fa2380 100644 --- a/sqlparser/lib/src/ast/common/tuple.dart +++ b/sqlparser/lib/src/ast/common/tuple.dart @@ -17,7 +17,7 @@ class Tuple extends Expression { } @override - Iterable get childNodes => expressions; + List get childNodes => expressions; @override bool contentEquals(Tuple other) => true; diff --git a/sqlparser/lib/src/ast/expressions/simple.dart b/sqlparser/lib/src/ast/expressions/simple.dart index 2d7a4e14..9df69327 100644 --- a/sqlparser/lib/src/ast/expressions/simple.dart +++ b/sqlparser/lib/src/ast/expressions/simple.dart @@ -169,7 +169,7 @@ class InExpression extends Expression { } @override - Iterable get childNodes => [left, inside]; + List get childNodes => [left, inside]; @override bool contentEquals(InExpression other) => other.not == not; diff --git a/sqlparser/test/analysis/types2/misc_cases_test.dart b/sqlparser/test/analysis/types2/misc_cases_test.dart new file mode 100644 index 00000000..7d6c23f8 --- /dev/null +++ b/sqlparser/test/analysis/types2/misc_cases_test.dart @@ -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 _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)); + }); + }); + }); +} diff --git a/sqlparser/test/analysis/types2/resolver_test.dart b/sqlparser/test/analysis/types2/resolver_test.dart index 8bdfc6d0..dc8b1215 100644 --- a/sqlparser/test/analysis/types2/resolver_test.dart +++ b/sqlparser/test/analysis/types2/resolver_test.dart @@ -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() + .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)); }); }); }