drift/drift_dev/lib/src/analysis/serializer.dart

834 lines
28 KiB
Dart

import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show DriftSqlType, UpdateKind;
import 'package:sqlparser/sqlparser.dart' show ReferenceAction;
import 'driver/driver.dart';
import 'driver/state.dart';
import 'results/results.dart';
class SerializedElements {
final List<AnnotatedDartCode> dartTypes;
final Map<String, Object?> serializedData;
final Map<String, Object?> _serializedElements;
SerializedElements(
this.dartTypes, this.serializedData, this._serializedElements) {
serializedData['elements'] = _serializedElements;
}
}
/// Serializes [DriftElement]s to JSON.
///
/// By first analyzing elements and later generating code, drift's build setup
/// is more efficient and incremental (as not everything is analyzed again if
/// a single file changes). However, it means that we have to serialize analysis
/// results to read them back in in a later build step.
class ElementSerializer {
final SerializedElements _result = SerializedElements([], {}, {});
ElementSerializer._();
void _serializeElements(Iterable<DriftElement> elements) {
for (final element in elements) {
_result._serializedElements[element.id.name] = _serialize(element);
}
}
Map<String, Object?> _serialize(DriftElement element) {
Map<String, Object?> additionalInformation;
if (element is DriftTable) {
additionalInformation = {
'type': 'table',
'columns': [
for (final column in element.columns) _serializeColumn(column),
],
'existing_data_class': element.existingRowClass != null
? _serializeExistingRowClass(element.existingRowClass!)
: null,
'table_constraints': [
for (final constraint in element.tableConstraints)
_serializeTableConstraint(constraint),
],
'custom_parent_class': element.customParentClass?.toJson(),
'fixed_entity_info_name': element.fixedEntityInfoName,
'base_dart_name': element.baseDartName,
'row_class_name': element.nameOfRowClass,
'without_rowid': element.withoutRowId,
'strict': element.strict,
if (element.isVirtual)
'virtual': _serializeVirtualTableData(element.virtualTableData!),
'write_default_constraints': element.writeDefaultConstraints,
'custom_constraints': element.overrideTableConstraints,
};
} else if (element is DriftIndex) {
additionalInformation = {
'type': 'index',
'sql': element.createStmt,
};
} else if (element is DefinedSqlQuery) {
additionalInformation = {
'type': 'query',
'sql': element.sql,
'offset': element.sqlOffset,
'result_class': element.resultClassName,
'existing_type': _serializeType(element.existingDartType),
'mode': element.mode.name,
'dart_tokens': element.dartTokens,
'dart_types': {
for (final entry in element.dartTypes.entries)
entry.key: _serializeType(entry.value)
},
};
} else if (element is DriftTrigger) {
additionalInformation = {
'type': 'trigger',
'sql': element.createStmt,
if (element.on != null) 'on': _serializeElementReference(element.on!),
'onWrite': element.onWrite.name,
'writes': [
for (final write in element.writes)
{
'table': _serializeElementReference(write.table),
'kind': write.kind.name,
}
],
};
} else if (element is DriftView) {
Object? serializedSource;
final source = element.source;
if (source is SqlViewSource) {
serializedSource = {
'kind': 'sql',
'sql': source.sqlCreateViewStmt,
'schema_sql': source.sqlCreateViewStmt,
};
} else if (source is DartViewSource) {
serializedSource = {
'kind': 'dart',
'query': source.dartQuerySource.toJson(),
'primaryFrom': source.primaryFrom != null
? _serializeTableReferenceInDartView(source.primaryFrom!)
: null,
'staticReferences': [
for (final reference in source.staticReferences)
_serializeTableReferenceInDartView(reference),
]
};
}
additionalInformation = {
'type': 'view',
'columns': [
for (final column in element.columns) _serializeColumn(column),
],
'entity_info_name': element.entityInfoName,
'existing_data_class': element.existingRowClass != null
? _serializeExistingRowClass(element.existingRowClass!)
: null,
'custom_parent_class': element.customParentClass?.toJson(),
'name_of_row_class': element.nameOfRowClass,
'source': serializedSource,
};
} else if (element is BaseDriftAccessor) {
String type;
if (element is DriftDatabase) {
type = 'database';
} else {
type = 'dao';
}
additionalInformation = {
'type': type,
'tables': [
for (final table in element.declaredTables)
_serializeElementReference(table),
],
'views': [
for (final view in element.declaredViews)
_serializeElementReference(view),
],
'includes': [
for (final include in element.declaredIncludes) include.toString()
],
'queries': element.declaredQueries,
if (element is DatabaseAccessor) ...{
'dart_type': element.ownType.toJson(),
'database': element.databaseClass.toJson(),
},
if (element is DriftDatabase) ...{
'schema_version': element.schemaVersion,
'daos': [
for (final dao in element.accessors) _serializeElementReference(dao)
],
}
};
} else {
throw UnimplementedError('Unknown element $element');
}
return {
'id': element.id.toJson(),
'declaration': element.declaration.toJson(),
'references': [
for (final referenced in element.references)
_serializeElementReference(referenced),
],
...additionalInformation,
};
}
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 != null
? _serializeTypeConverter(column, column.typeConverter!)
: null,
'clientDefaultCode': column.clientDefaultCode?.toJson(),
'defaultArgument': column.defaultArgument?.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 UniqueColumn) {
return {'type': 'unique'};
} else if (constraint is PrimaryKeyColumn) {
return {'type': 'primary', ...constraint.toJson()};
} else if (constraint is ForeignKeyReference) {
return {
'type': 'foreign_key',
'column': _serializeColumnReference(constraint.otherColumn),
'onUpdate': _serializeReferenceAction(constraint.onUpdate),
'onDelete': _serializeReferenceAction(constraint.onDelete),
};
} else if (constraint is ColumnGeneratedAs) {
return {'type': 'generated_as', ...constraint.toJson()};
} else if (constraint is DartCheckExpression) {
return {'type': 'check', ...constraint.toJson()};
} else if (constraint is LimitingTextLength) {
return {'type': 'limit_text_length', ...constraint.toJson()};
} else {
throw UnimplementedError('Unsupported column constraint: $constraint');
}
}
Map<String, Object?> _serializeTableConstraint(
DriftTableConstraint constraint) {
if (constraint is UniqueColumns) {
return {
'type': 'unique',
'columns': [for (final column in constraint.uniqueSet) column.nameInSql]
};
} else if (constraint is PrimaryKeyColumns) {
return {
'type': 'primary_key',
'columns': [
for (final column in constraint.primaryKey) column.nameInSql,
],
};
} else if (constraint is ForeignKeyTable) {
return {
'type': 'foreign',
'local': [
for (final column in constraint.localColumns) column.nameInSql,
],
'table': _serializeElementReference(constraint.otherTable),
'foreign': [
for (final column in constraint.otherColumns)
_serializeColumnReference(column),
],
'onUpdate': _serializeReferenceAction(constraint.onUpdate),
'onDelete': _serializeReferenceAction(constraint.onDelete),
};
} else {
throw UnimplementedError('Unsupported table constraint: $constraint');
}
}
Map<String, Object?> _serializeVirtualTableData(VirtualTableData data) {
final recognized = data.recognized;
Object? serializedRecognized;
if (recognized is DriftFts5Table) {
serializedRecognized = {
'type': 'fts5',
'content_table': (recognized.externalContentTable != null)
? _serializeElementReference(recognized.externalContentTable!)
: null,
'content_rowid': (recognized.externalContentRowId != null)
? _serializeColumnReference(recognized.externalContentRowId!)
: null,
};
}
return {
'module': data.module,
'arguments': data.moduleArguments,
'recognized': serializedRecognized,
};
}
String? _serializeReferenceAction(ReferenceAction? action) {
return action?.name;
}
Map<String, Object?> _serializeTypeConverter(
DriftColumn appliedTo, AppliedTypeConverter converter) {
return {
'expression': converter.expression.toJson(),
'dart_type': _serializeType(converter.dartType),
'json_type': _serializeType(converter.jsonType),
'sql_type': converter.sqlType.name,
'dart_type_is_nullable': converter.dartTypeIsNullable,
'sql_type_is_nullable': converter.sqlTypeIsNullable,
'is_drift_enum_converter': converter.isDriftEnumTypeConverter,
if (converter.owningColumn != appliedTo)
'owner': _serializeColumnReference(converter.owningColumn!),
};
}
Map<String, Object?> _serializeExistingRowClass(ExistingRowClass existing) {
return {
'target_class': existing.targetClass?.toJson(),
'target_type': _serializeType(existing.targetType),
'constructor': existing.constructor,
'is_async_factory': existing.isAsyncFactory,
'positional': existing.positionalColumns,
'named': existing.namedColumns,
'generate_insertable': existing.generateInsertable,
};
}
Map<String, Object?> _serializeElementReference(DriftElement element) {
return element.id.toJson();
}
Map<String, Object?> _serializeColumnReference(DriftColumn column) {
return {
'table': _serializeElementReference(column.owner),
'name': column.nameInSql,
};
}
Map<String, Object?> _serializeTableReferenceInDartView(
TableReferenceInDartView ref) {
return {
'table': _serializeElementReference(ref.table),
'name': ref.name,
};
}
int? _serializeType(DartType? type) {
if (type == null) return null;
final code = AnnotatedDartCode.type(type);
final index = _result.dartTypes.length;
_result.dartTypes.add(code);
return index;
}
static SerializedElements serialize(Iterable<DriftElement> elements) {
return (ElementSerializer._().._serializeElements(elements))._result;
}
}
/// Deserializes the element structure emitted by [ElementSerializer].
class ElementDeserializer {
final Map<Uri, LibraryElement?> _typeHelperLibraries = {};
final List<DriftElementId> _currentlyReading = [];
final DriftAnalysisDriver driver;
ElementDeserializer(this.driver);
Future<DartType> _readDartType(Uri import, int typeId) async {
LibraryElement? element;
if (_typeHelperLibraries.containsKey(import)) {
element = _typeHelperLibraries[import];
} else {
element = _typeHelperLibraries[import] =
await driver.cacheReader!.readTypeHelperFor(import);
}
if (element == null) {
throw ArgumentError('Unknown serialized type: Helper does not exist.');
}
final typedef = element.exportNamespace.get('T$typeId') as TypeAliasElement;
return typedef.aliasedType;
}
Future<DriftElement> _readElementReference(Map json) {
return readDriftElement(DriftElementId.fromJson(json));
}
Future<DriftElement> readDriftElement(DriftElementId id) async {
final state = driver.cache.stateForUri(id.libraryUri).analysis[id] ??=
ElementAnalysisState(id);
if (state.result != null && state.isUpToDate) {
return state.result!;
}
final data = await driver.readStoredAnalysisResult(id.libraryUri);
if (data == null) {
throw CouldNotDeserializeException(
'Analysis data for ${id..libraryUri} not found');
}
if (_currentlyReading.contains(id)) {
throw StateError(
'Circular error when deserializing drift modules. This is a '
'bug in drift_dev!');
}
try {
_currentlyReading.add(id);
final result = await _readDriftElement(data[id.name] as Map);
state
..result = result
..isUpToDate = true;
return result;
} catch (e, s) {
if (e is CouldNotDeserializeException) rethrow;
throw CouldNotDeserializeException(
'Internal error while deserializing $id: $e at \n$s');
} finally {
final lastId = _currentlyReading.removeLast();
assert(lastId == id);
}
}
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);
final references = <DriftElement>[
for (final reference in json['references'])
await _readElementReference(reference as Map),
];
switch (type) {
case 'table':
final columns = [
for (final rawColumn in json['columns'] as List)
await _readColumn(rawColumn as Map, id),
];
final columnByName = {
for (final column in columns) column.nameInSql: column,
};
VirtualTableData? virtualTableData;
if (json['virtual'] != null) {
final data = json['virtual'] as Map;
RecognizedVirtualTableModule? recognizedModule;
final rawRecognized = data['recognized'];
if (rawRecognized != null) {
final rawTable = (rawRecognized as Map)['content_table'];
final rawRowid = rawRecognized['content_rowid'];
recognizedModule = DriftFts5Table(
rawTable != null
? await _readElementReference(rawTable as Map) as DriftTable
: null,
rawRowid != null
? await _readDriftColumnReference(rawRowid as Map)
: null,
);
}
virtualTableData = VirtualTableData(
data['module'] as String,
(data['arguments'] as List).cast(),
recognizedModule,
);
}
final table = DriftTable(
id,
declaration,
references: references,
columns: columns,
existingRowClass: json['existing_data_class'] != null
? await _readExistingRowClass(
id.libraryUri, json['existing_data_class'] as Map)
: null,
tableConstraints: [
for (final constraint in json['table_constraints'])
await _readTableConstraint(constraint as Map, columnByName),
],
customParentClass: json['custom_parent_class'] != null
? AnnotatedDartCode.fromJson(json['custom_parent_class'] as Map)
: null,
fixedEntityInfoName: json['fixed_entity_info_name'] as String?,
baseDartName: json['base_dart_name'] as String,
nameOfRowClass: json['row_class_name'] as String,
withoutRowId: json['without_rowid'] as bool,
strict: json['strict'] as bool,
virtualTableData: virtualTableData,
writeDefaultConstraints: json['write_default_constraints'] as bool,
overrideTableConstraints: json['custom_constraints'] != null
? (json['custom_constraints'] as List).cast()
: const [],
);
for (final column in columns) {
for (var i = 0; i < column.constraints.length; i++) {
final constraint = column.constraints[i];
if (constraint is _PendingReferenceToOwnTable) {
column.constraints[i] = ForeignKeyReference(
columns.singleWhere(
(e) => e.nameInSql == constraint.referencedColumn),
constraint.onUpdate,
constraint.onDelete,
);
}
}
}
return table;
case 'index':
return DriftIndex(
id,
declaration,
table: references.whereType<DriftTable>().firstOrNull,
createStmt: json['sql'] as String,
);
case 'query':
final rawExistingType = json['existing_type'];
final types = <String, DartType>{};
for (final entry in (json['dart_types'] as Map).entries) {
types[entry.key as String] =
await _readDartType(id.libraryUri, entry.value as int);
}
return DefinedSqlQuery(
id,
declaration,
references: references,
sql: json['sql'] as String,
sqlOffset: json['offset'] as int,
resultClassName: json['result_class'] as String?,
existingDartType: rawExistingType != null
? await _readDartType(id.libraryUri, rawExistingType as int)
: null,
mode: QueryMode.values.byName(json['mode'] as String),
dartTokens: (json['dart_tokens'] as List).cast(),
dartTypes: types,
);
case 'trigger':
DriftTable? on;
if (json['on'] != null) {
on = await _readElementReference(json['on'] as Map) as DriftTable;
}
return DriftTrigger(
id,
declaration,
references: references,
createStmt: json['sql'] as String,
on: on,
onWrite: UpdateKind.values.byName(json['onWrite'] as String),
writes: [
for (final write in json['writes'])
WrittenDriftTable(
await _readElementReference(write['table'] as Map)
as DriftTable,
UpdateKind.values.byName(write['kind'] as String),
)
],
);
case 'view':
final columns = [
for (final rawColumn in json['columns'] as List)
await _readColumn(rawColumn as Map, id),
];
final serializedSource = json['source'] as Map;
final sourceKind = serializedSource['kind'];
DriftViewSource source;
if (sourceKind == 'sql') {
source = SqlViewSource(serializedSource['sql'] as String);
} else if (sourceKind == 'dart') {
TableReferenceInDartView readReference(Map json) {
final id = DriftElementId.fromJson(json['table'] as Map);
final reference = references.singleWhere((e) => e.id == id);
return TableReferenceInDartView(
reference as DriftTable, json['name'] as String);
}
source = DartViewSource(
AnnotatedDartCode.fromJson(serializedSource['query'] as Map),
serializedSource['primaryFrom'] != null
? readReference(serializedSource['primaryFrom'] as Map)
: null,
[
for (final element in serializedSource['staticReferences'])
readReference(element as Map)
],
);
} else {
throw UnsupportedError('Unknown view source $serializedSource');
}
return DriftView(
id,
declaration,
references: references,
columns: columns,
entityInfoName: json['entity_info_name'] as String,
customParentClass: json['custom_parent_class'] != null
? AnnotatedDartCode.fromJson(json['custom_parent_class'] as Map)
: null,
nameOfRowClass: json['name_of_row_class'] as String,
existingRowClass: json['existing_data_class'] != null
? await _readExistingRowClass(
id.libraryUri, json['existing_data_class'] as Map)
: null,
source: source,
);
case 'database':
case 'dao':
final referenceById = {
for (final reference in references) reference.id: reference,
};
final tables = [
for (final tableId in json['tables'])
referenceById[DriftElementId.fromJson(tableId as Map)] as DriftTable
];
final views = [
for (final tableId in json['views'])
referenceById[DriftElementId.fromJson(tableId as Map)] as DriftView
];
final includes =
(json['includes'] as List).cast<String>().map(Uri.parse).toList();
final queries = (json['queries'] as List)
.cast<Map>()
.map(QueryOnAccessor.fromJson)
.toList();
if (type == 'database') {
return DriftDatabase(
id: id,
declaration: declaration,
declaredTables: tables,
declaredViews: views,
declaredIncludes: includes,
declaredQueries: queries,
schemaVersion: json['schema_version'] as int?,
accessors: [
for (final dao in json['daos'])
await readDriftElement(DriftElementId.fromJson(dao as Map))
as DatabaseAccessor,
],
);
} else {
assert(type == 'dao');
return DatabaseAccessor(
id: id,
declaration: declaration,
declaredTables: tables,
declaredViews: views,
declaredIncludes: includes,
declaredQueries: queries,
databaseClass: AnnotatedDartCode.fromJson(json['database'] as Map),
ownType: AnnotatedDartCode.fromJson(json['dart_type'] as Map),
);
}
default:
throw UnimplementedError('Unsupported element type: $type');
}
}
Future<DriftColumn> _readColumn(Map json, DriftElementId ownTable) async {
final rawConverter = json['typeConverter'] as Map?;
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: rawConverter != null
? await _readTypeConverter(ownTable.libraryUri, rawConverter)
: null,
foreignConverter: rawConverter != null && rawConverter['owner'] != 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, ownTable)
],
customConstraints: json['customConstraints'] as String?,
);
}
Future<AppliedTypeConverter> _readTypeConverter(
Uri definition, Map json) async {
final owner = json['owner'];
DriftColumn? readOwner;
if (owner != null) {
readOwner = await _readDriftColumnReference(owner as Map);
}
final converter = AppliedTypeConverter(
expression: AnnotatedDartCode.fromJson(json['expression'] as Map),
dartType: await _readDartType(definition, json['dart_type'] as int),
jsonType: json['json_type'] != null
? await _readDartType(definition, json['json_type'] as int)
: null,
sqlType: DriftSqlType.values.byName(json['sql_type'] as String),
dartTypeIsNullable: json['dart_type_is_nullable'] as bool,
sqlTypeIsNullable: json['sql_type_is_nullable'] as bool,
isDriftEnumTypeConverter: json['is_drift_enum_converter'] as bool,
);
if (readOwner != null) converter.owningColumn = readOwner;
return converter;
}
Future<ExistingRowClass> _readExistingRowClass(
Uri definition, Map json) async {
return ExistingRowClass(
targetClass: json['target_class'] != null
? AnnotatedDartCode.fromJson(json['target_class']! as Map)
: null,
targetType: await _readDartType(definition, json['target_type'] as int),
constructor: json['constructor'] as String,
isAsyncFactory: json['is_async_factory'] as bool,
positionalColumns: (json['positional'] as List).cast(),
namedColumns: (json['named'] as Map).cast(),
generateInsertable: json['generate_insertable'] as bool,
);
}
ReferenceAction? _readAction(String? value) {
return value == null ? null : ReferenceAction.values.byName(value);
}
Future<DriftColumnConstraint> _readConstraint(
Map json, DriftElementId ownTable) async {
final type = json['type'] as String;
switch (type) {
case 'unique':
return const UniqueColumn();
case 'primary':
return PrimaryKeyColumn.fromJson(json);
case 'foreign_key':
final table = DriftElementId.fromJson(json['column']['table'] as Map);
if (table == ownTable) {
return _PendingReferenceToOwnTable(
json['column']['name'] as String,
_readAction(json['onUpdate'] as String?),
_readAction(json['onDelete'] as String?),
);
} else {
return ForeignKeyReference(
await _readDriftColumnReference(json['column'] as Map),
_readAction(json['onUpdate'] as String?),
_readAction(json['onDelete'] as String?),
);
}
case 'generated_as':
return ColumnGeneratedAs.fromJson(json);
case 'check':
return DartCheckExpression.fromJson(json);
case 'limit_text_length':
return LimitingTextLength.fromJson(json);
default:
throw UnimplementedError('Unsupported constraint: $type');
}
}
Future<DriftTableConstraint> _readTableConstraint(
Map json, Map<String, DriftColumn> localColumns) async {
final type = json['type'] as String;
switch (type) {
case 'unique':
return UniqueColumns({
for (final ref in json['columns']) localColumns[ref]!,
});
case 'primary_key':
return PrimaryKeyColumns(
{for (final ref in json['columns']) localColumns[ref]!},
);
case 'foreign':
return ForeignKeyTable(
localColumns: [
for (final ref in json['local']) localColumns[ref]!,
],
otherTable:
await _readElementReference(json['table'] as Map) as DriftTable,
otherColumns: [
for (final ref in json['foreign'])
await _readDriftColumnReference(ref as Map)
],
onUpdate: _readAction(json['onUpdate'] as String?),
onDelete: _readAction(json['onDelete'] as String?),
);
default:
throw UnimplementedError('Unsupported constraint: $type');
}
}
}
class CouldNotDeserializeException implements Exception {
final String message;
const CouldNotDeserializeException(this.message);
@override
String toString() => message;
}
class _PendingReferenceToOwnTable extends DriftColumnConstraint {
final String referencedColumn;
final ReferenceAction? onUpdate, onDelete;
_PendingReferenceToOwnTable(
this.referencedColumn, this.onUpdate, this.onDelete);
}