mirror of https://github.com/AMT-Cheif/drift.git
Fix crash, support type converters in views
This commit is contained in:
parent
259e4cfdd3
commit
20e6b0d5fe
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;',
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue