mirror of https://github.com/AMT-Cheif/drift.git
Resolve nested result sets in generator
This commit is contained in:
parent
d5ad3c6d34
commit
dcb4c4b972
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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']);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue