diff --git a/packages/orm/angel_migration_runner/lib/src/mariadb/runner.dart b/packages/orm/angel_migration_runner/lib/src/mariadb/runner.dart index 1609a72f..24473657 100644 --- a/packages/orm/angel_migration_runner/lib/src/mariadb/runner.dart +++ b/packages/orm/angel_migration_runner/lib/src/mariadb/runner.dart @@ -71,7 +71,11 @@ class MariaDbMigrationRunner implements MigrationRunner { var curBatch = 0; if (result.isNotEmpty) { var firstRow = result.toList(); - curBatch = int.tryParse(firstRow[0][0] ?? '0') as int; + var firstBatch = firstRow[0][0] ?? 0; + if (firstBatch is! int) { + int.tryParse(firstBatch) as int; + } + curBatch = firstBatch; } var batch = curBatch + 1; @@ -105,7 +109,11 @@ class MariaDbMigrationRunner implements MigrationRunner { var curBatch = 0; if (result.isNotEmpty) { var firstRow = result.toList(); - curBatch = int.tryParse(firstRow[0][0]) as int; + var firstBatch = firstRow[0][0]; + if (firstBatch is! int) { + int.tryParse(firstBatch) as int; + } + curBatch = firstBatch; } result = await connection diff --git a/packages/orm/angel_orm_generator/lib/src/orm_build_context.dart b/packages/orm/angel_orm_generator/lib/src/orm_build_context.dart index 78ec5a4d..49bcdb47 100644 --- a/packages/orm/angel_orm_generator/lib/src/orm_build_context.dart +++ b/packages/orm/angel_orm_generator/lib/src/orm_build_context.dart @@ -125,12 +125,18 @@ Future buildOrmContext( buildCtx.resolveSerializedFieldType(field.name), ), ); - + var isEnumField = + (field.type is InterfaceType && field.type.element is EnumElement); + var hasColumnAnnotation = + (columnAnnotation?.getField('type')?.hasKnownValue ?? false); column = Column( isNullable: column.isNullable, length: column.length, indexType: column.indexType, - type: inferColumnType(field.type), + // Only infer type when not set by the @Column annotation, for enums + type: (isEnumField && hasColumnAnnotation) + ? column.type + : inferColumnType(field.type), defaultValue: column.defaultValue, ); diff --git a/packages/orm/angel_orm_generator/lib/src/orm_generator.dart b/packages/orm/angel_orm_generator/lib/src/orm_generator.dart index 69e607e9..e83507e5 100644 --- a/packages/orm/angel_orm_generator/lib/src/orm_generator.dart +++ b/packages/orm/angel_orm_generator/lib/src/orm_generator.dart @@ -3,6 +3,7 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:angel3_orm/angel3_orm.dart'; +import 'package:angel3_serialize/angel3_serialize.dart'; import 'package:angel3_serialize_generator/angel3_serialize_generator.dart'; import 'package:build/build.dart'; import 'package:code_builder/code_builder.dart' hide LibraryBuilder; @@ -277,11 +278,8 @@ class OrmGenerator extends GeneratorForAnnotation { * EnumType.values[(row[3] as int)] : null, */ var isNull = expr.equalTo(literalNull); - - Reference enumType = - convertTypeReference(fType, ignoreNullabilityCheck: true); - expr = isNull.conditional(literalNull, - enumType.property('values').index(expr.asA(refer('int')))); + final parseExpression = _deserializeEnumExpression(field, expr); + expr = isNull.conditional(literalNull, parseExpression); } else if (fType.isDartCoreBool) { // Generated Code: mapToBool(row[i]) expr = refer('mapToBool').call([expr]); @@ -892,9 +890,7 @@ class OrmGenerator extends GeneratorForAnnotation { var value = refer('values').index(literalString(name!)); if (fType is InterfaceType && fType.element is EnumElement) { - var asInt = value.asA(refer('int')); - var t = convertTypeReference(fType, ignoreNullabilityCheck: true); - value = t.property('values').index(asInt); + value = _deserializeEnumExpression(field, value); } else if (const TypeChecker.fromRuntime(List) .isAssignableFromType(fType)) { value = refer('json') @@ -926,7 +922,7 @@ class OrmGenerator extends GeneratorForAnnotation { Expression value = refer('value'); if (fType is InterfaceType && fType.element is EnumElement) { - value = CodeExpression(Code('value?.index')); + value = _serializeEnumExpression(field, value); } else if (const TypeChecker.fromRuntime(List) .isAssignableFromType(fType)) { value = refer('json').property('encode').call([value]); @@ -1005,4 +1001,45 @@ class OrmGenerator extends GeneratorForAnnotation { })); }); } + + /// Retrieve the [Expression] to parse a serialized enumeration field. + /// Takes into account the [SerializableField] properties. + /// Defaults to `enum.values[index as int]` + Expression _deserializeEnumExpression(FieldElement field, Expression expr) { + Reference enumType = + convertTypeReference(field.type, ignoreNullabilityCheck: true); + const TypeChecker serializableFieldTypeChecker = + TypeChecker.fromRuntime(SerializableField); + final annotation = serializableFieldTypeChecker.firstAnnotationOf(field); + Expression? parseExpr; + if (null != annotation) { + final deserializer = annotation.getField('deserializer')?.toSymbolValue(); + if (null != deserializer) { + var type = 'int'; + final serializesTo = annotation.getField('serializesTo')?.toTypeValue(); + if (null != serializesTo) { + type = serializesTo.element!.displayName; + } + parseExpr = Reference(deserializer).expression([expr.asA(refer(type))]); + } + } + return parseExpr ?? + enumType.property('values').index(expr.asA(refer('int'))); + } + + /// Retrieve the [Expression] to serialize the enumeration field. + /// Takes into account the [SerializableField] properties. + Expression _serializeEnumExpression(FieldElement field, Expression expr) { + const TypeChecker serializableFieldTypeChecker = + TypeChecker.fromRuntime(SerializableField); + final annotation = serializableFieldTypeChecker.firstAnnotationOf(field); + Expression? parseExpr; + if (null != annotation) { + final serializer = annotation.getField('serializer')?.toSymbolValue(); + if (null != serializer) { + parseExpr = Reference(serializer).expression([expr]); + } + } + return parseExpr ?? CodeExpression(Code('value?.index')); + } } diff --git a/packages/orm/angel_orm_mysql/pubspec.yaml b/packages/orm/angel_orm_mysql/pubspec.yaml index 361e8cb4..31f5dee8 100644 --- a/packages/orm/angel_orm_mysql/pubspec.yaml +++ b/packages/orm/angel_orm_mysql/pubspec.yaml @@ -17,15 +17,16 @@ dev_dependencies: build_runner: ^2.0.1 test: ^1.21.0 lints: ^2.0.0 -# dependency_overrides: + +dependency_overrides: # angel3_serialize: # path: ../../serialize/angel_serialize # angel3_serialize_generator: # path: ../../serialize/angel_serialize_generator # angel3_model: # path: ../../model -# angel3_orm_test: -# path: ../angel_orm_test + angel3_orm_test: + path: ../angel_orm_test # angel3_orm: # path: ../angel_orm # angel3_orm_generator: diff --git a/packages/orm/angel_orm_mysql/test/common.dart b/packages/orm/angel_orm_mysql/test/common.dart index 00c5230c..3da3be16 100644 --- a/packages/orm/angel_orm_mysql/test/common.dart +++ b/packages/orm/angel_orm_mysql/test/common.dart @@ -84,6 +84,8 @@ Future _connectToMariaDb(List schemas) async { } // Executor for MySQL +// create user 'test'@'localhost' identified by 'test123'; +// GRANT ALL PRIVILEGES ON orm_test.* to 'test'@'localhost' WITH GRANT OPTION; Future _connectToMySql(List schemas) async { var connection = await MySQLConnection.createConnection( databaseName: 'orm_test', @@ -91,7 +93,7 @@ Future _connectToMySql(List schemas) async { host: "localhost", userName: Platform.environment['MYSQL_USERNAME'] ?? 'test', password: Platform.environment['MYSQL_PASSWORD'] ?? 'test123', - secure: true); + secure: !('false' == Platform.environment['MYSQL_SECURE'])); await connection.connect(timeoutMs: 10000); diff --git a/packages/orm/angel_orm_mysql/test/migrations/has_car.sql b/packages/orm/angel_orm_mysql/test/migrations/has_car.sql index 82ff3644..b67e0d00 100644 --- a/packages/orm/angel_orm_mysql/test/migrations/has_car.sql +++ b/packages/orm/angel_orm_mysql/test/migrations/has_car.sql @@ -1,6 +1,7 @@ CREATE TABLE IF NOT EXISTS has_cars ( id serial PRIMARY KEY, type int not null, + color varchar(1), created_at datetime, updated_at datetime ); \ No newline at end of file diff --git a/packages/orm/angel_orm_postgres/test/migrations/has_car.sql b/packages/orm/angel_orm_postgres/test/migrations/has_car.sql index 67794aed..dce87d3f 100644 --- a/packages/orm/angel_orm_postgres/test/migrations/has_car.sql +++ b/packages/orm/angel_orm_postgres/test/migrations/has_car.sql @@ -1,6 +1,7 @@ CREATE TEMPORARY TABLE "has_cars" ( id serial PRIMARY KEY, type int not null, + color varchar(1), created_at timestamp, updated_at timestamp ); \ No newline at end of file diff --git a/packages/orm/angel_orm_test/lib/src/enum_and_nested_test.dart b/packages/orm/angel_orm_test/lib/src/enum_and_nested_test.dart index 1fdf2530..6dfa85b4 100644 --- a/packages/orm/angel_orm_test/lib/src/enum_and_nested_test.dart +++ b/packages/orm/angel_orm_test/lib/src/enum_and_nested_test.dart @@ -13,11 +13,14 @@ void enumAndNestedTests(FutureOr Function() createExecutor, }); test('insert', () async { - var query = HasCarQuery()..values.type = CarType.sedan; + var query = HasCarQuery() + ..values.type = CarType.sedan + ..values.color = Color.red; var resultOpt = await (query.insert(executor)); expect(resultOpt.isPresent, true); resultOpt.ifPresent((result) { expect(result.type, CarType.sedan); + expect(result.color, Color.red); }); }); diff --git a/packages/orm/angel_orm_test/lib/src/enum_test.dart b/packages/orm/angel_orm_test/lib/src/enum_test.dart new file mode 100644 index 00000000..3e5ff727 --- /dev/null +++ b/packages/orm/angel_orm_test/lib/src/enum_test.dart @@ -0,0 +1,11 @@ +import 'package:angel3_orm_test/src/models/has_car.dart'; +import 'package:test/test.dart'; + +void main() async { + /// See https://github.com/dukefirehawk/angel/pull/98 + test('enum field with custom deserializer should be parsed consistently', () { + final query = HasCarQuery(); + final hasCar = query.parseRow([null, null, null, 'R', null]).value; + expect(hasCar.color, equals(Color.red)); + }); +} diff --git a/packages/orm/angel_orm_test/lib/src/models/has_car.dart b/packages/orm/angel_orm_test/lib/src/models/has_car.dart index 7fef8b81..d5ea3bc3 100644 --- a/packages/orm/angel_orm_test/lib/src/models/has_car.dart +++ b/packages/orm/angel_orm_test/lib/src/models/has_car.dart @@ -11,6 +11,22 @@ part 'has_car.g.dart'; enum CarType { sedan, suv, atv } +Color? codeToColor(String? code) => code == null + ? null + : Color.values.firstWhere((color) => color.code == code); + +String? colorToCode(Color? color) => color?.code; + +enum Color { + red('R'), + green('G'), + blue('B'); + + const Color(this.code); + + final String code; +} + @orm @serializable abstract class _HasCar extends Model { @@ -21,4 +37,12 @@ abstract class _HasCar extends Model { @SerializableField(isNullable: false, defaultValue: CarType.sedan) CarType? get type; + + @SerializableField( + serializesTo: String, + serializer: #colorToCode, + deserializer: #codeToColor, + ) + @Column(type: ColumnType.varChar, length: 1) + Color? color; } diff --git a/packages/orm/angel_orm_test/lib/src/models/has_car.g.dart b/packages/orm/angel_orm_test/lib/src/models/has_car.g.dart index 2cc6fb15..4a243220 100644 --- a/packages/orm/angel_orm_test/lib/src/models/has_car.g.dart +++ b/packages/orm/angel_orm_test/lib/src/models/has_car.g.dart @@ -15,6 +15,13 @@ class HasCarMigration extends Migration { table.serial('id').primaryKey(); table.timeStamp('created_at'); table.timeStamp('updated_at'); + table.declareColumn( + 'color', + Column( + type: ColumnType('varchar'), + length: 1, + ), + ); table.integer('type').defaultsTo(0); }, ); @@ -63,6 +70,7 @@ class HasCarQuery extends Query { 'id', 'created_at', 'updated_at', + 'color', 'type', ]; return _selectedFields.isEmpty @@ -95,10 +103,15 @@ class HasCarQuery extends Query { fields.contains('created_at') ? mapToNullableDateTime(row[1]) : null, updatedAt: fields.contains('updated_at') ? mapToNullableDateTime(row[2]) : null, - type: fields.contains('type') + color: fields.contains('color') ? row[3] == null ? null - : CarType.values[(row[3] as int)] + : codeToColor((row[3] as String)) + : null, + type: fields.contains('type') + ? row[4] == null + ? null + : CarType.values[(row[4] as int)] : null, ); return Optional.of(model); @@ -124,6 +137,10 @@ class HasCarQueryWhere extends QueryWhere { query, 'updated_at', ), + color = StringSqlExpressionBuilder( + query, + 'color', + ), type = EnumSqlExpressionBuilder( query, 'type', @@ -136,6 +153,8 @@ class HasCarQueryWhere extends QueryWhere { final DateTimeSqlExpressionBuilder updatedAt; + final StringSqlExpressionBuilder color; + final EnumSqlExpressionBuilder type; @override @@ -144,6 +163,7 @@ class HasCarQueryWhere extends QueryWhere { id, createdAt, updatedAt, + color, type, ]; } @@ -170,6 +190,11 @@ class HasCarQueryValues extends MapQueryValues { } set updatedAt(DateTime? value) => values['updated_at'] = value; + Color? get color { + return codeToColor((values['color'] as String)); + } + + set color(Color? value) => values['color'] = colorToCode(value); CarType? get type { return CarType.values[(values['type'] as int)]; } @@ -178,6 +203,7 @@ class HasCarQueryValues extends MapQueryValues { void copyFrom(HasCar model) { createdAt = model.createdAt; updatedAt = model.updatedAt; + color = model.color; type = model.type; } } @@ -192,6 +218,7 @@ class HasCar extends _HasCar { this.id, this.createdAt, this.updatedAt, + this.color, this.type = CarType.sedan, }); @@ -207,6 +234,9 @@ class HasCar extends _HasCar { @override DateTime? updatedAt; + @override + Color? color; + @override CarType? type; @@ -214,12 +244,14 @@ class HasCar extends _HasCar { String? id, DateTime? createdAt, DateTime? updatedAt, + Color? color, CarType? type, }) { return HasCar( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + color: color ?? this.color, type: type ?? this.type); } @@ -229,6 +261,7 @@ class HasCar extends _HasCar { other.id == id && other.createdAt == createdAt && other.updatedAt == updatedAt && + other.color == color && other.type == type; } @@ -238,13 +271,14 @@ class HasCar extends _HasCar { id, createdAt, updatedAt, + color, type, ]); } @override String toString() { - return 'HasCar(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, type=$type)'; + return 'HasCar(id=$id, createdAt=$createdAt, updatedAt=$updatedAt, color=$color, type=$type)'; } Map toJson() { @@ -296,6 +330,7 @@ class HasCarSerializer extends Codec { ? (map['updated_at'] as DateTime) : DateTime.parse(map['updated_at'].toString())) : null, + color: codeToColor(map['color']), type: map['type'] as CarType? ?? CarType.sedan); } @@ -307,6 +342,7 @@ class HasCarSerializer extends Codec { 'id': model.id, 'created_at': model.createdAt?.toIso8601String(), 'updated_at': model.updatedAt?.toIso8601String(), + 'color': colorToCode(model.color), 'type': model.type }; } @@ -317,6 +353,7 @@ abstract class HasCarFields { id, createdAt, updatedAt, + color, type, ]; @@ -326,5 +363,7 @@ abstract class HasCarFields { static const String updatedAt = 'updated_at'; + static const String color = 'color'; + static const String type = 'type'; } diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..eb689c93 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,7 @@ +name: angel_workspace + +environment: + sdk: '>=2.18.0 <3.0.0' + +dependencies: + melos: 3.0.0