Serialize drift analysis results

This commit is contained in:
Simon Binder 2022-09-01 23:10:54 +02:00
parent 5637afe35a
commit 9eb15149d7
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
16 changed files with 552 additions and 11 deletions

View File

@ -11,3 +11,8 @@ targets:
disallow_unrecognized_keys: true
field_rename: snake
explicit_to_json: true
# https://simonbinder.eu/posts/build_directory_moves/#generating-into-a-directory-with-source_gen
source_gen:combining_builder:
options:
build_extensions:
'^lib/src/{{}}.dart': 'lib/src/generated/{{}}.g.dart'

View File

@ -5,7 +5,7 @@ import 'package:path/path.dart' show url;
import '../utils/string_escaper.dart';
import 'backend.dart';
part 'preprocess_drift.g.dart';
part '../generated/analysis/preprocess_drift.g.dart';
@JsonSerializable(constructor: '_')
class DriftPreprocessorResult {

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:recase/recase.dart';
import 'package:sqlparser/sqlparser.dart';
@ -40,6 +41,7 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
for (final column in table.resultColumns) {
String? overriddenDartName;
final type = column.type.sqlTypeToDrift(resolver.driver.options);
final constraints = <DriftColumnConstraint>[];
for (final constraint in column.constraints) {
if (constraint is DriftDartName) {
@ -60,6 +62,23 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
if (referenced != null) {
references.add(referenced);
// Try to resolve this column to track the exact dependency. Don't
// report a warning if this fails, a separate lint step does that.
final columnName =
constraint.clause.columnNames.firstOrNull?.columnName;
if (columnName != null) {
final targetColumn = referenced.columns
.firstWhereOrNull((c) => c.hasEqualSqlName(columnName));
if (targetColumn != null) {
constraints.add(ForeignKeyReference(
targetColumn,
constraint.clause.onUpdate,
constraint.clause.onDelete,
));
}
}
}
}
}
@ -69,12 +88,20 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
nullable: column.type.nullable != false,
nameInSql: column.name,
nameInDart: overriddenDartName ?? ReCase(column.name).camelCase,
constraints: constraints,
declaration: DriftDeclaration(
state.ownId.libraryUri,
column.definition!.nameToken!.span.start.offset,
),
));
}
return DriftTable(
discovered.ownId,
null,
DriftDeclaration(
state.ownId.libraryUri,
discovered.createTable.firstPosition,
),
columns: columns,
references: references.toList(),
);

View File

@ -1,8 +1,13 @@
import 'package:drift/drift.dart' show DriftSqlType;
import 'package:json_annotation/json_annotation.dart';
import 'package:sqlparser/sqlparser.dart' show ReferenceAction;
import '../../analyzer/options.dart';
import 'dart.dart';
import 'element.dart';
part '../../generated/analysis/results/column.g.dart';
class DriftColumn {
final DriftSqlType sqlType;
@ -13,23 +18,86 @@ class DriftColumn {
/// constraint present on the column's definition.
final bool nullable;
/// The (unescaped) name of this column in the database schema.
final String nameInSql;
/// The getter name of this column in the table class. It will also be used
/// as getter name in the TableInfo class (as it needs to override the field)
/// and in the generated data class that will be generated for each table.
final String nameInDart;
/// The documentation comment associated with this column
///
/// Stored as a multi line string with leading triple-slashes `///` for every
/// line.
final String? documentationComment;
/// An (optional) name to use as a json key instead of the [nameInDart].
final String? overriddenJsonName;
/// Column constraints that should be applied to this column.
final List<DriftColumnConstraint> constraints;
/// If this columns has custom constraints that should be used instead of the
/// default ones.
final String? customConstraints;
/// The Dart code generating the default expression for this column (as an
/// `Expression` instance from `package:drift`).
final AnnotatedDartCode? defaultArgument;
/// Dart code for the `clientDefault` expression, or null if it hasn't been
/// set.
final AnnotatedDartCode? clientDefaultCode;
final AppliedTypeConverter? typeConverter;
final DriftDeclaration? declaration;
final DriftDeclaration declaration;
/// The table or view owning this column.
@JsonKey(ignore: true)
late DriftElement owner;
DriftColumn({
required this.sqlType,
required this.nullable,
required this.nameInSql,
required this.nameInDart,
required this.declaration,
this.typeConverter,
this.declaration,
this.clientDefaultCode,
this.defaultArgument,
this.overriddenJsonName,
this.documentationComment,
this.constraints = const [],
this.customConstraints,
});
/// Whether this column was declared inside a `.drift` file.
bool get declaredInDriftFile => declaration.isDriftDeclaration;
/// The actual json key to use when serializing a data class of this table
/// to json.
///
/// This respectts the [overriddenJsonName], if any, as well as [options].
String getJsonKey([DriftOptions options = const DriftOptions.defaults()]) {
if (overriddenJsonName != null) return overriddenJsonName!;
final useColumnName = options.useColumnNameAsJsonKeyWhenDefinedInMoorFile &&
declaredInDriftFile;
return useColumnName ? nameInSql : nameInDart;
}
bool hasEqualSqlName(String otherSqlName) =>
nameInSql.toLowerCase() == otherSqlName.toLowerCase();
@override
String toString() {
return 'Column $nameInSql in $owner';
}
}
@JsonSerializable()
class AppliedTypeConverter {
/// The Dart expression creating an instance of the applied type converter.
final AnnotatedDartCode expression;
@ -71,4 +139,25 @@ class AppliedTypeConverter {
required this.sqlTypeIsNullable,
this.alsoAppliesToJsonConversion = false,
});
factory AppliedTypeConverter.fromJson(Map json) =>
_$AppliedTypeConverterFromJson(json);
Map<String, Object?> toJson() => _$AppliedTypeConverterToJson(this);
}
abstract class DriftColumnConstraint {}
class ForeignKeyReference extends DriftColumnConstraint {
final DriftColumn otherColumn;
final ReferenceAction? onUpdate;
final ReferenceAction? onDelete;
ForeignKeyReference(this.otherColumn, this.onUpdate, this.onDelete);
@override
String toString() {
return 'ForeignKeyReference(to $otherColumn, onUpdate = $onUpdate, '
'onDelete = $onDelete)';
}
}

View File

@ -1,14 +1,42 @@
import 'package:json_annotation/json_annotation.dart';
import 'element.dart';
part '../../generated/analysis/results/dart.g.dart';
class AnnotatedDartCode {
final List<dynamic> elements;
final List<dynamic /* String|DartTopLevelSymbol */ > elements;
AnnotatedDartCode(this.elements);
factory AnnotatedDartCode.fromJson(Map json) {
final serializedElements = json['elements'] as List;
return AnnotatedDartCode([
for (final part in serializedElements)
if (part is Map) DartTopLevelSymbol.fromJson(json) else part as String
]);
}
Map<String, Object?> toJson() {
return {
'elements': [
for (final element in elements)
if (element is DartTopLevelSymbol) element.toJson() else element
],
};
}
}
@JsonSerializable()
class DartTopLevelSymbol {
final String lexeme;
final DriftElementId elementId;
DartTopLevelSymbol(this.lexeme, this.elementId);
factory DartTopLevelSymbol.fromJson(Map json) =>
_$DartTopLevelSymbolFromJson(json);
Map<String, Object?> toJson() => _$DartTopLevelSymbolToJson(this);
}

View File

@ -1,20 +1,28 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' show url;
import 'column.dart';
part '../../generated/analysis/results/element.g.dart';
@sealed
@JsonSerializable()
class DriftElementId {
final Uri libraryUri;
final String name;
DriftElementId(this.libraryUri, this.name);
factory DriftElementId.fromJson(Map json) => _$DriftElementIdFromJson(json);
bool get isDefinedInDart => url.extension(libraryUri.path) == '.dart';
bool get isDefinedInDrift => url.extension(libraryUri.path) == '.drift';
bool sameName(String name) => this.name.toLowerCase() == name.toLowerCase();
Map<String, Object?> toJson() => _$DriftElementIdToJson(this);
@override
int get hashCode => Object.hash(DriftElementId, libraryUri, name);
@ -31,16 +39,25 @@ class DriftElementId {
}
}
@JsonSerializable()
class DriftDeclaration {
final Uri sourceUri;
final int offset;
DriftDeclaration(this.sourceUri, this.offset);
factory DriftDeclaration.fromJson(Map json) =>
_$DriftDeclarationFromJson(json);
Map<String, Object?> toJson() => _$DriftDeclarationToJson(this);
bool get isDartDeclaration => url.extension(sourceUri.path) == '.dart';
bool get isDriftDeclaration => url.extension(sourceUri.path) == '.drift';
}
abstract class DriftElement {
final DriftElementId id;
final DriftDeclaration? declaration;
final DriftDeclaration declaration;
Iterable<DriftElement> get references => const Iterable.empty();

View File

@ -14,5 +14,9 @@ class DriftTable extends DriftElementWithResultSet {
super.declaration, {
required this.columns,
this.references = const [],
});
}) {
for (final column in columns) {
column.owner = this;
}
}
}

View File

@ -0,0 +1,163 @@
import 'dart:convert' as convert;
import 'package:drift/drift.dart' show DriftSqlType;
import 'package:drift_dev/src/analysis/results/dart.dart';
import 'package:sqlparser/sqlparser.dart' show ReferenceAction;
import 'results/column.dart';
import 'results/element.dart';
import 'results/table.dart';
class ElementSerializer {
Map<String, Object?> serialize(DriftElement element) {
if (element is DriftTable) {
return {
'type': 'table',
'id': element.id.toJson(),
'declaration': element.declaration.toJson(),
'references': [
for (final referenced in element.references)
_serializeElementReference(referenced),
],
'columns': [
for (final column in element.columns) _serializeColumn(column),
],
};
}
throw UnimplementedError('Unknown element $element');
}
Map<String, Object?> _serializeColumn(DriftColumn column) {
return {
'sqlType': column.sqlType.name,
'nullable': column.nullable,
'nameInSql': column.nameInSql,
'nameInDart': column.nameInDart,
'declaration': column.declaration.toJson(),
'typeConverter': column.typeConverter?.toJson(),
'clientDefaultCode': column.clientDefaultCode?.toJson(),
'defaultArgument': column.clientDefaultCode?.toJson(),
'overriddenJsonName': column.overriddenJsonName,
'documentationComment': column.documentationComment,
'constraints': [
for (final constraint in column.constraints)
_serializeColumnConstraint(constraint),
],
'customConstraints': column.customConstraints,
};
}
Map<String, Object?> _serializeColumnConstraint(
DriftColumnConstraint constraint) {
if (constraint is ForeignKeyReference) {
return {
'type': 'foreign_key',
'column': _serializeColumnReference(constraint.otherColumn),
'onUpdate': constraint.onUpdate?.name,
'onDelete': constraint.onDelete?.name,
};
} else {
throw UnimplementedError('Unsupported column constrain: $constraint');
}
}
Map<String, Object?> _serializeElementReference(DriftElement element) {
return element.id.toJson();
}
Map<String, Object?> _serializeColumnReference(DriftColumn column) {
return {
'table': _serializeElementReference(column.owner),
'name': column.nameInSql,
};
}
}
abstract class ElementDeserializer {
final Map<Uri, Map<String, Object?>> _loadedJson = {};
final Map<DriftElementId, DriftElement> _deserializedElements = {};
/// Loads the serialized definitions of all elements with a
/// [DriftElementId.libraryUri] matching the [uri].
Future<String> loadStateForUri(Uri uri);
Future<DriftElement> _readElementReference(Map json) async {
final id = DriftElementId.fromJson(json);
final data = _loadedJson[id.libraryUri] ??= convert.json
.decode(await loadStateForUri(id.libraryUri)) as Map<String, Object?>;
return _deserializedElements[id] ??=
await _readDriftElement(data[id.name] as Map);
}
Future<DriftColumn> _readDriftColumnReference(Map json) async {
final table =
(await _readElementReference(json['table'] as Map)) as DriftTable;
final name = json['name'] as String;
return table.columns.singleWhere((c) => c.nameInSql == name);
}
Future<DriftElement> _readDriftElement(Map json) async {
final type = json['type'] as String;
final id = DriftElementId.fromJson(json['id'] as Map);
final declaration = DriftDeclaration.fromJson(json['declaration'] as Map);
switch (type) {
case 'table':
return DriftTable(id, declaration, columns: [
for (final rawColumn in json['columns'] as List)
await _readColumn(rawColumn as Map),
]);
default:
throw UnimplementedError('Unsupported element type: $type');
}
}
Future<DriftColumn> _readColumn(Map json) async {
return DriftColumn(
sqlType: DriftSqlType.values.byName(json['sqlType'] as String),
nullable: json['nullable'] as bool,
nameInSql: json['nameInSql'] as String,
nameInDart: json['nameInDart'] as String,
declaration: DriftDeclaration.fromJson(json['declaration'] as Map),
typeConverter: json['typeConverter'] != null
? AppliedTypeConverter.fromJson(json['typeConverter'] as Map)
: null,
clientDefaultCode: json['clientDefaultCode'] != null
? AnnotatedDartCode.fromJson(json['clientDefaultCode'] as Map)
: null,
defaultArgument: json['defaultArgument'] != null
? AnnotatedDartCode.fromJson(json['defaultArgument'] as Map)
: null,
overriddenJsonName: json['overriddenJsonName'] as String?,
documentationComment: json['documentationComment'] as String?,
constraints: [
for (final rawConstraint in json['constraints'] as List)
await _readConstraint(rawConstraint as Map)
],
customConstraints: json['customConstraints'] as String?,
);
}
Future<DriftColumnConstraint> _readConstraint(Map json) async {
final type = json['type'] as String;
switch (type) {
case 'foreign_key':
ReferenceAction? readAction(String? value) {
return value == null ? null : ReferenceAction.values.byName(value);
}
return ForeignKeyReference(
await _readDriftColumnReference(json['column'] as Map),
readAction(json['onUpdate'] as String?),
readAction(json['onDelete'] as String?),
);
default:
throw UnimplementedError('Unsupported constraint: $type');
}
}
}

View File

@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:meta/meta.dart';
import 'package:sqlparser/sqlparser.dart' show SqliteVersion;
part 'options.g.dart';
part '../generated/analyzer/options.g.dart';
/// Controllable options to define the behavior of the analyzer and the
/// generator.

View File

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'preprocess_drift.dart';
part of '../../analysis/preprocess_drift.dart';
// **************************************************************************
// JsonSerializableGenerator

View File

@ -0,0 +1,68 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of '../../../analysis/results/column.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AppliedTypeConverter _$AppliedTypeConverterFromJson(Map json) => $checkedCreate(
'AppliedTypeConverter',
json,
($checkedConvert) {
$checkKeys(
json,
allowedKeys: const [
'expression',
'dart_type',
'sql_type',
'dart_type_is_nullable',
'sql_type_is_nullable',
'also_applies_to_json_conversion'
],
);
final val = AppliedTypeConverter(
expression: $checkedConvert(
'expression', (v) => AnnotatedDartCode.fromJson(v as Map)),
dartType: $checkedConvert(
'dart_type', (v) => AnnotatedDartCode.fromJson(v as Map)),
sqlType: $checkedConvert(
'sql_type', (v) => $enumDecode(_$DriftSqlTypeEnumMap, v)),
dartTypeIsNullable:
$checkedConvert('dart_type_is_nullable', (v) => v as bool),
sqlTypeIsNullable:
$checkedConvert('sql_type_is_nullable', (v) => v as bool),
alsoAppliesToJsonConversion: $checkedConvert(
'also_applies_to_json_conversion', (v) => v as bool? ?? false),
);
return val;
},
fieldKeyMap: const {
'dartType': 'dart_type',
'sqlType': 'sql_type',
'dartTypeIsNullable': 'dart_type_is_nullable',
'sqlTypeIsNullable': 'sql_type_is_nullable',
'alsoAppliesToJsonConversion': 'also_applies_to_json_conversion'
},
);
Map<String, dynamic> _$AppliedTypeConverterToJson(
AppliedTypeConverter instance) =>
<String, dynamic>{
'expression': instance.expression.toJson(),
'dart_type': instance.dartType.toJson(),
'sql_type': _$DriftSqlTypeEnumMap[instance.sqlType]!,
'dart_type_is_nullable': instance.dartTypeIsNullable,
'sql_type_is_nullable': instance.sqlTypeIsNullable,
'also_applies_to_json_conversion': instance.alsoAppliesToJsonConversion,
};
const _$DriftSqlTypeEnumMap = {
DriftSqlType.bool: 'bool',
DriftSqlType.string: 'string',
DriftSqlType.bigInt: 'bigInt',
DriftSqlType.int: 'int',
DriftSqlType.dateTime: 'dateTime',
DriftSqlType.blob: 'blob',
DriftSqlType.double: 'double',
};

View File

@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of '../../../analysis/results/dart.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DartTopLevelSymbol _$DartTopLevelSymbolFromJson(Map json) => $checkedCreate(
'DartTopLevelSymbol',
json,
($checkedConvert) {
$checkKeys(
json,
allowedKeys: const ['lexeme', 'element_id'],
);
final val = DartTopLevelSymbol(
$checkedConvert('lexeme', (v) => v as String),
$checkedConvert(
'element_id', (v) => DriftElementId.fromJson(v as Map)),
);
return val;
},
fieldKeyMap: const {'elementId': 'element_id'},
);
Map<String, dynamic> _$DartTopLevelSymbolToJson(DartTopLevelSymbol instance) =>
<String, dynamic>{
'lexeme': instance.lexeme,
'element_id': instance.elementId.toJson(),
};

View File

@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of '../../../analysis/results/element.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
DriftElementId _$DriftElementIdFromJson(Map json) => $checkedCreate(
'DriftElementId',
json,
($checkedConvert) {
$checkKeys(
json,
allowedKeys: const ['library_uri', 'name'],
);
final val = DriftElementId(
$checkedConvert('library_uri', (v) => Uri.parse(v as String)),
$checkedConvert('name', (v) => v as String),
);
return val;
},
fieldKeyMap: const {'libraryUri': 'library_uri'},
);
Map<String, dynamic> _$DriftElementIdToJson(DriftElementId instance) =>
<String, dynamic>{
'library_uri': instance.libraryUri.toString(),
'name': instance.name,
};
DriftDeclaration _$DriftDeclarationFromJson(Map json) => $checkedCreate(
'DriftDeclaration',
json,
($checkedConvert) {
$checkKeys(
json,
allowedKeys: const ['source_uri', 'offset'],
);
final val = DriftDeclaration(
$checkedConvert('source_uri', (v) => Uri.parse(v as String)),
$checkedConvert('offset', (v) => v as int),
);
return val;
},
fieldKeyMap: const {'sourceUri': 'source_uri'},
);
Map<String, dynamic> _$DriftDeclarationToJson(DriftDeclaration instance) =>
<String, dynamic>{
'source_uri': instance.sourceUri.toString(),
'offset': instance.offset,
};

View File

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'options.dart';
part of '../../analyzer/options.dart';
// **************************************************************************
// JsonSerializableGenerator

View File

@ -14,7 +14,7 @@ dependencies:
recase: '>=2.0.1 <5.0.0'
meta: ^1.1.0
path: ^1.6.0
json_annotation: ^4.5.0
json_annotation: ^4.6.0
stream_transform: '>=0.1.0 <3.0.0'
# CLI

View File

@ -0,0 +1,56 @@
import 'package:drift/drift.dart' show DriftSqlType;
import 'package:drift_dev/src/analysis/results/column.dart';
import 'package:drift_dev/src/analysis/results/table.dart';
import 'package:test/test.dart';
import '../../test_utils.dart';
void main() {
test('reports foreign keys in drift model', () async {
final backend = TestBackend.inTest({
'a|lib/a.drift': '''
CREATE TABLE a (
foo INTEGER PRIMARY KEY,
bar INTEGER REFERENCES b (bar)
);
CREATE TABLE b (
bar INTEGER NOT NULL
);
''',
});
final state =
await backend.driver.fullyAnalyze(Uri.parse('package:a/a.drift'));
expect(state, hasNoErrors);
final results = state.analysis.values.toList();
final a = results[0].result! as DriftTable;
final aFoo = a.columns[0];
final aBar = a.columns[1];
final b = results[1].result! as DriftTable;
final bBar = b.columns[0];
expect(aFoo.sqlType, DriftSqlType.int);
expect(aFoo.nullable, isFalse);
expect(aFoo.constraints, isEmpty);
expect(aFoo.customConstraints, isNull);
expect(aBar.sqlType, DriftSqlType.int);
expect(aBar.nullable, isTrue);
expect(aBar.constraints, [
isA<ForeignKeyReference>()
.having((e) => e.otherColumn, 'otherColumn', bBar)
.having((e) => e.onUpdate, 'onUpdate', isNull)
.having((e) => e.onDelete, 'onDelete', isNull)
]);
expect(aBar.customConstraints, isNull);
expect(bBar.sqlType, DriftSqlType.int);
expect(bBar.nullable, isFalse);
expect(bBar.constraints, isEmpty);
expect(bBar.customConstraints, isNull);
});
}