API and parser for custom types

This commit is contained in:
Simon Binder 2019-07-18 12:02:16 +02:00
parent dcc7f29492
commit c2bff3ae42
No known key found for this signature in database
GPG Key ID: 7891917E4147B8C0
16 changed files with 200 additions and 66 deletions

View File

@ -1,3 +1,6 @@
## unreleased
- Support custom columns
## 1.6.0 ## 1.6.0
- Experimental web support! See [the documentation](https://moor.simonbinder.eu/web) for details. - Experimental web support! See [the documentation](https://moor.simonbinder.eu/web) for details.
- Make transactions easier to use: Thanks to some Dart async magic, you no longer need to run - Make transactions easier to use: Thanks to some Dart async magic, you no longer need to run

View File

@ -107,6 +107,10 @@ class ColumnBuilder<
/// store the current date/time as a default value. /// store the current date/time as a default value.
Builder withDefault(Expression<ResultDartType, ResultSqlType> e) => null; Builder withDefault(Expression<ResultDartType, ResultSqlType> e) => null;
/// Uses a custom [converter] to store custom Dart objects in a single column
/// and automatically mapping them from and to sql.
Builder map<T>(TypeConverter<T, ResultDartType> converter) => null;
/// Turns this column builder into a column. This method won't actually be /// Turns this column builder into a column. This method won't actually be
/// called in your code. Instead, moor_generator will take a look at your /// called in your code. Instead, moor_generator will take a look at your
/// source code to figure out your table structure. /// source code to figure out your table structure.

View File

@ -93,7 +93,6 @@ abstract class Table {
/// ``` /// ```
/// RealColumn get averageSpeed => real()(); /// RealColumn get averageSpeed => real()();
/// ``` /// ```
/// Note
@protected @protected
RealColumnBuilder real() => null; RealColumnBuilder real() => null;
} }

View File

@ -25,7 +25,9 @@ class InsertStatement<D extends DataClass> {
/// Otherwise, an exception will be thrown. /// Otherwise, an exception will be thrown.
/// ///
/// If the table contains an auto-increment column, the generated value will /// If the table contains an auto-increment column, the generated value will
/// be returned. /// be returned. If there is no auto-increment column, you can't rely on the
/// return value, but the future will resolve to an error when the insert
/// fails.
Future<int> insert(Insertable<D> entity, {bool orReplace = false}) async { Future<int> insert(Insertable<D> entity, {bool orReplace = false}) async {
_validateIntegrity(entity); _validateIntegrity(entity);
final ctx = _createContext(entity, orReplace); final ctx = _createContext(entity, orReplace);

View File

@ -0,0 +1,20 @@
part of 'sql_types.dart';
/// Maps a custom dart object of type [D] into a primitive type [S] understood
/// by the sqlite backend.
///
/// Moor currently supports [DateTime], [double], [int], [Uint8List], [bool]
/// and [String] for [S].
abstract class TypeConverter<D, S> {
/// Empty constant constructor so that subclasses can have a constant
/// constructor.
const TypeConverter();
/// Map a value from an object in Dart into something that will be understood
/// by the database. Be aware that [value] is nullable.
S mapToSql(D value);
/// Maps a column from the database back to Dart. Be aware that [fromDb] is
/// nullable.
D mapToDart(S fromDb);
}

View File

@ -1,5 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
part 'custom_type.dart';
/// A type that can be mapped from Dart to sql. The generic type parameter here /// A type that can be mapped from Dart to sql. The generic type parameter here
/// denotes the resolved dart type. /// denotes the resolved dart type.
abstract class SqlType<T> { abstract class SqlType<T> {

View File

@ -50,6 +50,8 @@ class SharedTodos extends Table {
class TableWithoutPK extends Table { class TableWithoutPK extends Table {
IntColumn get notReallyAnId => integer()(); IntColumn get notReallyAnId => integer()();
RealColumn get someFloat => real()(); RealColumn get someFloat => real()();
TextColumn get custom => text().map(const CustomConverter())();
} }
class PureDefaults extends Table { class PureDefaults extends Table {
@ -57,6 +59,25 @@ class PureDefaults extends Table {
TextColumn get txt => text().nullable()(); TextColumn get txt => text().nullable()();
} }
// example object used for custom mapping
class MyCustomObject {
final String data;
MyCustomObject(this.data);
}
class CustomConverter extends TypeConverter<MyCustomObject, String> {
const CustomConverter();
@override
MyCustomObject mapToDart(String fromDb) {
return fromDb == null ? null : MyCustomObject(fromDb);
}
@override
String mapToSql(MyCustomObject value) {
return value?.data;
}
}
@UseMoor( @UseMoor(
tables: [ tables: [
TodosTable, TodosTable,

View File

@ -842,18 +842,25 @@ class TableWithoutPKData extends DataClass
implements Insertable<TableWithoutPKData> { implements Insertable<TableWithoutPKData> {
final int notReallyAnId; final int notReallyAnId;
final double someFloat; final double someFloat;
TableWithoutPKData({@required this.notReallyAnId, @required this.someFloat}); final MyCustomObject custom;
TableWithoutPKData(
{@required this.notReallyAnId,
@required this.someFloat,
@required this.custom});
factory TableWithoutPKData.fromData( factory TableWithoutPKData.fromData(
Map<String, dynamic> data, GeneratedDatabase db, Map<String, dynamic> data, GeneratedDatabase db,
{String prefix}) { {String prefix}) {
final effectivePrefix = prefix ?? ''; final effectivePrefix = prefix ?? '';
final intType = db.typeSystem.forDartType<int>(); final intType = db.typeSystem.forDartType<int>();
final doubleType = db.typeSystem.forDartType<double>(); final doubleType = db.typeSystem.forDartType<double>();
final myCustomObjectType = db.typeSystem.forDartType<MyCustomObject>();
return TableWithoutPKData( return TableWithoutPKData(
notReallyAnId: intType notReallyAnId: intType
.mapFromDatabaseResponse(data['${effectivePrefix}not_really_an_id']), .mapFromDatabaseResponse(data['${effectivePrefix}not_really_an_id']),
someFloat: doubleType someFloat: doubleType
.mapFromDatabaseResponse(data['${effectivePrefix}some_float']), .mapFromDatabaseResponse(data['${effectivePrefix}some_float']),
custom: myCustomObjectType
.mapFromDatabaseResponse(data['${effectivePrefix}custom']),
); );
} }
factory TableWithoutPKData.fromJson(Map<String, dynamic> json, factory TableWithoutPKData.fromJson(Map<String, dynamic> json,
@ -861,6 +868,7 @@ class TableWithoutPKData extends DataClass
return TableWithoutPKData( return TableWithoutPKData(
notReallyAnId: serializer.fromJson<int>(json['notReallyAnId']), notReallyAnId: serializer.fromJson<int>(json['notReallyAnId']),
someFloat: serializer.fromJson<double>(json['someFloat']), someFloat: serializer.fromJson<double>(json['someFloat']),
custom: serializer.fromJson<MyCustomObject>(json['custom']),
); );
} }
@override @override
@ -869,6 +877,7 @@ class TableWithoutPKData extends DataClass
return { return {
'notReallyAnId': serializer.toJson<int>(notReallyAnId), 'notReallyAnId': serializer.toJson<int>(notReallyAnId),
'someFloat': serializer.toJson<double>(someFloat), 'someFloat': serializer.toJson<double>(someFloat),
'custom': serializer.toJson<MyCustomObject>(custom),
}; };
} }
@ -882,40 +891,49 @@ class TableWithoutPKData extends DataClass
someFloat: someFloat == null && nullToAbsent someFloat: someFloat == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(someFloat), : Value(someFloat),
custom:
custom == null && nullToAbsent ? const Value.absent() : Value(custom),
) as T; ) as T;
} }
TableWithoutPKData copyWith({int notReallyAnId, double someFloat}) => TableWithoutPKData copyWith(
{int notReallyAnId, double someFloat, MyCustomObject custom}) =>
TableWithoutPKData( TableWithoutPKData(
notReallyAnId: notReallyAnId ?? this.notReallyAnId, notReallyAnId: notReallyAnId ?? this.notReallyAnId,
someFloat: someFloat ?? this.someFloat, someFloat: someFloat ?? this.someFloat,
custom: custom ?? this.custom,
); );
@override @override
String toString() { String toString() {
return (StringBuffer('TableWithoutPKData(') return (StringBuffer('TableWithoutPKData(')
..write('notReallyAnId: $notReallyAnId, ') ..write('notReallyAnId: $notReallyAnId, ')
..write('someFloat: $someFloat') ..write('someFloat: $someFloat, ')
..write('custom: $custom')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => int get hashCode => $mrjf($mrjc(
$mrjf($mrjc($mrjc(0, notReallyAnId.hashCode), someFloat.hashCode)); $mrjc($mrjc(0, notReallyAnId.hashCode), someFloat.hashCode),
custom.hashCode));
@override @override
bool operator ==(other) => bool operator ==(other) =>
identical(this, other) || identical(this, other) ||
(other is TableWithoutPKData && (other is TableWithoutPKData &&
other.notReallyAnId == notReallyAnId && other.notReallyAnId == notReallyAnId &&
other.someFloat == someFloat); other.someFloat == someFloat &&
other.custom == custom);
} }
class TableWithoutPKCompanion extends UpdateCompanion<TableWithoutPKData> { class TableWithoutPKCompanion extends UpdateCompanion<TableWithoutPKData> {
final Value<int> notReallyAnId; final Value<int> notReallyAnId;
final Value<double> someFloat; final Value<double> someFloat;
final Value<MyCustomObject> custom;
const TableWithoutPKCompanion({ const TableWithoutPKCompanion({
this.notReallyAnId = const Value.absent(), this.notReallyAnId = const Value.absent(),
this.someFloat = const Value.absent(), this.someFloat = const Value.absent(),
this.custom = const Value.absent(),
}); });
} }
@ -950,8 +968,20 @@ class $TableWithoutPKTable extends TableWithoutPK
); );
} }
final VerificationMeta _customMeta = const VerificationMeta('custom');
GeneratedTextColumn _custom;
@override @override
List<GeneratedColumn> get $columns => [notReallyAnId, someFloat]; GeneratedTextColumn get custom => _custom ??= _constructCustom();
GeneratedTextColumn _constructCustom() {
return GeneratedTextColumn(
'custom',
$tableName,
false,
);
}
@override
List<GeneratedColumn> get $columns => [notReallyAnId, someFloat, custom];
@override @override
$TableWithoutPKTable get asDslTable => this; $TableWithoutPKTable get asDslTable => this;
@override @override
@ -976,6 +1006,12 @@ class $TableWithoutPKTable extends TableWithoutPK
} else if (someFloat.isRequired && isInserting) { } else if (someFloat.isRequired && isInserting) {
context.missing(_someFloatMeta); context.missing(_someFloatMeta);
} }
if (d.custom.present) {
context.handle(
_customMeta, custom.isAcceptableValue(d.custom.value, _customMeta));
} else if (custom.isRequired && isInserting) {
context.missing(_customMeta);
}
return context; return context;
} }
@ -996,6 +1032,9 @@ class $TableWithoutPKTable extends TableWithoutPK
if (d.someFloat.present) { if (d.someFloat.present) {
map['some_float'] = Variable<double, RealType>(d.someFloat.value); map['some_float'] = Variable<double, RealType>(d.someFloat.value);
} }
if (d.custom.present) {
map['custom'] = Variable<MyCustomObject, StringType>(d.custom.value);
}
return map; return map;
} }

View File

@ -26,9 +26,8 @@ void main() {
test('can insert floating point values', () async { test('can insert floating point values', () async {
// regression test for https://github.com/simolus3/moor/issues/30 // regression test for https://github.com/simolus3/moor/issues/30
await db await db.into(db.tableWithoutPK).insert(
.into(db.tableWithoutPK) TableWithoutPKData(notReallyAnId: 42, someFloat: 3.1415, custom: null));
.insert(TableWithoutPKData(notReallyAnId: 42, someFloat: 3.1415));
verify(executor.runInsert( verify(executor.runInsert(
'INSERT INTO table_without_p_k ' 'INSERT INTO table_without_p_k '

View File

@ -16,11 +16,12 @@ class DaoGenerator extends GeneratorForAnnotation<UseDao> {
@override @override
generateForAnnotatedElement( generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) { Element element, ConstantReader annotation, BuildStep buildStep) async {
final tableTypes = final tableTypes =
annotation.peek('tables').listValue.map((obj) => obj.toTypeValue()); annotation.peek('tables').listValue.map((obj) => obj.toTypeValue());
final parsedTables = final parsedTables = await Stream.fromIterable(tableTypes)
tableTypes.map((type) => state.parseType(type, element)).toList(); .asyncMap((type) => state.parseType(type, element))
.toList();
final queries = annotation.peek('queries')?.mapValue ?? {}; final queries = annotation.peek('queries')?.mapValue ?? {};
if (element is! ClassElement) { if (element is! ClassElement) {

View File

@ -1,4 +1,5 @@
import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:built_value/built_value.dart'; import 'package:built_value/built_value.dart';
part 'specified_column.g.dart'; part 'specified_column.g.dart';
@ -99,9 +100,27 @@ class SpecifiedColumn {
/// expression. /// expression.
final Expression defaultArgument; final Expression defaultArgument;
/// If a type converter has been specified as the argument of
/// ColumnBuilder.map, this contains the Dart code that references that type
/// converter.
final Expression typeConverter;
/// If the type of this column has been overridden, contains the actual Dart
/// type. Otherwise null.
///
/// Column types can be overridden with type converters. For instance, if
/// `C` was a type converter that converts `D` to `num`s, the column generated
/// by `real().map(const C())()` would have type `D` instead of `num`.
final DartType overriddenDartType;
/// The dart type that matches the values of this column. For instance, if a /// The dart type that matches the values of this column. For instance, if a
/// table has declared an `IntColumn`, the matching dart type name would be [int]. /// table has declared an `IntColumn`, the matching dart type name would be [int].
String get dartTypeName => dartTypeNames[type]; String get dartTypeName {
if (overriddenDartType != null) {
return overriddenDartType.name;
}
return dartTypeNames[type];
}
/// The column type from the dsl library. For instance, if a table has /// The column type from the dsl library. For instance, if a table has
/// declared an `IntColumn`, the matching dsl column name would also be an /// declared an `IntColumn`, the matching dsl column name would also be an
@ -148,6 +167,8 @@ class SpecifiedColumn {
this.nullable = false, this.nullable = false,
this.features = const [], this.features = const [],
this.defaultArgument, this.defaultArgument,
this.typeConverter,
this.overriddenDartType,
}); });
} }

View File

@ -19,7 +19,7 @@ class MoorGenerator extends GeneratorForAnnotation<UseMoor> {
@override @override
generateForAnnotatedElement( generateForAnnotatedElement(
Element element, ConstantReader annotation, BuildStep buildStep) { Element element, ConstantReader annotation, BuildStep buildStep) async {
final tableTypes = final tableTypes =
annotation.peek('tables').listValue.map((obj) => obj.toTypeValue()); annotation.peek('tables').listValue.map((obj) => obj.toTypeValue());
final daoTypes = annotation final daoTypes = annotation
@ -33,7 +33,7 @@ class MoorGenerator extends GeneratorForAnnotation<UseMoor> {
var resolvedQueries = <SqlQuery>[]; var resolvedQueries = <SqlQuery>[];
for (var table in tableTypes) { for (var table in tableTypes) {
tablesForThisDb.add(state.parseType(table, element)); tablesForThisDb.add(await state.parseType(table, element));
} }
if (queries.isNotEmpty) { if (queries.isNotEmpty) {

View File

@ -1,5 +1,6 @@
import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:moor_generator/src/errors.dart'; import 'package:moor_generator/src/errors.dart';
import 'package:moor_generator/src/model/specified_column.dart'; import 'package:moor_generator/src/model/specified_column.dart';
import 'package:moor_generator/src/parser/parser.dart'; import 'package:moor_generator/src/parser/parser.dart';
@ -20,7 +21,7 @@ final Set<String> starters = {
startBool, startBool,
startDateTime, startDateTime,
startBlob, startBlob,
startReal startReal,
}; };
const String _methodNamed = 'named'; const String _methodNamed = 'named';
@ -31,6 +32,7 @@ const String _methodWithLength = 'withLength';
const String _methodNullable = 'nullable'; const String _methodNullable = 'nullable';
const String _methodCustomConstraint = 'customConstraint'; const String _methodCustomConstraint = 'customConstraint';
const String _methodDefault = 'withDefault'; const String _methodDefault = 'withDefault';
const String _methodMap = 'map';
const String _errorMessage = 'This getter does not create a valid column that ' const String _errorMessage = 'This getter does not create a valid column that '
'can be parsed by moor. Please refer to the readme from moor to see how ' 'can be parsed by moor. Please refer to the readme from moor to see how '
@ -49,6 +51,7 @@ class ColumnParser extends ParserBase {
we can extract what it means for the column (name, auto increment, PK, we can extract what it means for the column (name, auto increment, PK,
constraints...). constraints...).
*/ */
final expr = returnExpressionOfMethod(getter); final expr = returnExpressionOfMethod(getter);
if (!(expr is FunctionExpressionInvocation)) { if (!(expr is FunctionExpressionInvocation)) {
@ -68,6 +71,8 @@ class ColumnParser extends ParserBase {
String foundExplicitName; String foundExplicitName;
String foundCustomConstraint; String foundCustomConstraint;
Expression foundDefaultExpression; Expression foundDefaultExpression;
Expression foundTypeConverter;
DartType overrideDartType;
var wasDeclaredAsPrimaryKey = false; var wasDeclaredAsPrimaryKey = false;
var nullable = false; var nullable = false;
@ -148,6 +153,18 @@ class ColumnParser extends ParserBase {
final args = remainingExpr.argumentList; final args = remainingExpr.argumentList;
final expression = args.arguments.single; final expression = args.arguments.single;
foundDefaultExpression = expression; foundDefaultExpression = expression;
break;
case _methodMap:
final args = remainingExpr.argumentList;
final expression = args.arguments.single;
// the map method has a parameter type that resolved to the runtime
// type of the custom object
final type = remainingExpr.typeArgumentTypes.single;
foundTypeConverter = expression;
overrideDartType = type;
break;
} }
// We're not at a starting method yet, so we need to go deeper! // We're not at a starting method yet, so we need to go deeper!
@ -172,6 +189,8 @@ class ColumnParser extends ParserBase {
nullable: nullable, nullable: nullable,
features: foundFeatures, features: foundFeatures,
defaultArgument: foundDefaultExpression, defaultArgument: foundDefaultExpression,
typeConverter: foundTypeConverter,
overriddenDartType: overrideDartType,
); );
} }

View File

@ -13,18 +13,18 @@ import 'package:moor/sqlite_keywords.dart';
class TableParser extends ParserBase { class TableParser extends ParserBase {
TableParser(SharedState state) : super(state); TableParser(SharedState state) : super(state);
SpecifiedTable parse(ClassElement element) { Future<SpecifiedTable> parse(ClassElement element) async {
final sqlName = _parseTableName(element); final sqlName = await _parseTableName(element);
if (sqlName == null) return null; if (sqlName == null) return null;
final columns = _parseColumns(element); final columns = await _parseColumns(element);
return SpecifiedTable( return SpecifiedTable(
fromClass: element, fromClass: element,
columns: columns, columns: columns,
sqlName: escapeIfNeeded(sqlName), sqlName: escapeIfNeeded(sqlName),
dartTypeName: _readDartTypeName(element), dartTypeName: _readDartTypeName(element),
primaryKey: _readPrimaryKey(element, columns), primaryKey: await _readPrimaryKey(element, columns),
); );
} }
@ -40,7 +40,7 @@ class TableParser extends ParserBase {
} }
} }
String _parseTableName(ClassElement element) { Future<String> _parseTableName(ClassElement element) async {
// todo allow override via a field (final String tableName = '') as well // todo allow override via a field (final String tableName = '') as well
final tableNameGetter = element.getGetter('tableName'); final tableNameGetter = element.getGetter('tableName');
@ -52,7 +52,8 @@ class TableParser extends ParserBase {
// we expect something like get tableName => "myTableName", the getter // we expect something like get tableName => "myTableName", the getter
// must do nothing more complicated // must do nothing more complicated
final tableNameDeclaration = state.loadElementDeclaration(tableNameGetter); final tableNameDeclaration =
await state.loadElementDeclaration(tableNameGetter);
final returnExpr = returnExpressionOfMethod( final returnExpr = returnExpressionOfMethod(
tableNameDeclaration.node as MethodDeclaration); tableNameDeclaration.node as MethodDeclaration);
@ -67,15 +68,15 @@ class TableParser extends ParserBase {
return tableName; return tableName;
} }
Set<SpecifiedColumn> _readPrimaryKey( Future<Set<SpecifiedColumn>> _readPrimaryKey(
ClassElement element, List<SpecifiedColumn> columns) { ClassElement element, List<SpecifiedColumn> columns) async {
final primaryKeyGetter = element.getGetter('primaryKey'); final primaryKeyGetter = element.getGetter('primaryKey');
if (primaryKeyGetter == null) { if (primaryKeyGetter == null) {
return null; return null;
} }
final ast = state.loadElementDeclaration(primaryKeyGetter).node final resolved = await state.loadElementDeclaration(primaryKeyGetter);
as MethodDeclaration; final ast = resolved.node as MethodDeclaration;
final body = ast.body; final body = ast.body;
if (body is! ExpressionFunctionBody) { if (body is! ExpressionFunctionBody) {
state.errors.add(MoorError( state.errors.add(MoorError(
@ -110,12 +111,12 @@ class TableParser extends ParserBase {
return parsedPrimaryKey; return parsedPrimaryKey;
} }
List<SpecifiedColumn> _parseColumns(ClassElement element) { Future<List<SpecifiedColumn>> _parseColumns(ClassElement element) {
return element.fields return Stream.fromIterable(element.fields)
.where((field) => isColumn(field.type) && field.getter != null) .where((field) => isColumn(field.type) && field.getter != null)
.map((field) { .asyncMap((field) async {
final node = final resolved = await state.loadElementDeclaration(field.getter);
state.loadElementDeclaration(field.getter).node as MethodDeclaration; final node = resolved.node as MethodDeclaration;
return state.columnParser.parse(node, field.getter); return state.columnParser.parse(node, field.getter);
}).toList(); }).toList();

View File

@ -21,21 +21,23 @@ class SharedState {
final tableTypeChecker = const TypeChecker.fromRuntime(Table); final tableTypeChecker = const TypeChecker.fromRuntime(Table);
final Map<DartType, SpecifiedTable> foundTables = {}; final Map<DartType, Future<SpecifiedTable>> _foundTables = {};
SharedState(this.options) { SharedState(this.options) {
tableParser = TableParser(this); tableParser = TableParser(this);
columnParser = ColumnParser(this); columnParser = ColumnParser(this);
} }
ElementDeclarationResult loadElementDeclaration(Element element) { Future<ElementDeclarationResult> loadElementDeclaration(
final result = Element element) async {
element.library.session.getParsedLibraryByElement(element.library); final resolvedLibrary = await element.library.session
return result.getElementDeclaration(element); .getResolvedLibraryByElement(element.library);
return resolvedLibrary.getElementDeclaration(element);
} }
SpecifiedTable parseType(DartType type, Element initializedBy) { Future<SpecifiedTable> parseType(DartType type, Element initializedBy) {
return foundTables.putIfAbsent(type, () { return _foundTables.putIfAbsent(type, () {
if (!tableTypeChecker.isAssignableFrom(type.element)) { if (!tableTypeChecker.isAssignableFrom(type.element)) {
errors.add(MoorError( errors.add(MoorError(
critical: true, critical: true,

View File

@ -1,5 +1,6 @@
import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/element.dart';
import 'package:moor_generator/src/model/specified_column.dart'; import 'package:moor_generator/src/model/specified_column.dart';
import 'package:moor_generator/src/model/specified_table.dart';
import 'package:moor_generator/src/options.dart'; import 'package:moor_generator/src/options.dart';
import 'package:moor_generator/src/parser/column_parser.dart'; import 'package:moor_generator/src/parser/column_parser.dart';
import 'package:moor_generator/src/parser/table_parser.dart'; import 'package:moor_generator/src/parser/table_parser.dart';
@ -56,46 +57,47 @@ void main() async {
..tableParser = TableParser(state); ..tableParser = TableParser(state);
}); });
Future<SpecifiedTable> parse(String name) {
return TableParser(state).parse(testLib.getType(name));
}
group('SQL table name', () { group('SQL table name', () {
test('should parse correctly when valid', () { test('should parse correctly when valid', () async {
expect( final parsed = await parse('TableWithCustomName');
TableParser(state) expect(parsed.sqlName, equals('my-fancy-table'));
.parse(testLib.getType('TableWithCustomName'))
.sqlName,
equals('my-fancy-table'));
}); });
test('should use class name if table name is not specified', () { test('should use class name if table name is not specified', () async {
expect(TableParser(state).parse(testLib.getType('Users')).sqlName, final parsed = await parse('Users');
equals('users')); expect(parsed.sqlName, equals('users'));
}); });
test('should not parse for complex methods', () async { test('should not parse for complex methods', () async {
TableParser(state).parse(testLib.getType('WrongName')); await TableParser(state).parse(testLib.getType('WrongName'));
expect(state.errors.errors, isNotEmpty); expect(state.errors.errors, isNotEmpty);
}); });
}); });
group('Columns', () { group('Columns', () {
test('should use field name if no name has been set explicitely', () { test('should use field name if no name has been set explicitely', () async {
final table = TableParser(state).parse(testLib.getType('Users')); final table = await parse('Users');
final idColumn = final idColumn =
table.columns.singleWhere((col) => col.name.name == 'id'); table.columns.singleWhere((col) => col.name.name == 'id');
expect(idColumn.name, equals(ColumnName.implicitly('id'))); expect(idColumn.name, equals(ColumnName.implicitly('id')));
}); });
test('should use explicit name, if it exists', () { test('should use explicit name, if it exists', () async {
final table = TableParser(state).parse(testLib.getType('Users')); final table = await parse('Users');
final idColumn = final idColumn =
table.columns.singleWhere((col) => col.name.name == 'user_name'); table.columns.singleWhere((col) => col.name.name == 'user_name');
expect(idColumn.name, equals(ColumnName.explicitly('user_name'))); expect(idColumn.name, equals(ColumnName.explicitly('user_name')));
}); });
test('should parse min and max length for text columns', () { test('should parse min and max length for text columns', () async {
final table = TableParser(state).parse(testLib.getType('Users')); final table = await parse('Users');
final idColumn = final idColumn =
table.columns.singleWhere((col) => col.name.name == 'user_name'); table.columns.singleWhere((col) => col.name.name == 'user_name');
@ -103,8 +105,8 @@ void main() async {
contains(LimitingTextLength.withLength(min: 6, max: 32))); contains(LimitingTextLength.withLength(min: 6, max: 32)));
}); });
test('should only parse max length when relevant', () { test('should only parse max length when relevant', () async {
final table = TableParser(state).parse(testLib.getType('Users')); final table = await parse('Users');
final idColumn = final idColumn =
table.columns.singleWhere((col) => col.dartGetterName == 'onlyMax'); table.columns.singleWhere((col) => col.dartGetterName == 'onlyMax');
@ -112,9 +114,8 @@ void main() async {
idColumn.features, contains(LimitingTextLength.withLength(max: 100))); idColumn.features, contains(LimitingTextLength.withLength(max: 100)));
}); });
test('parses custom constraints', () { test('parses custom constraints', () async {
final table = final table = await parse('CustomPrimaryKey');
TableParser(state).parse(testLib.getType('CustomPrimaryKey'));
final partA = final partA =
table.columns.singleWhere((c) => c.dartGetterName == 'partA'); table.columns.singleWhere((c) => c.dartGetterName == 'partA');
@ -125,8 +126,8 @@ void main() async {
expect(partA.customConstraints, isNull); expect(partA.customConstraints, isNull);
}); });
test('parsed default values', () { test('parsed default values', () async {
final table = TableParser(state).parse(testLib.getType('Users')); final table = await parse('Users');
final defaultsColumn = final defaultsColumn =
table.columns.singleWhere((c) => c.name.name == 'defaults'); table.columns.singleWhere((c) => c.name.name == 'defaults');
@ -134,8 +135,8 @@ void main() async {
}); });
}); });
test('parses custom primary keys', () { test('parses custom primary keys', () async {
final table = TableParser(state).parse(testLib.getType('CustomPrimaryKey')); final table = await parse('CustomPrimaryKey');
expect(table.primaryKey, containsAll(table.columns)); expect(table.primaryKey, containsAll(table.columns));
expect(table.columns.any((column) => column.hasAI), isFalse); expect(table.columns.any((column) => column.hasAI), isFalse);