mirror of https://github.com/AMT-Cheif/drift.git
Finish type resolution for simple expressions and columns
This commit is contained in:
parent
7e916b9d74
commit
1271e730b8
|
@ -16,7 +16,6 @@ part 'steps/type_resolver.dart';
|
|||
|
||||
part 'types/data.dart';
|
||||
part 'types/resolver.dart';
|
||||
|
||||
part 'types/typeable.dart';
|
||||
|
||||
part 'error.dart';
|
||||
|
|
|
@ -11,4 +11,14 @@ class AnalysisContext {
|
|||
void reportError(AnalysisError error) {
|
||||
errors.add(error);
|
||||
}
|
||||
|
||||
ResolveResult typeOf(Typeable t) {
|
||||
if (t is Column) {
|
||||
return types.resolveColumn(t);
|
||||
} else if (t is Expression) {
|
||||
return types.resolveExpression(t);
|
||||
}
|
||||
|
||||
throw StateError('Unknown typeable $t');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,13 +2,16 @@ part of '../analysis.dart';
|
|||
|
||||
abstract class Column with Referencable implements Typeable {
|
||||
String get name;
|
||||
|
||||
const Column();
|
||||
}
|
||||
|
||||
class TableColumn extends Column {
|
||||
@override
|
||||
final String name;
|
||||
final ResolvedType type;
|
||||
|
||||
TableColumn(this.name);
|
||||
const TableColumn(this.name, this.type);
|
||||
}
|
||||
|
||||
class ExpressionColumn extends Column {
|
||||
|
|
|
@ -1,16 +1,5 @@
|
|||
part of '../analysis.dart';
|
||||
|
||||
const _comparisons = [
|
||||
TokenType.less,
|
||||
TokenType.lessEqual,
|
||||
TokenType.more,
|
||||
TokenType.moreEqual,
|
||||
TokenType.equal,
|
||||
TokenType.doubleEqual,
|
||||
TokenType.exclamationEqual,
|
||||
TokenType.lessMore,
|
||||
];
|
||||
|
||||
class TypeResolvingVisitor extends RecursiveVisitor<void> {
|
||||
final AnalysisContext context;
|
||||
TypeResolver get types => context.types;
|
||||
|
@ -18,99 +7,14 @@ class TypeResolvingVisitor extends RecursiveVisitor<void> {
|
|||
TypeResolvingVisitor(this.context);
|
||||
|
||||
@override
|
||||
void visitSelectStatement(SelectStatement e) {
|
||||
if (e.where != null) {
|
||||
context.types.suggestBool(e.where);
|
||||
void visitChildren(AstNode e) {
|
||||
// called for every ast node, so we implement this here
|
||||
if (e is Expression) {
|
||||
types.resolveExpression(e);
|
||||
} else if (e is SelectStatement) {
|
||||
e.resolvedColumns.forEach(types.resolveColumn);
|
||||
}
|
||||
|
||||
visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitResultColumn(ResultColumn e) {
|
||||
visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitFunction(FunctionExpression e) {
|
||||
// todo handle function calls
|
||||
visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitLimit(Limit e) {
|
||||
if (e.count != null) {
|
||||
types.suggestType(e.count, const SqlType.int());
|
||||
}
|
||||
if (e.offset != null) {
|
||||
types.suggestType(e.offset, const SqlType.int());
|
||||
}
|
||||
|
||||
visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitBinaryExpression(BinaryExpression e) {
|
||||
final operator = e.operator.type;
|
||||
if (operator == TokenType.doublePipe) {
|
||||
// string concatenation: Will return a string, makes most sense with a
|
||||
// string.
|
||||
types
|
||||
..forceType(e, const SqlType.text())
|
||||
..suggestType(e.left, const SqlType.text())
|
||||
..suggestType(e.right, const SqlType.text());
|
||||
} else if (operator == TokenType.and || operator == TokenType.or) {
|
||||
types
|
||||
..suggestBool(e.left)
|
||||
..suggestBool(e.right)
|
||||
..forceType(e, const SqlType.int())
|
||||
..addTypeHint(e, const IsBoolean());
|
||||
} else if (_comparisons.contains(operator)) {
|
||||
types
|
||||
..suggestBool(e)
|
||||
..suggestSame(e.left, e.right);
|
||||
} else {
|
||||
// arithmetic operator
|
||||
types
|
||||
..forceType(e, const AnyNumericType())
|
||||
..suggestType(e.right, const AnyNumericType())
|
||||
..suggestType(e.left, const AnyNumericType());
|
||||
}
|
||||
|
||||
visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitUnaryExpression(UnaryExpression e) {
|
||||
final operator = e.operator.type;
|
||||
if (operator == TokenType.plus) {
|
||||
// unary type does nothing, just returns the value
|
||||
types.suggestSame(e, e.inner);
|
||||
} else if (operator == TokenType.minus) {
|
||||
types
|
||||
..forceType(e, const AnyNumericType())
|
||||
..suggestType(e.inner, const AnyNumericType());
|
||||
}
|
||||
|
||||
visitChildren(e);
|
||||
}
|
||||
|
||||
@override
|
||||
void visitLiteral(Literal e) {
|
||||
if (e is NullLiteral) {
|
||||
types.forceType(e, const SqlType.nullType());
|
||||
} else if (e is NumericLiteral) {
|
||||
if (e.number.toInt() == e.number) {
|
||||
types.forceType(e, const SqlType.int());
|
||||
} else {
|
||||
types.forceType(e, const SqlType.real());
|
||||
}
|
||||
|
||||
if (e is BooleanLiteral) {
|
||||
types.addTypeHint(e, const IsBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
visitChildren(e);
|
||||
super.visitChildren(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,62 +1,29 @@
|
|||
part of '../analysis.dart';
|
||||
|
||||
/// A type that sql expressions can have at runtime.
|
||||
abstract class SqlType {
|
||||
const SqlType();
|
||||
const factory SqlType.nullType() = NullType._;
|
||||
const factory SqlType.int() = IntegerType._;
|
||||
const factory SqlType.real() = RealType._;
|
||||
const factory SqlType.text() = TextType._;
|
||||
const factory SqlType.blob() = BlobType._;
|
||||
|
||||
bool isSubTypeOf(SqlType other);
|
||||
enum BasicType {
|
||||
nullType,
|
||||
int,
|
||||
real,
|
||||
text,
|
||||
blob,
|
||||
}
|
||||
|
||||
class NullType extends SqlType {
|
||||
const NullType._();
|
||||
class ResolvedType {
|
||||
final BasicType type;
|
||||
final TypeHint hint;
|
||||
final bool nullable;
|
||||
|
||||
@override
|
||||
bool isSubTypeOf(SqlType other) => true;
|
||||
}
|
||||
const ResolvedType({this.type, this.hint, this.nullable = false});
|
||||
const ResolvedType.bool()
|
||||
: this(type: BasicType.int, hint: const IsBoolean());
|
||||
|
||||
class IntegerType extends SqlType {
|
||||
const IntegerType._();
|
||||
|
||||
@override
|
||||
bool isSubTypeOf(SqlType other) => other is IntegerType;
|
||||
}
|
||||
|
||||
class RealType extends SqlType {
|
||||
const RealType._();
|
||||
|
||||
@override
|
||||
bool isSubTypeOf(SqlType other) => other is RealType;
|
||||
}
|
||||
|
||||
class TextType extends SqlType {
|
||||
const TextType._();
|
||||
|
||||
@override
|
||||
bool isSubTypeOf(SqlType other) => other is TextType;
|
||||
}
|
||||
|
||||
class BlobType extends SqlType {
|
||||
const BlobType._();
|
||||
|
||||
@override
|
||||
bool isSubTypeOf(SqlType other) => other is BlobType;
|
||||
}
|
||||
|
||||
class AnyNumericType extends SqlType {
|
||||
const AnyNumericType();
|
||||
|
||||
@override
|
||||
bool isSubTypeOf(SqlType other) {
|
||||
return other is RealType || other is IntegerType;
|
||||
ResolvedType withNullable(bool nullable) {
|
||||
return ResolvedType(type: type, hint: hint, nullable: nullable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides more precise hints than the [SqlType]. For instance, booleans are
|
||||
/// Provides more precise hints than the [BasicType]. For instance, booleans are
|
||||
/// stored as ints in sqlite, but it might be desirable to know whether an
|
||||
/// expression will actually be a boolean.
|
||||
abstract class TypeHint {
|
||||
|
|
|
@ -1,49 +1,134 @@
|
|||
part of '../analysis.dart';
|
||||
|
||||
/// Attempts to resolve types of expressions, result columns and variables.
|
||||
/// As sqlite is pretty lenient at typing and pretty much accepts every value
|
||||
/// everywhere, this
|
||||
const comparisonOperators = [
|
||||
TokenType.equal,
|
||||
TokenType.doubleEqual,
|
||||
TokenType.exclamationEqual,
|
||||
TokenType.lessMore,
|
||||
TokenType.and,
|
||||
TokenType.or,
|
||||
TokenType.less,
|
||||
TokenType.lessEqual,
|
||||
TokenType.more,
|
||||
TokenType.moreEqual
|
||||
];
|
||||
|
||||
class TypeResolver {
|
||||
final Map<Typeable, _ResolvingState> _matchedStates = {};
|
||||
final Map<Typeable, ResolveResult> _results = {};
|
||||
|
||||
_ResolvingState _stateFor(Typeable typeable) {
|
||||
return _matchedStates.putIfAbsent(
|
||||
typeable, () => _ResolvingState(typeable, this));
|
||||
ResolveResult _cache<T extends Typeable>(
|
||||
ResolveResult Function(T param) resolver, T typeable) {
|
||||
if (_results.containsKey(typeable)) {
|
||||
return _results[typeable];
|
||||
}
|
||||
|
||||
final calculated = resolver(typeable);
|
||||
if (calculated.type != null) {
|
||||
_results[typeable] = calculated;
|
||||
}
|
||||
return calculated;
|
||||
}
|
||||
|
||||
void finish() {}
|
||||
ResolveResult resolveColumn(Column column) {
|
||||
return _cache((column) {
|
||||
if (column is TableColumn) {
|
||||
// todo probably needs to be nullable when coming from a join?
|
||||
return ResolveResult(column.type);
|
||||
} else if (column is ExpressionColumn) {
|
||||
return resolveExpression(column.expression);
|
||||
}
|
||||
|
||||
/// Suggest that [t] should have the type [type].
|
||||
void suggestType(Typeable t, SqlType type) {
|
||||
_stateFor(t).suggested.add(type);
|
||||
throw StateError('Unknown column $column');
|
||||
}, column);
|
||||
}
|
||||
|
||||
void suggestSame(Typeable a, Typeable b) {}
|
||||
ResolveResult resolveExpression(Expression expr) {
|
||||
return _cache((expr) {
|
||||
if (expr is Literal) {
|
||||
return resolveLiteral(expr);
|
||||
} else if (expr is UnaryExpression) {
|
||||
return resolveExpression(expr.inner);
|
||||
} else if (expr is Parentheses) {
|
||||
return resolveExpression(expr.expression);
|
||||
} else if (expr is Variable) {
|
||||
return const ResolveResult.needsContext();
|
||||
} else if (expr is Reference) {
|
||||
return resolveColumn(expr.resolved as Column);
|
||||
} else if (expr is FunctionExpression) {
|
||||
return resolveFunctionCall(expr);
|
||||
} else if (expr is IsExpression) {
|
||||
return const ResolveResult(ResolvedType.bool());
|
||||
} else if (expr is BinaryExpression) {
|
||||
final operator = expr.operator.type;
|
||||
if (comparisonOperators.contains(operator)) {
|
||||
return const ResolveResult(ResolvedType.bool());
|
||||
} else {
|
||||
final type = _encapsulate(expr.childNodes.cast(),
|
||||
[BasicType.int, BasicType.real, BasicType.text, BasicType.blob]);
|
||||
return ResolveResult(type);
|
||||
}
|
||||
} else if (expr is SubQuery) {
|
||||
// todo
|
||||
}
|
||||
|
||||
void suggestBool(Typeable t) {
|
||||
final state = _stateFor(t);
|
||||
state.suggested.add(const SqlType.int());
|
||||
state.hints.add(const IsBoolean());
|
||||
throw StateError('Unknown expression $expr');
|
||||
}, expr);
|
||||
}
|
||||
|
||||
/// Marks that [t] will definitely have the type [type].
|
||||
void forceType(Typeable t, SqlType type) {
|
||||
_stateFor(t).forced = type;
|
||||
ResolveResult resolveLiteral(Literal l) {
|
||||
return _cache((l) {
|
||||
if (l is StringLiteral) {
|
||||
return ResolveResult(
|
||||
ResolvedType(type: l.isBinary ? BasicType.blob : BasicType.text));
|
||||
} else if (l is NumericLiteral) {
|
||||
if (l is BooleanLiteral) {
|
||||
return const ResolveResult(ResolvedType.bool());
|
||||
} else {
|
||||
return ResolveResult(
|
||||
ResolvedType(type: l.isInt ? BasicType.int : BasicType.real));
|
||||
}
|
||||
} else if (l is NullLiteral) {
|
||||
return const ResolveResult(
|
||||
ResolvedType(type: BasicType.nullType, nullable: true));
|
||||
}
|
||||
|
||||
throw StateError('Unknown literal $l');
|
||||
}, l);
|
||||
}
|
||||
|
||||
/// Add an additional hint
|
||||
void addTypeHint(Typeable t, TypeHint hint) {
|
||||
_stateFor(t).hints.add(hint);
|
||||
ResolveResult resolveFunctionCall(FunctionExpression call) {
|
||||
// todo
|
||||
return const ResolveResult.unknown();
|
||||
}
|
||||
|
||||
/// Returns the type of an expression in [expressions] that has the highest
|
||||
/// order in [types].
|
||||
ResolvedType _encapsulate(
|
||||
Iterable<Expression> expressions, List<BasicType> types) {
|
||||
final argTypes = expressions
|
||||
.map((expr) => resolveExpression(expr).type)
|
||||
.where((t) => t != null);
|
||||
final type = types.lastWhere((t) => argTypes.any((arg) => arg.type == t));
|
||||
final notNull = argTypes.any((t) => !t.nullable);
|
||||
|
||||
return ResolvedType(type: type, nullable: !notNull);
|
||||
}
|
||||
}
|
||||
|
||||
class _ResolvingState {
|
||||
final Typeable typeable;
|
||||
final TypeResolver resolver;
|
||||
class ResolveResult {
|
||||
final ResolvedType type;
|
||||
final bool needsContext;
|
||||
final bool unknown;
|
||||
|
||||
final List<TypeHint> hints = [];
|
||||
final List<SqlType> suggested = [];
|
||||
SqlType forced;
|
||||
|
||||
_ResolvingState(this.typeable, this.resolver);
|
||||
const ResolveResult(this.type)
|
||||
: needsContext = false,
|
||||
unknown = false;
|
||||
const ResolveResult.needsContext()
|
||||
: type = null,
|
||||
needsContext = true,
|
||||
unknown = false;
|
||||
const ResolveResult.unknown()
|
||||
: type = null,
|
||||
needsContext = false,
|
||||
unknown = true;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ class NullLiteral extends Literal {
|
|||
class NumericLiteral extends Literal {
|
||||
final num number;
|
||||
|
||||
bool get isInt => number.toInt() == number;
|
||||
|
||||
NumericLiteral(this.number, Token token) : super(token);
|
||||
|
||||
@override
|
||||
|
|
|
@ -12,8 +12,9 @@ void main() {
|
|||
});
|
||||
|
||||
test('correctly resolves return columns', () {
|
||||
final id = TableColumn('id');
|
||||
final content = TableColumn('content');
|
||||
final id = const TableColumn('id', ResolvedType(type: BasicType.int));
|
||||
final content =
|
||||
const TableColumn('content', ResolvedType(type: BasicType.text));
|
||||
|
||||
final demoTable = Table(
|
||||
name: 'demo',
|
||||
|
@ -30,6 +31,14 @@ void main() {
|
|||
expect(resolvedColumns.map((c) => c.name),
|
||||
['id', 'content', 'id', 'content', '3 + 4']);
|
||||
|
||||
expect(resolvedColumns.map((c) => context.typeOf(c).type.type), [
|
||||
BasicType.int,
|
||||
BasicType.text,
|
||||
BasicType.int,
|
||||
BasicType.text,
|
||||
BasicType.int,
|
||||
]);
|
||||
|
||||
final firstColumn = select.columns[0] as ExpressionResultColumn;
|
||||
final secondColumn = select.columns[1] as ExpressionResultColumn;
|
||||
final from = select.from[0] as TableReference;
|
||||
|
|
Loading…
Reference in New Issue