mirror of https://github.com/AMT-Cheif/drift.git
Analyze Dart tables with new analyzer
This commit is contained in:
parent
e88ec41555
commit
0f97b42a43
|
@ -9,6 +9,10 @@ abstract class DriftBackend {
|
|||
|
||||
Future<String> readAsString(Uri uri);
|
||||
|
||||
Future<Uri> uriOfDart(Element element) async {
|
||||
return element.source!.uri;
|
||||
}
|
||||
|
||||
Future<LibraryElement> readDart(Uri uri);
|
||||
Future<AstNode?> loadElementDeclaration(Element element);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,466 @@
|
|||
import 'package:analyzer/dart/ast/ast.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;
|
||||
import 'package:recase/recase.dart';
|
||||
import 'package:sqlparser/sqlparser.dart' show ReferenceAction;
|
||||
|
||||
import '../../driver/error.dart';
|
||||
import '../../results/results.dart';
|
||||
import '../resolver.dart';
|
||||
import '../shared/dart_types.dart';
|
||||
import 'helper.dart';
|
||||
import 'table.dart';
|
||||
|
||||
const String _startInt = 'integer';
|
||||
const String _startInt64 = 'int64';
|
||||
const String _startEnum = 'intEnum';
|
||||
const String _startString = 'text';
|
||||
const String _startBool = 'boolean';
|
||||
const String _startDateTime = 'dateTime';
|
||||
const String _startBlob = 'blob';
|
||||
const String _startReal = 'real';
|
||||
|
||||
const Set<String> _starters = {
|
||||
_startInt,
|
||||
_startInt64,
|
||||
_startEnum,
|
||||
_startString,
|
||||
_startBool,
|
||||
_startDateTime,
|
||||
_startBlob,
|
||||
_startReal,
|
||||
};
|
||||
|
||||
const String _methodNamed = 'named';
|
||||
const String _methodReferences = 'references';
|
||||
const String _methodAutoIncrement = 'autoIncrement';
|
||||
const String _methodWithLength = 'withLength';
|
||||
const String _methodNullable = 'nullable';
|
||||
const String _methodUnique = 'unique';
|
||||
const String _methodCustomConstraint = 'customConstraint';
|
||||
const String _methodDefault = 'withDefault';
|
||||
const String _methodClientDefault = 'clientDefault';
|
||||
const String _methodMap = 'map';
|
||||
const String _methodGenerated = 'generatedAs';
|
||||
const String _methodCheck = 'check';
|
||||
const Set<String> _addsSqlConstraint = {
|
||||
_methodReferences,
|
||||
_methodAutoIncrement,
|
||||
_methodUnique,
|
||||
_methodDefault,
|
||||
_methodGenerated,
|
||||
_methodCheck,
|
||||
};
|
||||
|
||||
const String _errorMessage = 'This getter does not create a valid column that '
|
||||
'can be parsed by drift. Please refer to the readme from drift to see how '
|
||||
'columns are formed. If you have any questions, feel free to raise an '
|
||||
'issue.';
|
||||
|
||||
/// Parses a single column defined in a Dart table. These columns are a chain
|
||||
/// or [MethodInvocation]s. An example getter might look like this:
|
||||
/// ```dart
|
||||
/// IntColumn get id => integer().autoIncrement()();
|
||||
/// ```
|
||||
/// The last call `()` is a [FunctionExpressionInvocation], the entries for
|
||||
/// before that (in this case `autoIncrement()` and `integer()` are a)
|
||||
/// [MethodInvocation]. We work our way through that syntax until we hit a
|
||||
/// method that starts the chain (contained in [starters]). By visiting all
|
||||
/// the invocations on our way, we can extract the constraint for the column
|
||||
/// (e.g. its name, whether it has auto increment, is a primary key and so on).
|
||||
class ColumnParser {
|
||||
final DartTableResolver _resolver;
|
||||
|
||||
ColumnParser(this._resolver);
|
||||
|
||||
Future<PendingColumnInformation?> parse(
|
||||
MethodDeclaration getter, Element element) async {
|
||||
final expr = returnExpressionOfMethod(getter);
|
||||
|
||||
if (expr is! FunctionExpressionInvocation) {
|
||||
_resolver.reportError(
|
||||
DriftAnalysisError.forDartElement(element, _errorMessage));
|
||||
return null;
|
||||
}
|
||||
|
||||
var remainingExpr = expr.function as MethodInvocation;
|
||||
|
||||
String? foundStartMethod;
|
||||
String? foundExplicitName;
|
||||
String? foundCustomConstraint;
|
||||
AnnotatedDartCode? foundDefaultExpression;
|
||||
AnnotatedDartCode? clientDefaultExpression;
|
||||
Expression? mappedAs;
|
||||
String? referencesColumnInSameTable;
|
||||
|
||||
var nullable = false;
|
||||
var hasDefaultConstraints = false;
|
||||
|
||||
final foundConstraints = <DriftColumnConstraint>[];
|
||||
|
||||
while (true) {
|
||||
final methodName = remainingExpr.methodName.name;
|
||||
|
||||
if (_starters.contains(methodName)) {
|
||||
foundStartMethod = methodName;
|
||||
break;
|
||||
}
|
||||
|
||||
if (_addsSqlConstraint.contains(methodName)) {
|
||||
hasDefaultConstraints = true;
|
||||
}
|
||||
|
||||
switch (methodName) {
|
||||
case _methodNamed:
|
||||
if (foundExplicitName != null) {
|
||||
_resolver.reportError(
|
||||
DriftAnalysisError.forDartElement(
|
||||
element,
|
||||
"You're setting more than one name here, the first will "
|
||||
'be used',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
foundExplicitName =
|
||||
readStringLiteral(remainingExpr.argumentList.arguments.first);
|
||||
if (foundExplicitName == null) {
|
||||
_resolver.reportError(DriftAnalysisError.inDartAst(
|
||||
element,
|
||||
remainingExpr.argumentList,
|
||||
'This table name is cannot be resolved! Please only use '
|
||||
'a constant string as parameter for .named().'));
|
||||
}
|
||||
break;
|
||||
case _methodReferences:
|
||||
final args = remainingExpr.argumentList.arguments;
|
||||
final first = args.first;
|
||||
|
||||
if (first is! Identifier) {
|
||||
_resolver.reportError(DriftAnalysisError.inDartAst(
|
||||
element,
|
||||
first,
|
||||
'This parameter should be a simple class name',
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
final staticElement = first.staticElement;
|
||||
if (staticElement is! ClassElement) {
|
||||
_resolver.reportError(DriftAnalysisError.inDartAst(
|
||||
element,
|
||||
first,
|
||||
'`${first.name}` is not a class!',
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
final columnNameNode = args[1];
|
||||
if (columnNameNode is! SymbolLiteral) {
|
||||
_resolver.reportError(DriftAnalysisError.inDartAst(
|
||||
element,
|
||||
columnNameNode,
|
||||
'This should be a symbol literal (`#columnName`)',
|
||||
));
|
||||
break;
|
||||
}
|
||||
|
||||
final columnName =
|
||||
columnNameNode.components.map((token) => token.lexeme).join('.');
|
||||
|
||||
ReferenceAction? onUpdate, onDelete;
|
||||
|
||||
ReferenceAction? parseAction(Expression expr) {
|
||||
if (expr is! PrefixedIdentifier) {
|
||||
_resolver.reportError(DriftAnalysisError.inDartAst(element, expr,
|
||||
'Should be a direct enum reference (`KeyAction.cascade`)'));
|
||||
return null;
|
||||
}
|
||||
|
||||
final name = expr.identifier.name;
|
||||
switch (name) {
|
||||
case 'setNull':
|
||||
return ReferenceAction.setNull;
|
||||
case 'setDefault':
|
||||
return ReferenceAction.setDefault;
|
||||
case 'cascade':
|
||||
return ReferenceAction.cascade;
|
||||
case 'restrict':
|
||||
return ReferenceAction.restrict;
|
||||
case 'noAction':
|
||||
default:
|
||||
return ReferenceAction.noAction;
|
||||
}
|
||||
}
|
||||
|
||||
for (final expr in args) {
|
||||
if (expr is! NamedExpression) continue;
|
||||
|
||||
final name = expr.name.label.name;
|
||||
final value = expr.expression;
|
||||
if (name == 'onUpdate') {
|
||||
onUpdate = parseAction(value);
|
||||
} else if (name == 'onDelete') {
|
||||
onDelete = parseAction(value);
|
||||
}
|
||||
}
|
||||
|
||||
final referencedTable = await _resolver.resolver
|
||||
.resolveDartReference(_resolver.discovered.ownId, staticElement);
|
||||
|
||||
if (referencedTable is ReferencesItself) {
|
||||
// "Foreign" key to a column in the same table.
|
||||
foundConstraints
|
||||
.add(ForeignKeyReference.unresolved(onUpdate, onDelete));
|
||||
referencesColumnInSameTable = columnName;
|
||||
} else if (referencedTable is ResolvedReferenceFound) {
|
||||
final driftElement = referencedTable.element;
|
||||
|
||||
if (driftElement is DriftTable) {
|
||||
final column = driftElement.columns.firstWhereOrNull(
|
||||
(element) => element.nameInDart == columnName);
|
||||
|
||||
if (column == null) {
|
||||
_resolver.reportError(DriftAnalysisError.inDartAst(
|
||||
element,
|
||||
first,
|
||||
'The referenced table `${driftElement.schemaName}` has no '
|
||||
'column named `$columnName` in Dart.',
|
||||
));
|
||||
} else {
|
||||
foundConstraints
|
||||
.add(ForeignKeyReference(column, onUpdate, onDelete));
|
||||
}
|
||||
} else {
|
||||
_resolver.reportError(
|
||||
DriftAnalysisError.inDartAst(element, first, 'Not a table'));
|
||||
}
|
||||
} else {
|
||||
// Could not resolve foreign table, emit warning
|
||||
_resolver.reportErrorForUnresolvedReference(referencedTable,
|
||||
(msg) => DriftAnalysisError.inDartAst(element, first, msg));
|
||||
}
|
||||
|
||||
break;
|
||||
case _methodWithLength:
|
||||
final args = remainingExpr.argumentList;
|
||||
final minArg = findNamedArgument(args, 'min');
|
||||
final maxArg = findNamedArgument(args, 'max');
|
||||
|
||||
foundConstraints.add(LimitingTextLength(
|
||||
minLength: minArg != null ? readIntLiteral(minArg) : null,
|
||||
maxLength: maxArg != null ? readIntLiteral(maxArg) : null,
|
||||
));
|
||||
break;
|
||||
case _methodAutoIncrement:
|
||||
foundConstraints.add(PrimaryKeyColumn(true));
|
||||
break;
|
||||
case _methodNullable:
|
||||
nullable = true;
|
||||
break;
|
||||
case _methodUnique:
|
||||
foundConstraints.add(const UniqueColumn());
|
||||
break;
|
||||
case _methodCustomConstraint:
|
||||
if (foundCustomConstraint != null) {
|
||||
_resolver.reportError(
|
||||
DriftAnalysisError.inDartAst(
|
||||
element,
|
||||
remainingExpr.methodName,
|
||||
"You've already set custom constraints on this column, "
|
||||
'they will be overriden by this call.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
foundCustomConstraint =
|
||||
readStringLiteral(remainingExpr.argumentList.arguments.first);
|
||||
|
||||
if (foundCustomConstraint == null) {
|
||||
_resolver.reportError(DriftAnalysisError.forDartElement(
|
||||
element,
|
||||
'This constraint is cannot be resolved! Please only use '
|
||||
'a constant string as parameter for .customConstraint().',
|
||||
));
|
||||
}
|
||||
break;
|
||||
case _methodDefault:
|
||||
final args = remainingExpr.argumentList;
|
||||
final expression = args.arguments.single;
|
||||
foundDefaultExpression = AnnotatedDartCode.ast(expression);
|
||||
break;
|
||||
case _methodClientDefault:
|
||||
clientDefaultExpression = AnnotatedDartCode.ast(
|
||||
remainingExpr.argumentList.arguments.single);
|
||||
break;
|
||||
case _methodMap:
|
||||
final args = remainingExpr.argumentList;
|
||||
mappedAs = args.arguments.single;
|
||||
break;
|
||||
case _methodGenerated:
|
||||
Expression? generatedExpression;
|
||||
var stored = false;
|
||||
|
||||
for (final expr in remainingExpr.argumentList.arguments) {
|
||||
if (expr is NamedExpression && expr.name.label.name == 'stored') {
|
||||
final storedValue = expr.expression;
|
||||
if (storedValue is BooleanLiteral) {
|
||||
stored = storedValue.value;
|
||||
} else {
|
||||
_resolver.reportError(DriftAnalysisError.inDartAst(
|
||||
element, expr, 'Must be a boolean literal'));
|
||||
}
|
||||
} else {
|
||||
generatedExpression = expr;
|
||||
}
|
||||
}
|
||||
|
||||
if (generatedExpression != null) {
|
||||
final code = AnnotatedDartCode.ast(generatedExpression);
|
||||
foundConstraints.add(ColumnGeneratedAs(code, stored));
|
||||
}
|
||||
break;
|
||||
case _methodCheck:
|
||||
final expr = remainingExpr.argumentList.arguments.first;
|
||||
foundConstraints
|
||||
.add(DartCheckExpression(AnnotatedDartCode.ast(expr)));
|
||||
}
|
||||
|
||||
// We're not at a starting method yet, so we need to go deeper!
|
||||
final inner = remainingExpr.target as MethodInvocation;
|
||||
remainingExpr = inner;
|
||||
}
|
||||
|
||||
final sqlName = foundExplicitName ?? ReCase(getter.name2.lexeme).snakeCase;
|
||||
final sqlType = _startMethodToColumnType(foundStartMethod);
|
||||
|
||||
AppliedTypeConverter? converter;
|
||||
if (mappedAs != null) {
|
||||
converter = readTypeConverter(
|
||||
element.library!,
|
||||
mappedAs,
|
||||
sqlType,
|
||||
nullable,
|
||||
(message) => _resolver.reportError(
|
||||
DriftAnalysisError.inDartAst(element, mappedAs!, message)),
|
||||
await _resolver.resolver.driver.loadKnownTypes(),
|
||||
);
|
||||
}
|
||||
|
||||
if (foundStartMethod == _startEnum) {
|
||||
if (converter != null) {
|
||||
_resolver.reportError(DriftAnalysisError.forDartElement(
|
||||
element,
|
||||
'Using $_startEnum will apply a custom converter by default, '
|
||||
"so you can't add an additional converter",
|
||||
));
|
||||
}
|
||||
|
||||
final enumType = remainingExpr.typeArgumentTypes![0];
|
||||
converter = readEnumConverter(
|
||||
(msg) => DriftAnalysisError.inDartAst(element,
|
||||
remainingExpr.typeArguments ?? remainingExpr.methodName, msg),
|
||||
enumType,
|
||||
);
|
||||
}
|
||||
|
||||
if (foundDefaultExpression != null && clientDefaultExpression != null) {
|
||||
_resolver.reportError(
|
||||
DriftAnalysisError.forDartElement(
|
||||
element,
|
||||
'clientDefault() and withDefault() are mutually exclusive, '
|
||||
"they can't both be used. Use clientDefault() for values that "
|
||||
'are different for each row and withDefault() otherwise.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (foundConstraints.contains(const UniqueColumn()) &&
|
||||
foundConstraints.any((e) => e is PrimaryKeyColumn)) {
|
||||
_resolver.reportError(
|
||||
DriftAnalysisError.forDartElement(
|
||||
element,
|
||||
'Primary key column cannot have UNIQUE constraint',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (hasDefaultConstraints && foundCustomConstraint != null) {
|
||||
_resolver.reportError(
|
||||
DriftAnalysisError.forDartElement(
|
||||
element,
|
||||
'This column definition is using both drift-defined '
|
||||
'constraints (like references, autoIncrement, ...) and a '
|
||||
'customConstraint(). Only the custom constraint will be added '
|
||||
'to the column in SQL!',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final docString =
|
||||
getter.documentationComment?.tokens.map((t) => t.toString()).join('\n');
|
||||
|
||||
return PendingColumnInformation(
|
||||
DriftColumn(
|
||||
sqlType: sqlType,
|
||||
nullable: nullable,
|
||||
nameInSql: sqlName,
|
||||
nameInDart: element.name!,
|
||||
declaration: DriftDeclaration.dartElement(element),
|
||||
typeConverter: converter,
|
||||
clientDefaultCode: clientDefaultExpression,
|
||||
defaultArgument: foundDefaultExpression,
|
||||
overriddenJsonName: _readJsonKey(element),
|
||||
documentationComment: docString,
|
||||
constraints: foundConstraints,
|
||||
customConstraints: foundCustomConstraint,
|
||||
),
|
||||
referencesColumnInSameTable: referencesColumnInSameTable,
|
||||
);
|
||||
}
|
||||
|
||||
DriftSqlType _startMethodToColumnType(String name) {
|
||||
return const {
|
||||
_startBool: DriftSqlType.bool,
|
||||
_startString: DriftSqlType.string,
|
||||
_startInt: DriftSqlType.int,
|
||||
_startInt64: DriftSqlType.bigInt,
|
||||
_startEnum: DriftSqlType.int,
|
||||
_startDateTime: DriftSqlType.dateTime,
|
||||
_startBlob: DriftSqlType.blob,
|
||||
_startReal: DriftSqlType.double,
|
||||
}[name]!;
|
||||
}
|
||||
|
||||
String? _readJsonKey(Element getter) {
|
||||
final annotations = getter.metadata;
|
||||
final object = annotations.firstWhereOrNull((e) {
|
||||
final value = e.computeConstantValue();
|
||||
final valueType = value?.type;
|
||||
|
||||
return valueType is InterfaceType &&
|
||||
isFromDrift(valueType) &&
|
||||
valueType.element2.name == 'JsonKey';
|
||||
});
|
||||
|
||||
if (object == null) return null;
|
||||
|
||||
return object.computeConstantValue()!.getField('key')!.toStringValue();
|
||||
}
|
||||
}
|
||||
|
||||
class PendingColumnInformation {
|
||||
final DriftColumn column;
|
||||
|
||||
/// If the returned column references another column in the same table, its
|
||||
/// [ForeignKeyReference] is still unresolved when the local column resolver
|
||||
/// returns.
|
||||
///
|
||||
/// It is the responsibility of the table resolver to patch the reference for
|
||||
/// this column in that case.
|
||||
final String? referencesColumnInSameTable;
|
||||
|
||||
PendingColumnInformation(this.column, {this.referencesColumnInSameTable});
|
||||
}
|
|
@ -2,6 +2,7 @@ import 'package:analyzer/dart/ast/ast.dart';
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
import 'package:analyzer/dart/element/nullability_suffix.dart';
|
||||
import 'package:analyzer/dart/element/type.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../../driver/driver.dart';
|
||||
|
||||
|
@ -75,6 +76,33 @@ Expression? returnExpressionOfMethod(MethodDeclaration method,
|
|||
return body.expression;
|
||||
}
|
||||
|
||||
String? readStringLiteral(Expression expression) {
|
||||
if (expression is StringLiteral) {
|
||||
final value = expression.stringValue;
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int? readIntLiteral(Expression expression) {
|
||||
if (expression is IntegerLiteral) {
|
||||
return expression.value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Expression? findNamedArgument(ArgumentList args, String argName) {
|
||||
final argument = args.arguments.singleWhereOrNull(
|
||||
(e) => e is NamedExpression && e.name.label.name == argName,
|
||||
) as NamedExpression?;
|
||||
|
||||
return argument?.expression;
|
||||
}
|
||||
|
||||
bool isColumn(DartType type) {
|
||||
final name = type.nameIfInterfaceType;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import '../intermediate_state.dart';
|
|||
import '../resolver.dart';
|
||||
import '../shared/dart_types.dart';
|
||||
import '../shared/data_class.dart';
|
||||
import 'column.dart';
|
||||
import 'helper.dart';
|
||||
|
||||
class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
||||
|
@ -19,7 +20,8 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
Future<DriftElement> resolve() async {
|
||||
final element = discovered.element;
|
||||
|
||||
final columns = (await _parseColumns(element)).toList();
|
||||
final pendingColumns = (await _parseColumns(element)).toList();
|
||||
final columns = [for (final column in pendingColumns) column.column];
|
||||
final primaryKey = await _readPrimaryKey(element, columns);
|
||||
final uniqueKeys = await _readUniqueKeys(element, columns);
|
||||
|
||||
|
@ -29,7 +31,7 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
discovered.ownId,
|
||||
DriftDeclaration.dartElement(element),
|
||||
columns: columns,
|
||||
dartTypeName: dataClassInfo.enforcedName,
|
||||
nameOfRowClass: dataClassInfo.enforcedName,
|
||||
existingRowClass: dataClassInfo.existingClass,
|
||||
customParentClass: dataClassInfo.extending,
|
||||
baseDartName: element.name,
|
||||
|
@ -38,6 +40,18 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
withoutRowId: await _overrideWithoutRowId(element) ?? false,
|
||||
);
|
||||
|
||||
// Resolve local foreign key references in pending columns
|
||||
for (final column in pendingColumns) {
|
||||
if (column.referencesColumnInSameTable != null) {
|
||||
final ref =
|
||||
column.column.constraints.whereType<ForeignKeyReference>().first;
|
||||
final referencedColumn = columns.firstWhere(
|
||||
(e) => e.nameInDart == column.referencesColumnInSameTable);
|
||||
|
||||
ref.otherColumn = referencedColumn;
|
||||
}
|
||||
}
|
||||
|
||||
if (primaryKey != null &&
|
||||
columns.any((c) => c.constraints.any((e) => e is PrimaryKeyColumn))) {
|
||||
reportError(DriftAnalysisError.forDartElement(
|
||||
|
@ -271,7 +285,8 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<Iterable<DriftColumn>> _parseColumns(ClassElement element) async {
|
||||
Future<Iterable<PendingColumnInformation>> _parseColumns(
|
||||
ClassElement element) async {
|
||||
final columnNames = element.allSupertypes
|
||||
.map((t) => t.element2)
|
||||
.followedBy([element])
|
||||
|
@ -290,8 +305,8 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
});
|
||||
|
||||
final results = await Future.wait(fields.map((field) async {
|
||||
final node = await resolver.driver.backend.loadElementDeclaration(element)
|
||||
as MethodDeclaration;
|
||||
final node = await resolver.driver.backend
|
||||
.loadElementDeclaration(field.getter!) as MethodDeclaration;
|
||||
|
||||
return await _parseColumn(node, field.getter!);
|
||||
}));
|
||||
|
@ -299,8 +314,10 @@ class DartTableResolver extends LocalElementResolver<DiscoveredDartTable> {
|
|||
return results.whereType();
|
||||
}
|
||||
|
||||
Future<DriftColumn> _parseColumn(
|
||||
MethodDeclaration declaration, Element element) async {}
|
||||
Future<PendingColumnInformation?> _parseColumn(
|
||||
MethodDeclaration declaration, Element element) async {
|
||||
return ColumnParser(this).parse(declaration, element);
|
||||
}
|
||||
}
|
||||
|
||||
class _DataClassInformation {
|
||||
|
|
|
@ -51,9 +51,7 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
|
|||
// Note: Warnings about whether the referenced column exists or not
|
||||
// are reported later, we just need to know dependencies before the
|
||||
// lint step of the analysis.
|
||||
final referenced =
|
||||
await resolver.resolveReferenceOrReportError<DriftTable>(
|
||||
this,
|
||||
final referenced = await resolveSqlReferenceOrReportError<DriftTable>(
|
||||
constraint.clause.foreignTable.tableName,
|
||||
(msg) => DriftAnalysisError.inDriftFile(
|
||||
constraint.clause.foreignTable.tableNameToken ?? constraint,
|
||||
|
@ -140,9 +138,10 @@ class DriftTableResolver extends LocalElementResolver<DiscoveredDriftTable> {
|
|||
),
|
||||
columns: columns,
|
||||
references: references.toList(),
|
||||
dartTypeName: dataClassName,
|
||||
nameOfRowClass: dataClassName,
|
||||
baseDartName: dartTableName,
|
||||
existingRowClass: existingRowClass,
|
||||
withoutRowId: table.withoutRowId,
|
||||
strict: table.isStrict,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:analyzer/dart/element/element.dart';
|
||||
|
||||
import '../driver/driver.dart';
|
||||
import '../driver/error.dart';
|
||||
import '../driver/state.dart';
|
||||
|
@ -90,6 +92,14 @@ class DriftResolver {
|
|||
'Unknown pending element $reference, this is a bug in drift_dev');
|
||||
}
|
||||
|
||||
Future<ResolveReferencedElementResult> resolveDartReference(
|
||||
DriftElementId owner, Element element) async {
|
||||
final uri = await driver.backend.uriOfDart(element.library!);
|
||||
final id = DriftElementId(uri, element.name!);
|
||||
|
||||
return resolveReferencedElement(owner, id);
|
||||
}
|
||||
|
||||
Future<ResolveReferencedElementResult> resolveReference(
|
||||
DriftElementId owner, String reference) async {
|
||||
final candidates = <DriftElementId>[];
|
||||
|
@ -125,29 +135,6 @@ class DriftResolver {
|
|||
|
||||
return resolveReferencedElement(owner, candidates.single);
|
||||
}
|
||||
|
||||
Future<T?> resolveReferenceOrReportError<T extends DriftElement>(
|
||||
LocalElementResolver owner,
|
||||
String reference,
|
||||
DriftAnalysisError Function(String msg) createError,
|
||||
) async {
|
||||
final result = await resolveReference(owner.discovered.ownId, reference);
|
||||
|
||||
if (result is ResolvedReferenceFound) {
|
||||
final element = result.element;
|
||||
if (element is T) {
|
||||
return element;
|
||||
} else {
|
||||
// todo: Better type description in error message
|
||||
owner.state.errorsDuringAnalysis.add(
|
||||
createError('Expected a $T, but got a ${element.runtimeType}'));
|
||||
}
|
||||
} else if (result is InvalidReferenceResult) {
|
||||
owner.state.errorsDuringAnalysis.add(createError(result.message));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class LocalElementResolver<T extends DiscoveredElement> {
|
||||
|
@ -162,6 +149,38 @@ abstract class LocalElementResolver<T extends DiscoveredElement> {
|
|||
state.errorsDuringAnalysis.add(error);
|
||||
}
|
||||
|
||||
Future<E?> resolveSqlReferenceOrReportError<E extends DriftElement>(
|
||||
String reference,
|
||||
DriftAnalysisError Function(String msg) createError,
|
||||
) async {
|
||||
final result = await resolver.resolveReference(discovered.ownId, reference);
|
||||
|
||||
if (result is ResolvedReferenceFound) {
|
||||
final element = result.element;
|
||||
if (element is E) {
|
||||
return element;
|
||||
} else {
|
||||
// todo: Better type description in error message
|
||||
reportError(
|
||||
createError('Expected a $T, but got a ${element.runtimeType}'));
|
||||
}
|
||||
} else {
|
||||
reportErrorForUnresolvedReference(result, createError);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void reportErrorForUnresolvedReference(ResolveReferencedElementResult result,
|
||||
DriftAnalysisError Function(String msg) createError) {
|
||||
if (result is InvalidReferenceResult) {
|
||||
reportError(createError(result.message));
|
||||
} else if (result is ReferencedElementCouldNotBeResolved) {
|
||||
reportError(createError(
|
||||
'The referenced element could not be analyzed due to a bug in drift.'));
|
||||
}
|
||||
}
|
||||
|
||||
Future<DriftElement> resolve();
|
||||
}
|
||||
|
||||
|
|
|
@ -220,6 +220,40 @@ AppliedTypeConverter? readTypeConverter(
|
|||
);
|
||||
}
|
||||
|
||||
AppliedTypeConverter readEnumConverter(
|
||||
void Function(String) reportError,
|
||||
DartType enumType,
|
||||
) {
|
||||
if (enumType is! InterfaceType) {
|
||||
reportError('Not a class: `$enumType`');
|
||||
}
|
||||
|
||||
final creatingClass = enumType.element2;
|
||||
if (creatingClass is! EnumElement) {
|
||||
reportError('Not an enum: `${creatingClass!.displayName}`');
|
||||
}
|
||||
|
||||
// `const EnumIndexConverter<EnumType>(EnumType.values)`
|
||||
final expression = AnnotatedDartCode.build((builder) {
|
||||
builder
|
||||
..addText('const ')
|
||||
..addSymbol('EnumIndexConverter', AnnotatedDartCode.drift)
|
||||
..addText('<')
|
||||
..addDartType(enumType)
|
||||
..addText('>(')
|
||||
..addDartType(enumType)
|
||||
..addText('.values)');
|
||||
});
|
||||
|
||||
return AppliedTypeConverter(
|
||||
expression: expression,
|
||||
dartType: enumType,
|
||||
sqlType: DriftSqlType.int,
|
||||
dartTypeIsNullable: false,
|
||||
sqlTypeIsNullable: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _checkParameterType(ParameterElement element, DriftColumn column,
|
||||
LocalElementResolver resolver) {
|
||||
final type = element.type;
|
||||
|
|
|
@ -79,6 +79,9 @@ class DriftColumn implements HasType {
|
|||
this.customConstraints,
|
||||
});
|
||||
|
||||
/// Whether this column has a `GENERATED AS` column constraint.
|
||||
bool get isGenerated => constraints.any((e) => e is ColumnGeneratedAs);
|
||||
|
||||
/// Whether this column was declared inside a `.drift` file.
|
||||
bool get declaredInDriftFile => declaration.isDriftDeclaration;
|
||||
|
||||
|
@ -167,15 +170,50 @@ class PrimaryKeyColumn extends DriftColumnConstraint {
|
|||
}
|
||||
|
||||
class ForeignKeyReference extends DriftColumnConstraint {
|
||||
final DriftColumn otherColumn;
|
||||
late final DriftColumn otherColumn;
|
||||
final ReferenceAction? onUpdate;
|
||||
final ReferenceAction? onDelete;
|
||||
|
||||
ForeignKeyReference(this.otherColumn, this.onUpdate, this.onDelete);
|
||||
|
||||
ForeignKeyReference.unresolved(this.onUpdate, this.onDelete);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ForeignKeyReference(to $otherColumn, onUpdate = $onUpdate, '
|
||||
'onDelete = $onDelete)';
|
||||
}
|
||||
}
|
||||
|
||||
class ColumnGeneratedAs extends DriftColumnConstraint {
|
||||
final AnnotatedDartCode dartExpression;
|
||||
final bool stored;
|
||||
|
||||
ColumnGeneratedAs(this.dartExpression, this.stored);
|
||||
}
|
||||
|
||||
/// A column with a `CHECK()` generated from a Dart expression.
|
||||
class DartCheckExpression extends DriftColumnConstraint {
|
||||
final AnnotatedDartCode dartExpression;
|
||||
|
||||
DartCheckExpression(this.dartExpression);
|
||||
}
|
||||
|
||||
class LimitingTextLength extends DriftColumnConstraint {
|
||||
final int? minLength;
|
||||
|
||||
final int? maxLength;
|
||||
|
||||
LimitingTextLength({this.minLength, this.maxLength});
|
||||
|
||||
@override
|
||||
int get hashCode => minLength.hashCode ^ maxLength.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) return false;
|
||||
final typedOther = other as LimitingTextLength;
|
||||
return typedOther.minLength == minLength &&
|
||||
typedOther.maxLength == maxLength;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,8 @@ import 'package:json_annotation/json_annotation.dart';
|
|||
part '../../generated/analysis/results/dart.g.dart';
|
||||
|
||||
class AnnotatedDartCode {
|
||||
static final Uri drift = Uri.parse('package:drift/drift.dart');
|
||||
|
||||
final List<dynamic /* String|DartTopLevelSymbol */ > elements;
|
||||
|
||||
AnnotatedDartCode(this.elements);
|
||||
|
@ -115,7 +117,7 @@ class DartTopLevelSymbol {
|
|||
DartTopLevelSymbol(this.lexeme, this.importUri);
|
||||
|
||||
factory DartTopLevelSymbol.topLevelElement(Element element) {
|
||||
assert(element.enclosingElement3 is LibraryElement);
|
||||
assert(element.library?.topLevelElements.contains(element) == true);
|
||||
|
||||
// We're using this to recover the right import URI when using
|
||||
// `package:build`:
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:drift/drift.dart' show DriftSqlType;
|
||||
|
||||
import 'dart.dart';
|
||||
import 'element.dart';
|
||||
|
||||
|
@ -27,7 +29,7 @@ class DriftTable extends DriftElementWithResultSet {
|
|||
final String baseDartName;
|
||||
|
||||
/// The name for the data class associated with this table
|
||||
final String dartTypeName;
|
||||
final String nameOfRowClass;
|
||||
|
||||
final bool withoutRowId;
|
||||
|
||||
|
@ -38,7 +40,7 @@ class DriftTable extends DriftElementWithResultSet {
|
|||
super.declaration, {
|
||||
required this.columns,
|
||||
required this.baseDartName,
|
||||
required this.dartTypeName,
|
||||
required this.nameOfRowClass,
|
||||
this.references = const [],
|
||||
this.existingRowClass,
|
||||
this.customParentClass,
|
||||
|
@ -53,6 +55,43 @@ class DriftTable extends DriftElementWithResultSet {
|
|||
}
|
||||
}
|
||||
|
||||
/// The primary key for this table, computed by looking at the
|
||||
/// [primaryKeyFromTableConstraint] and primary key constraints applied to
|
||||
/// individiual columns.
|
||||
Set<DriftColumn> get fullPrimaryKey {
|
||||
if (primaryKeyFromTableConstraint != null) {
|
||||
return primaryKeyFromTableConstraint!;
|
||||
}
|
||||
|
||||
return columns
|
||||
.where((c) => c.constraints.any((f) => f is PrimaryKeyColumn))
|
||||
.toSet();
|
||||
}
|
||||
|
||||
/// Determines whether [column] would be required for inserts performed via
|
||||
/// companions.
|
||||
bool isColumnRequiredForInsert(DriftColumn column) {
|
||||
assert(columns.contains(column));
|
||||
|
||||
if (column.defaultArgument != null ||
|
||||
column.clientDefaultCode != null ||
|
||||
column.nullable ||
|
||||
column.isGenerated) {
|
||||
// default value would be applied, so it's not required for inserts
|
||||
return false;
|
||||
}
|
||||
|
||||
// A column isn't required if it's an alias for the rowid, as explained
|
||||
// at https://www.sqlite.org/lang_createtable.html#rowid
|
||||
final fullPk = fullPrimaryKey;
|
||||
final isAliasForRowId = !withoutRowId &&
|
||||
column.sqlType == DriftSqlType.int &&
|
||||
fullPk.length == 1 &&
|
||||
fullPk.single == column;
|
||||
|
||||
return !isAliasForRowId;
|
||||
}
|
||||
|
||||
@override
|
||||
String get entityInfoName {
|
||||
// if this table was parsed from sql, a user might want to refer to it
|
||||
|
@ -61,7 +100,7 @@ class DriftTable extends DriftElementWithResultSet {
|
|||
// "$UsersTable".
|
||||
final name =
|
||||
fixedEntityInfoName ?? _tableInfoNameForTableClass(baseDartName);
|
||||
if (name == dartTypeName) {
|
||||
if (name == nameOfRowClass) {
|
||||
// resolve clashes if the table info class has the same name as the data
|
||||
// class. This can happen because the data class name can be specified by
|
||||
// the user.
|
||||
|
|
|
@ -32,6 +32,19 @@ class ElementSerializer {
|
|||
for (final column in element.columns) _serializeColumn(column),
|
||||
],
|
||||
'existing_data_class': element.existingRowClass?.toJson(),
|
||||
'primary_key_table_constraint': element.primaryKeyFromTableConstraint
|
||||
?.map((e) => e.nameInSql)
|
||||
.toList(),
|
||||
'unique_keys_table_constraint': [
|
||||
for (final unique in element.uniqueKeysFromTableConstraint)
|
||||
[for (final column in unique) column.nameInSql]
|
||||
],
|
||||
'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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -225,17 +238,49 @@ abstract class ElementDeserializer {
|
|||
|
||||
switch (type) {
|
||||
case 'table':
|
||||
final columns = [
|
||||
for (final rawColumn in json['columns'] as List)
|
||||
await _readColumn(rawColumn as Map),
|
||||
];
|
||||
final columnByName = {
|
||||
for (final column in columns) column.nameInSql: column,
|
||||
};
|
||||
|
||||
Set<DriftColumn>? primaryKeyFromTableConstraint;
|
||||
final serializedPk = json['primary_key_table_constraint'];
|
||||
if (serializedPk != null) {
|
||||
primaryKeyFromTableConstraint = {
|
||||
for (final entry in serializedPk) columnByName[entry]!,
|
||||
};
|
||||
}
|
||||
|
||||
List<Set<DriftColumn>> uniqueKeysFromTableConstraint = const [];
|
||||
final serializedUnique = json['unique_keys_table_constraint'];
|
||||
if (serializedUnique != null) {
|
||||
uniqueKeysFromTableConstraint = [
|
||||
for (final entry in serializedUnique)
|
||||
{for (final column in entry) columnByName[column]!},
|
||||
];
|
||||
}
|
||||
|
||||
return DriftTable(
|
||||
id,
|
||||
declaration,
|
||||
references: references,
|
||||
columns: [
|
||||
for (final rawColumn in json['columns'] as List)
|
||||
await _readColumn(rawColumn as Map),
|
||||
],
|
||||
columns: columns,
|
||||
existingRowClass: json['existing_data_class'] != null
|
||||
? ExistingRowClass.fromJson(json['existing_data_class'] as Map)
|
||||
: null,
|
||||
primaryKeyFromTableConstraint: primaryKeyFromTableConstraint,
|
||||
uniqueKeysFromTableConstraint: uniqueKeysFromTableConstraint,
|
||||
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,
|
||||
);
|
||||
default:
|
||||
throw UnimplementedError('Unsupported element type: $type');
|
||||
|
|
|
@ -25,6 +25,12 @@ class DriftBuildBackend extends DriftBackend {
|
|||
return _buildStep.readAsString(AssetId.resolve(uri));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Uri> uriOfDart(Element element) async {
|
||||
final id = await _buildStep.resolver.assetIdForElement(element);
|
||||
return id.uri;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LibraryElement> readDart(Uri uri) {
|
||||
return _buildStep.resolver.libraryFor(AssetId.resolve(uri));
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:drift_dev/src/analysis/driver/state.dart';
|
||||
import 'package:drift_dev/src/analysis/results/results.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../test_utils.dart';
|
||||
|
||||
void main() {
|
||||
late TestBackend backend;
|
||||
|
||||
setUpAll(() {
|
||||
backend = TestBackend.inTest({
|
||||
'a|lib/main.dart': '''
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
TypeConverter<Dart, SQL> typeConverter<Dart, SQL>() {
|
||||
throw 'stub';
|
||||
}
|
||||
|
||||
class TableWithCustomName extends Table {
|
||||
@override String get tableName => 'my-fancy-table';
|
||||
|
||||
@override bool get withoutRowId => true;
|
||||
}
|
||||
|
||||
class Users extends Table {
|
||||
/// The user id
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
/// The username
|
||||
///
|
||||
/// The username must be between 6-32 characters
|
||||
TextColumn get name => text().named("user_name").withLength(min: 6, max: 32)();
|
||||
TextColumn get onlyMax => text().withLength(max: 100)();
|
||||
|
||||
DateTimeColumn get defaults => dateTime().withDefault(currentDate)();
|
||||
}
|
||||
|
||||
class CustomPrimaryKey extends Table {
|
||||
IntColumn get partA => integer()();
|
||||
IntColumn get partB => integer().customConstraint('custom')();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {partA, partB};
|
||||
}
|
||||
|
||||
class WrongName extends Table {
|
||||
String constructTableName() => 'my-table-name';
|
||||
String get tableName => constructTableName();
|
||||
}
|
||||
|
||||
mixin IntIdTable on Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
}
|
||||
|
||||
abstract class HasNameTable extends Table {
|
||||
TextColumn get name => text()();
|
||||
}
|
||||
|
||||
class Foos extends HasNameTable with IntIdTable {
|
||||
TextColumn get name => text().nullable()();
|
||||
}
|
||||
|
||||
class Socks extends Table {
|
||||
TextColumn get name => text()();
|
||||
IntColumn get id => integer()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class ArchivedSocks extends Socks {
|
||||
TextColumn get archivedBy => text()();
|
||||
DateTimeColumn get archivedOn => dateTime()();
|
||||
}
|
||||
|
||||
class WithAliasForRowId extends Table {
|
||||
IntColumn get id => integer()();
|
||||
TextColumn get name => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class PrimaryKeyAndAutoIncrement extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get other => text()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {other};
|
||||
}
|
||||
|
||||
class InvalidConstraints extends Table {
|
||||
IntColumn get a => integer().autoIncrement().customConstraint('foo')();
|
||||
IntColumn get b => integer().customConstraint('a').customConstraint('b')();
|
||||
}
|
||||
''',
|
||||
});
|
||||
});
|
||||
|
||||
tearDownAll(() => backend.dispose());
|
||||
|
||||
final uri = Uri.parse('package:a/main.dart');
|
||||
|
||||
Future<ElementAnalysisState?> findTable(String dartName) async {
|
||||
final state = await backend.driver.fullyAnalyze(uri);
|
||||
|
||||
return state.analysis.values.firstWhereOrNull((e) {
|
||||
final result = e.result;
|
||||
if (result is DriftTable) {
|
||||
return result.baseDartName == dartName;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
group('table names', () {
|
||||
test('use overridden name', () async {
|
||||
final result = await findTable('TableWithCustomName');
|
||||
final table = result!.result as DriftTable;
|
||||
|
||||
expect(result.errorsDuringAnalysis, isEmpty);
|
||||
expect(table.schemaName, 'my-fancy-table');
|
||||
expect(table.withoutRowId, isTrue);
|
||||
});
|
||||
|
||||
test('use re-cased class name', () async {
|
||||
final parsed = await findTable('Users');
|
||||
final table = parsed!.result as DriftTable;
|
||||
|
||||
expect(parsed.errorsDuringAnalysis, isEmpty);
|
||||
expect(table.schemaName, 'users');
|
||||
});
|
||||
|
||||
test('reports discovery error for table with wrong name', () async {
|
||||
final state = await backend.driver.fullyAnalyze(uri);
|
||||
expect(state.errorsDuringDiscovery, [
|
||||
isDriftError(
|
||||
contains('This getter must directly return a string literal'),
|
||||
).withSpan('tableName'),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
group('Columns', () {
|
||||
test('should use field name if no name has been set explicitly', () async {
|
||||
final result = await findTable('Users');
|
||||
final table = result!.result as DriftTable;
|
||||
final idColumn =
|
||||
table.columns.singleWhere((col) => col.nameInDart == 'id');
|
||||
|
||||
expect(idColumn.nameInSql, 'id');
|
||||
});
|
||||
|
||||
test('should use explicit name, if it exists', () async {
|
||||
final result = await findTable('Users');
|
||||
final table = result!.result as DriftTable;
|
||||
final idColumn =
|
||||
table.columns.singleWhere((col) => col.nameInDart == 'name');
|
||||
|
||||
expect(idColumn.nameInSql, 'user_name');
|
||||
});
|
||||
|
||||
test('should parse min and max length for text columns', () async {
|
||||
final result = await findTable('Users');
|
||||
final table = result!.result as DriftTable;
|
||||
final idColumn =
|
||||
table.columns.singleWhere((col) => col.nameInDart == 'name');
|
||||
|
||||
expect(idColumn.constraints,
|
||||
contains(LimitingTextLength(minLength: 6, maxLength: 32)));
|
||||
});
|
||||
|
||||
test('should only parse max length when relevant', () async {
|
||||
final table = (await findTable('Users'))!.result as DriftTable;
|
||||
final idColumn =
|
||||
table.columns.singleWhere((col) => col.nameInDart == 'onlyMax');
|
||||
|
||||
expect(
|
||||
idColumn.constraints, contains(LimitingTextLength(maxLength: 100)));
|
||||
});
|
||||
|
||||
test('parses custom constraints', () async {
|
||||
final table = (await findTable('CustomPrimaryKey'))!.result as DriftTable;
|
||||
|
||||
final partA = table.columns.singleWhere((c) => c.nameInDart == 'partA');
|
||||
final partB = table.columns.singleWhere((c) => c.nameInDart == 'partB');
|
||||
|
||||
expect(partB.customConstraints, 'custom');
|
||||
expect(partA.customConstraints, isNull);
|
||||
});
|
||||
|
||||
test('parsed default values', () async {
|
||||
final table = (await findTable('Users'))!.result as DriftTable;
|
||||
final defaultsColumn =
|
||||
table.columns.singleWhere((c) => c.nameInSql == 'defaults');
|
||||
|
||||
expect(defaultsColumn.defaultArgument.toString(), 'currentDate');
|
||||
});
|
||||
|
||||
test('parses documentation comments', () async {
|
||||
final table = (await findTable('Users'))!.result as DriftTable;
|
||||
final idColumn =
|
||||
table.columns.singleWhere((col) => col.nameInSql == 'id');
|
||||
|
||||
final usernameColumn =
|
||||
table.columns.singleWhere((col) => col.nameInSql == 'user_name');
|
||||
|
||||
expect(idColumn.documentationComment, '/// The user id');
|
||||
expect(
|
||||
usernameColumn.documentationComment,
|
||||
'/// The username\n///\n/// The username must be between 6-32 characters',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('parses custom primary keys', () async {
|
||||
final table = (await findTable('CustomPrimaryKey'))!.result as DriftTable;
|
||||
|
||||
expect(table.primaryKeyFromTableConstraint, containsAll(table.columns));
|
||||
expect(
|
||||
table.columns.any(
|
||||
(column) => column.constraints.any((c) => c is PrimaryKeyColumn)),
|
||||
isFalse,
|
||||
);
|
||||
});
|
||||
|
||||
test('warns when using primaryKey and autoIncrement()', () async {
|
||||
final result = await findTable('PrimaryKeyAndAutoIncrement');
|
||||
|
||||
expect(
|
||||
result!.errorsDuringAnalysis,
|
||||
contains(isDriftError(
|
||||
contains('override primaryKey and use autoIncrement()'))),
|
||||
);
|
||||
});
|
||||
|
||||
test('recognizes aliases for rowid', () async {
|
||||
final table = (await findTable('WithAliasForRowId'))!.result as DriftTable;
|
||||
final idColumn = table.columns.singleWhere((c) => c.nameInSql == 'id');
|
||||
|
||||
expect(table.isColumnRequiredForInsert(idColumn), isFalse);
|
||||
});
|
||||
|
||||
group('inheritance', () {
|
||||
test('from abstract classes or mixins', () async {
|
||||
final table = (await findTable('Foos'))!.result as DriftTable;
|
||||
|
||||
expect(table.columns, hasLength(2));
|
||||
expect(
|
||||
table.columns.map((c) => c.nameInSql), containsAll(['id', 'name']));
|
||||
});
|
||||
|
||||
test('from regular classes', () async {
|
||||
final socks = (await findTable('Socks'))!.result as DriftTable;
|
||||
final archivedSocks =
|
||||
(await findTable('ArchivedSocks'))!.result as DriftTable;
|
||||
|
||||
expect(socks.columns, hasLength(2));
|
||||
expect(socks.columns.map((c) => c.nameInSql), ['name', 'id']);
|
||||
|
||||
expect(archivedSocks.columns, hasLength(4));
|
||||
expect(archivedSocks.columns.map((c) => c.nameInSql),
|
||||
['name', 'id', 'archived_by', 'archived_on']);
|
||||
expect(
|
||||
archivedSocks.primaryKeyFromTableConstraint!.map((e) => e.nameInSql),
|
||||
['id']);
|
||||
});
|
||||
});
|
||||
|
||||
test('reports errors around suspicous customConstraint uses', () async {
|
||||
final result = await findTable('InvalidConstraints');
|
||||
|
||||
expect(
|
||||
result!.errorsDuringAnalysis,
|
||||
containsAll(
|
||||
[
|
||||
isDriftError(allOf(contains('both drift-defined constraints'),
|
||||
contains('and a customConstraint()')))
|
||||
.withSpan('a'),
|
||||
isDriftError(contains(
|
||||
"You've already set custom constraints on this column"))
|
||||
.withSpan('customConstraint'),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
|
@ -137,6 +137,12 @@ Matcher get hasNoErrors => isA<FileState>()
|
|||
.having((e) => e.analysis.values.expand((e) => e.errorsDuringAnalysis),
|
||||
'(errors in analyzed elements)', isEmpty);
|
||||
|
||||
Matcher isDriftError(dynamic message) {
|
||||
TypeMatcher<DriftAnalysisError> isDriftError(dynamic message) {
|
||||
return isA<DriftAnalysisError>().having((e) => e.message, 'message', message);
|
||||
}
|
||||
|
||||
extension DriftErrorMatchers on TypeMatcher<DriftAnalysisError> {
|
||||
TypeMatcher<DriftAnalysisError> withSpan(lexemeMatcher) {
|
||||
return having((e) => e.span?.text, 'span.text', lexemeMatcher);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue