Fix crash, support type converters in views

This commit is contained in:
Simon Binder 2023-01-06 14:27:15 +01:00
parent 259e4cfdd3
commit 20e6b0d5fe
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
11 changed files with 232 additions and 72 deletions

View File

@ -349,6 +349,7 @@ Future<List<Route>> routesByStart(int startPointId) {
You can import and use [type converters]({{ "../Advanced Features/type_converters.md" | pageUrl }})
written in Dart in a drift file. Importing a Dart file works with a regular `import` statement.
To apply a type converter on a column definition, you can use the `MAPPED BY` column constraints:
```sql
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
@ -357,6 +358,18 @@ CREATE TABLE users (
);
```
Queries or views that reference a table-column with a type converter will also inherit that
converter. In addition, both queries and views can specify a type converter to use for a
specific column as well:
```sql
CREATE VIEW my_view AS SELECT 'foo' MAPPED BY `const PreferenceConverter()`
SELECT
id,
json_extract(preferences, '$.settings') MAPPED BY `const PreferenceConverter()`
FROM users;
```
More details on type converts in drift files are available
[here]({{ "../Advanced Features/type_converters.md#using-converters-in-moor" | pageUrl }}).
@ -365,17 +378,6 @@ When using type converters, we recommend the [`apply_converters_on_variables`]({
build option. This will also apply the converter from Dart to SQL, for instance if used on variables: `SELECT * FROM users WHERE preferences = ?`.
With that option, the variable will be inferred to `Preferences` instead of `String`.
The `MAPPED BY` syntax can also used on individual columns in a query:
```sql
SELECT
id,
json_extract(preferences, '$.settings') MAPPED BY `const PreferenceConverter`
FROM users;
```
Type converters applied to columns in a select query will be used to map that column
from SQL to Dart when the query is executed.
### Existing row classes

View File

@ -1,7 +1,7 @@
## 2.5.0-dev
- Support `MAPPED BY` for individual columns in a select statement
- Support `MAPPED BY` for individual columns in queries or in views defined with SQL.
## 2.4.1

View File

@ -1,7 +1,9 @@
import 'package:analyzer/dart/ast/ast.dart' as dart;
import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:collection/collection.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:drift/drift.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/utils/find_referenced_tables.dart';
@ -18,6 +20,35 @@ abstract class DriftElementResolver<T extends DiscoveredElement>
DriftElementResolver(
super.file, super.discovered, super.resolver, super.state);
Future<AppliedTypeConverter?> typeConverterFromMappedBy(
DriftSqlType sqlType, bool nullable, MappedBy mapper) async {
final code = mapper.mapper.dartCode;
dart.Expression expression;
try {
expression = await resolver.driver.backend.resolveExpression(
file.ownUri,
code,
file.discovery!.importDependencies
.map((e) => e.toString())
.where((e) => e.endsWith('.dart')),
);
} on CannotReadExpressionException catch (e) {
reportError(DriftAnalysisError.inDriftFile(mapper, e.msg));
return null;
}
final knownTypes = await resolver.driver.loadKnownTypes();
return readTypeConverter(
knownTypes.helperLibrary,
expression,
sqlType,
nullable,
(msg) => reportError(DriftAnalysisError.inDriftFile(mapper, msg)),
knownTypes,
);
}
void reportLints(AnalysisContext context, Iterable<DriftElement> references) {
context.errors.forEach(reportLint);

View File

@ -1,4 +1,3 @@
import 'package:analyzer/dart/ast/ast.dart' as dart;
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show DriftSqlType;
import 'package:drift_dev/src/analysis/driver/driver.dart';
@ -8,7 +7,6 @@ import 'package:sqlparser/sqlparser.dart' as sql;
import 'package:sqlparser/utils/node_to_text.dart';
import '../../../utils/string_escaper.dart';
import '../../backend.dart';
import '../../driver/error.dart';
import '../../driver/state.dart';
import '../../results/results.dart';
@ -97,7 +95,8 @@ class DriftTableResolver extends DriftElementResolver<DiscoveredDriftTable> {
continue;
}
converter = await _readTypeConverter(type, nullable, constraint);
converter =
await typeConverterFromMappedBy(type, nullable, constraint);
} else if (constraint is sql.JsonKey) {
writeIntoTable = false;
overriddenJsonName = constraint.jsonKey;
@ -343,33 +342,4 @@ class DriftTableResolver extends DriftElementResolver<DiscoveredDriftTable> {
return driftTable;
}
Future<AppliedTypeConverter?> _readTypeConverter(
DriftSqlType sqlType, bool nullable, MappedBy mapper) async {
final code = mapper.mapper.dartCode;
dart.Expression expression;
try {
expression = await resolver.driver.backend.resolveExpression(
file.ownUri,
code,
file.discovery!.importDependencies
.map((e) => e.toString())
.where((e) => e.endsWith('.dart')),
);
} on CannotReadExpressionException catch (e) {
reportError(DriftAnalysisError.inDriftFile(mapper, e.msg));
return null;
}
final knownTypes = await resolver.driver.loadKnownTypes();
return readTypeConverter(
knownTypes.helperLibrary,
expression,
sqlType,
nullable,
(msg) => reportError(DriftAnalysisError.inDriftFile(mapper, msg)),
knownTypes,
);
}
}

View File

@ -1,7 +1,8 @@
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart';
import 'package:sqlparser/utils/node_to_text.dart';
import 'package:sqlparser/sqlparser.dart' as sql;
import '../../../writer/queries/sql_writer.dart';
import '../../driver/state.dart';
import '../../results/results.dart';
import '../intermediate_state.dart';
@ -27,26 +28,48 @@ class DriftViewResolver extends DriftElementResolver<DiscoveredDriftView> {
final columns = <DriftColumn>[];
final columnDartNames = <String>{};
for (final column in parserView.resolvedColumns) {
final type = column.type;
final driftType = resolver.driver.typeMapping.sqlTypeToDrift(type);
final nullable = type?.nullable ?? true;
AppliedTypeConverter? converter;
var ownsConverter = false;
// If this column has a `MAPPED BY` constraint, we can apply the converter
// through that.
final source = column.source;
if (source is ExpressionColumn) {
final mappedBy = source.mappedBy;
if (mappedBy != null) {
converter =
await typeConverterFromMappedBy(driftType, nullable, mappedBy);
ownsConverter = true;
}
}
if (type != null && type.hint is TypeConverterHint) {
converter = (type.hint as TypeConverterHint).converter;
converter ??= (type.hint as TypeConverterHint).converter;
}
final driftColumn = DriftColumn(
sqlType: resolver.driver.typeMapping.sqlTypeToDrift(type),
sqlType: driftType,
nameInSql: column.name,
nameInDart:
dartNameForSqlColumn(column.name, existingNames: columnDartNames),
declaration: DriftDeclaration.driftFile(stmt, file.ownUri),
nullable: type?.nullable == true,
nullable: nullable,
typeConverter: converter,
foreignConverter: true,
);
columns.add(driftColumn);
columnDartNames.add(driftColumn.nameInDart);
if (ownsConverter) {
converter?.owningColumn = driftColumn;
}
}
var entityInfoName = ReCase(stmt.viewName).pascalCase;
@ -74,7 +97,7 @@ class DriftViewResolver extends DriftElementResolver<DiscoveredDriftView> {
query: stmt.query,
// Remove drift-specific syntax
driftTableName: null,
).toSql();
).toSqlWithoutDriftSpecificSyntax(resolver.driver.options);
return DriftView(
discovered.ownId,

View File

@ -149,9 +149,10 @@ class QueryAnalyzer {
final expression = await driver.backend.resolveExpression(
fromFile.ownUri,
mappedBy.mapper.dartCode,
fromFile.discovery!.importDependencies
.map((e) => e.toString())
.where((e) => e.endsWith('.dart')),
fromFile.discovery?.importDependencies
.map((e) => e.toString())
.where((e) => e.endsWith('.dart')) ??
const Iterable.empty(),
);
_resolvedExpressions[mappedBy.mapper] = expression;

View File

@ -24,6 +24,13 @@ String placeholderContextName(FoundDartPlaceholder placeholder) {
return 'generated${placeholder.name}';
}
extension ToSqlText on AstNode {
String toSqlWithoutDriftSpecificSyntax(DriftOptions options) {
final writer = SqlWriter(options);
return writer.writeSql(this);
}
}
class SqlWriter extends NodeSqlBuilder {
final StringBuffer _out;
final SqlQuery? query;

View File

@ -145,4 +145,79 @@ CREATE VIEW IF NOT EXISTS repro AS
'fooBarBaz', // fooBarBaz
]);
});
test('copies type converter from table', () async {
final backend = TestBackend.inTest({
'a|lib/a.drift': '''
import 'converter.dart';
CREATE TABLE users (
id INTEGER NOT NULL PRIMARY KEY,
foo INTEGER NOT NULL MAPPED BY `createConverter()`
);
CREATE VIEW foos AS SELECT foo FROM users;
''',
'a|lib/converter.dart': '''
import 'package:drift/drift.dart';
TypeConverter<Object, int> createConverter() => throw UnimplementedError();
''',
});
final state = await backend.analyze('package:a/a.drift');
backend.expectNoErrors();
final table = state.analysis[state.id('users')]!.result as DriftTable;
final tableColumn = table.columnBySqlName['foo'];
final view = state.analysis[state.id('foos')]!.result as DriftView;
final column = view.columns.single;
expect(
column.typeConverter,
isA<AppliedTypeConverter>()
.having(
(e) => e.expression.toString(), 'expression', 'createConverter()')
.having((e) => e.owningColumn, 'owningColumn', tableColumn),
);
});
test('can declare type converter on view column', () async {
final backend = TestBackend.inTest({
'a|lib/a.drift': '''
import 'converter.dart';
CREATE VIEW v AS SELECT 1 MAPPED BY `createConverter()` AS r;
''',
'a|lib/converter.dart': '''
import 'package:drift/drift.dart';
TypeConverter<Object, int> createConverter() => throw UnimplementedError();
''',
});
final state = await backend.analyze('package:a/a.drift');
backend.expectNoErrors();
final view = state.analyzedElements.single as DriftView;
final column = view.columns.single;
expect(
column.typeConverter,
isA<AppliedTypeConverter>()
.having(
(e) => e.expression.toString(), 'expression', 'createConverter()')
.having((e) => e.owningColumn, 'owningColumn', column),
);
expect(
view.source,
isA<SqlViewSource>().having(
(e) => e.sqlCreateViewStmt,
'sqlCreateViewStmt',
'CREATE VIEW v AS SELECT 1 AS r;',
),
);
});
}

View File

@ -4,7 +4,7 @@ import 'dart:isolate';
import 'package:analyzer/dart/analysis/analysis_context.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/ast.dart' as dart;
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/file_system/overlay_file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
@ -34,6 +34,7 @@ class TestBackend extends DriftBackend {
late final DriftAnalysisDriver driver;
AnalysisContext? _dartContext;
OverlayResourceProvider? _resourceProvider;
TestBackend(
Map<String, String> sourceContents, {
@ -72,8 +73,21 @@ class TestBackend extends DriftBackend {
}
}
String _pathFor(Uri uri) {
if (uri.scheme == 'package') {
final package = uri.pathSegments.first;
final path =
p.url.joinAll(['/$package/lib', ...uri.pathSegments.skip(1)]);
return path;
}
return uri.path;
}
Future<void> _setupDartAnalyzer() async {
final provider = OverlayResourceProvider(PhysicalResourceProvider.INSTANCE);
final provider = _resourceProvider =
OverlayResourceProvider(PhysicalResourceProvider.INSTANCE);
// Analyze example sources against the drift sources from the current
// drift_dev test runner.
@ -93,15 +107,8 @@ class TestBackend extends DriftBackend {
// Also put sources into the overlay:
sourceContents.forEach((key, value) {
final uri = Uri.parse(key);
if (uri.scheme == 'package') {
final package = uri.pathSegments.first;
final path =
p.url.joinAll(['/$package/lib', ...uri.pathSegments.skip(1)]);
provider.setOverlay(path, content: value, modificationStamp: 1);
}
final path = _pathFor(Uri.parse(key));
provider.setOverlay(path, content: value, modificationStamp: 1);
});
if (analyzerExperiments.isNotEmpty) {
@ -142,9 +149,38 @@ class TestBackend extends DriftBackend {
}
@override
Future<Never> resolveExpression(
Future<dart.Expression> resolveExpression(
Uri context, String dartExpression, Iterable<String> imports) async {
throw UnsupportedError('Not currently supported in tests');
final fileContents = StringBuffer();
for (final import in imports) {
fileContents.writeln("import '$import';");
}
fileContents.writeln('var field = $dartExpression;');
final path = '${_pathFor(context)}.exp.dart';
await _setupDartAnalyzer();
final resourceProvider = _resourceProvider!;
final analysisContext = _dartContext!;
resourceProvider.setOverlay(path,
content: fileContents.toString(), modificationStamp: 1);
try {
final result =
await analysisContext.currentSession.getResolvedLibrary(path);
if (result is ResolvedLibraryResult) {
final unit = result.units.single.unit;
final field =
unit.declarations.single as dart.TopLevelVariableDeclaration;
return field.variables.variables.single.initializer!;
} else {
throw CannotReadExpressionException('Could not resolve temp file');
}
} finally {
resourceProvider.removeOverlay(path);
}
}
@override
@ -157,7 +193,7 @@ class TestBackend extends DriftBackend {
}
@override
Future<AstNode?> loadElementDeclaration(Element element) async {
Future<dart.AstNode?> loadElementDeclaration(Element element) async {
final library = element.library;
if (library == null) return null;

View File

@ -198,7 +198,15 @@ class ExpressionColumn extends Column {
/// The expression returned by this column.
final Expression expression;
ExpressionColumn({required this.name, required this.expression});
/// When drift extensions are enabled and this column was defined with a
/// `MAPPED BY` clause, a reference to that clause.
final MappedBy? mappedBy;
ExpressionColumn({
required this.name,
required this.expression,
this.mappedBy,
});
}
/// A column that is created by a reference expression. The difference to an
@ -219,7 +227,8 @@ class ReferenceExpressionColumn extends ExpressionColumn {
final String? overriddenName;
ReferenceExpressionColumn(Reference ref, {this.overriddenName})
ReferenceExpressionColumn(Reference ref,
{this.overriddenName, super.mappedBy})
: super(name: '_', expression: ref);
}

View File

@ -293,12 +293,18 @@ class ColumnResolver extends RecursiveVisitor<void, void> {
Column column;
if (expression is Reference) {
column = ReferenceExpressionColumn(expression,
overriddenName: resultColumn.as);
column = ReferenceExpressionColumn(
expression,
overriddenName: resultColumn.as,
mappedBy: resultColumn.mappedBy,
);
} else {
final name = _nameOfResultColumn(resultColumn)!;
column =
ExpressionColumn(name: name, expression: resultColumn.expression);
column = ExpressionColumn(
name: name,
expression: resultColumn.expression,
mappedBy: resultColumn.mappedBy,
);
}
usedColumns.add(column);