mirror of https://github.com/AMT-Cheif/drift.git
360 lines
12 KiB
Dart
360 lines
12 KiB
Dart
part of 'parser.dart';
|
|
|
|
/// Parses a [DriftTable] from a Dart class.
|
|
class TableParser {
|
|
final DriftDartParser base;
|
|
|
|
TableParser(this.base);
|
|
|
|
Future<DriftTable?> parseTable(ClassElement element) async {
|
|
final sqlName = await _parseTableName(element);
|
|
if (sqlName == null) return null;
|
|
|
|
final columns = (await _parseColumns(element)).toList();
|
|
final primaryKey = await _readPrimaryKey(element, columns);
|
|
final uniqueKeys = await _readUniqueKeys(element, columns);
|
|
|
|
final dataClassInfo = _readDataClassInformation(columns, element);
|
|
|
|
final table = DriftTable(
|
|
fromClass: element,
|
|
columns: columns,
|
|
sqlName: sqlName,
|
|
dartTypeName: dataClassInfo.enforcedName,
|
|
existingRowClass: dataClassInfo.existingClass,
|
|
customParentClass: dataClassInfo.extending,
|
|
primaryKey: primaryKey,
|
|
uniqueKeys: uniqueKeys,
|
|
overrideWithoutRowId: await _overrideWithoutRowId(element),
|
|
declaration: DartTableDeclaration(element, base.step.file),
|
|
);
|
|
|
|
if (primaryKey != null && columns.any((element) => element.hasAI)) {
|
|
base.step.errors.report(ErrorInDartCode(
|
|
message: "Tables can't override primaryKey and use autoIncrement()",
|
|
affectedElement: element,
|
|
));
|
|
}
|
|
|
|
if (primaryKey != null &&
|
|
primaryKey.length == 1 &&
|
|
primaryKey.first.features.contains(const UniqueKey())) {
|
|
base.step.errors.report(ErrorInDartCode(
|
|
message: 'Primary key column cannot have UNIQUE constraint',
|
|
affectedElement: element,
|
|
));
|
|
}
|
|
|
|
if (uniqueKeys != null &&
|
|
uniqueKeys.any((key) =>
|
|
uniqueKeys.length == 1 &&
|
|
key.first.features.contains(const UniqueKey()))) {
|
|
base.step.errors.report(ErrorInDartCode(
|
|
message:
|
|
'Column provided in a single-column uniqueKey set already has a '
|
|
'column-level UNIQUE constraint',
|
|
affectedElement: element,
|
|
));
|
|
}
|
|
|
|
if (uniqueKeys != null &&
|
|
primaryKey != null &&
|
|
uniqueKeys
|
|
.any((unique) => const SetEquality().equals(unique, primaryKey))) {
|
|
base.step.errors.report(ErrorInDartCode(
|
|
message: 'The uniqueKeys override contains the primary key, which is '
|
|
'already unique by default.',
|
|
affectedElement: element,
|
|
));
|
|
}
|
|
|
|
var index = 0;
|
|
for (final converter in table.converters) {
|
|
converter
|
|
..index = index++
|
|
..table = table;
|
|
}
|
|
|
|
return table;
|
|
}
|
|
|
|
_DataClassInformation _readDataClassInformation(
|
|
List<DriftColumn> columns, ClassElement element) {
|
|
DartObject? dataClassName;
|
|
DartObject? useRowClass;
|
|
|
|
for (final annotation in element.metadata) {
|
|
final computed = annotation.computeConstantValue();
|
|
final annotationClass = computed!.type!.element!.name;
|
|
|
|
if (annotationClass == 'DataClassName') {
|
|
dataClassName = computed;
|
|
} else if (annotationClass == 'UseRowClass') {
|
|
useRowClass = computed;
|
|
}
|
|
}
|
|
|
|
if (dataClassName != null && useRowClass != null) {
|
|
base.step.reportError(ErrorInDartCode(
|
|
message: "A table can't be annotated with both @DataClassName and "
|
|
'@UseRowClass',
|
|
affectedElement: element,
|
|
));
|
|
}
|
|
|
|
String name;
|
|
String? customParentClass;
|
|
FoundDartClass? existingClass;
|
|
String? constructorInExistingClass;
|
|
bool? generateInsertable;
|
|
|
|
if (dataClassName != null) {
|
|
name = dataClassName.getField('name')!.toStringValue()!;
|
|
customParentClass =
|
|
parseCustomParentClass(name, dataClassName, element, base);
|
|
} else {
|
|
name = dataClassNameForClassName(element.name);
|
|
}
|
|
|
|
if (useRowClass != null) {
|
|
final type = useRowClass.getField('type')!.toTypeValue();
|
|
constructorInExistingClass =
|
|
useRowClass.getField('constructor')!.toStringValue()!;
|
|
generateInsertable =
|
|
useRowClass.getField('generateInsertable')!.toBoolValue()!;
|
|
|
|
if (type is InterfaceType) {
|
|
existingClass = FoundDartClass(type.element, type.typeArguments);
|
|
name = type.element.name;
|
|
} else {
|
|
base.step.reportError(ErrorInDartCode(
|
|
message: 'The @UseRowClass annotation must be used with a class',
|
|
affectedElement: element,
|
|
));
|
|
}
|
|
}
|
|
|
|
final verified = existingClass == null
|
|
? null
|
|
: validateExistingClass(columns, existingClass,
|
|
constructorInExistingClass!, generateInsertable!, base.step);
|
|
return _DataClassInformation(name, customParentClass, verified);
|
|
}
|
|
|
|
Future<String?> _parseTableName(ClassElement element) async {
|
|
// todo allow override via a field (final String tableName = '') as well
|
|
|
|
final tableNameGetter = element.lookUpGetter('tableName', element.library);
|
|
if (tableNameGetter == null ||
|
|
tableNameGetter.isFromDefaultTable ||
|
|
tableNameGetter.isAbstract) {
|
|
// class does not override tableName. So just use the dart class name
|
|
// instead. Will use placed_orders for a class called PlacedOrders
|
|
return ReCase(element.name).snakeCase;
|
|
}
|
|
|
|
// we expect something like get tableName => "myTableName", the getter
|
|
// must do nothing more complicated
|
|
final node = await base.loadElementDeclaration(tableNameGetter);
|
|
final returnExpr = base.returnExpressionOfMethod(node as MethodDeclaration);
|
|
if (returnExpr == null) return null;
|
|
|
|
final tableName = base.readStringLiteral(returnExpr, () {
|
|
base.step.reportError(ErrorInDartCode(
|
|
severity: Severity.criticalError,
|
|
message:
|
|
'This getter must return a string literal, and do nothing more',
|
|
affectedElement: tableNameGetter));
|
|
});
|
|
|
|
return tableName;
|
|
}
|
|
|
|
Future<Set<DriftColumn>?> _readPrimaryKey(
|
|
ClassElement element, List<DriftColumn> columns) async {
|
|
final primaryKeyGetter =
|
|
element.lookUpGetter('primaryKey', element.library);
|
|
|
|
if (primaryKeyGetter == null || primaryKeyGetter.isFromDefaultTable) {
|
|
// resolved primaryKey is from the Table dsl superclass. That means there
|
|
// is no primary key
|
|
return null;
|
|
}
|
|
|
|
final ast = await base.loadElementDeclaration(primaryKeyGetter)
|
|
as MethodDeclaration;
|
|
final body = ast.body;
|
|
if (body is! ExpressionFunctionBody) {
|
|
base.step.reportError(ErrorInDartCode(
|
|
affectedElement: primaryKeyGetter,
|
|
message: 'This must return a set literal using the => syntax!'));
|
|
return null;
|
|
}
|
|
final expression = body.expression;
|
|
final parsedPrimaryKey = <DriftColumn>{};
|
|
|
|
if (expression is SetOrMapLiteral) {
|
|
for (final entry in expression.elements) {
|
|
if (entry is Identifier) {
|
|
final column = columns.singleWhereOrNull(
|
|
(column) => column.dartGetterName == entry.name);
|
|
if (column == null) {
|
|
base.step.reportError(
|
|
ErrorInDartCode(
|
|
affectedElement: primaryKeyGetter,
|
|
affectedNode: entry,
|
|
message: 'Column not found in this table',
|
|
),
|
|
);
|
|
} else {
|
|
parsedPrimaryKey.add(column);
|
|
}
|
|
} else {
|
|
print('Unexpected entry in expression.elements: $entry');
|
|
}
|
|
}
|
|
} else {
|
|
base.step.reportError(ErrorInDartCode(
|
|
affectedElement: primaryKeyGetter,
|
|
message: 'This must return a set literal!'));
|
|
}
|
|
|
|
return parsedPrimaryKey;
|
|
}
|
|
|
|
Future<List<Set<DriftColumn>>?> _readUniqueKeys(
|
|
ClassElement element, List<DriftColumn> columns) async {
|
|
final uniqueKeyGetter = element.lookUpGetter('uniqueKeys', element.library);
|
|
|
|
if (uniqueKeyGetter == null || uniqueKeyGetter.isFromDefaultTable) {
|
|
// resolved uniqueKeys is from the Table dsl superclass. That means there
|
|
// is no unique key list
|
|
return null;
|
|
}
|
|
|
|
final ast =
|
|
await base.loadElementDeclaration(uniqueKeyGetter) as MethodDeclaration;
|
|
final body = ast.body;
|
|
if (body is! ExpressionFunctionBody) {
|
|
base.step.reportError(ErrorInDartCode(
|
|
affectedElement: uniqueKeyGetter,
|
|
message: 'This must return a list of set literal using the => '
|
|
'syntax!'));
|
|
return null;
|
|
}
|
|
final expression = body.expression;
|
|
final parsedUniqueKeys = <Set<DriftColumn>>[];
|
|
|
|
if (expression is ListLiteral) {
|
|
for (final keySet in expression.elements) {
|
|
if (keySet is SetOrMapLiteral) {
|
|
final uniqueKey = <DriftColumn>{};
|
|
for (final entry in keySet.elements) {
|
|
if (entry is Identifier) {
|
|
final column = columns.singleWhereOrNull(
|
|
(column) => column.dartGetterName == entry.name);
|
|
if (column == null) {
|
|
base.step.reportError(
|
|
ErrorInDartCode(
|
|
affectedElement: uniqueKeyGetter,
|
|
affectedNode: entry,
|
|
message: 'Column not found in this table',
|
|
),
|
|
);
|
|
} else {
|
|
uniqueKey.add(column);
|
|
}
|
|
} else {
|
|
print('Unexpected entry in expression.elements: $entry');
|
|
}
|
|
}
|
|
parsedUniqueKeys.add(uniqueKey);
|
|
} else {
|
|
base.step.reportError(ErrorInDartCode(
|
|
affectedElement: uniqueKeyGetter,
|
|
message: 'This must return a set list literal!'));
|
|
}
|
|
}
|
|
} else {
|
|
base.step.reportError(ErrorInDartCode(
|
|
affectedElement: uniqueKeyGetter,
|
|
message: 'This must return a set list literal!'));
|
|
}
|
|
|
|
return parsedUniqueKeys;
|
|
}
|
|
|
|
Future<bool?> _overrideWithoutRowId(ClassElement element) async {
|
|
final getter = element.lookUpGetter('withoutRowId', element.library);
|
|
|
|
// Was the getter overridden at all?
|
|
if (getter == null || getter.isFromDefaultTable) return null;
|
|
|
|
final ast = await base.loadElementDeclaration(getter) as MethodDeclaration;
|
|
final expr = base.returnExpressionOfMethod(ast);
|
|
|
|
if (expr == null) return null;
|
|
|
|
if (expr is BooleanLiteral) {
|
|
return expr.value;
|
|
} else {
|
|
base.step.reportError(ErrorInDartCode(
|
|
affectedElement: getter,
|
|
message: 'This must directly return a boolean literal.',
|
|
));
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
Future<Iterable<DriftColumn>> _parseColumns(ClassElement element) async {
|
|
final columnNames = element.allSupertypes
|
|
.map((t) => t.element)
|
|
.followedBy([element])
|
|
.expand((e) => e.fields)
|
|
.where((field) =>
|
|
isColumn(field.type) &&
|
|
field.getter != null &&
|
|
!field.getter!.isSynthetic)
|
|
.map((field) => field.name)
|
|
.toSet();
|
|
|
|
final fields = columnNames.map((name) {
|
|
final getter = element.getGetter(name) ??
|
|
element.lookUpInheritedConcreteGetter(name, element.library);
|
|
return getter!.variable;
|
|
});
|
|
|
|
final results = await Future.wait(fields.map((field) async {
|
|
final node =
|
|
await base.loadElementDeclaration(field.getter!) as MethodDeclaration;
|
|
|
|
return await base.parseColumn(node, field.getter!);
|
|
}));
|
|
|
|
return results.whereType();
|
|
}
|
|
}
|
|
|
|
class _DataClassInformation {
|
|
final String enforcedName;
|
|
final String? extending;
|
|
final ExistingRowClass? existingClass;
|
|
|
|
_DataClassInformation(
|
|
this.enforcedName,
|
|
this.extending,
|
|
this.existingClass,
|
|
);
|
|
}
|
|
|
|
extension on Element {
|
|
bool get isFromDefaultTable {
|
|
final parent = enclosingElement;
|
|
|
|
return parent is ClassElement &&
|
|
parent.name == 'Table' &&
|
|
isFromMoor(parent.thisType);
|
|
}
|
|
}
|