Resolve nested result sets in generator

This commit is contained in:
Simon Binder 2020-04-03 20:30:41 +02:00
parent d5ad3c6d34
commit dcb4c4b972
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
9 changed files with 177 additions and 10 deletions

View File

@ -9,12 +9,14 @@ class Linter {
final AnalysisContext context;
final TypeMapper mapper;
final List<AnalysisError> lints = [];
final bool contextRootIsQuery;
Linter(this.context, this.mapper);
Linter(this.context, this.mapper, {this.contextRootIsQuery = false});
Linter.forHandler(QueryHandler handler)
: context = handler.context,
mapper = handler.mapper;
mapper = handler.mapper,
contextRootIsQuery = true;
void reportLints() {
context.root.acceptWithoutArg(_LintingVisitor(this));
@ -57,6 +59,31 @@ class _LintingVisitor extends RecursiveVisitor<void, void> {
));
}
}
if (e is NestedStarResultColumn) {
// check that a table.** column only appears in a top-level select
// statement
if (!linter.contextRootIsQuery || e.parent != linter.context.root) {
linter.lints.add(AnalysisError(
type: AnalysisErrorType.other,
message: 'Nested star columns may only appear in a top-level select '
"query. They're not supported in compound selects or select "
'expressions',
relevantNode: e,
));
}
// check that it actually refers to a table
if (e.resultSet is! Table) {
linter.lints.add(AnalysisError(
type: AnalysisErrorType.other,
message: 'Nested star columns must refer to a table directly. They '
"can't refer to a table-valued function or another select "
'statement.',
relevantNode: e,
));
}
}
}
@override

View File

@ -84,6 +84,7 @@ class QueryHandler {
final columns = <ResultColumn>[];
final rawColumns = _select.resolvedColumns;
// First, go through regular result columns
for (final column in rawColumns) {
final type = context.typeOf(column).type;
final moorType = mapper.resolvedToMoor(type);
@ -99,6 +100,13 @@ class QueryHandler {
candidatesForSingleTable.removeWhere((t) => t != table);
}
final nestedResults = _findNestedResultTables();
if (nestedResults.isNotEmpty) {
// The single table optimization doesn't make sense when nested result
// sets are present.
candidatesForSingleTable.clear();
}
// if all columns read from the same table, and all columns in that table
// are present in the result set, we can use the data class we generate for
// that table instead of generating another class just for this result set.
@ -146,7 +154,27 @@ class QueryHandler {
}
}
return InferredResultSet(null, columns);
return InferredResultSet(null, columns, nestedResults: nestedResults);
}
List<NestedResultTable> _findNestedResultTables() {
final query = _select;
// We don't currently support nested results for compound statements
if (query is! SelectStatement) return const [];
final nestedTables = <NestedResultTable>[];
for (final column in (query as SelectStatement).columns) {
if (column is NestedStarResultColumn) {
final result = column.resultSet;
if (result is! Table) continue;
final moorTable = mapper.tableToMoor(result as Table);
nestedTables.add(NestedResultTable(column.tableName, moorTable));
}
}
return nestedTables;
}
/// The table a given result column is from, or null if this column doesn't

View File

@ -147,11 +147,18 @@ class InferredResultSet {
/// If the result columns of a SELECT statement exactly match one table, we
/// can just use the data class generated for that table. Otherwise, we'd have
/// to create another class.
final MoorTable matchingTable;
final MoorTable /*?*/ matchingTable;
/// Tables in the result set that should appear as a class.
///
/// See [NestedResultTable] for further discussion and examples.
final List<NestedResultTable> nestedResults;
final List<ResultColumn> columns;
final Map<ResultColumn, String> _dartNames = {};
InferredResultSet(this.matchingTable, this.columns);
InferredResultSet(this.matchingTable, this.columns,
{this.nestedResults = const []});
/// Whether a new class needs to be written to store the result of this query.
/// We don't need to do that for queries which return an existing table model
@ -217,6 +224,39 @@ class ResultColumn {
}
}
/// A nested table extracted from a `**` column.
///
/// For instance, consider this query:
/// ```sql
/// CREATE TABLE groups (id INTEGER NOT NULL PRIMARY KEY);
/// CREATE TABLE users (id INTEGER NOT NULL PRIMARY KEY);
/// CREATE TABLE members (
/// group INT REFERENCES ..,
/// user INT REFERENCES ...,
/// is_admin BOOLEAN
/// );
///
/// membersOf: SELECT users.**, members.is_admin FROM members
/// INNER JOIN users ON users.id = members.user;
/// ```
///
/// The generated result set should now look like this:
/// ```dart
/// class MembersOfResult {
/// final User users;
/// final bool isAdmin;
/// }
/// ```
///
/// Knowing that `User` should be extracted into a field is represented with a
/// [NestedResultTable] information as part of the result set.
class NestedResultTable {
final String name;
final MoorTable table;
NestedResultTable(this.name, this.table);
}
/// Something in the query that needs special attention when generating code,
/// such as variables or Dart placeholders.
abstract class FoundElement {

View File

@ -1,6 +1,7 @@
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/moor/create_table_reader.dart';
import 'package:moor_generator/src/analyzer/runner/file_graph.dart';
import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:moor_generator/src/analyzer/runner/steps.dart';
import 'package:moor_generator/src/analyzer/runner/task.dart';
import 'package:moor_generator/src/analyzer/sql_queries/query_handler.dart';
@ -8,6 +9,8 @@ import 'package:moor_generator/src/analyzer/sql_queries/type_mapping.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:test/test.dart';
import '../utils.dart';
const createFoo = '''
CREATE TABLE foo (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@ -67,4 +70,40 @@ Future<void> main() async {
expect(() => parse('SELECT ?1 = ?3'), throwsStateError);
expect(() => parse('SELECT ?1 = ?3 OR ?2'), returnsNormally);
});
test('resolves nested result sets', () async {
final state = TestState.withContent({
'foo|lib/main.moor': r'''
CREATE TABLE points (
id INTEGER NOT NULL PRIMARY KEY,
lat REAL NOT NULL,
long REAL NOT NULL
);
CREATE TABLE routes (
id INTEGER NOT NULL PRIMARY KEY,
"from" INTEGER NOT NULL REFERENCES points (id),
to INTEGER NOT NULL REFERENCES points (id)
);
allRoutes: SELECT routes.*, "from".**, to.**
FROM routes r
INNER JOIN points "from" ON "from".id = routes.from
INNER JOIN points to ON to.id = routes.to;
''',
});
final file = await state.analyze('package:foo/main.moor');
final result = file.currentResult as ParsedMoorFile;
expect(file.errors.errors, isEmpty);
final query = result.resolvedQueries.single;
final resultSet = (query as SqlSelectQuery).resultSet;
expect(resultSet.columns.map((e) => e.name), ['id', 'from', 'to']);
expect(resultSet.matchingTable, isNull);
expect(resultSet.nestedResults.map((e) => e.name), ['from', 'to']);
expect(resultSet.nestedResults.map((e) => e.table.sqlName),
['points', 'points']);
});
}

View File

@ -225,6 +225,17 @@ class ColumnResolver extends RecursiveVisitor<void, void> {
availableColumns.add(column);
}
}
} else if (resultColumn is NestedStarResultColumn) {
final target = scope
.resolve<ResolvesToResultSet>(resultColumn.tableName, orElse: () {
context.reportError(AnalysisError(
type: AnalysisErrorType.referencedUnknownTable,
message: 'Unknown table: ${resultColumn.tableName}',
relevantNode: resultColumn,
));
});
if (target != null) resultColumn.resultSet = target.resultSet;
}
}

View File

@ -5,6 +5,17 @@ part of '../ast.dart';
/// Nested star result columns behave similar to a regular [StarResultColumn]
/// when the query is actually run. However, they will affect generated code
/// when using moor.
class NestedStarResultColumn extends StarResultColumn {
NestedStarResultColumn(String tableName) : super(tableName);
class NestedStarResultColumn extends ResultColumn {
final String tableName;
ResultSet resultSet;
NestedStarResultColumn(this.tableName);
@override
Iterable<AstNode> get childNodes => const [];
@override
bool contentEquals(NestedStarResultColumn other) {
return other.tableName == tableName;
}
}

View File

@ -198,13 +198,13 @@ mixin CrudParser on ParserBase {
/// Parses a [ResultColumn] or throws if none is found.
/// https://www.sqlite.org/syntax/result-column.html
ResultColumn _resultColumn() {
if (_match(const [TokenType.star])) {
if (_matchOne(TokenType.star)) {
return StarResultColumn(null)..setSpan(_previous, _previous);
}
final positionBefore = _current;
if (_match(const [TokenType.identifier])) {
if (_matchOne(TokenType.identifier)) {
// two options. the identifier could be followed by ".*", in which case
// we have a star result column. If it's followed by anything else, it can
// still refer to a column in a table as part of a expression

View File

@ -80,8 +80,9 @@ class Scanner {
case charStar:
if (scanMoorTokens && _match(charStar)) {
_addToken(TokenType.doubleStar);
}
} else {
_addToken(TokenType.star);
}
break;
case charSlash:
if (_match(charStar)) {

View File

@ -41,6 +41,16 @@ void main() {
expect((where.left as Reference).resolved, id);
});
test("resolved columns don't include moor nested results", () {
final engine = SqlEngine(EngineOptions(useMoorExtensions: true))
..registerTable(demoTable);
final context = engine.analyze('SELECT demo.** FROM demo;');
expect(context.errors, isEmpty);
expect((context.root as SelectStatement).resolvedColumns, isEmpty);
});
test('resolves the column for order by clauses', () {
final engine = SqlEngine()..registerTable(demoTable);