mirror of https://github.com/AMT-Cheif/drift.git
Smarter many-to-one propagation in types2
This commit is contained in:
parent
ddda6797e1
commit
8ae68707f8
|
@ -7,7 +7,7 @@ targets:
|
||||||
use_column_name_as_json_key_when_defined_in_moor_file: true
|
use_column_name_as_json_key_when_defined_in_moor_file: true
|
||||||
generate_connect_constructor: true
|
generate_connect_constructor: true
|
||||||
write_from_json_string_constructor: true
|
write_from_json_string_constructor: true
|
||||||
use_experimental_inference: true
|
# use_experimental_inference: true
|
||||||
sqlite_modules:
|
sqlite_modules:
|
||||||
- json1
|
- json1
|
||||||
- fts5
|
- fts5
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:moor_generator/moor_generator.dart';
|
||||||
|
import 'package:moor_generator/src/analyzer/options.dart';
|
||||||
|
import 'package:moor_generator/src/analyzer/runner/results.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import 'utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
test('experimental inference - integration test', () async {
|
||||||
|
final state = TestState.withContent({
|
||||||
|
'foo|lib/a.moor': '''
|
||||||
|
CREATE TABLE artists (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name VARCHAR NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE albums (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
artist INTEGER NOT NULL REFERENCES artists (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE tracks (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
album INTEGER NOT NULL REFERENCES albums (id),
|
||||||
|
duration_seconds INTEGER NOT NULL,
|
||||||
|
was_single BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
totalDurationByArtist:
|
||||||
|
SELECT a.*, SUM(tracks.duration_seconds) AS duration
|
||||||
|
FROM artists a
|
||||||
|
INNER JOIN albums ON albums.artist = a.id
|
||||||
|
INNER JOIN tracks ON tracks.album = albums.id
|
||||||
|
GROUP BY artists.id;
|
||||||
|
'''
|
||||||
|
}, options: const MoorOptions(useExperimentalInference: true));
|
||||||
|
|
||||||
|
final file = await state.analyze('package:foo/a.moor');
|
||||||
|
final result = file.currentResult as ParsedMoorFile;
|
||||||
|
final queries = result.resolvedQueries;
|
||||||
|
|
||||||
|
expect(state.session.errorsInFileAndImports(file), isEmpty);
|
||||||
|
|
||||||
|
final totalDurationByArtist =
|
||||||
|
queries.singleWhere((q) => q.name == 'totalDurationByArtist');
|
||||||
|
expect(
|
||||||
|
totalDurationByArtist,
|
||||||
|
returnsColumns({
|
||||||
|
'id': ColumnType.integer,
|
||||||
|
'name': ColumnType.text,
|
||||||
|
'duration': ColumnType.integer,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
import 'package:build/build.dart';
|
import 'package:build/build.dart';
|
||||||
|
import 'package:moor_generator/moor_generator.dart';
|
||||||
|
import 'package:moor_generator/src/analyzer/options.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
|
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
|
||||||
import 'package:moor_generator/src/analyzer/runner/task.dart';
|
import 'package:moor_generator/src/analyzer/runner/task.dart';
|
||||||
import 'package:moor_generator/src/analyzer/session.dart';
|
import 'package:moor_generator/src/analyzer/session.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../utils/test_backend.dart';
|
import '../utils/test_backend.dart';
|
||||||
|
|
||||||
|
@ -11,12 +14,16 @@ class TestState {
|
||||||
|
|
||||||
TestState(this.backend, this.session);
|
TestState(this.backend, this.session);
|
||||||
|
|
||||||
factory TestState.withContent(Map<String, String> content) {
|
factory TestState.withContent(Map<String, String> content,
|
||||||
|
{MoorOptions options}) {
|
||||||
final backend = TestBackend({
|
final backend = TestBackend({
|
||||||
for (final entry in content.entries)
|
for (final entry in content.entries)
|
||||||
AssetId.parse(entry.key): entry.value,
|
AssetId.parse(entry.key): entry.value,
|
||||||
});
|
});
|
||||||
final session = MoorSession(backend);
|
final session = MoorSession(backend);
|
||||||
|
if (options != null) {
|
||||||
|
session.options = options;
|
||||||
|
}
|
||||||
return TestState(backend, session);
|
return TestState(backend, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,3 +43,24 @@ class TestState {
|
||||||
return file(uri);
|
return file(uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Matchers
|
||||||
|
Matcher returnsColumns(Map<String, ColumnType> columns) {
|
||||||
|
return _HasInferredColumnTypes(columns);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HasInferredColumnTypes extends CustomMatcher {
|
||||||
|
_HasInferredColumnTypes(dynamic expected)
|
||||||
|
: super('Select query with inferred columns', 'columns', expected);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Object featureValueOf(dynamic actual) {
|
||||||
|
if (actual is! SqlSelectQuery) {
|
||||||
|
return actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
final query = actual as SqlSelectQuery;
|
||||||
|
final resultSet = query.resultSet;
|
||||||
|
return {for (final column in resultSet.columns) column.name: column.type};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
part of '../types.dart';
|
part of '../types.dart';
|
||||||
|
|
||||||
/// Dependency declaring that [target] is nullable if any in [other] is
|
/// Dependency declaring that [target] is nullable if any in [from] is
|
||||||
/// nullable.
|
/// nullable.
|
||||||
class NullableIfSomeOtherIs extends TypeRelationship {
|
class NullableIfSomeOtherIs extends TypeRelationship
|
||||||
|
implements MultiSourceRelation {
|
||||||
|
@override
|
||||||
final Typeable target;
|
final Typeable target;
|
||||||
final List<Typeable> other;
|
@override
|
||||||
|
final List<Typeable> from;
|
||||||
|
|
||||||
NullableIfSomeOtherIs(this.target, this.other);
|
NullableIfSomeOtherIs(this.target, this.from);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dependency declaring that [target] has exactly the same type as [other].
|
/// Dependency declaring that [target] has exactly the same type as [other].
|
||||||
class CopyTypeFrom extends TypeRelationship {
|
class CopyTypeFrom extends TypeRelationship implements DirectedRelation {
|
||||||
|
@override
|
||||||
final Typeable target;
|
final Typeable target;
|
||||||
final Typeable other;
|
final Typeable other;
|
||||||
|
|
||||||
|
@ -18,8 +22,11 @@ class CopyTypeFrom extends TypeRelationship {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dependency declaring that [target] has a type that matches all of [from].
|
/// Dependency declaring that [target] has a type that matches all of [from].
|
||||||
class CopyEncapsulating extends TypeRelationship {
|
class CopyEncapsulating extends TypeRelationship
|
||||||
|
implements MultiSourceRelation {
|
||||||
|
@override
|
||||||
final Typeable target;
|
final Typeable target;
|
||||||
|
@override
|
||||||
final List<Typeable> from;
|
final List<Typeable> from;
|
||||||
|
|
||||||
CopyEncapsulating(this.target, this.from);
|
CopyEncapsulating(this.target, this.from);
|
||||||
|
@ -42,7 +49,8 @@ class HaveSameType extends TypeRelationship {
|
||||||
|
|
||||||
/// Dependency declaring that, if no better option is found, [target] should
|
/// Dependency declaring that, if no better option is found, [target] should
|
||||||
/// have the specified [defaultType].
|
/// have the specified [defaultType].
|
||||||
class DefaultType extends TypeRelationship {
|
class DefaultType extends TypeRelationship implements DirectedRelation {
|
||||||
|
@override
|
||||||
final Typeable target;
|
final Typeable target;
|
||||||
final ResolvedType defaultType;
|
final ResolvedType defaultType;
|
||||||
|
|
||||||
|
@ -53,7 +61,8 @@ enum CastMode { numeric, boolean }
|
||||||
|
|
||||||
/// Dependency declaring that [target] has the same type as [other] after
|
/// Dependency declaring that [target] has the same type as [other] after
|
||||||
/// casting it with [cast].
|
/// casting it with [cast].
|
||||||
class CopyAndCast extends TypeRelationship {
|
class CopyAndCast extends TypeRelationship implements DirectedRelation {
|
||||||
|
@override
|
||||||
final Typeable target;
|
final Typeable target;
|
||||||
final Typeable other;
|
final Typeable other;
|
||||||
final CastMode cast;
|
final CastMode cast;
|
||||||
|
|
|
@ -10,7 +10,6 @@ class TypeGraph {
|
||||||
|
|
||||||
final Map<Typeable, List<TypeRelationship>> _edges = {};
|
final Map<Typeable, List<TypeRelationship>> _edges = {};
|
||||||
final List<DefaultType> _defaultTypes = [];
|
final List<DefaultType> _defaultTypes = [];
|
||||||
final List<CopyEncapsulating> _manyToOne = [];
|
|
||||||
|
|
||||||
TypeGraph();
|
TypeGraph();
|
||||||
|
|
||||||
|
@ -48,17 +47,6 @@ class TypeGraph {
|
||||||
_propagateTypeInfo(queue, queue.removeLast());
|
_propagateTypeInfo(queue, queue.removeLast());
|
||||||
}
|
}
|
||||||
|
|
||||||
// propagate many-to-one changes
|
|
||||||
for (final edge in _manyToOne) {
|
|
||||||
if (!knowsType(edge.target)) {
|
|
||||||
final fromTypes = edge.from.map((t) => this[t]).where((e) => e != null);
|
|
||||||
final encapsulated = _encapsulate(fromTypes);
|
|
||||||
if (encapsulated != null) {
|
|
||||||
this[edge.target] = encapsulated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply default types
|
// apply default types
|
||||||
for (final applyDefault in _defaultTypes) {
|
for (final applyDefault in _defaultTypes) {
|
||||||
if (!knowsType(applyDefault.target)) {
|
if (!knowsType(applyDefault.target)) {
|
||||||
|
@ -70,21 +58,43 @@ class TypeGraph {
|
||||||
void _propagateTypeInfo(List<Typeable> resolved, Typeable t) {
|
void _propagateTypeInfo(List<Typeable> resolved, Typeable t) {
|
||||||
if (!_edges.containsKey(t)) return;
|
if (!_edges.containsKey(t)) return;
|
||||||
|
|
||||||
// propagate one-to-one and one-to-many changes
|
// propagate changes
|
||||||
for (final edge in _edges[t]) {
|
for (final edge in _edges[t]) {
|
||||||
if (edge is CopyTypeFrom) {
|
if (edge is CopyTypeFrom) {
|
||||||
_copyType(resolved, edge.other, edge.target);
|
_copyType(resolved, edge.other, edge.target);
|
||||||
} else if (edge is HaveSameType) {
|
} else if (edge is HaveSameType) {
|
||||||
_copyType(resolved, t, edge.getOther(t));
|
_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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _copyType(List<Typeable> resolved, Typeable from, Typeable to) {
|
void _propagateManyToOne(
|
||||||
|
MultiSourceRelation edge, List<Typeable> resolved, Typeable t) {
|
||||||
|
if (!knowsType(edge.target)) {
|
||||||
|
final fromTypes = edge.from.map((t) => this[t]).where((e) => e != null);
|
||||||
|
final encapsulated = _encapsulate(fromTypes);
|
||||||
|
if (encapsulated != null) {
|
||||||
|
this[edge.target] = encapsulated;
|
||||||
|
resolved.add(edge.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _copyType(List<Typeable> resolved, Typeable from, Typeable to,
|
||||||
|
[ResolvedType type]) {
|
||||||
// if the target hasn't been resolved yet, copy the current type and
|
// if the target hasn't been resolved yet, copy the current type and
|
||||||
// visit the target later
|
// visit the target later
|
||||||
if (!knowsType(to)) {
|
if (!knowsType(to)) {
|
||||||
this[to] = this[from];
|
this[to] = type ?? this[from];
|
||||||
resolved.add(to);
|
resolved.add(to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,11 +120,11 @@ class TypeGraph {
|
||||||
|
|
||||||
for (final relation in _relationships) {
|
for (final relation in _relationships) {
|
||||||
if (relation is NullableIfSomeOtherIs) {
|
if (relation is NullableIfSomeOtherIs) {
|
||||||
putAll(relation.other, relation);
|
putAll(relation.from, relation);
|
||||||
} else if (relation is CopyTypeFrom) {
|
} else if (relation is CopyTypeFrom) {
|
||||||
put(relation.other, relation);
|
put(relation.other, relation);
|
||||||
} else if (relation is CopyEncapsulating) {
|
} else if (relation is CopyEncapsulating) {
|
||||||
_manyToOne.add(relation);
|
putAll(relation.from, relation);
|
||||||
} else if (relation is HaveSameType) {
|
} else if (relation is HaveSameType) {
|
||||||
put(relation.first, relation);
|
put(relation.first, relation);
|
||||||
put(relation.second, relation);
|
put(relation.second, relation);
|
||||||
|
@ -131,6 +141,14 @@ class TypeGraph {
|
||||||
|
|
||||||
abstract class TypeRelationship {}
|
abstract class TypeRelationship {}
|
||||||
|
|
||||||
|
abstract class DirectedRelation {
|
||||||
|
Typeable get target;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class MultiSourceRelation implements DirectedRelation {
|
||||||
|
List<Typeable> get from;
|
||||||
|
}
|
||||||
|
|
||||||
/// Keeps track of resolved variable types so that they can be re-used.
|
/// Keeps track of resolved variable types so that they can be re-used.
|
||||||
/// Different [Variable] instances can refer to the same logical sql variable,
|
/// Different [Variable] instances can refer to the same logical sql variable,
|
||||||
/// so we keep track of them.
|
/// so we keep track of them.
|
||||||
|
@ -158,4 +176,17 @@ extension on ResolvedType {
|
||||||
// fallback. todo: Support more cases
|
// fallback. todo: Support more cases
|
||||||
return const ResolvedType(type: BasicType.text, nullable: true);
|
return const ResolvedType(type: BasicType.text, nullable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ResolvedType cast(CastMode mode) {
|
||||||
|
switch (mode) {
|
||||||
|
case CastMode.numeric:
|
||||||
|
if (type == BasicType.int || type == BasicType.real) return this;
|
||||||
|
|
||||||
|
return const ResolvedType(type: BasicType.real);
|
||||||
|
case CastMode.boolean:
|
||||||
|
return const ResolvedType.bool();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw AssertionError('all switch statements handled');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,11 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
|
||||||
|
|
||||||
void run(AstNode root) {
|
void run(AstNode root) {
|
||||||
visit(root, const NoTypeExpectation());
|
visit(root, const NoTypeExpectation());
|
||||||
|
|
||||||
|
// annotate Columns as well. They implement Typeable, but aren't an ast
|
||||||
|
// node, which means this visitor doesn't find them
|
||||||
|
root.acceptWithoutArg(_ResultColumnVisitor(this));
|
||||||
|
|
||||||
session.finish();
|
session.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,8 +143,24 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void visitVariable(Variable e, TypeExpectation arg) {
|
void visitVariable(Variable e, TypeExpectation arg) {
|
||||||
final resolved = session.context.stmtOptions.specifiedTypeOf(e) ??
|
_inferAsVariable(e, arg);
|
||||||
_inferFromContext(arg);
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void visitDartPlaceholder(DartPlaceholder e, TypeExpectation arg) {
|
||||||
|
if (e is DartExpressionPlaceholder) {
|
||||||
|
_inferAsVariable(e, arg);
|
||||||
|
} else {
|
||||||
|
super.visitDartPlaceholder(e, arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _inferAsVariable(Expression e, TypeExpectation arg) {
|
||||||
|
ResolvedType resolved;
|
||||||
|
if (e is Variable) {
|
||||||
|
resolved = session.context.stmtOptions.specifiedTypeOf(e);
|
||||||
|
}
|
||||||
|
resolved ??= _inferFromContext(arg);
|
||||||
|
|
||||||
if (resolved != null) {
|
if (resolved != null) {
|
||||||
session.checkAndResolve(e, resolved, arg);
|
session.checkAndResolve(e, resolved, arg);
|
||||||
|
@ -511,3 +532,16 @@ class TypeResolver extends RecursiveVisitor<TypeExpectation, void> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ResultColumnVisitor extends RecursiveVisitor<void, void> {
|
||||||
|
final TypeResolver resolver;
|
||||||
|
|
||||||
|
_ResultColumnVisitor(this.resolver);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void visitBaseSelectStatement(BaseSelectStatement stmt, void arg) {
|
||||||
|
if (stmt.resolvedColumns != null) {
|
||||||
|
stmt.resolvedColumns.forEach(resolver._handleColumn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -203,8 +203,7 @@ class SqlEngine {
|
||||||
if (options.enableExperimentalTypeInference) {
|
if (options.enableExperimentalTypeInference) {
|
||||||
final session = t2.TypeInferenceSession(context, options);
|
final session = t2.TypeInferenceSession(context, options);
|
||||||
final resolver = t2.TypeResolver(session);
|
final resolver = t2.TypeResolver(session);
|
||||||
node.acceptWithoutArg(resolver);
|
resolver.run(node);
|
||||||
session.finish();
|
|
||||||
context.types2 = session.results;
|
context.types2 = session.results;
|
||||||
} else {
|
} else {
|
||||||
node.acceptWithoutArg(TypeResolvingVisitor(context));
|
node.acceptWithoutArg(TypeResolvingVisitor(context));
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:test/test.dart';
|
||||||
import '../data.dart';
|
import '../data.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final engine = SqlEngine()
|
final engine = SqlEngine.withOptions(EngineOptions(useMoorExtensions: true))
|
||||||
..registerTable(demoTable)
|
..registerTable(demoTable)
|
||||||
..registerTable(anotherTable);
|
..registerTable(anotherTable);
|
||||||
|
|
||||||
|
@ -27,8 +27,7 @@ void main() {
|
||||||
final resolver = _obtainResolver(sql);
|
final resolver = _obtainResolver(sql);
|
||||||
final session = resolver.session;
|
final session = resolver.session;
|
||||||
final stmt = session.context.root as SelectStatement;
|
final stmt = session.context.root as SelectStatement;
|
||||||
return session
|
return session.typeOf(stmt.resolvedColumns.single);
|
||||||
.typeOf((stmt.columns.single as ExpressionResultColumn).expression);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test('resolves literals', () {
|
test('resolves literals', () {
|
||||||
|
@ -165,6 +164,21 @@ void main() {
|
||||||
expect(type, const ResolvedType(type: BasicType.text));
|
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('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());
|
||||||
|
});
|
||||||
|
|
||||||
test('handles recursive CTEs', () {
|
test('handles recursive CTEs', () {
|
||||||
final type = _resolveResultColumn('''
|
final type = _resolveResultColumn('''
|
||||||
WITH RECURSIVE
|
WITH RECURSIVE
|
||||||
|
|
Loading…
Reference in New Issue