Initial support for custom data classes for views

This commit is contained in:
Simon Binder 2021-06-10 21:54:57 +02:00
parent dd196df25b
commit 0775c093e3
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
16 changed files with 279 additions and 152 deletions

View File

@ -1,5 +1,4 @@
//@dart=2.9
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/errors.dart';
@ -17,6 +16,7 @@ import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart';
import '../custom_row_class.dart';
import 'find_dart_class.dart';
class CreateTableReader {
/// The AST of this `CREATE TABLE` statement.
@ -173,7 +173,7 @@ class CreateTableReader {
final overriddenNames = moorTableInfo.overriddenDataClassName;
if (moorTableInfo.useExistingDartClass) {
final clazz = await _findDartClass(overriddenNames);
final clazz = await findDartClass(step, imports, overriddenNames);
if (clazz == null) {
step.reportError(ErrorInMoorFile(
span: stmt.tableNameToken.span,
@ -262,34 +262,11 @@ class CreateTableReader {
}
Future<DartType> _readDartType(String typeIdentifier) async {
final foundClass = await _findDartClass(typeIdentifier);
final foundClass = await findDartClass(step, imports, typeIdentifier);
return foundClass?.instantiate(
typeArguments: const [],
nullabilitySuffix: NullabilitySuffix.none,
);
}
Future<ClassElement> _findDartClass(String identifier) async {
final dartImports = imports
.map((import) => import.importedFile)
.where((importUri) => importUri.endsWith('.dart'));
for (final import in dartImports) {
final resolved = step.task.session.resolve(step.file, import);
LibraryElement library;
try {
library = await step.task.backend.resolveDart(resolved.uri);
} on NotALibraryException {
continue;
}
final foundElement = library.exportNamespace.get(identifier);
if (foundElement is ClassElement) {
return foundElement;
}
}
return null;
}
}

View File

@ -0,0 +1,29 @@
// @dart=2.9
import 'package:analyzer/dart/element/element.dart';
import 'package:moor_generator/src/analyzer/runner/steps.dart';
import 'package:moor_generator/src/backends/backend.dart';
import 'package:sqlparser/sqlparser.dart';
Future<ClassElement> findDartClass(
Step step, List<ImportStatement> imports, String identifier) async {
final dartImports = imports
.map((import) => import.importedFile)
.where((importUri) => importUri.endsWith('.dart'));
for (final import in dartImports) {
final resolved = step.task.session.resolve(step.file, import);
LibraryElement library;
try {
library = await step.task.backend.resolveDart(resolved.uri);
} on NotALibraryException {
continue;
}
final foundElement = library.exportNamespace.get(identifier);
if (foundElement is ClassElement) {
return foundElement;
}
}
return null;
}

View File

@ -34,6 +34,8 @@ class MoorParser {
// the table will be resolved in the analysis step
createdEntities.add(MoorTrigger.fromMoor(parsedStmt, step.file));
} else if (parsedStmt is CreateViewStatement) {
// The view's columns and other data will be analyzed later, in
// ViewAnalyzer.
createdEntities.add(MoorView.fromMoor(parsedStmt, step.file));
} else if (parsedStmt is CreateIndexStatement) {
createdEntities.add(MoorIndex.fromMoor(parsedStmt, step.file));

View File

@ -4,7 +4,7 @@ part of '../steps.dart';
class AnalyzeMoorStep extends AnalyzingStep {
AnalyzeMoorStep(Task task, FoundFile file) : super(task, file);
void analyze() {
Future<void> analyze() async {
if (file.currentResult == null) {
// Error during parsing, ignore.
return;
@ -36,7 +36,9 @@ class AnalyzeMoorStep extends AnalyzingStep {
EntityHandler(this, parseResult, availableTables).handle();
ViewAnalyzer(this, availableTables, availableViews).resolve();
await ViewAnalyzer(
this, availableTables, availableViews, parseResult.imports)
.resolve();
final parser =
SqlAnalyzer(this, availableTables, availableViews, parseResult.queries)

View File

@ -221,7 +221,8 @@ class Task {
step = AnalyzeDartStep(this, file)..analyze();
break;
case FileType.moor:
step = AnalyzeMoorStep(this, file)..analyze();
final analyzeMoor = step = AnalyzeMoorStep(this, file);
await analyzeMoor.analyze();
break;
default:
break;

View File

@ -1,5 +1,7 @@
//@dart=2.9
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/errors.dart';
import 'package:moor_generator/src/analyzer/moor/find_dart_class.dart';
import 'package:moor_generator/src/analyzer/runner/steps.dart';
import 'package:moor_generator/src/analyzer/sql_queries/query_analyzer.dart';
import 'package:moor_generator/src/model/table.dart';
@ -7,25 +9,30 @@ import 'package:moor_generator/src/model/view.dart';
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart';
import '../custom_row_class.dart';
class ViewAnalyzer extends BaseAnalyzer {
final List<MoorView> viewsToAnalyze;
final List<ImportStatement> imports;
ViewAnalyzer(Step step, List<MoorTable> tables, this.viewsToAnalyze)
ViewAnalyzer(
Step step, List<MoorTable> tables, this.viewsToAnalyze, this.imports)
: // We're about to analyze views and add them to the engine, but don't
// add the unfinished views right away
super(tables, const [], step);
/// Resolves all the views in topological order.
void resolve() {
Future<void> resolve() async {
// Going through the topologically sorted list and analyzing each view.
for (final view in viewsToAnalyze) {
final ctx =
engine.analyzeNode(view.declaration.node, view.file.parseResult.sql);
lintContext(ctx, view.name);
final declaration = view.declaration.creatingStatement;
final parserView = view.parserView =
const SchemaFromCreateTable(moorExtensions: true)
.readView(ctx, view.declaration.creatingStatement);
.readView(ctx, declaration);
final columns = [
for (final column in parserView.resolvedColumns)
@ -38,8 +45,31 @@ class ViewAnalyzer extends BaseAnalyzer {
];
view.columns = columns;
engine.registerView(mapper.extractView(view));
final desiredNames = declaration.moorTableName;
if (desiredNames != null) {
final dataClassName = desiredNames.overriddenDataClassName;
if (desiredNames.useExistingDartClass) {
final clazz = await findDartClass(step, imports, dataClassName);
if (clazz == null) {
step.reportError(ErrorInMoorFile(
span: declaration.viewNameToken.span,
message: 'Existing Dart class $dataClassName was not found, are '
'you missing an import?',
));
} else {
final rowClass = view.existingRowClass =
validateExistingClass(columns, clazz, '', step.errors);
final newName = rowClass?.targetClass?.name;
if (newName != null) {
view.dartTypeName = rowClass?.targetClass?.name;
}
}
} else {
view.dartTypeName = dataClassName;
}
}
engine.registerView(mapper.extractView(view));
view.references = findReferences(view.declaration.node).toList();
}
}

View File

@ -1,4 +1,5 @@
//@dart=2.9
import 'package:analyzer/dart/element/element.dart';
import 'package:moor_generator/moor_generator.dart';
/// Some schema entity found.
@ -37,8 +38,25 @@ abstract class MoorEntityWithResultSet extends MoorSchemaEntity {
/// converters.
String get entityInfoName;
/// The existing class designed to hold a row, if there is any.
ExistingRowClass /*?*/ get existingRowClass;
/// The name of the Dart class storing the right column getters for this type.
///
/// This class is equal to, or a superclass of, [entityInfoName].
String get dslName => entityInfoName;
/// Whether this table has an existing row class, meaning that moor doesn't
/// have to generate one on its own.
bool get hasExistingRowClass => existingRowClass != null;
}
/// Information used by the generator to generate code for a custom data class
/// written by users.
class ExistingRowClass {
final ClassElement targetClass;
final ConstructorElement constructor;
final Map<MoorColumn, ParameterElement> mapping;
ExistingRowClass(this.targetClass, this.constructor, this.mapping);
}

View File

@ -12,7 +12,7 @@ import 'declarations/declaration.dart';
/// A parsed table, declared in code by extending `Table` and referencing that
/// table in `@UseMoor` or `@UseDao`.
class MoorTable implements MoorEntityWithResultSet {
class MoorTable extends MoorEntityWithResultSet {
/// The [ClassElement] for the class that declares this table or null if
/// the table was inferred from a `CREATE TABLE` statement.
final ClassElement fromClass;
@ -24,7 +24,7 @@ class MoorTable implements MoorEntityWithResultSet {
/// sql queries. Note that this field is set lazily.
Table parserTable;
/// The existing class designed to hold a row, if there is any.
@override
final ExistingRowClass /*?*/ existingRowClass;
/// If [fromClass] is null, another source to use when determining the name
@ -35,10 +35,6 @@ class MoorTable implements MoorEntityWithResultSet {
/// a Dart class.
bool get isFromSql => _overriddenName != null;
/// Whether this table has an existing row class, meaning that moor doesn't
/// have to generate one on its own.
bool get hasExistingRowClass => existingRowClass != null;
String get _baseName => _overriddenName ?? fromClass.name;
@override
@ -207,16 +203,6 @@ class WrittenMoorTable {
WrittenMoorTable(this.table, this.kind);
}
/// Information used by the generator to generate code for a custom data class
/// written by users.
class ExistingRowClass {
final ClassElement targetClass;
final ConstructorElement constructor;
final Map<MoorColumn, ParameterElement> mapping;
ExistingRowClass(this.targetClass, this.constructor, this.mapping);
}
String dbFieldName(String className) => ReCase(className).camelCase;
String tableInfoNameForTableClass(String className) => '\$${className}Table';

View File

@ -30,16 +30,20 @@ class MoorView extends MoorEntityWithResultSet {
List<MoorColumn> columns;
@override
final String dartTypeName;
String dartTypeName;
@override
final String entityInfoName;
String entityInfoName;
@override
ExistingRowClass /*?*/ existingRowClass;
MoorView({
this.declaration,
this.name,
this.dartTypeName,
this.entityInfoName,
this.existingRowClass,
});
/// Obtains all tables transitively referenced by the declaration of this

View File

@ -78,6 +78,71 @@ abstract class TableOrViewWriter {
);
}
void writeMappingMethod(Scope scope) {
if (!scope.generationOptions.writeDataClasses) {
final nullableString = scope.nullableType('String');
buffer.writeln('''
@override
Never map(Map<String, dynamic> data, {$nullableString tablePrefix}) {
throw UnsupportedError('TableInfo.map in schema verification code');
}
''');
return;
}
final dataClassName = tableOrView.dartTypeName;
buffer.write('@override\n$dataClassName map(Map<String, dynamic> data, '
'{${scope.nullableType('String')} tablePrefix}) {\n');
if (tableOrView.hasExistingRowClass) {
buffer.write('final effectivePrefix = '
"tablePrefix != null ? '\$tablePrefix.' : '';");
final info = tableOrView.existingRowClass;
final positionalToIndex = <MoorColumn, int>{};
final named = <MoorColumn, String>{};
final parameters = info.constructor.parameters;
info.mapping.forEach((column, parameter) {
if (parameter.isNamed) {
named[column] = parameter.name;
} else {
positionalToIndex[column] = parameters.indexOf(parameter);
}
});
// Sort positional columns by the position of their respective parameter
// in the constructor.
final positional = positionalToIndex.keys.toList()
..sort((a, b) => positionalToIndex[a].compareTo(positionalToIndex[b]));
final writer = RowMappingWriter(
positional,
named,
tableOrView,
scope.generationOptions,
dbName: '_db',
);
final classElement = info.targetClass;
final ctor = info.constructor;
buffer..write('return ')..write(classElement.name);
if (ctor.name != null && ctor.name.isNotEmpty) {
buffer..write('.')..write(ctor.name);
}
writer.writeArguments(buffer);
buffer.write(';\n');
} else {
// Use default .fromData constructor in the moor-generated data class
buffer.write('return $dataClassName.fromData(data, _db, '
"prefix: tablePrefix != null ? '\$tablePrefix.' : null);\n");
}
buffer.write('}\n');
}
void writeGetColumnsOverride() {
final columnsWithGetters =
tableOrView.columns.map((c) => c.dartGetterName).join(', ');
@ -174,7 +239,7 @@ class TableWriter extends TableOrViewWriter {
_writeValidityCheckMethod();
_writePrimaryKeyOverride();
_writeMappingMethod();
writeMappingMethod(scope);
// _writeReverseMappingMethod();
_writeAliasGenerator();
@ -194,71 +259,6 @@ class TableWriter extends TableOrViewWriter {
}
}
void _writeMappingMethod() {
if (!scope.generationOptions.writeDataClasses) {
final nullableString = scope.nullableType('String');
buffer.writeln('''
@override
Never map(Map<String, dynamic> data, {$nullableString tablePrefix}) {
throw UnsupportedError('TableInfo.map in schema verification code');
}
''');
return;
}
final dataClassName = table.dartTypeName;
buffer.write('@override\n$dataClassName map(Map<String, dynamic> data, '
'{${scope.nullableType('String')} tablePrefix}) {\n');
if (table.hasExistingRowClass) {
buffer.write('final effectivePrefix = '
"tablePrefix != null ? '\$tablePrefix.' : '';");
final info = table.existingRowClass;
final positionalToIndex = <MoorColumn, int>{};
final named = <MoorColumn, String>{};
final parameters = info.constructor.parameters;
info.mapping.forEach((column, parameter) {
if (parameter.isNamed) {
named[column] = parameter.name;
} else {
positionalToIndex[column] = parameters.indexOf(parameter);
}
});
// Sort positional columns by the position of their respective parameter
// in the constructor.
final positional = positionalToIndex.keys.toList()
..sort((a, b) => positionalToIndex[a].compareTo(positionalToIndex[b]));
final writer = RowMappingWriter(
positional,
named,
table,
scope.generationOptions,
dbName: '_db',
);
final classElement = info.targetClass;
final ctor = info.constructor;
buffer..write('return ')..write(classElement.name);
if (ctor.name != null && ctor.name.isNotEmpty) {
buffer..write('.')..write(ctor.name);
}
writer.writeArguments(buffer);
buffer.write(';\n');
} else {
// Use default .fromData constructor in the moor-generated data class
buffer.write('return $dataClassName.fromData(data, _db, '
"prefix: tablePrefix != null ? '\$tablePrefix.' : null);\n");
}
buffer.write('}\n');
}
void _writeColumnVerificationMeta(MoorColumn column) {
if (!_skipVerification) {
buffer

View File

@ -19,7 +19,8 @@ class ViewWriter extends TableOrViewWriter {
ViewWriter(this.view, this.scope);
void write() {
if (scope.generationOptions.writeDataClasses) {
if (scope.generationOptions.writeDataClasses &&
tableOrView.hasExistingRowClass) {
DataClassWriter(view, scope).write();
}
@ -45,7 +46,7 @@ class ViewWriter extends TableOrViewWriter {
writeGetColumnsOverride();
writeAsDslTable();
_writeMappingMethod();
writeMappingMethod(scope);
for (final column in view.columns) {
writeColumnGetter(column, scope.generationOptions, false);
@ -53,30 +54,4 @@ class ViewWriter extends TableOrViewWriter {
buffer.writeln('}');
}
// After we support custom row classes for views, we can move this into the
// shared writer
void _writeMappingMethod() {
if (!scope.generationOptions.writeDataClasses) {
final nullableString = scope.nullableType('String');
buffer.writeln('''
@override
Never map(Map<String, dynamic> data, {$nullableString tablePrefix}) {
throw UnsupportedError('TableInfo.map in schema verification code');
}
''');
return;
}
final dataClassName = view.dartTypeName;
buffer.write('@override\n$dataClassName map(Map<String, dynamic> data, '
'{${scope.nullableType('String')} tablePrefix}) {\n');
// Use default .fromData constructor in the moor-generated data class
buffer.write('return $dataClassName.fromData(data, '
"prefix: tablePrefix != null ? '\$tablePrefix.' : null);\n");
buffer.write('}\n');
}
}

View File

@ -0,0 +1,68 @@
// @dart=2.9
import 'package:moor_generator/moor_generator.dart';
import 'package:moor_generator/src/analyzer/runner/results.dart';
import 'package:test/scaffolding.dart';
import 'package:test/test.dart';
import '../utils.dart';
void main() {
test('can use existing row classes in moor files', () async {
final state = TestState.withContent({
'a|lib/db.moor': '''
import 'rows.dart';
CREATE TABLE custom_name (
id INTEGER NOT NULL PRIMARY KEY,
foo TEXT
) AS MyCustomClass;
CREATE VIEW custom_name_view AS CustomViewClass (foo, bar)
AS SELECT 1, 2;
CREATE TABLE existing (
id INTEGER NOT NULL PRIMARY KEY,
foo TEXT
) WITH ExistingRowClass;
CREATE VIEW existing_view WITH ExistingForView (foo, bar)
AS SELECT 1, 2;
''',
'a|lib/rows.dart': '''
class ExistingRowClass {
ExistingRowClass(int id, String? foo);
}
class ExistingForView {
ExistingForView(int foo, int bar);
}
''',
});
addTearDown(state.close);
final file = await state.analyze('package:a/db.moor');
expect(file.errors.errors, isEmpty);
final result = file.currentResult as ParsedMoorFile;
final customName = result.declaredEntities
.singleWhere((e) => e.displayName == 'custom_name') as MoorTable;
final existing = result.declaredEntities
.singleWhere((e) => e.displayName == 'existing') as MoorTable;
final customNameView = result.declaredEntities
.singleWhere((e) => e.displayName == 'custom_name_view') as MoorView;
final existingView = result.declaredEntities
.singleWhere((e) => e.displayName == 'existing_view') as MoorView;
expect(customName.dartTypeName, 'MyCustomClass');
expect(customName.existingRowClass, isNull);
expect(customNameView.dartTypeName, 'CustomViewClass');
expect(customNameView.existingRowClass, isNull);
expect(existing.dartTypeName, 'ExistingRowClass');
expect(existing.existingRowClass.targetClass.name, 'ExistingRowClass');
expect(existingView.dartTypeName, 'ExistingForView');
expect(existingView.existingRowClass.targetClass.name, 'ExistingForView');
});
}

View File

@ -15,11 +15,19 @@ class CreateViewStatement extends Statement implements CreatingStatement {
final List<String>? columns;
CreateViewStatement(
{this.ifNotExists = false,
required this.viewName,
this.columns,
required this.query});
/// Moor-specific information about the desired name of a Dart class for this
/// table.
///
/// This will always be `null` when moor extensions are not enabled.
MoorTableName? moorTableName;
CreateViewStatement({
this.ifNotExists = false,
required this.viewName,
this.columns,
required this.query,
this.moorTableName,
});
@override
String get createdName => viewName;
@ -32,8 +40,11 @@ class CreateViewStatement extends Statement implements CreatingStatement {
@override
void transformChildren<A>(Transformer<A> transformer, A arg) {
query = transformer.transformChild(query, this, arg);
moorTableName =
transformer.transformNullableChild(moorTableName, this, arg);
}
@override
Iterable<AstNode> get childNodes => [query];
Iterable<AstNode> get childNodes =>
[query, if (moorTableName != null) moorTableName!];
}

View File

@ -2071,6 +2071,7 @@ class Parser {
final ifNotExists = _ifNotExists();
final name = _consumeIdentifier('Expected a name for this view');
final moorTableName = _moorTableName();
List<String>? columnNames;
if (_matchOne(TokenType.leftParen)) {
@ -2080,13 +2081,17 @@ class Parser {
_consume(TokenType.as, 'Expected AS SELECT');
final query = _fullSelect()!;
final query = _fullSelect();
if (query == null) {
_error('Expected a SELECT statement here');
}
return CreateViewStatement(
ifNotExists: ifNotExists,
viewName: name.identifier,
columns: columnNames,
query: query,
moorTableName: moorTableName,
)
..viewNameToken = name
..setSpan(create, _previous);

View File

@ -18,6 +18,24 @@ void main() {
);
});
test('parses a CREATE VIEW statement with an existing Dart class', () {
testStatement(
'CREATE VIEW my_view AS SELECT 1 WITH ExistingDartClass',
CreateViewStatement(
viewName: 'my_view',
query: SelectStatement(
columns: [
ExpressionResultColumn(
expression: NumericLiteral(1, token(TokenType.numberLiteral)),
),
],
),
moorTableName: MoorTableName('ExistingDartClass', true),
),
moorMode: true,
);
});
test('parses a complex CREATE View statement', () {
testStatement(
'CREATE VIEW IF NOT EXISTS my_complex_view (ids, name, count, type) AS '

View File

@ -1,3 +1,4 @@
@TestOn('vm')
import 'dart:convert';
import 'dart:ffi';